@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
@@ -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"
@@ -20,6 +21,7 @@ import (
20
21
  "os"
21
22
  "path/filepath"
22
23
  "strings"
24
+ "time"
23
25
 
24
26
  shimast "github.com/microsoft/typescript-go/shim/ast"
25
27
  shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
@@ -59,26 +61,42 @@ func RunTransform(args []string) int {
59
61
  tsconfig := fs.String("tsconfig", "tsconfig.json", "tsconfig owning --file")
60
62
  cwd := fs.String("cwd", "", "override the working directory")
61
63
  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 {
64
+ singleThreaded := fs.Bool("singleThreaded", false, "run TypeScript-Go single-threaded")
65
+ checkers := fs.Int("checkers", 0, "type-checker pool size (0 = TypeScript-Go default)")
66
+ tsgoArgsRaw := fs.String("tsgo-args", "", "JSON array of forwarded tsgo CLI flags")
67
+ _ = fs.Bool("diagnostics", false, "print @ttsc/lint diagnostics timing")
68
+ _ = fs.Bool("extendedDiagnostics", false, "print @ttsc/lint diagnostics timing")
69
+ if err := fs.Parse(filterKnownFlags(args, LintFlagAllowList)); err != nil {
69
70
  return 2
70
71
  }
71
72
  if *file == "" {
72
73
  fmt.Fprintln(os.Stderr, "@ttsc/lint transform: --file is required")
73
74
  return 2
74
75
  }
76
+ tsgoArgs, err := decodeTsgoArgs(*tsgoArgsRaw)
77
+ if err != nil {
78
+ fmt.Fprintln(os.Stderr, err)
79
+ return 2
80
+ }
75
81
  resolvedCwd, err := resolveCwd(*cwd)
76
82
  if err != nil {
77
83
  fmt.Fprintln(os.Stderr, err)
78
84
  return 2
79
85
  }
86
+ rules, err := loadRules(*pluginsJSON, resolvedCwd, *tsconfig)
87
+ if err != nil {
88
+ fmt.Fprintln(os.Stderr, err)
89
+ return 2
90
+ }
91
+ engine := NewEngineWithResolver(rules)
92
+ engine.SetSerial(*singleThreaded)
93
+
80
94
  prog, parseDiags, err := loadProgram(resolvedCwd, *tsconfig, loadProgramOptions{
81
- forceEmit: true,
95
+ forceEmit: true,
96
+ needsRuleChecker: engine.NeedsTypeChecker(),
97
+ singleThreaded: *singleThreaded,
98
+ checkers: *checkers,
99
+ tsgoArgs: tsgoArgs,
82
100
  })
83
101
  if err != nil {
84
102
  fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
@@ -90,21 +108,12 @@ func RunTransform(args []string) int {
90
108
  }
91
109
  defer prog.close()
92
110
 
93
- rules, err := loadRules(*pluginsJSON, resolvedCwd, *tsconfig)
111
+ astDiags, lintDiags, err := collectDiagnostics(prog, engine)
94
112
  if err != nil {
95
113
  fmt.Fprintln(os.Stderr, err)
96
114
  return 2
97
115
  }
98
- engine := NewEngineWithResolver(rules)
99
-
100
- astDiags, lintDiags, externalRan, err := collectDiagnostics(prog, engine)
101
- if err != nil {
102
- fmt.Fprintln(os.Stderr, err)
103
- return 2
104
- }
105
- if !externalRan {
106
- warnUnknownRules(os.Stderr, engine.UnknownRules())
107
- }
116
+ warnUnknownRules(os.Stderr, engine.UnknownRules())
108
117
  if errors := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, resolvedCwd); errors > 0 {
109
118
  return 2
110
119
  }
@@ -158,14 +167,18 @@ func RunTransform(args []string) int {
158
167
  }
159
168
 
160
169
  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
170
+ cwd string
171
+ tsconfig string
172
+ pluginsJSON string
173
+ emit bool
174
+ noEmit bool
175
+ quiet bool
176
+ verbose bool
177
+ diagnostics bool
178
+ outDir string
179
+ singleThreaded bool
180
+ checkers int
181
+ tsgoArgs []string
169
182
  }
170
183
 
171
184
  // parseSubcommandFlags parses the shared flag set used by the `check`,
@@ -181,46 +194,76 @@ func parseSubcommandFlags(name string, args []string) (*subcommandOpts, error) {
181
194
  noEmit := fs.Bool("noEmit", false, "")
182
195
  quiet := fs.Bool("quiet", false, "")
183
196
  verbose := fs.Bool("verbose", false, "")
197
+ diagnostics := fs.Bool("diagnostics", false, "")
198
+ extendedDiagnostics := fs.Bool("extendedDiagnostics", false, "")
184
199
  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 {
200
+ singleThreaded := fs.Bool("singleThreaded", false, "")
201
+ checkers := fs.Int("checkers", 0, "")
202
+ tsgoArgsRaw := fs.String("tsgo-args", "", "")
203
+ if err := fs.Parse(filterKnownFlags(args, LintFlagAllowList)); err != nil {
195
204
  return nil, err
196
205
  }
197
206
  if *emit && *noEmit {
198
207
  return nil, errors.New("@ttsc/lint: --emit and --noEmit are mutually exclusive")
199
208
  }
209
+ tsgoArgs, err := decodeTsgoArgs(*tsgoArgsRaw)
210
+ if err != nil {
211
+ return nil, err
212
+ }
200
213
  resolvedCwd, err := resolveCwd(*cwd)
201
214
  if err != nil {
202
215
  return nil, err
203
216
  }
204
217
  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,
218
+ cwd: resolvedCwd,
219
+ tsconfig: *tsconfig,
220
+ pluginsJSON: *pluginsJSON,
221
+ emit: *emit,
222
+ noEmit: *noEmit,
223
+ quiet: *quiet,
224
+ verbose: *verbose,
225
+ diagnostics: *diagnostics || *extendedDiagnostics,
226
+ outDir: *outDir,
227
+ singleThreaded: *singleThreaded,
228
+ checkers: *checkers,
229
+ tsgoArgs: tsgoArgs,
213
230
  }, nil
214
231
  }
215
232
 
233
+ // decodeTsgoArgs decodes the JSON-array value of the `--tsgo-args` flag — the
234
+ // tsgo CLI flags the `ttsc` launcher forwarded — into a string slice. An empty
235
+ // flag yields a nil slice.
236
+ func decodeTsgoArgs(raw string) ([]string, error) {
237
+ if raw == "" {
238
+ return nil, nil
239
+ }
240
+ var args []string
241
+ if err := json.Unmarshal([]byte(raw), &args); err != nil {
242
+ return nil, fmt.Errorf("@ttsc/lint: invalid --tsgo-args: %w", err)
243
+ }
244
+ return args, nil
245
+ }
246
+
216
247
  // runProject is the shared body of RunCheck and RunBuild. It loads the
217
248
  // program, collects diagnostics, renders them, and optionally emits
218
249
  // JavaScript output when the config allows it.
219
250
  func runProject(opts *subcommandOpts) int {
251
+ rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
252
+ if err != nil {
253
+ fmt.Fprintln(os.Stderr, err)
254
+ return 2
255
+ }
256
+ engine := NewEngineWithResolver(rules)
257
+ engine.SetSerial(opts.singleThreaded)
258
+
220
259
  prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
221
- forceEmit: opts.emit,
222
- forceNoEmit: opts.noEmit,
223
- outDir: opts.outDir,
260
+ forceEmit: opts.emit,
261
+ forceNoEmit: opts.noEmit,
262
+ outDir: opts.outDir,
263
+ needsRuleChecker: engine.NeedsTypeChecker(),
264
+ singleThreaded: opts.singleThreaded,
265
+ checkers: opts.checkers,
266
+ tsgoArgs: opts.tsgoArgs,
224
267
  })
225
268
  if err != nil {
226
269
  fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
@@ -232,21 +275,13 @@ func runProject(opts *subcommandOpts) int {
232
275
  }
233
276
  defer prog.close()
234
277
 
235
- rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
236
- if err != nil {
237
- fmt.Fprintln(os.Stderr, err)
238
- return 2
239
- }
240
- engine := NewEngineWithResolver(rules)
241
-
242
- astDiags, lintDiags, externalRan, err := collectDiagnostics(prog, engine)
278
+ astDiags, lintDiags, diagnosticsTiming, err := collectDiagnosticsTimed(prog, engine)
243
279
  if err != nil {
244
280
  fmt.Fprintln(os.Stderr, err)
245
281
  return 2
246
282
  }
247
- if !externalRan {
248
- warnUnknownRules(os.Stderr, engine.UnknownRules())
249
- }
283
+ printLintDiagnosticsTiming(os.Stdout, opts.diagnostics, diagnosticsTiming)
284
+ warnUnknownRules(os.Stderr, engine.UnknownRules())
250
285
  if errCount := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, opts.cwd); errCount > 0 {
251
286
  return 2
252
287
  }
@@ -298,8 +333,9 @@ func loadRules(pluginsJSON, cwd, tsconfigPath string) (RuleResolver, error) {
298
333
  }
299
334
 
300
335
  // 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).
336
+ // Called after engine construction so a config that names a rule the native
337
+ // engine does not implement surfaces a loud warning instead of silently
338
+ // linting nothing for that rule.
303
339
  func warnUnknownRules(w io.Writer, unknown []string) {
304
340
  for _, name := range unknown {
305
341
  fmt.Fprintf(w, "@ttsc/lint: ignoring unknown rule %q\n", name)
@@ -344,10 +380,22 @@ func filterKnownFlags(args []string, known map[string]bool) []string {
344
380
  // collectDiagnostics merges tsgo typecheck diagnostics with the lint
345
381
  // engine's findings. The renderer takes the two slices and walks them in
346
382
  // source order, so we don't need to interleave here.
347
- func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic, bool, error) {
383
+ func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic, error) {
384
+ astDiags, lintDiags, _, err := collectDiagnosticsTimed(prog, engine)
385
+ return astDiags, lintDiags, err
386
+ }
387
+
388
+ type lintDiagnosticsTiming struct {
389
+ lint time.Duration
390
+ }
391
+
392
+ func collectDiagnosticsTimed(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic, lintDiagnosticsTiming, error) {
393
+ timing := lintDiagnosticsTiming{}
348
394
  astDiags := prog.programDiagnostics()
349
395
  files := prog.userSourceFiles()
396
+ lintStarted := time.Now()
350
397
  findings := engine.Run(files, prog.checker)
398
+ timing.lint = time.Since(lintStarted)
351
399
  nativeDiags := make([]*shimdw.LintDiagnostic, 0, len(findings))
352
400
  for _, finding := range findings {
353
401
  category := shimdw.LintCategoryError
@@ -363,68 +411,18 @@ func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, [
363
411
  fmt.Sprintf("[%s] %s", finding.Rule, finding.Message),
364
412
  ))
365
413
  }
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
414
+ return astDiags, nativeDiags, timing, nil
372
415
  }
373
416
 
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)
417
+ func printLintDiagnosticsTiming(w io.Writer, enabled bool, timing lintDiagnosticsTiming) {
418
+ if !enabled {
419
+ return
398
420
  }
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]
421
+ fmt.Fprintf(w, "@ttsc/lint time: %s\n", formatTimingSeconds(timing.lint))
418
422
  }
419
423
 
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
424
+ func formatTimingSeconds(duration time.Duration) string {
425
+ return fmt.Sprintf("%.3fs", duration.Seconds())
428
426
  }
429
427
 
430
428
  // RuleCode hashes a rule name into a stable, positive int32 so the