@ttsc/lint 0.12.3 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/defaultFormat.d.ts +10 -11
- package/lib/defaultFormat.js +10 -11
- package/lib/defaultFormat.js.map +1 -1
- package/lib/index.js +233 -135
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +2 -2
- package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
- package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
- package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
- package/lib/structures/TtscLintRule.d.ts +10 -1
- package/lib/structures/TtscLintRuleMap.d.ts +2 -2
- package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
- package/linthost/ast_helpers.go +80 -11
- package/linthost/compile.go +116 -112
- package/linthost/config.go +647 -687
- package/linthost/config_format.go +272 -247
- package/linthost/contrib_adapter.go +7 -0
- package/linthost/directives.go +116 -0
- package/linthost/dispatch.go +33 -33
- package/linthost/engine.go +156 -43
- package/linthost/fix.go +47 -27
- package/linthost/flags_gen.go +31 -0
- package/linthost/format.go +119 -3
- package/linthost/host.go +115 -5
- package/linthost/print_dispatch.go +128 -28
- package/linthost/print_doc.go +23 -0
- package/linthost/print_engine.go +168 -4
- package/linthost/print_nodes_array.go +17 -7
- package/linthost/print_nodes_call.go +138 -20
- package/linthost/print_nodes_function.go +353 -0
- package/linthost/print_nodes_imports.go +46 -29
- package/linthost/print_nodes_list.go +86 -5
- package/linthost/print_nodes_object.go +56 -11
- package/linthost/rules_arrays.go +5 -2
- package/linthost/rules_debugger.go +3 -2
- package/linthost/rules_dupes.go +7 -4
- package/linthost/rules_empty.go +3 -2
- package/linthost/rules_escape.go +35 -3
- package/linthost/rules_eval.go +3 -0
- package/linthost/rules_finally.go +11 -0
- package/linthost/rules_format_jsdoc.go +7 -0
- package/linthost/rules_format_print_width.go +270 -15
- package/linthost/rules_format_quotes.go +3 -0
- package/linthost/rules_format_sort_imports.go +4 -0
- package/linthost/rules_gap.go +75 -3
- package/linthost/rules_imports.go +5 -7
- package/linthost/rules_logic.go +75 -5
- package/linthost/rules_loops.go +4 -0
- package/linthost/rules_misc.go +4 -2
- package/linthost/rules_params.go +7 -11
- package/linthost/rules_problems.go +89 -22
- package/linthost/rules_promise.go +15 -0
- package/linthost/rules_protos.go +3 -2
- package/linthost/rules_self.go +6 -0
- package/linthost/rules_strings.go +10 -0
- package/linthost/rules_suggestions.go +174 -9
- package/linthost/rules_throw.go +2 -0
- package/linthost/rules_ts.go +13 -0
- package/linthost/rules_ts_extra.go +25 -8
- package/linthost/rules_var.go +15 -2
- package/package.json +3 -3
- package/plugin/main.go +4 -4
- package/rule/astutil/astutil.go +12 -0
- package/rule/rule.go +123 -123
- package/src/defaultFormat.ts +10 -11
- package/src/index.ts +257 -168
- package/src/structures/ITtscLintConfig.ts +2 -2
- package/src/structures/ITtscLintFormatConfig.ts +53 -55
- package/src/structures/ITtscLintPluginConfig.ts +10 -83
- package/src/structures/ITtscLintPluginMeta.ts +9 -1
- package/src/structures/TtscLintRule.ts +10 -1
- package/src/structures/TtscLintRuleMap.ts +17 -18
- package/src/structures/TtscLintRuleOptions.ts +15 -0
- package/linthost/eslint_runtime.go +0 -320
package/linthost/directives.go
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
// Inline-disable directive parser and filter for the lint engine.
|
|
2
|
+
//
|
|
3
|
+
// Supports both `eslint-disable` and `lint-disable` comment families in
|
|
4
|
+
// four forms: `disable`, `enable`, `disable-line`, and
|
|
5
|
+
// `disable-next-line`. Rule lists are comma- or space-separated; an
|
|
6
|
+
// empty list disables all rules. The `--` description separator (ESLint
|
|
7
|
+
// convention) is also recognized and stripped before parsing rule names.
|
|
1
8
|
package linthost
|
|
2
9
|
|
|
3
10
|
import (
|
|
@@ -7,36 +14,61 @@ import (
|
|
|
7
14
|
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
8
15
|
)
|
|
9
16
|
|
|
17
|
+
// lintDirectiveKind classifies the four comment forms the parser recognizes.
|
|
10
18
|
type lintDirectiveKind int
|
|
11
19
|
|
|
12
20
|
const (
|
|
21
|
+
// lintDirectiveDisable activates suppression from the comment position until
|
|
22
|
+
// a matching `enable` is seen (or end of file).
|
|
13
23
|
lintDirectiveDisable lintDirectiveKind = iota
|
|
24
|
+
// lintDirectiveEnable cancels a prior `disable` for the named rules (or all
|
|
25
|
+
// rules when no rule list is given).
|
|
14
26
|
lintDirectiveEnable
|
|
27
|
+
// lintDirectiveDisableLine suppresses findings on the same source line as
|
|
28
|
+
// the directive comment.
|
|
15
29
|
lintDirectiveDisableLine
|
|
30
|
+
// lintDirectiveDisableNextLine suppresses findings on the line immediately
|
|
31
|
+
// following the directive comment.
|
|
16
32
|
lintDirectiveDisableNextLine
|
|
17
33
|
)
|
|
18
34
|
|
|
35
|
+
// lintDirective is the parsed representation of one directive comment.
|
|
19
36
|
type lintDirective struct {
|
|
20
37
|
kind lintDirectiveKind
|
|
21
38
|
rules lintDirectiveRules
|
|
22
39
|
}
|
|
23
40
|
|
|
41
|
+
// lintDirectiveRules holds the rule scope of a directive. When `all` is
|
|
42
|
+
// true the directive applies to every rule; otherwise only the named rules
|
|
43
|
+
// in `rules` are affected.
|
|
24
44
|
type lintDirectiveRules struct {
|
|
25
45
|
all bool
|
|
26
46
|
rules map[string]struct{}
|
|
27
47
|
}
|
|
28
48
|
|
|
49
|
+
// lintDirectiveEvent is one enable/disable transition recorded in source
|
|
50
|
+
// order. `pos` is the byte offset of the comment. `on` is true for a
|
|
51
|
+
// disable event and false for an enable event.
|
|
29
52
|
type lintDirectiveEvent struct {
|
|
30
53
|
pos int
|
|
31
54
|
rules lintDirectiveRules
|
|
32
55
|
on bool
|
|
33
56
|
}
|
|
34
57
|
|
|
58
|
+
// lintInlineDirectives accumulates the per-file directive information
|
|
59
|
+
// extracted by parseLintInlineDirectives. `lines` maps a zero-based line
|
|
60
|
+
// number to any disable-line / disable-next-line directives on that line.
|
|
61
|
+
// `events` is the ordered list of range-style disable/enable transitions.
|
|
35
62
|
type lintInlineDirectives struct {
|
|
36
63
|
lines map[int][]lintDirectiveRules
|
|
37
64
|
events []lintDirectiveEvent
|
|
38
65
|
}
|
|
39
66
|
|
|
67
|
+
// lintDisableState tracks the cumulative suppress/allow state as
|
|
68
|
+
// lintDirectiveEvents are replayed in order up to a finding's position.
|
|
69
|
+
// `all` means every rule is suppressed. `rules` is the set of individually
|
|
70
|
+
// suppressed rules. `enabledInAll` is the set of rules that were
|
|
71
|
+
// re-enabled via `eslint-enable <rule>` while `all` was active.
|
|
40
72
|
type lintDisableState struct {
|
|
41
73
|
all bool
|
|
42
74
|
rules map[string]struct{}
|
|
@@ -60,6 +92,21 @@ func filterInlineDisabledFindings(file *shimast.SourceFile, findings []*Finding)
|
|
|
60
92
|
return filtered
|
|
61
93
|
}
|
|
62
94
|
|
|
95
|
+
// parseLintInlineDirectives scans all comment tokens in `file` for
|
|
96
|
+
// recognized directive markers and returns a structured summary of the
|
|
97
|
+
// per-line and range-style suppressions found.
|
|
98
|
+
//
|
|
99
|
+
// The raw scanner does not split `KindTemplateExpression` on its own:
|
|
100
|
+
// after returning `KindTemplateHead`/`KindTemplateMiddle`, it resumes
|
|
101
|
+
// lexing the substitution as ordinary code, and a later `}` is reported
|
|
102
|
+
// as `KindCloseBraceToken` instead of re-entering the template body.
|
|
103
|
+
// Without intervention the next backtick would open a fresh template
|
|
104
|
+
// scan that swallows the rest of the file (including every disable
|
|
105
|
+
// directive comment) as one runaway unterminated literal. The parser
|
|
106
|
+
// avoids this by calling `ReScanTemplateToken` on the matching `}`;
|
|
107
|
+
// this loop mirrors that behavior with a brace-depth stack so comment
|
|
108
|
+
// positions stay aligned with the source bytes past any template
|
|
109
|
+
// substitution.
|
|
63
110
|
func parseLintInlineDirectives(file *shimast.SourceFile) *lintInlineDirectives {
|
|
64
111
|
directives := &lintInlineDirectives{
|
|
65
112
|
lines: make(map[int][]lintDirectiveRules),
|
|
@@ -68,12 +115,44 @@ func parseLintInlineDirectives(file *shimast.SourceFile) *lintInlineDirectives {
|
|
|
68
115
|
scanner.SetText(file.Text())
|
|
69
116
|
scanner.SetSkipTrivia(false)
|
|
70
117
|
|
|
118
|
+
// templateBraceDepth tracks `{` nesting inside each open template
|
|
119
|
+
// substitution. A zero on top means the next `}` matches the original
|
|
120
|
+
// `${` and must be re-scanned as a template middle/tail token.
|
|
121
|
+
var templateBraceDepth []int
|
|
122
|
+
|
|
71
123
|
scan:
|
|
72
124
|
for {
|
|
73
125
|
kind := scanner.Scan()
|
|
74
126
|
switch kind {
|
|
75
127
|
case shimast.KindEndOfFile:
|
|
76
128
|
break scan
|
|
129
|
+
case shimast.KindTemplateHead, shimast.KindTemplateMiddle:
|
|
130
|
+
// Entering a `${...}` substitution; account for its closing `}`.
|
|
131
|
+
templateBraceDepth = append(templateBraceDepth, 0)
|
|
132
|
+
continue
|
|
133
|
+
case shimast.KindOpenBraceToken:
|
|
134
|
+
if n := len(templateBraceDepth); n > 0 {
|
|
135
|
+
templateBraceDepth[n-1]++
|
|
136
|
+
}
|
|
137
|
+
continue
|
|
138
|
+
case shimast.KindCloseBraceToken:
|
|
139
|
+
n := len(templateBraceDepth)
|
|
140
|
+
if n == 0 {
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
if templateBraceDepth[n-1] > 0 {
|
|
144
|
+
templateBraceDepth[n-1]--
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
// Matching `}` for the original `${`. Pop the substitution and
|
|
148
|
+
// rescan as template; a `KindTemplateMiddle` reopens a new
|
|
149
|
+
// substitution, a `KindTemplateTail` closes the template literal.
|
|
150
|
+
templateBraceDepth = templateBraceDepth[:n-1]
|
|
151
|
+
rescanned := scanner.ReScanTemplateToken(false /*isTaggedTemplate*/)
|
|
152
|
+
if rescanned == shimast.KindTemplateMiddle {
|
|
153
|
+
templateBraceDepth = append(templateBraceDepth, 0)
|
|
154
|
+
}
|
|
155
|
+
continue
|
|
77
156
|
case shimast.KindSingleLineCommentTrivia, shimast.KindMultiLineCommentTrivia:
|
|
78
157
|
default:
|
|
79
158
|
continue
|
|
@@ -111,10 +190,16 @@ scan:
|
|
|
111
190
|
return directives
|
|
112
191
|
}
|
|
113
192
|
|
|
193
|
+
// empty reports whether no directives were found in the file, allowing the
|
|
194
|
+
// caller to skip the filtering step entirely.
|
|
114
195
|
func (d *lintInlineDirectives) empty() bool {
|
|
115
196
|
return d == nil || (len(d.lines) == 0 && len(d.events) == 0)
|
|
116
197
|
}
|
|
117
198
|
|
|
199
|
+
// suppresses reports whether the directive set causes `finding` to be
|
|
200
|
+
// suppressed. It checks the per-line map first (disable-line and
|
|
201
|
+
// disable-next-line directives), then replays the ordered event list to
|
|
202
|
+
// compute the range-style disable/enable state at the finding's position.
|
|
118
203
|
func (d *lintInlineDirectives) suppresses(file *shimast.SourceFile, finding *Finding) bool {
|
|
119
204
|
if d == nil || finding == nil || file == nil {
|
|
120
205
|
return false
|
|
@@ -135,6 +220,10 @@ func (d *lintInlineDirectives) suppresses(file *shimast.SourceFile, finding *Fin
|
|
|
135
220
|
return state.matches(finding.Rule)
|
|
136
221
|
}
|
|
137
222
|
|
|
223
|
+
// apply updates the disable state by folding in one directive event.
|
|
224
|
+
// Disable events add rules; enable events remove them. When the event
|
|
225
|
+
// targets all rules (`event.rules.all`), the entire state is replaced
|
|
226
|
+
// rather than merged.
|
|
138
227
|
func (s *lintDisableState) apply(event lintDirectiveEvent) {
|
|
139
228
|
if event.on {
|
|
140
229
|
if event.rules.all {
|
|
@@ -169,6 +258,9 @@ func (s *lintDisableState) apply(event lintDirectiveEvent) {
|
|
|
169
258
|
}
|
|
170
259
|
}
|
|
171
260
|
|
|
261
|
+
// matches reports whether `rule` is currently suppressed given this state.
|
|
262
|
+
// The name is normalized before lookup so that `@typescript-eslint/` prefixes
|
|
263
|
+
// do not prevent a match.
|
|
172
264
|
func (s lintDisableState) matches(rule string) bool {
|
|
173
265
|
normalized := normalizeDirectiveRuleName(rule)
|
|
174
266
|
if _, ok := s.rules[normalized]; ok {
|
|
@@ -181,6 +273,9 @@ func (s lintDisableState) matches(rule string) bool {
|
|
|
181
273
|
return !enabled
|
|
182
274
|
}
|
|
183
275
|
|
|
276
|
+
// matches reports whether this rule-set covers `rule`. Returns true when the
|
|
277
|
+
// directive targeted all rules or when `rule` (normalized) appears in the
|
|
278
|
+
// named set.
|
|
184
279
|
func (r lintDirectiveRules) matches(rule string) bool {
|
|
185
280
|
if r.all {
|
|
186
281
|
return true
|
|
@@ -189,6 +284,9 @@ func (r lintDirectiveRules) matches(rule string) bool {
|
|
|
189
284
|
return ok
|
|
190
285
|
}
|
|
191
286
|
|
|
287
|
+
// parseLintDirectiveComment strips comment delimiters from `raw` and
|
|
288
|
+
// delegates to parseLintDirectiveLine. Returns (zero, false) when the
|
|
289
|
+
// comment does not contain a recognized directive marker.
|
|
192
290
|
func parseLintDirectiveComment(raw string) (lintDirective, bool) {
|
|
193
291
|
text := stripCommentDelimiters(raw)
|
|
194
292
|
if directive, ok := parseLintDirectiveLine(text); ok {
|
|
@@ -197,6 +295,9 @@ func parseLintDirectiveComment(raw string) (lintDirective, bool) {
|
|
|
197
295
|
return lintDirective{}, false
|
|
198
296
|
}
|
|
199
297
|
|
|
298
|
+
// stripCommentDelimiters removes `//` and `/* … */` syntax from `raw` and
|
|
299
|
+
// returns the trimmed inner text. Handles JSDoc-style `* ` prefix on the
|
|
300
|
+
// first content line.
|
|
200
301
|
func stripCommentDelimiters(raw string) string {
|
|
201
302
|
switch {
|
|
202
303
|
case strings.HasPrefix(raw, "//"):
|
|
@@ -216,6 +317,9 @@ func stripCommentDelimiters(raw string) string {
|
|
|
216
317
|
}
|
|
217
318
|
}
|
|
218
319
|
|
|
320
|
+
// parseLintDirectiveLine matches `text` against all recognized directive
|
|
321
|
+
// markers in declaration order (longest suffix first to avoid prefix
|
|
322
|
+
// ambiguity). Returns the first match found or (zero, false) if none match.
|
|
219
323
|
func parseLintDirectiveLine(text string) (lintDirective, bool) {
|
|
220
324
|
for _, prefix := range []string{"eslint", "lint"} {
|
|
221
325
|
for _, form := range []struct {
|
|
@@ -241,6 +345,9 @@ func parseLintDirectiveLine(text string) (lintDirective, bool) {
|
|
|
241
345
|
return lintDirective{}, false
|
|
242
346
|
}
|
|
243
347
|
|
|
348
|
+
// directivePayload returns the text after `marker` in `text` when `text`
|
|
349
|
+
// starts with `marker` followed by whitespace or end of string. The
|
|
350
|
+
// returned payload is trimmed of leading/trailing whitespace.
|
|
244
351
|
func directivePayload(text, marker string) (string, bool) {
|
|
245
352
|
if !strings.HasPrefix(text, marker) {
|
|
246
353
|
return "", false
|
|
@@ -252,6 +359,9 @@ func directivePayload(text, marker string) (string, bool) {
|
|
|
252
359
|
return strings.TrimSpace(rest), true
|
|
253
360
|
}
|
|
254
361
|
|
|
362
|
+
// parseDirectiveRules converts the payload (the text after the directive
|
|
363
|
+
// marker) into a lintDirectiveRules value. The `--` separator strips an
|
|
364
|
+
// optional human-readable description. An empty rule list means "all rules".
|
|
255
365
|
func parseDirectiveRules(payload string) lintDirectiveRules {
|
|
256
366
|
payload = stripDirectiveDescription(payload)
|
|
257
367
|
payload = strings.ReplaceAll(payload, ",", " ")
|
|
@@ -272,6 +382,10 @@ func parseDirectiveRules(payload string) lintDirectiveRules {
|
|
|
272
382
|
return lintDirectiveRules{rules: rules}
|
|
273
383
|
}
|
|
274
384
|
|
|
385
|
+
// stripDirectiveDescription returns the portion of `payload` before the
|
|
386
|
+
// first ` -- ` token (ESLint's description separator). The separator must
|
|
387
|
+
// be surrounded by whitespace or be at a string boundary to avoid
|
|
388
|
+
// stripping `--` from rule names like `no--foo`.
|
|
275
389
|
func stripDirectiveDescription(payload string) string {
|
|
276
390
|
for i := 0; i < len(payload)-1; i++ {
|
|
277
391
|
if payload[i] != '-' || payload[i+1] != '-' {
|
|
@@ -287,6 +401,8 @@ func stripDirectiveDescription(payload string) string {
|
|
|
287
401
|
return payload
|
|
288
402
|
}
|
|
289
403
|
|
|
404
|
+
// normalizeDirectiveRuleName strips the common ESLint namespace prefixes so
|
|
405
|
+
// both `@typescript-eslint/no-var` and `no-var` resolve to the same key.
|
|
290
406
|
func normalizeDirectiveRuleName(name string) string {
|
|
291
407
|
name = strings.TrimSpace(name)
|
|
292
408
|
name = strings.TrimPrefix(name, "@typescript-eslint/")
|
package/linthost/dispatch.go
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
package linthost
|
|
8
8
|
|
|
9
9
|
import (
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
"fmt"
|
|
11
|
+
"os"
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
// Version is the build banner string the `version` subcommand prints.
|
|
@@ -25,41 +25,41 @@ var Version = "dev"
|
|
|
25
25
|
// Recognized verbs: `version` / `-v` / `--version`, `check`, `fix`, `format`,
|
|
26
26
|
// `build`, `transform`. Anything else is a usage error (exit code 2).
|
|
27
27
|
func Main(args []string) int {
|
|
28
|
-
|
|
28
|
+
return run(args)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// run is the package-local dispatcher invoked by Main and by the in-tree
|
|
32
32
|
// test/command corpus, which exercises end-to-end subcommand routing through
|
|
33
33
|
// the same entry point the CLI uses.
|
|
34
34
|
func run(args []string) int {
|
|
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
|
-
|
|
35
|
+
if len(args) == 0 {
|
|
36
|
+
fmt.Fprintln(os.Stderr, "@ttsc/lint: command required (expected check|fix|format|build|transform|version)")
|
|
37
|
+
return 2
|
|
38
|
+
}
|
|
39
|
+
switch args[0] {
|
|
40
|
+
case "-v", "--version", "version":
|
|
41
|
+
// Don't pay contributor-registration cost for the version banner.
|
|
42
|
+
fmt.Fprintf(os.Stdout, "@ttsc/lint %s\n", Version)
|
|
43
|
+
return 0
|
|
44
|
+
case "check", "fix", "format", "build", "transform":
|
|
45
|
+
default:
|
|
46
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: unknown command %q\n", args[0])
|
|
47
|
+
return 2
|
|
48
|
+
}
|
|
49
|
+
// Wire contributor rules into the engine's dispatch table after every
|
|
50
|
+
// package init has settled. See contrib_adapter.go for the rationale.
|
|
51
|
+
registerContributors()
|
|
52
|
+
switch args[0] {
|
|
53
|
+
case "check":
|
|
54
|
+
return RunCheck(args[1:])
|
|
55
|
+
case "fix":
|
|
56
|
+
return RunFix(args[1:])
|
|
57
|
+
case "format":
|
|
58
|
+
return RunFormat(args[1:])
|
|
59
|
+
case "build":
|
|
60
|
+
return RunBuild(args[1:])
|
|
61
|
+
case "transform":
|
|
62
|
+
return RunTransform(args[1:])
|
|
63
|
+
}
|
|
64
|
+
return 2
|
|
65
65
|
}
|
package/linthost/engine.go
CHANGED
|
@@ -21,7 +21,9 @@ import (
|
|
|
21
21
|
"encoding/json"
|
|
22
22
|
"fmt"
|
|
23
23
|
"os"
|
|
24
|
+
"runtime"
|
|
24
25
|
"sort"
|
|
26
|
+
"sync"
|
|
25
27
|
|
|
26
28
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
27
29
|
shimchecker "github.com/microsoft/typescript-go/shim/checker"
|
|
@@ -62,12 +64,23 @@ type FormatRule interface {
|
|
|
62
64
|
IsFormat() bool
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
// typeAwareRule marks rules that need a live TypeScript checker in Context.
|
|
68
|
+
// Rules that do not implement it are assumed AST-only.
|
|
69
|
+
type typeAwareRule interface {
|
|
70
|
+
NeedsTypeChecker() bool
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
// isFormatRule reports whether `r` opts into the format category.
|
|
66
74
|
func isFormatRule(r Rule) bool {
|
|
67
75
|
fr, ok := r.(FormatRule)
|
|
68
76
|
return ok && fr.IsFormat()
|
|
69
77
|
}
|
|
70
78
|
|
|
79
|
+
func ruleNeedsTypeChecker(r Rule) bool {
|
|
80
|
+
tr, ok := r.(typeAwareRule)
|
|
81
|
+
return ok && tr.NeedsTypeChecker()
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
// Context is the per-(file, rule) handle the engine passes to `Check`.
|
|
72
85
|
//
|
|
73
86
|
// `Options` is the raw JSON blob the user wrote in their rule
|
|
@@ -184,6 +197,10 @@ func (c *Context) ReportRangeFix(pos, end int, message string, edits ...TextEdit
|
|
|
184
197
|
})
|
|
185
198
|
}
|
|
186
199
|
|
|
200
|
+
// cloneTextEdits returns a shallow copy of `edits` so that the caller's
|
|
201
|
+
// variadic slice cannot be mutated through the stored Finding. Returns nil
|
|
202
|
+
// when the input is empty, keeping the Finding.Fix field nil rather than
|
|
203
|
+
// a zero-length slice.
|
|
187
204
|
func cloneTextEdits(edits []TextEdit) []TextEdit {
|
|
188
205
|
if len(edits) == 0 {
|
|
189
206
|
return nil
|
|
@@ -230,10 +247,32 @@ func AllRuleNames() []string {
|
|
|
230
247
|
// Engine binds a rule configuration to a Program and walks the AST once
|
|
231
248
|
// per source file, dispatching each visited node to its interested rules.
|
|
232
249
|
type Engine struct {
|
|
233
|
-
config
|
|
234
|
-
rules
|
|
235
|
-
enabled
|
|
236
|
-
unknown
|
|
250
|
+
config RuleResolver
|
|
251
|
+
rules map[shimast.Kind][]Rule
|
|
252
|
+
enabled map[string]Severity
|
|
253
|
+
unknown []string
|
|
254
|
+
needsTypeChecker bool
|
|
255
|
+
serial bool
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// SetSerial forces Engine.Run to walk files one at a time. The host calls
|
|
259
|
+
// this when `--singleThreaded` reaches the lint sidecar so the benchmark
|
|
260
|
+
// (and any caller that wants a deterministic, low-overhead pass) can opt
|
|
261
|
+
// out of file-level parallelism. Type-aware rule sets always run serial
|
|
262
|
+
// regardless of this flag — the single shared checker is not concurrent —
|
|
263
|
+
// so callers do not need to clear it themselves.
|
|
264
|
+
func (e *Engine) SetSerial(serial bool) {
|
|
265
|
+
if e == nil {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
e.serial = serial
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// runsSerial reports whether Run must walk files one at a time — either
|
|
272
|
+
// because the caller asked for it or because a type-aware rule pins the
|
|
273
|
+
// engine to the shared single checker.
|
|
274
|
+
func (e *Engine) runsSerial() bool {
|
|
275
|
+
return e == nil || e.serial || e.needsTypeChecker
|
|
237
276
|
}
|
|
238
277
|
|
|
239
278
|
// NewEngine returns an engine configured for `config`. Rules whose
|
|
@@ -262,6 +301,9 @@ func NewEngineWithResolver(config RuleResolver) *Engine {
|
|
|
262
301
|
eng.unknown = append(eng.unknown, name)
|
|
263
302
|
continue
|
|
264
303
|
}
|
|
304
|
+
if ruleNeedsTypeChecker(rule) {
|
|
305
|
+
eng.needsTypeChecker = true
|
|
306
|
+
}
|
|
265
307
|
eng.enabled[name] = displaySeverities.Severity(name)
|
|
266
308
|
// Dedup kinds per rule so a contributor that accidentally lists the
|
|
267
309
|
// same Kind twice in `Visits()` doesn't end up firing twice per node.
|
|
@@ -282,23 +324,76 @@ func NewEngineWithResolver(config RuleResolver) *Engine {
|
|
|
282
324
|
// have no registered implementation.
|
|
283
325
|
func (e *Engine) UnknownRules() []string { return e.unknown }
|
|
284
326
|
|
|
327
|
+
// NeedsTypeChecker reports whether any active rule requires Context.Checker.
|
|
328
|
+
func (e *Engine) NeedsTypeChecker() bool {
|
|
329
|
+
return e != nil && e.needsTypeChecker
|
|
330
|
+
}
|
|
331
|
+
|
|
285
332
|
// EnabledRules returns the active rule set keyed by name. Mostly for
|
|
286
333
|
// tests + introspection.
|
|
287
334
|
func (e *Engine) EnabledRules() map[string]Severity { return e.enabled }
|
|
288
335
|
|
|
289
|
-
// Run walks every non-declaration source file in the program and
|
|
290
|
-
//
|
|
336
|
+
// Run walks every non-declaration source file in the program and returns
|
|
337
|
+
// the collected findings. By default files are processed in parallel,
|
|
338
|
+
// bounded by `runtime.NumCPU()`; the engine falls back to a serial walk
|
|
339
|
+
// when SetSerial(true) was called or when a type-aware rule is active.
|
|
340
|
+
// Findings are merged in source-file order so the diagnostic stream is
|
|
341
|
+
// deterministic across runs even when the per-file work happens out of
|
|
342
|
+
// order.
|
|
291
343
|
func (e *Engine) Run(files []*shimast.SourceFile, checker *shimchecker.Checker) []*Finding {
|
|
292
|
-
|
|
293
|
-
|
|
344
|
+
if e.runsSerial() {
|
|
345
|
+
var findings []*Finding
|
|
346
|
+
for _, file := range files {
|
|
347
|
+
if file == nil || file.IsDeclarationFile {
|
|
348
|
+
continue
|
|
349
|
+
}
|
|
350
|
+
findings = append(findings, e.runFile(file, checker)...)
|
|
351
|
+
}
|
|
352
|
+
return findings
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
perFile := make([][]*Finding, len(files))
|
|
356
|
+
var wg sync.WaitGroup
|
|
357
|
+
workers := runtime.NumCPU()
|
|
358
|
+
if workers < 1 {
|
|
359
|
+
workers = 1
|
|
360
|
+
}
|
|
361
|
+
sem := make(chan struct{}, workers)
|
|
362
|
+
for i, file := range files {
|
|
294
363
|
if file == nil || file.IsDeclarationFile {
|
|
295
364
|
continue
|
|
296
365
|
}
|
|
297
|
-
|
|
366
|
+
wg.Add(1)
|
|
367
|
+
sem <- struct{}{}
|
|
368
|
+
go func(idx int, f *shimast.SourceFile) {
|
|
369
|
+
defer wg.Done()
|
|
370
|
+
defer func() { <-sem }()
|
|
371
|
+
perFile[idx] = e.runFile(f, checker)
|
|
372
|
+
}(i, file)
|
|
373
|
+
}
|
|
374
|
+
wg.Wait()
|
|
375
|
+
|
|
376
|
+
total := 0
|
|
377
|
+
for _, fs := range perFile {
|
|
378
|
+
total += len(fs)
|
|
379
|
+
}
|
|
380
|
+
if total == 0 {
|
|
381
|
+
return nil
|
|
382
|
+
}
|
|
383
|
+
findings := make([]*Finding, 0, total)
|
|
384
|
+
for _, fs := range perFile {
|
|
385
|
+
findings = append(findings, fs...)
|
|
298
386
|
}
|
|
299
387
|
return findings
|
|
300
388
|
}
|
|
301
389
|
|
|
390
|
+
// boundRule pairs an active rule with the Context the engine reuses for
|
|
391
|
+
// every node it dispatches to that rule within one file. See runFile.
|
|
392
|
+
type boundRule struct {
|
|
393
|
+
rule Rule
|
|
394
|
+
ctx *Context
|
|
395
|
+
}
|
|
396
|
+
|
|
302
397
|
// runFile is the per-file driver. The visitor is allocated once per file
|
|
303
398
|
// to keep the per-node hot path branch-free; it visits children
|
|
304
399
|
// post-order so parents see their already-checked subtrees.
|
|
@@ -310,29 +405,53 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
|
|
|
310
405
|
return collected
|
|
311
406
|
}
|
|
312
407
|
fileRules := resolved.Rules
|
|
408
|
+
if !hasEnabledFileRules(fileRules) {
|
|
409
|
+
return collected
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Bind every active rule to a Context once per file. A Context's fields
|
|
413
|
+
// — File, Checker, the file-resolved Severity, the rule's Options blob,
|
|
414
|
+
// and the format marker — are all invariant across the file's nodes, so
|
|
415
|
+
// the engine builds them here. The earlier shape allocated a fresh
|
|
416
|
+
// Context for every (node, rule) pair, which on a large program meant
|
|
417
|
+
// millions of short-lived heap allocations and the GC pressure they
|
|
418
|
+
// carry. Rules never mutate their Context, so reuse is safe.
|
|
419
|
+
byKind := make(map[shimast.Kind][]boundRule, len(e.rules))
|
|
420
|
+
ctxByRule := make(map[string]*Context, len(e.enabled))
|
|
421
|
+
for kind, rules := range e.rules {
|
|
422
|
+
for _, rule := range rules {
|
|
423
|
+
name := rule.Name()
|
|
424
|
+
ctx, built := ctxByRule[name]
|
|
425
|
+
if !built {
|
|
426
|
+
if severity := fileRules.Severity(name); severity != SeverityOff {
|
|
427
|
+
ctx = &Context{
|
|
428
|
+
File: file,
|
|
429
|
+
Checker: checker,
|
|
430
|
+
Severity: severity,
|
|
431
|
+
Options: e.config.RuleOptions(name),
|
|
432
|
+
rule: rule,
|
|
433
|
+
isFormat: isFormatRule(rule),
|
|
434
|
+
collect: collect,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// A nil entry memoizes "off for this file" so a rule registered
|
|
438
|
+
// for several kinds resolves its severity only once.
|
|
439
|
+
ctxByRule[name] = ctx
|
|
440
|
+
}
|
|
441
|
+
if ctx == nil {
|
|
442
|
+
continue
|
|
443
|
+
}
|
|
444
|
+
byKind[kind] = append(byKind[kind], boundRule{rule: rule, ctx: ctx})
|
|
445
|
+
}
|
|
446
|
+
}
|
|
313
447
|
|
|
314
448
|
var walk func(node *shimast.Node)
|
|
315
449
|
walk = func(node *shimast.Node) {
|
|
316
450
|
if node == nil {
|
|
317
451
|
return
|
|
318
452
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
severity := fileRules.Severity(rule.Name())
|
|
322
|
-
if severity == SeverityOff {
|
|
323
|
-
continue
|
|
324
|
-
}
|
|
325
|
-
ctx := &Context{
|
|
326
|
-
File: file,
|
|
327
|
-
Checker: checker,
|
|
328
|
-
Severity: severity,
|
|
329
|
-
Options: e.config.RuleOptions(rule.Name()),
|
|
330
|
-
rule: rule,
|
|
331
|
-
isFormat: isFormatRule(rule),
|
|
332
|
-
collect: collect,
|
|
333
|
-
}
|
|
334
|
-
runRuleCheck(rule, ctx, node, collect)
|
|
335
|
-
}
|
|
453
|
+
for _, bound := range byKind[node.Kind] {
|
|
454
|
+
runRuleCheck(bound.rule, bound.ctx, node, collect)
|
|
336
455
|
}
|
|
337
456
|
node.ForEachChild(func(child *shimast.Node) bool {
|
|
338
457
|
walk(child)
|
|
@@ -344,23 +463,8 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
|
|
|
344
463
|
// statements explicitly so the file node itself can be inspected by
|
|
345
464
|
// rules (e.g., `ban-ts-comment` reads CommentDirectives off the
|
|
346
465
|
// SourceFile).
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
severity := fileRules.Severity(rule.Name())
|
|
350
|
-
if severity == SeverityOff {
|
|
351
|
-
continue
|
|
352
|
-
}
|
|
353
|
-
ctx := &Context{
|
|
354
|
-
File: file,
|
|
355
|
-
Checker: checker,
|
|
356
|
-
Severity: severity,
|
|
357
|
-
Options: e.config.RuleOptions(rule.Name()),
|
|
358
|
-
rule: rule,
|
|
359
|
-
isFormat: isFormatRule(rule),
|
|
360
|
-
collect: collect,
|
|
361
|
-
}
|
|
362
|
-
runRuleCheck(rule, ctx, file.AsNode(), collect)
|
|
363
|
-
}
|
|
466
|
+
for _, bound := range byKind[shimast.KindSourceFile] {
|
|
467
|
+
runRuleCheck(bound.rule, bound.ctx, file.AsNode(), collect)
|
|
364
468
|
}
|
|
365
469
|
|
|
366
470
|
statements := file.Statements
|
|
@@ -377,6 +481,15 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
|
|
|
377
481
|
return filterInlineDisabledFindings(file, collected)
|
|
378
482
|
}
|
|
379
483
|
|
|
484
|
+
func hasEnabledFileRules(rules RuleConfig) bool {
|
|
485
|
+
for _, severity := range rules {
|
|
486
|
+
if severity != SeverityOff {
|
|
487
|
+
return true
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
|
|
380
493
|
// runRuleCheck invokes a rule's `Check` with a `recover()` barrier so a
|
|
381
494
|
// panicking rule does not abort the entire `ttsc fix` / `ttsc check`
|
|
382
495
|
// run. Built-in rules are not expected to panic, but contributor rules
|