all repos

clerk @ 0c15ec4f9c402a46cd79ec6ec8535bc1c17764b6

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
linter: sort output by position of spans, 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) Flush(format string) error {
75
	switch format {
76
	case "json":
77
		return FprintJSON(r.w, r.style, r.finds)
78
	case "text":
79
		Fprint(r.w, r.style, r.finds)
80
		return nil
81
	default:
82
		return errors.New("unsupported format")
83
	}
84
}
85
86
func formatPath(style PathStyle, p string) string {
87
	switch style {
88
	case PathBasename:
89
		return filepath.Base(p)
90
	case PathAbsolute:
91
		return p
92
	case PathRelative:
93
		wd, err := os.Getwd()
94
		if err == nil {
95
			if rel, err := filepath.Rel(wd, p); err == nil {
96
				return rel
97
			}
98
		}
99
		return p
100
	default:
101
		panic("impossible PathStyle value")
102
	}
103
}
104
105
func sortFinds(finds []Find) {
106
	sort.Slice(finds, func(i, j int) bool {
107
		if finds[i].Span.Start.Line != finds[j].Span.Start.Line {
108
			return finds[i].Span.Start.Line < finds[j].Span.Start.Line
109
		}
110
		if finds[i].Span.Start.Col != finds[j].Span.Start.Col {
111
			return finds[i].Span.Start.Col < finds[j].Span.Start.Col
112
		}
113
		if finds[i].Code != finds[j].Code {
114
			return finds[i].Code < finds[j].Code
115
		}
116
		return finds[i].Message < finds[j].Message
117
	})
118
}