all repos

clerk @ 5d20567551c70f8472e1b3e8b68359bbdf9efbeb

missing tooling for ledger/hledger

clerk/lint.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
linter: refactor to use semantic context, 1 month ago
1
package main
2
3
import (
4
	"flag"
5
	"fmt"
6
	"io"
7
	"os"
8
	"path/filepath"
9
	"strings"
10
11
	"olexsmir.xyz/clerk/internal/linter"
12
	"olexsmir.xyz/clerk/journal"
13
	"olexsmir.xyz/clerk/journal/semantic"
14
)
15
16
func runLint(args []string) {
17
	fs := flag.NewFlagSet("lint", flag.ExitOnError)
18
	format := fs.String("format", "text", "output format: text, json")
19
	pathStyle := fs.String("path-style", "relative", "file path style: basename, relative, absolute")
20
	fs.Usage = func() {
21
		fmt.Fprintf(os.Stderr, "Usage: clerk lint [flags] [path...]\n")
22
		fs.PrintDefaults()
23
	}
24
	fs.Parse(args)
25
26
	style := parsePathStyle(*pathStyle)
27
	l := linter.NewLinter(linter.Rules)
28
	reporter := linter.NewReporter(os.Stdout, style)
29
	paths := fs.Args()
30
31
	if len(paths) == 0 {
32
		finds := lintStdin(l)
33
		reporter.Collect(finds)
34
		reporter.Flush(*format)
35
		if hasFatal(finds) {
36
			os.Exit(1)
37
		}
38
		os.Exit(0)
39
	}
40
41
	exitCode := 0
42
	for _, path := range paths {
43
		info, err := os.Stat(path)
44
		if err != nil {
45
			fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)
46
			exitCode = 1
47
			continue
48
		}
49
		if info.IsDir() {
50
			filepath.Walk(path, func(fpath string, finfo os.FileInfo, err error) error {
51
				if err != nil || finfo.IsDir() || !journal.IsJournalFile(fpath) {
52
					return nil
53
				}
54
				finds, fatal := lintFile(l, fpath)
55
				reporter.Collect(finds)
56
				if fatal {
57
					exitCode = 1
58
				}
59
				return nil
60
			})
61
			continue
62
		}
63
		finds, fatal := lintFile(l, path)
64
		reporter.Collect(finds)
65
		if fatal {
66
			exitCode = 1
67
		}
68
	}
69
	reporter.Flush(*format)
70
	os.Exit(exitCode)
71
}
72
73
func lintFile(l *linter.Linter, path string) ([]linter.Find, bool) {
74
	src, err := os.ReadFile(path)
75
	if err != nil {
76
		fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)
77
		return nil, true
78
	}
79
	pf, err := journal.NewLoader().LoadBytes(path, src)
80
	if err != nil {
81
		fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)
82
		return nil, true
83
	}
84
	_ = pf // top-level file kept via ldr, not directly needed
85
	ctx := semantic.Build(ldr.Ordered())
86
	finds := l.Run(ctx)
87
	return finds, hasFatal(finds)
88
}
89
90
func lintStdin(l *linter.Linter) []linter.Find {
91
	src, err := io.ReadAll(os.Stdin)
92
	if err != nil {
93
		fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)
94
		os.Exit(1)
95
	}
96
	pf, err := journal.NewLoader().LoadBytes("stdin", src)
97
	if err != nil {
98
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
99
		os.Exit(1)
100
	}
101
	ctx := semantic.Build(ldr.Ordered())
102
	return l.Run(ctx)
103
}
104
105
func hasFatal(finds []linter.Find) bool {
106
	for _, f := range finds {
107
		if f.Severity <= 2 {
108
			return true
109
		}
110
	}
111
	return false
112
}
113
114
func parsePathStyle(s string) linter.PathStyle {
115
	switch strings.ToLower(s) {
116
	default:
117
		return linter.PathRelative
118
	case "basename":
119
		return linter.PathBasename
120
	case "absolute":
121
		return linter.PathAbsolute
122
	}
123
}