@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.
Files changed (37) hide show
  1. package/lib/index.js +227 -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 +91 -124
  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 -43
  12. package/linthost/fix.go +24 -33
  13. package/linthost/flags_gen.go +31 -0
  14. package/linthost/format.go +96 -3
  15. package/linthost/host.go +104 -5
  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 +1 -1
  33. package/package.json +3 -3
  34. package/src/index.ts +245 -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
@@ -89,6 +89,20 @@ export interface ITtscLintPrintWidthRuleOptions {
89
89
  * @default "lf"
90
90
  */
91
91
  endOfLine?: "lf" | "crlf";
92
+ /**
93
+ * Trailing-comma policy the reflow honors when it breaks a list across
94
+ * lines. Mirrors prettier's `trailingComma` and must match the
95
+ * `format/trailing-comma` rule's `mode`; otherwise the two rules
96
+ * disagree on every cascade pass and oscillate against each other.
97
+ *
98
+ * When a `format` block is configured, `format.trailingComma` is mirrored
99
+ * into this option automatically. Set it directly only when overriding
100
+ * the print-width rule via a `rules` tuple — see the conflict-resolution
101
+ * notes in the README.
102
+ *
103
+ * @default "all"
104
+ */
105
+ trailingComma?: "all" | "es5" | "none";
92
106
  }
93
107
  /** `format/jsdoc` rule options. */
94
108
  export interface ITtscLintJsdocRuleOptions {
@@ -280,31 +280,83 @@ func walkDescendants(node *shimast.Node, visit func(*shimast.Node)) {
280
280
  })
281
281
  }
282
282
 
283
- // bindingIdentifierNames collects the declared identifier names from a
284
- // binding pattern. For a simple Identifier node it returns a single-element
285
- // slice. For ObjectBindingPattern and ArrayBindingPattern it walks the
286
- // descendants and returns all embedded Identifier texts. Returns nil for
287
- // unrecognized node kinds.
288
- func bindingIdentifierNames(node *shimast.Node) []string {
283
+ // assignmentTargetNames collects the identifier names written by an
284
+ // assignment's left-hand side. A bare Identifier yields one name. A
285
+ // destructuring-assignment target parsed as an ArrayLiteralExpression
286
+ // (`[a, b] = …`) or ObjectLiteralExpression (`({a} = …)`) rather than a
287
+ // binding pattern — is walked so every nested write position is counted:
288
+ // array elements, object property values, shorthand properties, defaults
289
+ // (`[a = 1]`, `{a = 1}`), nested patterns, and rest elements.
290
+ //
291
+ // Property names in `{key: target}` are read positions, not writes, so
292
+ // only the property value contributes; member-access targets (`obj.x`)
293
+ // declare no local binding and are skipped. Returns nil for other shapes.
294
+ func assignmentTargetNames(node *shimast.Node) []string {
289
295
  if node == nil {
290
296
  return nil
291
297
  }
292
298
  if name := identifierText(node); name != "" {
293
299
  return []string{name}
294
300
  }
295
- if node.Kind != shimast.KindObjectBindingPattern && node.Kind != shimast.KindArrayBindingPattern {
296
- return nil
297
- }
298
301
  var names []string
299
- walkDescendants(node, func(child *shimast.Node) {
300
- if child == node {
301
- return
302
+ collectAssignmentTargetNames(node, &names)
303
+ return names
304
+ }
305
+
306
+ // collectAssignmentTargetNames appends to `names` every identifier in a
307
+ // destructuring-assignment target. It descends only through write-target
308
+ // positions so reads (object property keys, computed-member expressions)
309
+ // never count as reassignments.
310
+ func collectAssignmentTargetNames(node *shimast.Node, names *[]string) {
311
+ if node == nil {
312
+ return
313
+ }
314
+ switch node.Kind {
315
+ case shimast.KindIdentifier:
316
+ if name := identifierText(node); name != "" {
317
+ *names = append(*names, name)
302
318
  }
303
- if name := identifierText(child); name != "" {
304
- names = append(names, name)
319
+ case shimast.KindParenthesizedExpression:
320
+ collectAssignmentTargetNames(stripParens(node), names)
321
+ case shimast.KindArrayLiteralExpression:
322
+ if arr := node.AsArrayLiteralExpression(); arr != nil && arr.Elements != nil {
323
+ for _, el := range arr.Elements.Nodes {
324
+ collectAssignmentTargetNames(el, names)
325
+ }
305
326
  }
306
- })
307
- return names
327
+ case shimast.KindObjectLiteralExpression:
328
+ if obj := node.AsObjectLiteralExpression(); obj != nil && obj.Properties != nil {
329
+ for _, prop := range obj.Properties.Nodes {
330
+ collectAssignmentTargetNames(prop, names)
331
+ }
332
+ }
333
+ case shimast.KindSpreadElement:
334
+ if spread := node.AsSpreadElement(); spread != nil {
335
+ collectAssignmentTargetNames(spread.Expression, names)
336
+ }
337
+ case shimast.KindSpreadAssignment:
338
+ if spread := node.AsSpreadAssignment(); spread != nil {
339
+ collectAssignmentTargetNames(spread.Expression, names)
340
+ }
341
+ case shimast.KindShorthandPropertyAssignment:
342
+ // `{a}` and `{a = 1}` — the property name is the write target; any
343
+ // ObjectAssignmentInitializer is a default value, not a target.
344
+ if short := node.AsShorthandPropertyAssignment(); short != nil {
345
+ collectAssignmentTargetNames(short.Name(), names)
346
+ }
347
+ case shimast.KindPropertyAssignment:
348
+ // `{key: target}` — only the value (initializer) is written to.
349
+ if assignment := node.AsPropertyAssignment(); assignment != nil {
350
+ collectAssignmentTargetNames(assignment.Initializer, names)
351
+ }
352
+ case shimast.KindBinaryExpression:
353
+ // A default inside a pattern (`[a = 1]`, `{key: a = 1}`) parses as an
354
+ // `=` BinaryExpression; only its left side is the write target.
355
+ if expr := node.AsBinaryExpression(); expr != nil &&
356
+ expr.OperatorToken != nil && expr.OperatorToken.Kind == shimast.KindEqualsToken {
357
+ collectAssignmentTargetNames(expr.Left, names)
358
+ }
359
+ }
308
360
  }
309
361
 
310
362
  // isLiteralLike reports whether `node` (after stripping parentheses) is a
@@ -13,6 +13,7 @@ package linthost
13
13
 
14
14
  import (
15
15
  "context"
16
+ "encoding/json"
16
17
  "errors"
17
18
  "flag"
18
19
  "fmt"
@@ -59,26 +60,40 @@ func RunTransform(args []string) int {
59
60
  tsconfig := fs.String("tsconfig", "tsconfig.json", "tsconfig owning --file")
60
61
  cwd := fs.String("cwd", "", "override the working directory")
61
62
  pluginsJSON := fs.String("plugins-json", "", "ttsc plugin manifest JSON")
62
- if err := fs.Parse(filterKnownFlags(args, map[string]bool{
63
- "cwd": true,
64
- "file": true,
65
- "out": true,
66
- "plugins-json": true,
67
- "tsconfig": true,
68
- })); err != nil {
63
+ singleThreaded := fs.Bool("singleThreaded", false, "run TypeScript-Go single-threaded")
64
+ checkers := fs.Int("checkers", 0, "type-checker pool size (0 = TypeScript-Go default)")
65
+ tsgoArgsRaw := fs.String("tsgo-args", "", "JSON array of forwarded tsgo CLI flags")
66
+ if err := fs.Parse(filterKnownFlags(args, LintFlagAllowList)); err != nil {
69
67
  return 2
70
68
  }
71
69
  if *file == "" {
72
70
  fmt.Fprintln(os.Stderr, "@ttsc/lint transform: --file is required")
73
71
  return 2
74
72
  }
73
+ tsgoArgs, err := decodeTsgoArgs(*tsgoArgsRaw)
74
+ if err != nil {
75
+ fmt.Fprintln(os.Stderr, err)
76
+ return 2
77
+ }
75
78
  resolvedCwd, err := resolveCwd(*cwd)
76
79
  if err != nil {
77
80
  fmt.Fprintln(os.Stderr, err)
78
81
  return 2
79
82
  }
83
+ rules, err := loadRules(*pluginsJSON, resolvedCwd, *tsconfig)
84
+ if err != nil {
85
+ fmt.Fprintln(os.Stderr, err)
86
+ return 2
87
+ }
88
+ engine := NewEngineWithResolver(rules)
89
+ engine.SetSerial(*singleThreaded)
90
+
80
91
  prog, parseDiags, err := loadProgram(resolvedCwd, *tsconfig, loadProgramOptions{
81
- forceEmit: true,
92
+ forceEmit: true,
93
+ needsRuleChecker: engine.NeedsTypeChecker(),
94
+ singleThreaded: *singleThreaded,
95
+ checkers: *checkers,
96
+ tsgoArgs: tsgoArgs,
82
97
  })
83
98
  if err != nil {
84
99
  fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
@@ -90,21 +105,12 @@ func RunTransform(args []string) int {
90
105
  }
91
106
  defer prog.close()
92
107
 
93
- rules, err := loadRules(*pluginsJSON, resolvedCwd, *tsconfig)
94
- if err != nil {
95
- fmt.Fprintln(os.Stderr, err)
96
- return 2
97
- }
98
- engine := NewEngineWithResolver(rules)
99
-
100
- astDiags, lintDiags, externalRan, err := collectDiagnostics(prog, engine)
108
+ astDiags, lintDiags, err := collectDiagnostics(prog, engine)
101
109
  if err != nil {
102
110
  fmt.Fprintln(os.Stderr, err)
103
111
  return 2
104
112
  }
105
- if !externalRan {
106
- warnUnknownRules(os.Stderr, engine.UnknownRules())
107
- }
113
+ warnUnknownRules(os.Stderr, engine.UnknownRules())
108
114
  if errors := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, resolvedCwd); errors > 0 {
109
115
  return 2
110
116
  }
@@ -158,14 +164,17 @@ func RunTransform(args []string) int {
158
164
  }
159
165
 
160
166
  type subcommandOpts struct {
161
- cwd string
162
- tsconfig string
163
- pluginsJSON string
164
- emit bool
165
- noEmit bool
166
- quiet bool
167
- verbose bool
168
- outDir string
167
+ cwd string
168
+ tsconfig string
169
+ pluginsJSON string
170
+ emit bool
171
+ noEmit bool
172
+ quiet bool
173
+ verbose bool
174
+ outDir string
175
+ singleThreaded bool
176
+ checkers int
177
+ tsgoArgs []string
169
178
  }
170
179
 
171
180
  // parseSubcommandFlags parses the shared flag set used by the `check`,
@@ -182,45 +191,72 @@ func parseSubcommandFlags(name string, args []string) (*subcommandOpts, error) {
182
191
  quiet := fs.Bool("quiet", false, "")
183
192
  verbose := fs.Bool("verbose", false, "")
184
193
  outDir := fs.String("outDir", "", "")
185
- if err := fs.Parse(filterKnownFlags(args, map[string]bool{
186
- "cwd": true,
187
- "emit": false,
188
- "noEmit": false,
189
- "outDir": true,
190
- "plugins-json": true,
191
- "quiet": false,
192
- "tsconfig": true,
193
- "verbose": false,
194
- })); err != nil {
194
+ singleThreaded := fs.Bool("singleThreaded", false, "")
195
+ checkers := fs.Int("checkers", 0, "")
196
+ tsgoArgsRaw := fs.String("tsgo-args", "", "")
197
+ if err := fs.Parse(filterKnownFlags(args, LintFlagAllowList)); err != nil {
195
198
  return nil, err
196
199
  }
197
200
  if *emit && *noEmit {
198
201
  return nil, errors.New("@ttsc/lint: --emit and --noEmit are mutually exclusive")
199
202
  }
203
+ tsgoArgs, err := decodeTsgoArgs(*tsgoArgsRaw)
204
+ if err != nil {
205
+ return nil, err
206
+ }
200
207
  resolvedCwd, err := resolveCwd(*cwd)
201
208
  if err != nil {
202
209
  return nil, err
203
210
  }
204
211
  return &subcommandOpts{
205
- cwd: resolvedCwd,
206
- tsconfig: *tsconfig,
207
- pluginsJSON: *pluginsJSON,
208
- emit: *emit,
209
- noEmit: *noEmit,
210
- quiet: *quiet,
211
- verbose: *verbose,
212
- outDir: *outDir,
212
+ cwd: resolvedCwd,
213
+ tsconfig: *tsconfig,
214
+ pluginsJSON: *pluginsJSON,
215
+ emit: *emit,
216
+ noEmit: *noEmit,
217
+ quiet: *quiet,
218
+ verbose: *verbose,
219
+ outDir: *outDir,
220
+ singleThreaded: *singleThreaded,
221
+ checkers: *checkers,
222
+ tsgoArgs: tsgoArgs,
213
223
  }, nil
214
224
  }
215
225
 
226
+ // decodeTsgoArgs decodes the JSON-array value of the `--tsgo-args` flag — the
227
+ // tsgo CLI flags the `ttsc` launcher forwarded — into a string slice. An empty
228
+ // flag yields a nil slice.
229
+ func decodeTsgoArgs(raw string) ([]string, error) {
230
+ if raw == "" {
231
+ return nil, nil
232
+ }
233
+ var args []string
234
+ if err := json.Unmarshal([]byte(raw), &args); err != nil {
235
+ return nil, fmt.Errorf("@ttsc/lint: invalid --tsgo-args: %w", err)
236
+ }
237
+ return args, nil
238
+ }
239
+
216
240
  // runProject is the shared body of RunCheck and RunBuild. It loads the
217
241
  // program, collects diagnostics, renders them, and optionally emits
218
242
  // JavaScript output when the config allows it.
219
243
  func runProject(opts *subcommandOpts) int {
244
+ rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
245
+ if err != nil {
246
+ fmt.Fprintln(os.Stderr, err)
247
+ return 2
248
+ }
249
+ engine := NewEngineWithResolver(rules)
250
+ engine.SetSerial(opts.singleThreaded)
251
+
220
252
  prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
221
- forceEmit: opts.emit,
222
- forceNoEmit: opts.noEmit,
223
- outDir: opts.outDir,
253
+ forceEmit: opts.emit,
254
+ forceNoEmit: opts.noEmit,
255
+ outDir: opts.outDir,
256
+ needsRuleChecker: engine.NeedsTypeChecker(),
257
+ singleThreaded: opts.singleThreaded,
258
+ checkers: opts.checkers,
259
+ tsgoArgs: opts.tsgoArgs,
224
260
  })
225
261
  if err != nil {
226
262
  fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
@@ -232,21 +268,12 @@ func runProject(opts *subcommandOpts) int {
232
268
  }
233
269
  defer prog.close()
234
270
 
235
- rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
271
+ astDiags, lintDiags, err := collectDiagnostics(prog, engine)
236
272
  if err != nil {
237
273
  fmt.Fprintln(os.Stderr, err)
238
274
  return 2
239
275
  }
240
- engine := NewEngineWithResolver(rules)
241
-
242
- astDiags, lintDiags, externalRan, err := collectDiagnostics(prog, engine)
243
- if err != nil {
244
- fmt.Fprintln(os.Stderr, err)
245
- return 2
246
- }
247
- if !externalRan {
248
- warnUnknownRules(os.Stderr, engine.UnknownRules())
249
- }
276
+ warnUnknownRules(os.Stderr, engine.UnknownRules())
250
277
  if errCount := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, opts.cwd); errCount > 0 {
251
278
  return 2
252
279
  }
@@ -298,8 +325,9 @@ func loadRules(pluginsJSON, cwd, tsconfigPath string) (RuleResolver, error) {
298
325
  }
299
326
 
300
327
  // warnUnknownRules writes one warning line per name in `unknown` to `w`.
301
- // Called after engine construction when no external ESLint process handled
302
- // the run (the external runner surfaces its own unknown-rule warnings).
328
+ // Called after engine construction so a config that names a rule the native
329
+ // engine does not implement surfaces a loud warning instead of silently
330
+ // linting nothing for that rule.
303
331
  func warnUnknownRules(w io.Writer, unknown []string) {
304
332
  for _, name := range unknown {
305
333
  fmt.Fprintf(w, "@ttsc/lint: ignoring unknown rule %q\n", name)
@@ -344,7 +372,7 @@ func filterKnownFlags(args []string, known map[string]bool) []string {
344
372
  // collectDiagnostics merges tsgo typecheck diagnostics with the lint
345
373
  // engine's findings. The renderer takes the two slices and walks them in
346
374
  // source order, so we don't need to interleave here.
347
- func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic, bool, error) {
375
+ func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic, error) {
348
376
  astDiags := prog.programDiagnostics()
349
377
  files := prog.userSourceFiles()
350
378
  findings := engine.Run(files, prog.checker)
@@ -363,68 +391,7 @@ func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, [
363
391
  fmt.Sprintf("[%s] %s", finding.Rule, finding.Message),
364
392
  ))
365
393
  }
366
- if externalDiags, ran, err := runExternalESLintDiagnostics(engine.config, prog.cwd, files); err != nil {
367
- return nil, nil, false, err
368
- } else if ran {
369
- return astDiags, mergeNativeAndExternalDiagnostics(nativeDiags, externalDiags), true, nil
370
- }
371
- return astDiags, nativeDiags, false, nil
372
- }
373
-
374
- // mergeNativeAndExternalDiagnostics combines native lint findings with
375
- // external ESLint diagnostics. Any native diagnostic whose rule name also
376
- // appears in the external set is dropped to avoid duplicate reporting —
377
- // the external runner's output is authoritative for rules it covered.
378
- func mergeNativeAndExternalDiagnostics(nativeDiags, externalDiags []*shimdw.LintDiagnostic) []*shimdw.LintDiagnostic {
379
- if len(nativeDiags) == 0 {
380
- return externalDiags
381
- }
382
- if len(externalDiags) == 0 {
383
- return nativeDiags
384
- }
385
- externalRules := make(map[string]struct{})
386
- for _, diag := range externalDiags {
387
- if rule := canonicalLintRule(lintDiagnosticRule(diag)); rule != "" {
388
- externalRules[rule] = struct{}{}
389
- }
390
- }
391
- out := make([]*shimdw.LintDiagnostic, 0, len(nativeDiags)+len(externalDiags))
392
- for _, diag := range nativeDiags {
393
- rule := canonicalLintRule(lintDiagnosticRule(diag))
394
- if _, exists := externalRules[rule]; rule != "" && exists {
395
- continue
396
- }
397
- out = append(out, diag)
398
- }
399
- return append(out, externalDiags...)
400
- }
401
-
402
- // lintDiagnosticRule extracts the rule name from the `[rule-name]` prefix
403
- // that `collectDiagnostics` injects into every lint diagnostic message.
404
- // Returns "" when the message does not follow the expected format.
405
- func lintDiagnosticRule(diag *shimdw.LintDiagnostic) string {
406
- if diag == nil {
407
- return ""
408
- }
409
- message := diag.Message()
410
- if !strings.HasPrefix(message, "[") {
411
- return ""
412
- }
413
- end := strings.Index(message, "]")
414
- if end <= 1 {
415
- return ""
416
- }
417
- return message[1:end]
418
- }
419
-
420
- // canonicalLintRule trims ESLint namespace prefixes from `rule` so that
421
- // `no-var`, `@typescript-eslint/no-var`, and `typescript-eslint/no-var`
422
- // all compare equal during deduplication.
423
- func canonicalLintRule(rule string) string {
424
- rule = strings.TrimSpace(rule)
425
- rule = strings.TrimPrefix(rule, "@typescript-eslint/")
426
- rule = strings.TrimPrefix(rule, "typescript-eslint/")
427
- return rule
394
+ return astDiags, nativeDiags, nil
428
395
  }
429
396
 
430
397
  // RuleCode hashes a rule name into a stable, positive int32 so the