all repos

clerk @ 225a2d31a31f0501b2133d0747baf2ebeba5a0d4

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
change cli frameworks, 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
// Fprint writes finds in text format: file:line:col code: message.
23
func Fprint(w io.Writer, style PathStyle, finds []Find) {
24
	sortFinds(finds)
25
	for _, find := range finds {
26
		fmt.Fprintf(w, "%s:%d:%d: %s: %s\n",
27
			formatPath(style, find.Span.Start.File),
28
			find.Span.Start.Line, find.Span.Start.Col,
29
			find.Code, find.Message)
30
	}
31
}
32
33
type FindJSON struct {
34
	Message  string `json:"message"`
35
	Severity string `json:"severity"`
36
	Code     string `json:"code"`
37
	File     string `json:"file"`
38
	Line     int    `json:"line"`
39
	Column   int    `json:"column"`
40
}
41
42
// FprintJSON writes finds as [FindJSON] array.
43
func FprintJSON(w io.Writer, style PathStyle, finds []Find) error {
44
	sortFinds(finds)
45
	jsonFinds := make([]FindJSON, len(finds))
46
	for i, find := range finds {
47
		jsonFinds[i] = FindJSON{
48
			Message:  find.Message,
49
			Severity: find.Severity.String(),
50
			Code:     string(find.Code),
51
			File:     formatPath(style, find.Span.Start.File),
52
			Line:     find.Span.Start.Line,
53
			Column:   find.Span.Start.Col,
54
		}
55
	}
56
	return json.NewEncoder(w).Encode(jsonFinds)
57
}
58
59
// Reporter collects lint findings across files and flushes them in the desired format.
60
type Reporter struct {
61
	w     io.Writer
62
	finds []Find
63
	style PathStyle
64
}
65
66
func NewReporter(w io.Writer, style PathStyle) *Reporter {
67
	return &Reporter{w: w, style: style}
68
}
69
70
func (r *Reporter) Collect(finds []Find) {
71
	r.finds = append(r.finds, finds...)
72
}
73
74
func (r *Reporter) HasIssues() bool {
75
	return len(r.finds) > 0
76
}
77
78
func (r *Reporter) Flush(format string) error {
79
	switch format {
80
	case "json":
81
		return FprintJSON(r.w, r.style, r.finds)
82
	case "text":
83
		Fprint(r.w, r.style, r.finds)
84
		return nil
85
	default:
86
		return errors.New("unsupported format")
87
	}
88
}
89
90
func formatPath(style PathStyle, p string) string {
91
	switch style {
92
	case PathBasename:
93
		return filepath.Base(p)
94
	case PathAbsolute:
95
		return p
96
	case PathRelative:
97
		wd, err := os.Getwd()
98
		if err == nil {
99
			if rel, err := filepath.Rel(wd, p); err == nil {
100
				return rel
101
			}
102
		}
103
		return p
104
	default:
105
		panic("impossible PathStyle value")
106
	}
107
}
108
109
func sortFinds(finds []Find) {
110
	sort.Slice(finds, func(i, j int) bool {
111
		if finds[i].Span.Start.Line != finds[j].Span.Start.Line {
112
			return finds[i].Span.Start.Line < finds[j].Span.Start.Line
113
		}
114
		if finds[i].Span.Start.Col != finds[j].Span.Start.Col {
115
			return finds[i].Span.Start.Col < finds[j].Span.Start.Col
116
		}
117
		if finds[i].Code != finds[j].Code {
118
			return finds[i].Code < finds[j].Code
119
		}
120
		return finds[i].Message < finds[j].Message
121
	})
122
}