all repos

clerk @ e0319e501d48d625d4f01e3cf44dfeca2c0a76c7

missing tooling for ledger/hledger

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

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