all repos

clerk @ 7638389

missing tooling for ledger/hledger

clerk/lint.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
implement basic linter, 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
)
14
15
func runLint(args []string) {
16
	fs := flag.NewFlagSet("lint", flag.ExitOnError)
17
	format := fs.String("format", "text", "output format: text, json")
18
	pathStyle := fs.String("path-style", "relative", "file path style: basename, relative, absolute")
19
	fs.Usage = func() {
20
		fmt.Fprintf(os.Stderr, "Usage: clerk lint [flags] [path...]\n")
21
		fs.PrintDefaults()
22
	}
23
	fs.Parse(args)
24
25
	style := parsePathStyle(*pathStyle)
26
	l := linter.NewLinter(linter.Rules)
27
	paths := fs.Args()
28
29
	if len(paths) == 0 {
30
		finds := lintStdin(l)
31
		report(finds, *format, style)
32
		if hasFatal(finds) {
33
			os.Exit(1)
34
		}
35
		os.Exit(0)
36
	}
37
38
	exitCode := 0
39
	for _, path := range paths {
40
		info, err := os.Stat(path)
41
		if err != nil {
42
			fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)
43
			exitCode = 1
44
			continue
45
		}
46
		if info.IsDir() {
47
			filepath.Walk(path, func(fpath string, finfo os.FileInfo, err error) error {
48
				if err != nil || finfo.IsDir() || !journal.IsJournalFile(fpath) {
49
					return nil
50
				}
51
				fatal := lintFileAndReport(l, fpath, *format, style)
52
				if fatal {
53
					exitCode = 1
54
				}
55
				return nil
56
			})
57
			continue
58
		}
59
		if lintFileAndReport(l, path, *format, style) {
60
			exitCode = 1
61
		}
62
	}
63
	os.Exit(exitCode)
64
}
65
66
// lintFileAndReport reads, lints, reports, and returns whether a fatal finding was found.
67
func lintFileAndReport(l *linter.Linter, path, format string, style linter.PathStyle) bool {
68
	src, err := os.ReadFile(path)
69
	if err != nil {
70
		fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)
71
		return true
72
	}
73
	pf, err := journal.NewLoader().LoadBytes(path, src)
74
	if err != nil {
75
		fmt.Fprintf(os.Stderr, "error: %s: %v\n", path, err)
76
		return true
77
	}
78
	finds := l.Run(pf.Ast)
79
	report(finds, format, style)
80
	return hasFatal(finds)
81
}
82
83
func lintStdin(l *linter.Linter) []linter.Find {
84
	src, err := io.ReadAll(os.Stdin)
85
	if err != nil {
86
		fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)
87
		os.Exit(1)
88
	}
89
	pf, err := journal.NewLoader().LoadBytes("stdin", src)
90
	if err != nil {
91
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
92
		os.Exit(1)
93
	}
94
	return l.Run(pf.Ast)
95
}
96
97
func report(finds []linter.Find, format string, style linter.PathStyle) {
98
	switch format {
99
	case "json":
100
		linter.FprintJSON(os.Stdout, style, finds)
101
	default:
102
		linter.Fprint(os.Stdout, style, finds)
103
	}
104
}
105
106
func hasFatal(finds []linter.Find) bool {
107
	for _, f := range finds {
108
		if f.Severity <= 2 {
109
			return true
110
		}
111
	}
112
	return false
113
}
114
115
func parsePathStyle(s string) linter.PathStyle {
116
	switch strings.ToLower(s) {
117
	default:
118
		return linter.PathRelative
119
	case "basename":
120
		return linter.PathBasename
121
	case "absolute":
122
		return linter.PathAbsolute
123
	}
124
}