forked from greut/eclint
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlint.go
205 lines (166 loc) · 4.84 KB
/
lint.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package eclint
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"github.com/editorconfig/editorconfig-core-go/v2"
"github.com/go-logr/logr"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
// DefaultTabWidth sets the width of a tab used when counting the line length.
const DefaultTabWidth = 8
const (
// UnsetValue is the value equivalent to an empty / unset one.
UnsetValue = "unset"
// TabValue is the value representing tab indentation (the ugly one).
TabValue = "tab"
// SpaceValue is the value representing space indentation (the good one).
SpaceValue = "space"
// Utf8 is the ubiquitous character set.
Utf8 = "utf-8"
// Latin1 is the legacy 7-bits character set.
Latin1 = "latin1"
)
// Lint does the hard work of validating the given file.
func Lint(ctx context.Context, filename string) []error {
def, err := editorconfig.GetDefinitionForFilename(filename)
if err != nil {
return []error{fmt.Errorf("cannot open file %s. %w", filename, err)}
}
return LintWithDefinition(ctx, def, filename)
}
// LintWithDefinition does the hard work of validating the given file.
func LintWithDefinition(ctx context.Context, d *editorconfig.Definition, filename string) []error { //nolint:funlen
log := logr.FromContextOrDiscard(ctx)
def, err := newDefinition(d)
if err != nil {
return []error{err}
}
stat, err := os.Stat(filename)
if err != nil {
return []error{fmt.Errorf("cannot stat %s. %w", filename, err)}
}
if stat.IsDir() {
log.V(2).Info("skipped directory")
return nil
}
fileSize := stat.Size()
fp, err := os.Open(filename)
if err != nil {
return []error{fmt.Errorf("cannot open %s. %w", filename, err)}
}
defer fp.Close()
r := bufio.NewReader(fp)
ok, err := probeReadable(fp, r)
if err != nil {
return []error{fmt.Errorf("cannot read %s. %w", filename, err)}
}
if !ok {
log.V(2).Info("skipped unreadable or empty file")
return nil
}
charset, isBinary, err := ProbeCharsetOrBinary(ctx, r, def.Charset)
if err != nil {
return []error{err}
}
if isBinary {
log.V(2).Info("binary file detected and skipped")
return nil
}
log.V(2).Info("charset probed", "filename", filename, "charset", charset)
var decoder *encoding.Decoder
switch charset {
case "utf-16be":
decoder = unicode.UTF16(unicode.BigEndian, unicode.ExpectBOM).NewDecoder()
case "utf-16le":
decoder = unicode.UTF16(unicode.LittleEndian, unicode.ExpectBOM).NewDecoder()
default:
decoder = unicode.UTF8.NewDecoder()
}
t := transform.NewReader(r, unicode.BOMOverride(decoder))
errs := validate(ctx, t, fileSize, charset, def)
// Enrich the errors with the filename
for i, err := range errs {
var ve ValidationError
if ok := errors.As(err, &ve); ok {
ve.Filename = filename
errs[i] = ve
} else if err != nil {
errs[i] = err
}
}
return errs
}
// validate is where the validations rules are applied.
func validate( //nolint:cyclop,gocognit,funlen
ctx context.Context,
r io.Reader,
fileSize int64,
charset string,
def *definition,
) []error {
return ReadLines(r, fileSize, func(index int, data []byte, isEOF bool) error {
var err error
if ctx.Err() != nil {
return fmt.Errorf("read lines got interrupted: %w", ctx.Err())
}
if isEOF {
if def.InsertFinalNewline != nil {
err = checkInsertFinalNewline(data, *def.InsertFinalNewline)
}
} else {
if def.EndOfLine != "" && def.EndOfLine != UnsetValue {
err = endOfLine(def.EndOfLine, data)
}
}
if err == nil && //nolint:nestif
def.IndentStyle != "" &&
def.IndentStyle != UnsetValue &&
def.Definition.IndentSize != UnsetValue {
err = indentStyle(def.IndentStyle, def.IndentSize, data)
if err != nil && def.InsideBlockComment && def.BlockComment != nil {
// The indentation may fail within a block comment.
var ve ValidationError
if ok := errors.As(err, &ve); ok {
err = checkBlockComment(ve.Position, def.BlockComment, data)
}
}
if def.InsideBlockComment && def.BlockCommentEnd != nil {
def.InsideBlockComment = !isBlockCommentEnd(def.BlockCommentEnd, data)
}
if err == nil && !def.InsideBlockComment && def.BlockCommentStart != nil {
def.InsideBlockComment = isBlockCommentStart(def.BlockCommentStart, data)
}
}
if err == nil && def.TrimTrailingWhitespace != nil && *def.TrimTrailingWhitespace {
err = checkTrimTrailingWhitespace(data)
}
if err == nil && def.MaxLength > 0 {
// Remove any BOM from the first line.
d := data
if index == 0 && charset != "" {
for _, bom := range [][]byte{utf8Bom} {
if bytes.HasPrefix(data, bom) {
d = data[len(utf8Bom):]
break
}
}
}
err = MaxLineLength(def.MaxLength, def.TabWidth, d)
}
// Enrich the error with the line number
var ve ValidationError
if ok := errors.As(err, &ve); ok {
ve.Line = data
ve.Index = index
return ve
}
return err
})
}