@ttsc/lint 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/lib/defaultFormat.d.ts +10 -11
  2. package/lib/defaultFormat.js +10 -11
  3. package/lib/defaultFormat.js.map +1 -1
  4. package/lib/index.js +233 -135
  5. package/lib/index.js.map +1 -1
  6. package/lib/structures/ITtscLintConfig.d.ts +2 -2
  7. package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
  8. package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
  9. package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
  10. package/lib/structures/TtscLintRule.d.ts +10 -1
  11. package/lib/structures/TtscLintRuleMap.d.ts +2 -2
  12. package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
  13. package/linthost/ast_helpers.go +80 -11
  14. package/linthost/compile.go +116 -112
  15. package/linthost/config.go +647 -687
  16. package/linthost/config_format.go +272 -247
  17. package/linthost/contrib_adapter.go +7 -0
  18. package/linthost/directives.go +116 -0
  19. package/linthost/dispatch.go +33 -33
  20. package/linthost/engine.go +156 -43
  21. package/linthost/fix.go +47 -27
  22. package/linthost/flags_gen.go +31 -0
  23. package/linthost/format.go +119 -3
  24. package/linthost/host.go +115 -5
  25. package/linthost/print_dispatch.go +128 -28
  26. package/linthost/print_doc.go +23 -0
  27. package/linthost/print_engine.go +168 -4
  28. package/linthost/print_nodes_array.go +17 -7
  29. package/linthost/print_nodes_call.go +138 -20
  30. package/linthost/print_nodes_function.go +353 -0
  31. package/linthost/print_nodes_imports.go +46 -29
  32. package/linthost/print_nodes_list.go +86 -5
  33. package/linthost/print_nodes_object.go +56 -11
  34. package/linthost/rules_arrays.go +5 -2
  35. package/linthost/rules_debugger.go +3 -2
  36. package/linthost/rules_dupes.go +7 -4
  37. package/linthost/rules_empty.go +3 -2
  38. package/linthost/rules_escape.go +35 -3
  39. package/linthost/rules_eval.go +3 -0
  40. package/linthost/rules_finally.go +11 -0
  41. package/linthost/rules_format_jsdoc.go +7 -0
  42. package/linthost/rules_format_print_width.go +270 -15
  43. package/linthost/rules_format_quotes.go +3 -0
  44. package/linthost/rules_format_sort_imports.go +4 -0
  45. package/linthost/rules_gap.go +75 -3
  46. package/linthost/rules_imports.go +5 -7
  47. package/linthost/rules_logic.go +75 -5
  48. package/linthost/rules_loops.go +4 -0
  49. package/linthost/rules_misc.go +4 -2
  50. package/linthost/rules_params.go +7 -11
  51. package/linthost/rules_problems.go +89 -22
  52. package/linthost/rules_promise.go +15 -0
  53. package/linthost/rules_protos.go +3 -2
  54. package/linthost/rules_self.go +6 -0
  55. package/linthost/rules_strings.go +10 -0
  56. package/linthost/rules_suggestions.go +174 -9
  57. package/linthost/rules_throw.go +2 -0
  58. package/linthost/rules_ts.go +13 -0
  59. package/linthost/rules_ts_extra.go +25 -8
  60. package/linthost/rules_var.go +15 -2
  61. package/package.json +3 -3
  62. package/plugin/main.go +4 -4
  63. package/rule/astutil/astutil.go +12 -0
  64. package/rule/rule.go +123 -123
  65. package/src/defaultFormat.ts +10 -11
  66. package/src/index.ts +257 -168
  67. package/src/structures/ITtscLintConfig.ts +2 -2
  68. package/src/structures/ITtscLintFormatConfig.ts +53 -55
  69. package/src/structures/ITtscLintPluginConfig.ts +10 -83
  70. package/src/structures/ITtscLintPluginMeta.ts +9 -1
  71. package/src/structures/TtscLintRule.ts +10 -1
  72. package/src/structures/TtscLintRuleMap.ts +17 -18
  73. package/src/structures/TtscLintRuleOptions.ts +15 -0
  74. package/linthost/eslint_runtime.go +0 -320
@@ -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,16 +164,22 @@ 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
 
180
+ // parseSubcommandFlags parses the shared flag set used by the `check`,
181
+ // `build`, and `fix`/`format` subcommands. Unknown flags are silently
182
+ // stripped by `filterKnownFlags` before the standard FlagSet sees them.
171
183
  func parseSubcommandFlags(name string, args []string) (*subcommandOpts, error) {
172
184
  fs := flag.NewFlagSet(name, flag.ContinueOnError)
173
185
  fs.SetOutput(os.Stderr)
@@ -179,42 +191,72 @@ func parseSubcommandFlags(name string, args []string) (*subcommandOpts, error) {
179
191
  quiet := fs.Bool("quiet", false, "")
180
192
  verbose := fs.Bool("verbose", false, "")
181
193
  outDir := fs.String("outDir", "", "")
182
- if err := fs.Parse(filterKnownFlags(args, map[string]bool{
183
- "cwd": true,
184
- "emit": false,
185
- "noEmit": false,
186
- "outDir": true,
187
- "plugins-json": true,
188
- "quiet": false,
189
- "tsconfig": true,
190
- "verbose": false,
191
- })); 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 {
192
198
  return nil, err
193
199
  }
194
200
  if *emit && *noEmit {
195
201
  return nil, errors.New("@ttsc/lint: --emit and --noEmit are mutually exclusive")
196
202
  }
203
+ tsgoArgs, err := decodeTsgoArgs(*tsgoArgsRaw)
204
+ if err != nil {
205
+ return nil, err
206
+ }
197
207
  resolvedCwd, err := resolveCwd(*cwd)
198
208
  if err != nil {
199
209
  return nil, err
200
210
  }
201
211
  return &subcommandOpts{
202
- cwd: resolvedCwd,
203
- tsconfig: *tsconfig,
204
- pluginsJSON: *pluginsJSON,
205
- emit: *emit,
206
- noEmit: *noEmit,
207
- quiet: *quiet,
208
- verbose: *verbose,
209
- 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,
210
223
  }, nil
211
224
  }
212
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
+
240
+ // runProject is the shared body of RunCheck and RunBuild. It loads the
241
+ // program, collects diagnostics, renders them, and optionally emits
242
+ // JavaScript output when the config allows it.
213
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
+
214
252
  prog, parseDiags, err := loadProgram(opts.cwd, opts.tsconfig, loadProgramOptions{
215
- forceEmit: opts.emit,
216
- forceNoEmit: opts.noEmit,
217
- 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,
218
260
  })
219
261
  if err != nil {
220
262
  fmt.Fprintf(os.Stderr, "@ttsc/lint: %v\n", err)
@@ -226,21 +268,12 @@ func runProject(opts *subcommandOpts) int {
226
268
  }
227
269
  defer prog.close()
228
270
 
229
- rules, err := loadRules(opts.pluginsJSON, opts.cwd, opts.tsconfig)
230
- if err != nil {
231
- fmt.Fprintln(os.Stderr, err)
232
- return 2
233
- }
234
- engine := NewEngineWithResolver(rules)
235
-
236
- astDiags, lintDiags, externalRan, err := collectDiagnostics(prog, engine)
271
+ astDiags, lintDiags, err := collectDiagnostics(prog, engine)
237
272
  if err != nil {
238
273
  fmt.Fprintln(os.Stderr, err)
239
274
  return 2
240
275
  }
241
- if !externalRan {
242
- warnUnknownRules(os.Stderr, engine.UnknownRules())
243
- }
276
+ warnUnknownRules(os.Stderr, engine.UnknownRules())
244
277
  if errCount := shimdw.FormatMixedDiagnostics(os.Stderr, astDiags, lintDiags, opts.cwd); errCount > 0 {
245
278
  return 2
246
279
  }
@@ -273,6 +306,9 @@ func runProject(opts *subcommandOpts) int {
273
306
  return 0
274
307
  }
275
308
 
309
+ // loadRules decodes `--plugins-json`, locates the `@ttsc/lint` entry, and
310
+ // returns its resolved RuleResolver. Returns an empty RuleConfig (no rules
311
+ // enabled) when the lint entry is absent from the plugin manifest.
276
312
  func loadRules(pluginsJSON, cwd, tsconfigPath string) (RuleResolver, error) {
277
313
  entries, err := ParsePlugins(pluginsJSON)
278
314
  if err != nil {
@@ -288,12 +324,22 @@ func loadRules(pluginsJSON, cwd, tsconfigPath string) (RuleResolver, error) {
288
324
  return LoadConfigResolver(entry, cwd, tsconfigPath)
289
325
  }
290
326
 
327
+ // warnUnknownRules writes one warning line per name in `unknown` to `w`.
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.
291
331
  func warnUnknownRules(w io.Writer, unknown []string) {
292
332
  for _, name := range unknown {
293
333
  fmt.Fprintf(w, "@ttsc/lint: ignoring unknown rule %q\n", name)
294
334
  }
295
335
  }
296
336
 
337
+ // filterKnownFlags strips flags from `args` that are not present in `known`.
338
+ // The `known` map value is true when the flag takes a separate value token
339
+ // (e.g. `--tsconfig tsconfig.json`) and false for boolean flags. Unknown
340
+ // flags are silently dropped along with their value token when present.
341
+ // This lets the host forward a superset of flags without confusing the
342
+ // standard library's FlagSet.
297
343
  func filterKnownFlags(args []string, known map[string]bool) []string {
298
344
  out := make([]string, 0, len(args))
299
345
  for i := 0; i < len(args); i++ {
@@ -326,7 +372,7 @@ func filterKnownFlags(args []string, known map[string]bool) []string {
326
372
  // collectDiagnostics merges tsgo typecheck diagnostics with the lint
327
373
  // engine's findings. The renderer takes the two slices and walks them in
328
374
  // source order, so we don't need to interleave here.
329
- func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic, bool, error) {
375
+ func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, []*shimdw.LintDiagnostic, error) {
330
376
  astDiags := prog.programDiagnostics()
331
377
  files := prog.userSourceFiles()
332
378
  findings := engine.Run(files, prog.checker)
@@ -345,58 +391,7 @@ func collectDiagnostics(prog *program, engine *Engine) ([]*shimast.Diagnostic, [
345
391
  fmt.Sprintf("[%s] %s", finding.Rule, finding.Message),
346
392
  ))
347
393
  }
348
- if externalDiags, ran, err := runExternalESLintDiagnostics(engine.config, prog.cwd, files); err != nil {
349
- return nil, nil, false, err
350
- } else if ran {
351
- return astDiags, mergeNativeAndExternalDiagnostics(nativeDiags, externalDiags), true, nil
352
- }
353
- return astDiags, nativeDiags, false, nil
354
- }
355
-
356
- func mergeNativeAndExternalDiagnostics(nativeDiags, externalDiags []*shimdw.LintDiagnostic) []*shimdw.LintDiagnostic {
357
- if len(nativeDiags) == 0 {
358
- return externalDiags
359
- }
360
- if len(externalDiags) == 0 {
361
- return nativeDiags
362
- }
363
- externalRules := make(map[string]struct{})
364
- for _, diag := range externalDiags {
365
- if rule := canonicalLintRule(lintDiagnosticRule(diag)); rule != "" {
366
- externalRules[rule] = struct{}{}
367
- }
368
- }
369
- out := make([]*shimdw.LintDiagnostic, 0, len(nativeDiags)+len(externalDiags))
370
- for _, diag := range nativeDiags {
371
- rule := canonicalLintRule(lintDiagnosticRule(diag))
372
- if _, exists := externalRules[rule]; rule != "" && exists {
373
- continue
374
- }
375
- out = append(out, diag)
376
- }
377
- return append(out, externalDiags...)
378
- }
379
-
380
- func lintDiagnosticRule(diag *shimdw.LintDiagnostic) string {
381
- if diag == nil {
382
- return ""
383
- }
384
- message := diag.Message()
385
- if !strings.HasPrefix(message, "[") {
386
- return ""
387
- }
388
- end := strings.Index(message, "]")
389
- if end <= 1 {
390
- return ""
391
- }
392
- return message[1:end]
393
- }
394
-
395
- func canonicalLintRule(rule string) string {
396
- rule = strings.TrimSpace(rule)
397
- rule = strings.TrimPrefix(rule, "@typescript-eslint/")
398
- rule = strings.TrimPrefix(rule, "typescript-eslint/")
399
- return rule
394
+ return astDiags, nativeDiags, nil
400
395
  }
401
396
 
402
397
  // RuleCode hashes a rule name into a stable, positive int32 so the
@@ -417,6 +412,9 @@ func RuleCode(name string) int32 {
417
412
  // that prefer the lowercase form.
418
413
  func ruleCode(name string) int32 { return RuleCode(name) }
419
414
 
415
+ // resolveCwd returns an absolute working directory. When `override` is
416
+ // non-empty it is made absolute; otherwise the process working directory
417
+ // is returned.
420
418
  func resolveCwd(override string) (string, error) {
421
419
  if override != "" {
422
420
  abs, err := filepath.Abs(override)
@@ -432,6 +430,9 @@ func resolveCwd(override string) (string, error) {
432
430
  return wd, nil
433
431
  }
434
432
 
433
+ // isJavaScriptOutput reports whether `name` has a JavaScript output
434
+ // extension (.js, .mjs, or .cjs). Used to filter the emit callback so
435
+ // that `RunTransform` captures only the JS output for the target file.
435
436
  func isJavaScriptOutput(name string) bool {
436
437
  switch strings.ToLower(filepath.Ext(name)) {
437
438
  case ".js", ".mjs", ".cjs":
@@ -441,6 +442,9 @@ func isJavaScriptOutput(name string) bool {
441
442
  }
442
443
  }
443
444
 
445
+ // defaultWriteFile creates all parent directories and writes `text` to
446
+ // `name` with mode 0644. Used as the WriteFile callback in `runProject`
447
+ // when the user requested emit.
444
448
  func defaultWriteFile(name string, text string) error {
445
449
  if err := os.MkdirAll(filepath.Dir(name), 0o755); err != nil {
446
450
  return err