all repos

clerk @ 25c8f80

missing tooling for ledger/hledger

clerk/internal/linter/report.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
linter: performance; resolve cwd once per flush, buffer output, 13 days ago
1
package linter
2
3
import (
4
	"bufio"
5
	"encoding/json"
6
	"errors"
7
	"io"
8
	"os"
9
	"path/filepath"
10
	"slices"
11
	"strconv"
12
	"strings"
13
)
14
15
// PathStyle controls how file paths are shown in output.
16
type PathStyle int
17
18
const (
19
	PathBasename PathStyle = iota // just the filename (e.g. "entry.journal")
20
	PathAbsolute                  // full absolute path
21
	PathRelative                  // relative to current directory
22
)
23
24
// Reporter collects lint findings across files and flushes them in the desired format.
25
type Reporter struct {
26
	w     *bufio.Writer
27
	finds []Find
28
	style PathStyle
29
	cfg   Config
30
}
31
32
func NewReporter(w io.Writer, style PathStyle, cfg Config) *Reporter {
33
	return &Reporter{w: bufio.NewWriter(w), style: style, cfg: cfg}
34
}
35
36
func (r *Reporter) Collect(finds []Find) {
37
	for i := range finds {
38
		finds[i].Severity = r.cfg.SeverityFor(finds[i].Code)
39
	}
40
	r.finds = append(r.finds, finds...)
41
}
42
43
func (r *Reporter) HasFailures() bool {
44
	for i := range r.finds {
45
		if r.finds[i].Severity <= SeverityWarning {
46
			return true
47
		}
48
	}
49
	return false
50
}
51
52
func (r *Reporter) Flush(format string) error {
53
	defer r.w.Flush()
54
	switch format {
55
	case "json":
56
		return fprintJSON(r.w, r.style, r.finds)
57
	case "text":
58
		fprint(r.w, r.style, r.finds)
59
		return nil
60
	default:
61
		return errors.New("unsupported format")
62
	}
63
}
64
65
// fprint writes finds in text format: file:line:col code: message.
66
func fprint(w io.Writer, style PathStyle, finds []Find) {
67
	sortFinds(finds)
68
	wd, _ := os.Getwd()
69
	for _, find := range finds {
70
		io.WriteString(w, formatPath(style, find.Span.File, wd))
71
		io.WriteString(w, ":")
72
		io.WriteString(w, strconv.Itoa(find.Span.Start.Line))
73
		io.WriteString(w, ":")
74
		io.WriteString(w, strconv.Itoa(find.Span.Start.Col))
75
		io.WriteString(w, ": ")
76
		io.WriteString(w, string(find.Code))
77
		io.WriteString(w, ": ")
78
		io.WriteString(w, find.Message)
79
		io.WriteString(w, "\n")
80
	}
81
}
82
83
type findJSON struct {
84
	Message  string `json:"message"`
85
	Severity string `json:"severity"`
86
	Code     string `json:"code"`
87
	File     string `json:"file"`
88
	Line     int    `json:"line"`
89
	Column   int    `json:"column"`
90
}
91
92
// fprintJSON writes finds as [findJSON] array.
93
func fprintJSON(w io.Writer, style PathStyle, finds []Find) error {
94
	sortFinds(finds)
95
	wd, _ := os.Getwd()
96
	jsonFinds := make([]findJSON, len(finds))
97
	for i, find := range finds {
98
		jsonFinds[i] = findJSON{
99
			Message:  find.Message,
100
			Severity: find.Severity.String(), // TODO: it's unset
101
			Code:     string(find.Code),
102
			File:     formatPath(style, find.Span.File, wd),
103
			Line:     find.Span.Start.Line,
104
			Column:   find.Span.Start.Col,
105
		}
106
	}
107
	return json.NewEncoder(w).Encode(jsonFinds)
108
}
109
110
func formatPath(style PathStyle, path, wdir string) string {
111
	switch style {
112
	case PathBasename:
113
		return filepath.Base(path)
114
	case PathAbsolute:
115
		return path
116
	case PathRelative:
117
		if wdir != "" {
118
			if rel, err := filepath.Rel(wdir, path); err == nil {
119
				return rel
120
			}
121
		}
122
		return path
123
	default:
124
		panic("impossible PathStyle value")
125
	}
126
}
127
128
func sortFinds(finds []Find) {
129
	slices.SortFunc(finds, func(a, b Find) int {
130
		if a.Span.Start.Line != b.Span.Start.Line {
131
			return a.Span.Start.Line - b.Span.Start.Line
132
		}
133
		if a.Span.Start.Col != b.Span.Start.Col {
134
			return a.Span.Start.Col - b.Span.Start.Col
135
		}
136
		if a.Code != b.Code {
137
			return strings.Compare(string(a.Code), string(b.Code))
138
		}
139
		return strings.Compare(a.Message, b.Message)
140
	})
141
}