@ttsc/lint 0.12.4 → 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/index.js +227 -135
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
- package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
- package/linthost/ast_helpers.go +68 -16
- package/linthost/compile.go +91 -124
- package/linthost/config.go +518 -707
- package/linthost/config_format.go +16 -4
- package/linthost/contrib_adapter.go +7 -0
- package/linthost/directives.go +44 -0
- package/linthost/engine.go +152 -43
- package/linthost/fix.go +24 -33
- package/linthost/flags_gen.go +31 -0
- package/linthost/format.go +96 -3
- package/linthost/host.go +104 -5
- package/linthost/print_dispatch.go +121 -23
- package/linthost/print_doc.go +19 -0
- package/linthost/print_engine.go +168 -4
- package/linthost/print_nodes_array.go +17 -7
- package/linthost/print_nodes_call.go +129 -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_escape.go +20 -3
- package/linthost/rules_format_print_width.go +267 -15
- package/linthost/rules_gap.go +55 -3
- package/linthost/rules_logic.go +64 -5
- package/linthost/rules_problems.go +65 -21
- package/linthost/rules_promise.go +3 -0
- package/linthost/rules_suggestions.go +160 -5
- package/linthost/rules_var.go +1 -1
- package/package.json +3 -3
- package/src/index.ts +245 -168
- package/src/structures/ITtscLintPluginConfig.ts +10 -83
- package/src/structures/TtscLintRuleOptions.ts +15 -0
- package/linthost/eslint_runtime.go +0 -351
|
@@ -10,9 +10,8 @@ import (
|
|
|
10
10
|
// The result is a `map[string]any` that mirrors what a user would
|
|
11
11
|
// have written under `rules` directly — entries like
|
|
12
12
|
// `"format/semi": ["off", {"prefer": "always"}]` — so the caller
|
|
13
|
-
// can route it through either `ParseRulesWithOptions`
|
|
14
|
-
// `parseExternalRuleMapInto`
|
|
15
|
-
// option-decoding logic.
|
|
13
|
+
// can route it through either `ParseRulesWithOptions` or
|
|
14
|
+
// `parseExternalRuleMapInto` without duplicating option-decoding logic.
|
|
16
15
|
//
|
|
17
16
|
// The block's default severity is off. That keeps check/build diagnostics
|
|
18
17
|
// independent from formatting policy unless the user explicitly sets
|
|
@@ -91,12 +90,22 @@ func expandFormatBlock(raw map[string]any) (map[string]any, error) {
|
|
|
91
90
|
out["format/trailing-comma"] = ruleEntry(map[string]any{"mode": tcMode})
|
|
92
91
|
|
|
93
92
|
// format/print-width
|
|
94
|
-
|
|
93
|
+
//
|
|
94
|
+
// `trailingComma` is mirrored into the print-width rule's options so
|
|
95
|
+
// the printer's broken-list reflow emits the same trailing-comma
|
|
96
|
+
// shape `format/trailing-comma` does. Without the mirror the two
|
|
97
|
+
// rules disagree on `es5` / `none` projects and oscillate on every
|
|
98
|
+
// cascade pass — the trailing-comma rule says "no comma" while the
|
|
99
|
+
// printer adds one back. See `printArgList` in print_nodes_call.go.
|
|
100
|
+
pwOpts := map[string]any{"trailingComma": tcMode}
|
|
95
101
|
if v, ok := raw["printWidth"]; ok {
|
|
96
102
|
n, err := asInt("format.printWidth", v)
|
|
97
103
|
if err != nil {
|
|
98
104
|
return nil, err
|
|
99
105
|
}
|
|
106
|
+
if n < 1 {
|
|
107
|
+
return nil, fmt.Errorf("@ttsc/lint: format.printWidth must be a positive integer; got %d", n)
|
|
108
|
+
}
|
|
100
109
|
pwOpts["printWidth"] = n
|
|
101
110
|
}
|
|
102
111
|
if v, ok := raw["tabWidth"]; ok {
|
|
@@ -104,6 +113,9 @@ func expandFormatBlock(raw map[string]any) (map[string]any, error) {
|
|
|
104
113
|
if err != nil {
|
|
105
114
|
return nil, err
|
|
106
115
|
}
|
|
116
|
+
if n < 1 {
|
|
117
|
+
return nil, fmt.Errorf("@ttsc/lint: format.tabWidth must be a positive integer; got %d", n)
|
|
118
|
+
}
|
|
107
119
|
pwOpts["tabWidth"] = n
|
|
108
120
|
}
|
|
109
121
|
if v, ok := raw["useTabs"]; ok {
|
|
@@ -68,6 +68,13 @@ type contributorAdapter struct {
|
|
|
68
68
|
inner rule.Rule
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// NeedsTypeChecker keeps contributor rules on the historical checker path.
|
|
72
|
+
// The public rule.Context exposes Checker and has no mandatory marker, so the
|
|
73
|
+
// host cannot safely infer that a third-party rule is AST-only.
|
|
74
|
+
func (a contributorAdapter) NeedsTypeChecker() bool {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
// formatContributorAdapter is the FormatRule-tagged variant of
|
|
72
79
|
// contributorAdapter. Wrapping the lint-only adapter (rather than
|
|
73
80
|
// duplicating its method set) keeps the marker addition trivial and
|
package/linthost/directives.go
CHANGED
|
@@ -95,6 +95,18 @@ func filterInlineDisabledFindings(file *shimast.SourceFile, findings []*Finding)
|
|
|
95
95
|
// parseLintInlineDirectives scans all comment tokens in `file` for
|
|
96
96
|
// recognized directive markers and returns a structured summary of the
|
|
97
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.
|
|
98
110
|
func parseLintInlineDirectives(file *shimast.SourceFile) *lintInlineDirectives {
|
|
99
111
|
directives := &lintInlineDirectives{
|
|
100
112
|
lines: make(map[int][]lintDirectiveRules),
|
|
@@ -103,12 +115,44 @@ func parseLintInlineDirectives(file *shimast.SourceFile) *lintInlineDirectives {
|
|
|
103
115
|
scanner.SetText(file.Text())
|
|
104
116
|
scanner.SetSkipTrivia(false)
|
|
105
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
|
+
|
|
106
123
|
scan:
|
|
107
124
|
for {
|
|
108
125
|
kind := scanner.Scan()
|
|
109
126
|
switch kind {
|
|
110
127
|
case shimast.KindEndOfFile:
|
|
111
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
|
|
112
156
|
case shimast.KindSingleLineCommentTrivia, shimast.KindMultiLineCommentTrivia:
|
|
113
157
|
default:
|
|
114
158
|
continue
|
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
|
|
@@ -234,10 +247,32 @@ func AllRuleNames() []string {
|
|
|
234
247
|
// Engine binds a rule configuration to a Program and walks the AST once
|
|
235
248
|
// per source file, dispatching each visited node to its interested rules.
|
|
236
249
|
type Engine struct {
|
|
237
|
-
config
|
|
238
|
-
rules
|
|
239
|
-
enabled
|
|
240
|
-
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
|
|
241
276
|
}
|
|
242
277
|
|
|
243
278
|
// NewEngine returns an engine configured for `config`. Rules whose
|
|
@@ -266,6 +301,9 @@ func NewEngineWithResolver(config RuleResolver) *Engine {
|
|
|
266
301
|
eng.unknown = append(eng.unknown, name)
|
|
267
302
|
continue
|
|
268
303
|
}
|
|
304
|
+
if ruleNeedsTypeChecker(rule) {
|
|
305
|
+
eng.needsTypeChecker = true
|
|
306
|
+
}
|
|
269
307
|
eng.enabled[name] = displaySeverities.Severity(name)
|
|
270
308
|
// Dedup kinds per rule so a contributor that accidentally lists the
|
|
271
309
|
// same Kind twice in `Visits()` doesn't end up firing twice per node.
|
|
@@ -286,23 +324,76 @@ func NewEngineWithResolver(config RuleResolver) *Engine {
|
|
|
286
324
|
// have no registered implementation.
|
|
287
325
|
func (e *Engine) UnknownRules() []string { return e.unknown }
|
|
288
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
|
+
|
|
289
332
|
// EnabledRules returns the active rule set keyed by name. Mostly for
|
|
290
333
|
// tests + introspection.
|
|
291
334
|
func (e *Engine) EnabledRules() map[string]Severity { return e.enabled }
|
|
292
335
|
|
|
293
|
-
// Run walks every non-declaration source file in the program and
|
|
294
|
-
//
|
|
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.
|
|
295
343
|
func (e *Engine) Run(files []*shimast.SourceFile, checker *shimchecker.Checker) []*Finding {
|
|
296
|
-
|
|
297
|
-
|
|
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 {
|
|
298
363
|
if file == nil || file.IsDeclarationFile {
|
|
299
364
|
continue
|
|
300
365
|
}
|
|
301
|
-
|
|
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...)
|
|
302
386
|
}
|
|
303
387
|
return findings
|
|
304
388
|
}
|
|
305
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
|
+
|
|
306
397
|
// runFile is the per-file driver. The visitor is allocated once per file
|
|
307
398
|
// to keep the per-node hot path branch-free; it visits children
|
|
308
399
|
// post-order so parents see their already-checked subtrees.
|
|
@@ -314,29 +405,53 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
|
|
|
314
405
|
return collected
|
|
315
406
|
}
|
|
316
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
|
+
}
|
|
317
447
|
|
|
318
448
|
var walk func(node *shimast.Node)
|
|
319
449
|
walk = func(node *shimast.Node) {
|
|
320
450
|
if node == nil {
|
|
321
451
|
return
|
|
322
452
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
severity := fileRules.Severity(rule.Name())
|
|
326
|
-
if severity == SeverityOff {
|
|
327
|
-
continue
|
|
328
|
-
}
|
|
329
|
-
ctx := &Context{
|
|
330
|
-
File: file,
|
|
331
|
-
Checker: checker,
|
|
332
|
-
Severity: severity,
|
|
333
|
-
Options: e.config.RuleOptions(rule.Name()),
|
|
334
|
-
rule: rule,
|
|
335
|
-
isFormat: isFormatRule(rule),
|
|
336
|
-
collect: collect,
|
|
337
|
-
}
|
|
338
|
-
runRuleCheck(rule, ctx, node, collect)
|
|
339
|
-
}
|
|
453
|
+
for _, bound := range byKind[node.Kind] {
|
|
454
|
+
runRuleCheck(bound.rule, bound.ctx, node, collect)
|
|
340
455
|
}
|
|
341
456
|
node.ForEachChild(func(child *shimast.Node) bool {
|
|
342
457
|
walk(child)
|
|
@@ -348,23 +463,8 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
|
|
|
348
463
|
// statements explicitly so the file node itself can be inspected by
|
|
349
464
|
// rules (e.g., `ban-ts-comment` reads CommentDirectives off the
|
|
350
465
|
// SourceFile).
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
severity := fileRules.Severity(rule.Name())
|
|
354
|
-
if severity == SeverityOff {
|
|
355
|
-
continue
|
|
356
|
-
}
|
|
357
|
-
ctx := &Context{
|
|
358
|
-
File: file,
|
|
359
|
-
Checker: checker,
|
|
360
|
-
Severity: severity,
|
|
361
|
-
Options: e.config.RuleOptions(rule.Name()),
|
|
362
|
-
rule: rule,
|
|
363
|
-
isFormat: isFormatRule(rule),
|
|
364
|
-
collect: collect,
|
|
365
|
-
}
|
|
366
|
-
runRuleCheck(rule, ctx, file.AsNode(), collect)
|
|
367
|
-
}
|
|
466
|
+
for _, bound := range byKind[shimast.KindSourceFile] {
|
|
467
|
+
runRuleCheck(bound.rule, bound.ctx, file.AsNode(), collect)
|
|
368
468
|
}
|
|
369
469
|
|
|
370
470
|
statements := file.Statements
|
|
@@ -381,6 +481,15 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
|
|
|
381
481
|
return filterInlineDisabledFindings(file, collected)
|
|
382
482
|
}
|
|
383
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
|
+
|
|
384
493
|
// runRuleCheck invokes a rule's `Check` with a `recover()` barrier so a
|
|
385
494
|
// panicking rule does not abort the entire `ttsc fix` / `ttsc check`
|
|
386
495
|
// run. Built-in rules are not expected to panic, but contributor rules
|
package/linthost/fix.go
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// Autofix orchestration for the `@ttsc/lint fix` subcommand.
|
|
2
2
|
//
|
|
3
|
-
// RunFix drives the fix cascade: it
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// surfaced in the normal error stream.
|
|
3
|
+
// RunFix drives the fix cascade: it repeatedly runs the native lint engine
|
|
4
|
+
// and applies any emitted TextEdit suggestions until no more fixable
|
|
5
|
+
// findings remain or maxFixPasses is reached. After the cascade settles, it
|
|
6
|
+
// runs a final diagnostic pass so remaining issues are surfaced in the
|
|
7
|
+
// normal error stream.
|
|
9
8
|
package linthost
|
|
10
9
|
|
|
11
10
|
import (
|
|
@@ -17,10 +16,9 @@ import (
|
|
|
17
16
|
shimdw "github.com/microsoft/typescript-go/shim/diagnosticwriter"
|
|
18
17
|
)
|
|
19
18
|
|
|
20
|
-
// maxFixPasses bounds the native cascade
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// edit cannot loop forever.
|
|
19
|
+
// maxFixPasses bounds the native fix cascade. Real-world cascades (no-var →
|
|
20
|
+
// prefer-const → eqeqeq …) settle in a handful of passes; the cap exists so
|
|
21
|
+
// a buggy rule that re-reports its own edit cannot loop forever.
|
|
24
22
|
const maxFixPasses = 10
|
|
25
23
|
|
|
26
24
|
// RunFix implements `@ttsc/lint fix` — apply autofixes, then report any
|
|
@@ -45,8 +43,11 @@ func runFix(opts *subcommandOpts) int {
|
|
|
45
43
|
fmt.Fprintln(os.Stderr, err)
|
|
46
44
|
return 2
|
|
47
45
|
}
|
|
46
|
+
engine := NewEngineWithResolver(rules)
|
|
47
|
+
engine.SetSerial(opts.singleThreaded)
|
|
48
|
+
needsRuleChecker := engine.NeedsTypeChecker()
|
|
48
49
|
|
|
49
|
-
prog, code := loadFixProgram(opts)
|
|
50
|
+
prog, code := loadFixProgram(opts, needsRuleChecker)
|
|
50
51
|
if code != 0 {
|
|
51
52
|
return code
|
|
52
53
|
}
|
|
@@ -57,16 +58,6 @@ func runFix(opts *subcommandOpts) int {
|
|
|
57
58
|
}()
|
|
58
59
|
|
|
59
60
|
totalFixes := 0
|
|
60
|
-
if fixed, err := runExternalESLintFixes(rules, opts.cwd, prog.userSourceFiles()); err != nil {
|
|
61
|
-
fmt.Fprintln(os.Stderr, err)
|
|
62
|
-
return 2
|
|
63
|
-
} else if fixed > 0 {
|
|
64
|
-
totalFixes += fixed
|
|
65
|
-
prog, code = reloadFixProgram(prog, opts)
|
|
66
|
-
if code != 0 {
|
|
67
|
-
return code
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
61
|
|
|
71
62
|
// `ttsc fix` applies edits from BOTH lint-class rules and
|
|
72
63
|
// format-class rules. The dual `ttsc format` subcommand exists for
|
|
@@ -75,7 +66,6 @@ func runFix(opts *subcommandOpts) int {
|
|
|
75
66
|
// kinds of findings in one pass — no filtering needed here.
|
|
76
67
|
cascadeConverged := false
|
|
77
68
|
for pass := 0; pass < maxFixPasses; pass++ {
|
|
78
|
-
engine := NewEngineWithResolver(rules)
|
|
79
69
|
findings := engine.Run(prog.userSourceFiles(), prog.checker)
|
|
80
70
|
fixed, err := applyFindingFixes(opts.cwd, findings)
|
|
81
71
|
if err != nil {
|
|
@@ -87,7 +77,7 @@ func runFix(opts *subcommandOpts) int {
|
|
|
87
77
|
break
|
|
88
78
|
}
|
|
89
79
|
totalFixes += fixed
|
|
90
|
-
prog, code = reloadFixProgram(prog, opts)
|
|
80
|
+
prog, code = reloadFixProgram(prog, opts, needsRuleChecker)
|
|
91
81
|
if code != 0 {
|
|
92
82
|
return code
|
|
93
83
|
}
|
|
@@ -104,15 +94,12 @@ func runFix(opts *subcommandOpts) int {
|
|
|
104
94
|
maxFixPasses)
|
|
105
95
|
}
|
|
106
96
|
|
|
107
|
-
|
|
108
|
-
astDiags, lintDiags, externalRan, err := collectDiagnostics(prog, engine)
|
|
97
|
+
astDiags, lintDiags, err := collectDiagnostics(prog, engine)
|
|
109
98
|
if err != nil {
|
|
110
99
|
fmt.Fprintln(os.Stderr, err)
|
|
111
100
|
return 2
|
|
112
101
|
}
|
|
113
|
-
|
|
114
|
-
warnUnknownRules(os.Stderr, engine.UnknownRules())
|
|
115
|
-
}
|
|
102
|
+
warnUnknownRules(os.Stderr, engine.UnknownRules())
|
|
116
103
|
errCount := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, opts.cwd)
|
|
117
104
|
if errCount > 0 {
|
|
118
105
|
return 2
|
|
@@ -131,10 +118,14 @@ func runFix(opts *subcommandOpts) int {
|
|
|
131
118
|
|
|
132
119
|
// loadFixProgram loads the TypeScript program for a fix/format pass with
|
|
133
120
|
// NoEmit forced on. Returns (nil, 2) when loading or config parsing fails.
|
|
134
|
-
func loadFixProgram(opts *subcommandOpts) (*program, int) {
|
|
121
|
+
func loadFixProgram(opts *subcommandOpts, needsRuleChecker bool) (*program, int) {
|
|
135
122
|
prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
|
|
136
|
-
forceNoEmit:
|
|
137
|
-
outDir:
|
|
123
|
+
forceNoEmit: true,
|
|
124
|
+
outDir: opts.outDir,
|
|
125
|
+
needsRuleChecker: needsRuleChecker,
|
|
126
|
+
singleThreaded: opts.singleThreaded,
|
|
127
|
+
checkers: opts.checkers,
|
|
128
|
+
tsgoArgs: opts.tsgoArgs,
|
|
138
129
|
})
|
|
139
130
|
if err != nil {
|
|
140
131
|
fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
|
|
@@ -150,11 +141,11 @@ func loadFixProgram(opts *subcommandOpts) (*program, int) {
|
|
|
150
141
|
// reloadFixProgram closes `current` and loads a fresh program from disk.
|
|
151
142
|
// Used between cascade passes so the engine sees edits applied in the
|
|
152
143
|
// previous pass rather than stale in-memory AST nodes.
|
|
153
|
-
func reloadFixProgram(current *program, opts *subcommandOpts) (*program, int) {
|
|
144
|
+
func reloadFixProgram(current *program, opts *subcommandOpts, needsRuleChecker bool) (*program, int) {
|
|
154
145
|
if current != nil {
|
|
155
146
|
current.close()
|
|
156
147
|
}
|
|
157
|
-
return loadFixProgram(opts)
|
|
148
|
+
return loadFixProgram(opts, needsRuleChecker)
|
|
158
149
|
}
|
|
159
150
|
|
|
160
151
|
// fileFixes groups all pending TextEdit suggestions for a single file.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Code generated by packages/ttsc/scripts/gen-flags.mts. DO NOT EDIT.
|
|
2
|
+
//
|
|
3
|
+
// Source of truth: packages/ttsc/src/flags/schema.ts.
|
|
4
|
+
// Regenerate with: pnpm format
|
|
5
|
+
// Verify in CI with: node packages/ttsc/scripts/check-flags.cjs
|
|
6
|
+
|
|
7
|
+
package linthost
|
|
8
|
+
|
|
9
|
+
// LintFlagAllowList is the allow-list of CLI flags this Go layer accepts. The map's
|
|
10
|
+
// value is true when the flag carries a separate value token (--flag VALUE
|
|
11
|
+
// or --flag=VALUE) and false when the flag is boolean (--flag).
|
|
12
|
+
//
|
|
13
|
+
// Generated from packages/ttsc/src/flags/schema.ts. Edit the schema, not
|
|
14
|
+
// this file.
|
|
15
|
+
var LintFlagAllowList = map[string]bool{
|
|
16
|
+
"checkers": true,
|
|
17
|
+
"cwd": true,
|
|
18
|
+
"emit": false,
|
|
19
|
+
"file": true,
|
|
20
|
+
"noEmit": false,
|
|
21
|
+
"out": true,
|
|
22
|
+
"outDir": true,
|
|
23
|
+
"p": true,
|
|
24
|
+
"plugins-json": true,
|
|
25
|
+
"project": true,
|
|
26
|
+
"quiet": false,
|
|
27
|
+
"singleThreaded": false,
|
|
28
|
+
"tsconfig": true,
|
|
29
|
+
"tsgo-args": true,
|
|
30
|
+
"verbose": false,
|
|
31
|
+
}
|