all repos

clerk @ 9444b5a

missing tooling for ledger/hledger

clerk/lint.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
linter: add undeclared-commodity, 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
			exitCode = lintDir(l, reporter, path)
51
			continue
52
		}
53
		finds, fatal := lintFile(l, path)
54
		reporter.Collect(finds)
55
		if fatal {
56
			exitCode = 1
57
		}
58
	}
59
	reporter.Flush(*format)
60
	os.Exit(exitCode)
61
}
62
63
func lintDir(l *linter.Linter, reporter *linter.Reporter, dir string) int {
64
	ldr := journal.NewLoader()
65
	var paths []string
66
	filepath.Walk(dir, func(fpath string, finfo os.FileInfo, err error) error {
67
		if err != nil || finfo.IsDir() || !journal.IsJournalFile(fpath) {
68
			return nil
69
		}
70
		paths = append(paths, fpath)
71
		return nil
72
	})
73
	if len(paths) == 0 {
74
		return 0
75
	}
76
	var lastErr error
77
	for _, fpath := range paths {
78
		src, err := os.ReadFile(fpath)
79
		if err != nil {
80
			fmt.Fprintf(os.Stderr, "error: %s: %v\n", fpath, err)
81
			lastErr = err
82
			continue
83
		}
84
		if _, err := ldr.LoadBytes(fpath, src); err != nil {
85
			fmt.Fprintf(os.Stderr, "error: %s: %v\n", fpath, err)
86
			lastErr = err
87
		}
88
	}
89
	ctx := semantic.Build(ldr.Ordered())
90
	finds := l.Run(ctx)
91
	reporter.Collect(finds)
92
	if lastErr != nil {
93
		return 1
94
	}
95
	if hasFatal(finds) {
96
		return 1
97
	}
98
	return 0
99
}
100
101
func lintFile(l *linter.Linter, path string) ([]linter.Find, bool) {
102
	src, err := os.ReadFile(path)
103
	if err != nil {
104
		fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)
105
		return nil, true
106
	}
107
	ldr := journal.NewLoader()
108
	pf, err := ldr.LoadBytes(path, src)
109
	if err != nil {
110
		fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)
111
		return nil, true
112
	}
113
	_ = pf // top-level file kept via ldr, not directly needed
114
	ctx := semantic.Build(ldr.Ordered())
115
	finds := l.Run(ctx)
116
	return finds, hasFatal(finds)
117
}
118
119
func lintStdin(l *linter.Linter) []linter.Find {
120
	src, err := io.ReadAll(os.Stdin)
121
	if err != nil {
122
		fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)
123
		os.Exit(1)
124
	}
125
	ldr := journal.NewLoader()
126
	_, err = ldr.LoadBytes("stdin", src)
127
	if err != nil {
128
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
129
		os.Exit(1)
130
	}
131
	ctx := semantic.Build(ldr.Ordered())
132
	return l.Run(ctx)
133
}
134
135
func hasFatal(finds []linter.Find) bool {
136
	for _, f := range finds {
137
		if f.Severity <= 2 {
138
			return true
139
		}
140
	}
141
	return false
142
}
143
144
func parsePathStyle(s string) linter.PathStyle {
145
	switch strings.ToLower(s) {
146
	default:
147
		return linter.PathRelative
148
	case "basename":
149
		return linter.PathBasename
150
	case "absolute":
151
		return linter.PathAbsolute
152
	}
153
}