@ttsc/lint 0.12.4 → 0.13.1

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.
Files changed (37) hide show
  1. package/lib/index.js +225 -135
  2. package/lib/index.js.map +1 -1
  3. package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
  4. package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
  5. package/linthost/ast_helpers.go +68 -16
  6. package/linthost/compile.go +117 -119
  7. package/linthost/config.go +518 -707
  8. package/linthost/config_format.go +16 -4
  9. package/linthost/contrib_adapter.go +7 -0
  10. package/linthost/directives.go +44 -0
  11. package/linthost/engine.go +152 -44
  12. package/linthost/fix.go +24 -33
  13. package/linthost/flags_gen.go +33 -0
  14. package/linthost/format.go +96 -3
  15. package/linthost/host.go +144 -8
  16. package/linthost/print_dispatch.go +121 -23
  17. package/linthost/print_doc.go +19 -0
  18. package/linthost/print_engine.go +168 -4
  19. package/linthost/print_nodes_array.go +17 -7
  20. package/linthost/print_nodes_call.go +129 -20
  21. package/linthost/print_nodes_function.go +353 -0
  22. package/linthost/print_nodes_imports.go +46 -29
  23. package/linthost/print_nodes_list.go +86 -5
  24. package/linthost/print_nodes_object.go +56 -11
  25. package/linthost/rules_escape.go +20 -3
  26. package/linthost/rules_format_print_width.go +267 -15
  27. package/linthost/rules_gap.go +55 -3
  28. package/linthost/rules_logic.go +64 -5
  29. package/linthost/rules_problems.go +65 -21
  30. package/linthost/rules_promise.go +3 -0
  31. package/linthost/rules_suggestions.go +160 -5
  32. package/linthost/rules_var.go +7 -1
  33. package/package.json +3 -3
  34. package/src/index.ts +243 -168
  35. package/src/structures/ITtscLintPluginConfig.ts +10 -83
  36. package/src/structures/TtscLintRuleOptions.ts +15 -0
  37. 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` (inline path) or
14
- // `parseExternalRuleMapInto` (flat-config path) without duplicating
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
- pwOpts := map[string]any{}
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
@@ -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
@@ -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 RuleResolver
238
- rules map[shimast.Kind][]Rule
239
- enabled map[string]Severity
240
- unknown []string
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,75 @@ 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
- // returns the collected findings.
336
+ // Run walks the source files supplied by the caller and returns the collected
337
+ // findings. By default files are processed in parallel, bounded by
338
+ // `runtime.NumCPU()`; the engine falls back to a serial walk when
339
+ // SetSerial(true) was called or when a type-aware rule is active. Findings are
340
+ // merged in source-file order so the diagnostic stream is deterministic across
341
+ // runs even when the per-file work happens out of order.
295
342
  func (e *Engine) Run(files []*shimast.SourceFile, checker *shimchecker.Checker) []*Finding {
296
- var findings []*Finding
297
- for _, file := range files {
298
- if file == nil || file.IsDeclarationFile {
343
+ if e.runsSerial() {
344
+ var findings []*Finding
345
+ for _, file := range files {
346
+ if file == nil {
347
+ continue
348
+ }
349
+ findings = append(findings, e.runFile(file, checker)...)
350
+ }
351
+ return findings
352
+ }
353
+
354
+ perFile := make([][]*Finding, len(files))
355
+ var wg sync.WaitGroup
356
+ workers := runtime.NumCPU()
357
+ if workers < 1 {
358
+ workers = 1
359
+ }
360
+ sem := make(chan struct{}, workers)
361
+ for i, file := range files {
362
+ if file == nil {
299
363
  continue
300
364
  }
301
- findings = append(findings, e.runFile(file, checker)...)
365
+ wg.Add(1)
366
+ sem <- struct{}{}
367
+ go func(idx int, f *shimast.SourceFile) {
368
+ defer wg.Done()
369
+ defer func() { <-sem }()
370
+ perFile[idx] = e.runFile(f, checker)
371
+ }(i, file)
372
+ }
373
+ wg.Wait()
374
+
375
+ total := 0
376
+ for _, fs := range perFile {
377
+ total += len(fs)
378
+ }
379
+ if total == 0 {
380
+ return nil
381
+ }
382
+ findings := make([]*Finding, 0, total)
383
+ for _, fs := range perFile {
384
+ findings = append(findings, fs...)
302
385
  }
303
386
  return findings
304
387
  }
305
388
 
389
+ // boundRule pairs an active rule with the Context the engine reuses for
390
+ // every node it dispatches to that rule within one file. See runFile.
391
+ type boundRule struct {
392
+ rule Rule
393
+ ctx *Context
394
+ }
395
+
306
396
  // runFile is the per-file driver. The visitor is allocated once per file
307
397
  // to keep the per-node hot path branch-free; it visits children
308
398
  // post-order so parents see their already-checked subtrees.
@@ -314,29 +404,53 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
314
404
  return collected
315
405
  }
316
406
  fileRules := resolved.Rules
407
+ if !hasEnabledFileRules(fileRules) {
408
+ return collected
409
+ }
410
+
411
+ // Bind every active rule to a Context once per file. A Context's fields
412
+ // — File, Checker, the file-resolved Severity, the rule's Options blob,
413
+ // and the format marker — are all invariant across the file's nodes, so
414
+ // the engine builds them here. The earlier shape allocated a fresh
415
+ // Context for every (node, rule) pair, which on a large program meant
416
+ // millions of short-lived heap allocations and the GC pressure they
417
+ // carry. Rules never mutate their Context, so reuse is safe.
418
+ byKind := make(map[shimast.Kind][]boundRule, len(e.rules))
419
+ ctxByRule := make(map[string]*Context, len(e.enabled))
420
+ for kind, rules := range e.rules {
421
+ for _, rule := range rules {
422
+ name := rule.Name()
423
+ ctx, built := ctxByRule[name]
424
+ if !built {
425
+ if severity := fileRules.Severity(name); severity != SeverityOff {
426
+ ctx = &Context{
427
+ File: file,
428
+ Checker: checker,
429
+ Severity: severity,
430
+ Options: e.config.RuleOptions(name),
431
+ rule: rule,
432
+ isFormat: isFormatRule(rule),
433
+ collect: collect,
434
+ }
435
+ }
436
+ // A nil entry memoizes "off for this file" so a rule registered
437
+ // for several kinds resolves its severity only once.
438
+ ctxByRule[name] = ctx
439
+ }
440
+ if ctx == nil {
441
+ continue
442
+ }
443
+ byKind[kind] = append(byKind[kind], boundRule{rule: rule, ctx: ctx})
444
+ }
445
+ }
317
446
 
318
447
  var walk func(node *shimast.Node)
319
448
  walk = func(node *shimast.Node) {
320
449
  if node == nil {
321
450
  return
322
451
  }
323
- if rules, ok := e.rules[node.Kind]; ok {
324
- for _, rule := range rules {
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
- }
452
+ for _, bound := range byKind[node.Kind] {
453
+ runRuleCheck(bound.rule, bound.ctx, node, collect)
340
454
  }
341
455
  node.ForEachChild(func(child *shimast.Node) bool {
342
456
  walk(child)
@@ -348,23 +462,8 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
348
462
  // statements explicitly so the file node itself can be inspected by
349
463
  // rules (e.g., `ban-ts-comment` reads CommentDirectives off the
350
464
  // SourceFile).
351
- if rules, ok := e.rules[shimast.KindSourceFile]; ok {
352
- for _, rule := range rules {
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
- }
465
+ for _, bound := range byKind[shimast.KindSourceFile] {
466
+ runRuleCheck(bound.rule, bound.ctx, file.AsNode(), collect)
368
467
  }
369
468
 
370
469
  statements := file.Statements
@@ -381,6 +480,15 @@ func (e *Engine) runFile(file *shimast.SourceFile, checker *shimchecker.Checker)
381
480
  return filterInlineDisabledFindings(file, collected)
382
481
  }
383
482
 
483
+ func hasEnabledFileRules(rules RuleConfig) bool {
484
+ for _, severity := range rules {
485
+ if severity != SeverityOff {
486
+ return true
487
+ }
488
+ }
489
+ return false
490
+ }
491
+
384
492
  // runRuleCheck invokes a rule's `Check` with a `recover()` barrier so a
385
493
  // panicking rule does not abort the entire `ttsc fix` / `ttsc check`
386
494
  // 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 applies ESLint runtime fixes first
4
- // (one pass, external process), then repeatedly runs the native lint
5
- // engine and applies any emitted TextEdit suggestions until no more
6
- // fixable findings remain or maxFixPasses is reached. After the cascade
7
- // settles, it runs a final diagnostic pass so remaining issues are
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 after the one-shot ESLint runtime
21
- // pass. Real-world cascades (no-var → prefer-const → eqeqeq …) settle in a
22
- // handful of passes; the cap exists so a buggy rule that re-reports its own
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
- engine := NewEngineWithResolver(rules)
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
- if !externalRan {
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: true,
137
- outDir: opts.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,33 @@
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
+ "diagnostics": false,
19
+ "emit": false,
20
+ "extendedDiagnostics": false,
21
+ "file": true,
22
+ "noEmit": false,
23
+ "out": true,
24
+ "outDir": true,
25
+ "p": true,
26
+ "plugins-json": true,
27
+ "project": true,
28
+ "quiet": false,
29
+ "singleThreaded": false,
30
+ "tsconfig": true,
31
+ "tsgo-args": true,
32
+ "verbose": false,
33
+ }