@ttsc/lint 0.19.3 → 0.20.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 (63) hide show
  1. package/README.md +1 -1
  2. package/lib/index.js +26 -4
  3. package/lib/index.js.map +1 -1
  4. package/lib/structures/format/ITtscLintFormat.d.ts +14 -10
  5. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +24 -9
  6. package/lib/structures/rules/ITtscLintSecurityRules.d.ts +18 -0
  7. package/lib/structures/rules/ITtscLintSolidRules.d.ts +22 -2
  8. package/lib/structures/rules/ITtscLintStorybookRules.d.ts +21 -0
  9. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +10 -1
  10. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +13 -2
  11. package/linthost/compile.go +9 -4
  12. package/linthost/config.go +18 -14
  13. package/linthost/contrib_adapter.go +57 -0
  14. package/linthost/dispatch.go +5 -1
  15. package/linthost/display_width.go +179 -41
  16. package/linthost/engine.go +120 -4
  17. package/linthost/flags_gen.go +4 -4
  18. package/linthost/hints.go +207 -0
  19. package/linthost/host.go +68 -0
  20. package/linthost/lsp.go +148 -29
  21. package/linthost/print_dispatch.go +24 -0
  22. package/linthost/print_nodes_array.go +88 -15
  23. package/linthost/print_nodes_call.go +38 -31
  24. package/linthost/print_nodes_control_flow.go +319 -0
  25. package/linthost/print_nodes_function.go +140 -13
  26. package/linthost/print_nodes_list.go +22 -10
  27. package/linthost/project_engine.go +18 -1
  28. package/linthost/project_rules.go +47 -0
  29. package/linthost/rule_docs.go +114 -0
  30. package/linthost/rules_boundaries.go +80 -2
  31. package/linthost/rules_format_bracket_spacing.go +18 -6
  32. package/linthost/rules_format_clause_join.go +11 -8
  33. package/linthost/rules_format_indent.go +155 -4
  34. package/linthost/rules_format_print_width.go +12 -35
  35. package/linthost/rules_format_quote_props.go +88 -27
  36. package/linthost/rules_format_statement_split.go +81 -0
  37. package/linthost/rules_format_trailing_comma.go +62 -94
  38. package/linthost/rules_gap.go +26 -17
  39. package/linthost/rules_jsdoc.go +54 -0
  40. package/linthost/rules_logic.go +30 -15
  41. package/linthost/rules_no_redeclare.go +12 -3
  42. package/linthost/rules_promise.go +37 -15
  43. package/linthost/rules_regexp.go +443 -49
  44. package/linthost/rules_security.go +243 -4
  45. package/linthost/rules_solid.go +430 -10
  46. package/linthost/rules_storybook.go +255 -10
  47. package/linthost/rules_suggestions.go +60 -39
  48. package/linthost/rules_ts.go +7 -0
  49. package/linthost/rules_ts_async.go +80 -2
  50. package/linthost/rules_ts_extra.go +21 -7
  51. package/linthost/serve.go +318 -0
  52. package/linthost/width_tables_gen.go +219 -0
  53. package/package.json +2 -2
  54. package/rule/hint.go +142 -0
  55. package/rule/rule.go +197 -1
  56. package/src/index.ts +35 -4
  57. package/src/structures/format/ITtscLintFormat.ts +14 -10
  58. package/src/structures/rules/ITtscLintRegexpRules.ts +24 -9
  59. package/src/structures/rules/ITtscLintSecurityRules.ts +18 -0
  60. package/src/structures/rules/ITtscLintSolidRules.ts +22 -2
  61. package/src/structures/rules/ITtscLintStorybookRules.ts +21 -0
  62. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +10 -1
  63. package/src/structures/rules/ITtscLintTypeScriptRules.ts +13 -2
package/rule/hint.go ADDED
@@ -0,0 +1,142 @@
1
+ package rule
2
+
3
+ import "encoding/json"
4
+
5
+ // HintScope names the syntactic region a hint is offered in. It exists because
6
+ // a line prefix alone cannot tell `@evidence` in a doc comment from
7
+ // `@Injectable` above a class: both lines end in `@`, and a corpus that ignored
8
+ // the difference would offer doc-comment tags in every decorator position.
9
+ //
10
+ // One value ships today. The field is present anyway: a hint with no scope
11
+ // would mean "anywhere on any line", which is never what a rule meant, and
12
+ // widening that default later would break every corpus already published.
13
+ type HintScope string
14
+
15
+ const (
16
+ // HintScopeJSDoc offers the hint inside a `/** */` documentation comment.
17
+ HintScopeJSDoc HintScope = "jsdoc"
18
+ )
19
+
20
+ // HintTrigger is the declarative answer to "does this hint apply at the
21
+ // cursor?".
22
+ //
23
+ // It must be declarative because the rule that produced it is gone. The lint
24
+ // engine is a separate process that reloads the Program on every invocation, so
25
+ // nothing can ask a rule a question per keystroke. A Go predicate is the obvious
26
+ // API and is exactly the one that cannot ship: it does not survive the process
27
+ // boundary. Every editor-assistance system that lets a rule answer live does so
28
+ // in-process on a shared AST; this host cannot, so the corpus travels instead of
29
+ // the question.
30
+ //
31
+ // The host matches a trigger against the current line up to the cursor: the hint
32
+ // applies when the cursor sits inside Scope and the line prefix contains After.
33
+ // Text following the LAST occurrence of After is the filter the editor matches
34
+ // against, and the range the completion replaces. Two consequences to design
35
+ // around:
36
+ //
37
+ // After must end exactly where the completed token begins — `"@evidence "` with
38
+ // its trailing space, not `"@evidence"` — or the token swallows the separator
39
+ // and nothing filters.
40
+ //
41
+ // When several triggers match one line, the occurrence nearest the cursor wins.
42
+ // At that occurrence, the longest After wins, and only hints with that same
43
+ // trigger merge. That keeps a corpus layerable while preventing an earlier,
44
+ // longer trigger from drowning a later one.
45
+ type HintTrigger struct {
46
+ Scope HintScope `json:"scope"`
47
+ After string `json:"after"`
48
+ }
49
+
50
+ // Hint is one completion an editor may offer.
51
+ //
52
+ // It is a value, not a behavior: the host serializes the corpus and hands it to
53
+ // the LSP proxy, which answers from cache long after the lint process exited. A
54
+ // closure, a channel, or an AST node cannot be carried here, and that constraint
55
+ // is the whole shape of the type.
56
+ type Hint struct {
57
+ // Insert is the text replacing the token being completed. Plain text,
58
+ // inserted verbatim: there is no snippet expansion, so `$` and tabs are
59
+ // literal.
60
+ Insert string `json:"insert"`
61
+
62
+ // Label is what the editor lists and filters on. Empty means Insert, which
63
+ // is the common case. Set it only when the two genuinely differ, and
64
+ // remember the filter is what the user typed AFTER the trigger: a Label
65
+ // repeating the trigger text will not prefix-match anything.
66
+ Label string `json:"label,omitempty"`
67
+
68
+ // Detail is a short annotation rendered beside Label. Use it for the fact
69
+ // distinguishing two similar entries — a heading's text, a count. It is not
70
+ // documentation: editors truncate it, so a sentence is wasted.
71
+ Detail string `json:"detail,omitempty"`
72
+
73
+ // Trigger is where this hint applies. A zero Trigger is dropped by the host
74
+ // rather than offered everywhere: a hint with no scope is one nobody asked
75
+ // for, surfacing in every decorator and every string literal.
76
+ Trigger HintTrigger `json:"trigger"`
77
+ }
78
+
79
+ // HintContext is the read-only handle the host passes to Hints.
80
+ //
81
+ // It carries State because a rule value is stateless: contributors register
82
+ // `myRule{}`, not a pointer with fields, and the host owns everything Check
83
+ // produced. Without State here, Hints could only ever return constants.
84
+ type HintContext struct {
85
+ // Identity names the Program this corpus is built for, as during Check.
86
+ Identity ProjectIdentity
87
+
88
+ // State is the value the rule passed to ProjectContext.SetState.
89
+ // Type-assert it back, exactly as a file rule does with
90
+ // ProjectRuleResult.State. The host calls Hints only for a rule that passed
91
+ // and published, so a failed assertion means the rule published something
92
+ // other than it believes.
93
+ State any
94
+
95
+ // Severity and Options are the resolved configuration Check ran under,
96
+ // repeated so a rule shaping its corpus by option need not stash a decoded
97
+ // struct inside State.
98
+ Severity Severity
99
+ Options json.RawMessage
100
+ }
101
+
102
+ // DecodeOptions unmarshals the configured options into out. A missing options
103
+ // tuple leaves out unchanged and returns nil.
104
+ func (c *HintContext) DecodeOptions(out interface{}) error {
105
+ if c == nil || len(c.Options) == 0 {
106
+ return nil
107
+ }
108
+ return json.Unmarshal(c.Options, out)
109
+ }
110
+
111
+ // HintRule is an optional marker a ProjectRule implements to publish editor
112
+ // completions for the Program it just indexed.
113
+ //
114
+ // The host calls Hints at most once per Program, always after Check, and only
115
+ // when a consumer asks for the corpus — never during `ttsc check`. It is not
116
+ // called unless Check passed and published state, the same gate a file rule
117
+ // writes by hand against ProjectRulePassed. A rule configured off is never
118
+ // asked, so `off` means no hints with no code in the rule, and a rule's options
119
+ // shape its corpus for free because the corpus is a projection of the state
120
+ // Check built under them.
121
+ //
122
+ // Pull, not push. Report is push because a finding is discovered mid-walk and
123
+ // belongs to the node under it. A corpus is the opposite: a projection of
124
+ // FINISHED state. A rule pushing hints while building that state would publish
125
+ // the anchors it had found so far rather than the ones the document has.
126
+ //
127
+ // The corpus outlives the process, so slice order is the only ranking channel
128
+ // there is — the host preserves it and derives the editor's sort key from it.
129
+ // Return what should be offered first, first. Nothing else about a Hint
130
+ // influences ordering, by design: a sort key field would be a second, silently
131
+ // conflicting answer to a question the slice already answers.
132
+ //
133
+ // This embeds ProjectRule rather than standing alone as OptionsRule does,
134
+ // because a per-file corpus is not a coherent thing. File rules run in a
135
+ // parallel walk, so their hints would arrive — and therefore rank —
136
+ // nondeterministically, and a corpus keyed to one file cannot answer a keystroke
137
+ // in another. A contributor wanting hints from file-level facts registers a
138
+ // ProjectRule alongside, which is what those facts wanted anyway.
139
+ type HintRule interface {
140
+ ProjectRule
141
+ Hints(ctx *HintContext) []Hint
142
+ }
package/rule/rule.go CHANGED
@@ -112,6 +112,44 @@ type DeclarationFileRule interface {
112
112
  VisitsDeclarationFiles() bool
113
113
  }
114
114
 
115
+ // DiagnosticTag classifies what a finding IS, orthogonally to how severe it is.
116
+ // The values match the LSP DiagnosticTag enum, and an editor renders them
117
+ // distinctively: unnecessary code is greyed out, deprecated code struck through.
118
+ type DiagnosticTag int
119
+
120
+ const (
121
+ // DiagnosticTagUnnecessary marks code that is safe to delete — an unused
122
+ // import, an unreachable branch. The editor fades it.
123
+ //
124
+ // This is a claim about what the code is, not how bad it is, and the
125
+ // distinction bites: "unnecessary" says "remove this." A finding that means
126
+ // "this is not done yet" is the opposite and must never carry it, or the
127
+ // editor tells the author to delete the work they have not finished. Tag by
128
+ // what deletion would mean, never by severity.
129
+ DiagnosticTagUnnecessary DiagnosticTag = 1
130
+ // DiagnosticTagDeprecated marks code that still works but should be migrated
131
+ // away from. The editor strikes it through.
132
+ DiagnosticTagDeprecated DiagnosticTag = 2
133
+ )
134
+
135
+ // TaggedRule is an optional marker a rule implements to classify its findings
136
+ // with DiagnosticTags. Every finding the rule produces carries the returned
137
+ // tags — the rule-level grain fits the rules that want this, since a rule that
138
+ // flags unused code flags only unused code.
139
+ //
140
+ // It is separate from severity on purpose. Severity is how much a finding
141
+ // matters and is the user's to configure; a tag is what the finding is and is
142
+ // the rule's to state. A host that does not read tags loses the greying, not the
143
+ // diagnostic — the same graceful degradation the other optional markers give.
144
+ //
145
+ // Return nil, or do not implement it, for a rule whose findings are neither
146
+ // unnecessary nor deprecated. Most findings are neither, and guessing wrong is
147
+ // worse than saying nothing: a spurious Unnecessary tells the author to delete
148
+ // correct code.
149
+ type TaggedRule interface {
150
+ DiagnosticTags() []DiagnosticTag
151
+ }
152
+
115
153
  // TypeAwareRule is an optional marker contributors implement to declare
116
154
  // whether their rule reads `Context.Checker`. The host cannot infer a
117
155
  // third-party rule's shape, so a contributor that does not implement this
@@ -128,8 +166,12 @@ type DeclarationFileRule interface {
128
166
  // preserving the engine's parallel file walk. Returning `true` is equivalent
129
167
  // to not implementing the interface at all. A rule that returns `false` must
130
168
  // not read `Context.Checker`: the host is free to leave it nil.
169
+ //
170
+ // The method name is domain-specific so an unrelated generic method on an
171
+ // existing contributor cannot opt out by accident. ProjectRule implementations
172
+ // may use the same marker; the serial walk it governs is engine-wide, so one
173
+ // type-aware project rule serializes every file rule in the run.
131
174
  type TypeAwareRule interface {
132
- Rule
133
175
  NeedsTypeChecker() bool
134
176
  }
135
177
 
@@ -176,6 +218,43 @@ type FixReporter interface {
176
218
  ReportRangeFix(pos, end int, message string, edits ...TextEdit)
177
219
  }
178
220
 
221
+ // RelatedReporter is the optional extension a host implements to receive a
222
+ // finding's related source locations. Like FixReporter, the public
223
+ // `rule.Context` type-asserts against this shape, so any host whose reporter
224
+ // exposes both methods opts into related locations without depending on a
225
+ // private interface name. A host that does not implement it loses the related
226
+ // locations, not the diagnostic — the same graceful degradation the other
227
+ // optional reporter extensions give.
228
+ //
229
+ // Rule production code does NOT touch RelatedReporter directly — call
230
+ // `ctx.ReportRelated` / `ctx.ReportRangeRelated`, and the host's reporter
231
+ // receives the locations. The only place a contributor sees this interface is in
232
+ // test code that fakes the reporter: such a fake must implement BOTH `Reporter`
233
+ // AND `RelatedReporter` to observe the locations, because Go interface
234
+ // satisfaction is all-or-nothing. Declaring `var _ rule.RelatedReporter =
235
+ // &myFake{}` compile-checks the fake covers the related surface.
236
+ type RelatedReporter interface {
237
+ ReportRelated(node *shimast.Node, message string, related ...RelatedInformation)
238
+ ReportRangeRelated(pos, end int, message string, related ...RelatedInformation)
239
+ }
240
+
241
+ // RelatedInformation is a secondary source location a finding points at, paired
242
+ // with a message naming the connection. LSP renders each as its own clickable
243
+ // line beneath the diagnostic, so "'x' is already defined." can lead the reader
244
+ // to the first definition instead of only naming it.
245
+ //
246
+ // Pos/End are byte offsets into the CURRENT file — the same offsets a shim AST
247
+ // node exposes and `ReportRange` consumes — so a related location lives in the
248
+ // file the finding is in, and the host fills in that file's URI. A location in
249
+ // ANOTHER file would need a URI this API does not yet carry, and is a separate
250
+ // extension left deliberately out of scope so the same-file case ships without
251
+ // waiting on it.
252
+ type RelatedInformation struct {
253
+ Pos int
254
+ End int
255
+ Message string
256
+ }
257
+
179
258
  // TextEdit is one byte-range replacement offered by an autofixable finding.
180
259
  // Positions use the same byte offsets as shim AST nodes and must point inside
181
260
  // the current source file. An empty `Text` deletes the range; positions are
@@ -198,6 +277,42 @@ type TextEdit struct {
198
277
  Text string
199
278
  }
200
279
 
280
+ // Suggestion is one of several candidate fixes offered for a finding, each with
281
+ // its own title. It exists for the case `ReportFix` cannot serve: when a rule
282
+ // knows more than one valid repair and cannot pick among them for the author.
283
+ //
284
+ // The distinction is the same one the built-in rules already draw and, until
285
+ // now, kept to themselves. A fix is imposed; a suggestion is chosen. A rule that
286
+ // found three valid renames must either impose one arbitrarily through
287
+ // `ReportFix` or describe the three in prose and offer none — both worse than
288
+ // letting the editor present the choice, which is what the built-ins do through
289
+ // this shape and contributors could not reach.
290
+ //
291
+ // Edits within one Suggestion follow the same non-overlap policy as `TextEdit`
292
+ // in a `ReportFix` call.
293
+ type Suggestion struct {
294
+ // Title is what the editor shows for this choice, e.g. "Rename to `frames`".
295
+ Title string
296
+ // Edits apply this suggestion. Empty means the suggestion is a label with no
297
+ // edit — a "did you mean" the author acts on by hand.
298
+ Edits []TextEdit
299
+ }
300
+
301
+ // SuggestionReporter is the optional half of the reporter a host implements to
302
+ // carry suggestions. A host that does not implement it still receives the
303
+ // finding through `Reporter`, without the choices — the same graceful
304
+ // degradation `FixReporter` gives autofixes.
305
+ //
306
+ // It is separate from `FixReporter` rather than folded into it because the two
307
+ // answer different questions: `ReportFix` offers the one right rewrite, this
308
+ // offers a choice among several. A rule reaches it through
309
+ // `Context.ReportSuggestion` / `ReportRangeSuggestion`; it is not called
310
+ // directly.
311
+ type SuggestionReporter interface {
312
+ ReportSuggestion(node *shimast.Node, message string, suggestions ...Suggestion)
313
+ ReportRangeSuggestion(pos, end int, message string, suggestions ...Suggestion)
314
+ }
315
+
201
316
  // Context is the per-(file, rule) handle the engine passes to `Check`.
202
317
  // The `Reporter` is supplied by the host when constructing the context;
203
318
  // contributors call `ctx.Report` / `ctx.ReportRange` directly through
@@ -342,6 +457,87 @@ func (c *Context) ReportRangeFix(pos, end int, message string, edits ...TextEdit
342
457
  fixer.ReportRangeFix(pos, end, message, edits...)
343
458
  }
344
459
 
460
+ // ReportSuggestion records a finding at the node's range with a choice of
461
+ // candidate fixes. A host that does not implement `SuggestionReporter` receives
462
+ // the diagnostic without the choices, so design the rule so the message alone is
463
+ // useful — the same best-effort contract as `ReportFix`.
464
+ //
465
+ // Use this over `ReportFix` only when there genuinely is a choice. One correct
466
+ // rewrite is a fix; imposing it is the right thing. Several valid rewrites is a
467
+ // suggestion; imposing one arbitrarily is not.
468
+ func (c *Context) ReportSuggestion(node *shimast.Node, message string, suggestions ...Suggestion) {
469
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff || node == nil {
470
+ return
471
+ }
472
+ if len(suggestions) == 0 {
473
+ c.reporter.Report(node, message)
474
+ return
475
+ }
476
+ suggester, ok := c.reporter.(SuggestionReporter)
477
+ if !ok {
478
+ c.reporter.Report(node, message)
479
+ return
480
+ }
481
+ suggester.ReportSuggestion(node, message, suggestions...)
482
+ }
483
+
484
+ // ReportRangeSuggestion records a finding at an explicit byte range with a
485
+ // choice of candidate fixes. See `ReportSuggestion`.
486
+ func (c *Context) ReportRangeSuggestion(pos, end int, message string, suggestions ...Suggestion) {
487
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff {
488
+ return
489
+ }
490
+ if len(suggestions) == 0 {
491
+ c.reporter.ReportRange(pos, end, message)
492
+ return
493
+ }
494
+ suggester, ok := c.reporter.(SuggestionReporter)
495
+ if !ok {
496
+ c.reporter.ReportRange(pos, end, message)
497
+ return
498
+ }
499
+ suggester.ReportRangeSuggestion(pos, end, message, suggestions...)
500
+ }
501
+
502
+ // ReportRelated records a finding at the given node's source range with related
503
+ // source locations. Older hosts that do not implement RelatedReporter receive
504
+ // the diagnostic without them, so design the rule to read well from the message
505
+ // alone. With no related locations it is exactly `Report`.
506
+ func (c *Context) ReportRelated(node *shimast.Node, message string, related ...RelatedInformation) {
507
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff || node == nil {
508
+ return
509
+ }
510
+ if len(related) == 0 {
511
+ c.reporter.Report(node, message)
512
+ return
513
+ }
514
+ reporter, ok := c.reporter.(RelatedReporter)
515
+ if !ok {
516
+ c.reporter.Report(node, message)
517
+ return
518
+ }
519
+ reporter.ReportRelated(node, message, related...)
520
+ }
521
+
522
+ // ReportRangeRelated records a finding at an explicit byte range with related
523
+ // source locations. Falls back to a plain range finding on a host without
524
+ // RelatedReporter, and equals `ReportRange` when no related locations are given.
525
+ func (c *Context) ReportRangeRelated(pos, end int, message string, related ...RelatedInformation) {
526
+ if c == nil || c.reporter == nil || c.Severity == SeverityOff {
527
+ return
528
+ }
529
+ if len(related) == 0 {
530
+ c.reporter.ReportRange(pos, end, message)
531
+ return
532
+ }
533
+ reporter, ok := c.reporter.(RelatedReporter)
534
+ if !ok {
535
+ c.reporter.ReportRange(pos, end, message)
536
+ return
537
+ }
538
+ reporter.ReportRangeRelated(pos, end, message, related...)
539
+ }
540
+
345
541
  var registry []Rule
346
542
 
347
543
  // Register adds a contributor rule to the global registry. Called from a
package/src/index.ts CHANGED
@@ -223,10 +223,10 @@ function resolveConfigFileContributors(
223
223
  if (!configPath || !fs.existsSync(configPath)) return [];
224
224
 
225
225
  const entries = readConfigPluginEntries(configPath, context);
226
- // Dedup on the Go-subpackage form (post hyphen→underscore transform)
227
- // so two namespaces that collapse to the same Go identifier surface
228
- // here instead of as the contributor validator's opaque
229
- // `duplicate name "a_b"` error.
226
+ assertContributorNamespacesDoNotCollide(entries, configPath);
227
+ // Dedup exact repeated namespaces on the Go-subpackage form. Config-array
228
+ // folding can surface the same namespace more than once; that existing
229
+ // behavior stays intact after distinct namespaces are rejected above.
230
230
  const occupied = new Set<string>();
231
231
  const out: TtscPluginContributor[] = [];
232
232
  for (const entry of entries) {
@@ -238,6 +238,37 @@ function resolveConfigFileContributors(
238
238
  return out;
239
239
  }
240
240
 
241
+ function assertContributorNamespacesDoNotCollide(
242
+ entries: ConfigPluginEntry[],
243
+ configPath: string,
244
+ ): void {
245
+ const namespacesByGoName = new Map<string, Set<string>>();
246
+ for (const entry of entries) {
247
+ const goName = goSubpackageName(entry.namespace);
248
+ let namespaces = namespacesByGoName.get(goName);
249
+ if (namespaces === undefined) {
250
+ namespaces = new Set<string>();
251
+ namespacesByGoName.set(goName, namespaces);
252
+ }
253
+ namespaces.add(entry.namespace);
254
+ }
255
+ const collisions = [...namespacesByGoName]
256
+ .map(([goName, namespaces]) => [goName, [...namespaces].sort()] as const)
257
+ .filter(([, namespaces]) => namespaces.length > 1)
258
+ .sort(([left], [right]) => left.localeCompare(right));
259
+ if (collisions.length === 0) return;
260
+
261
+ const details = collisions
262
+ .map(
263
+ ([goName, namespaces]) =>
264
+ `${namespaces.map((namespace) => JSON.stringify(namespace)).join(", ")} all normalize to ${JSON.stringify(goName)}`,
265
+ )
266
+ .join("; ");
267
+ throw new Error(
268
+ `@ttsc/lint: lint config ${configPath} contributor namespaces collide after Go normalization: ${details}`,
269
+ );
270
+ }
271
+
241
272
  /**
242
273
  * Rejects any tsconfig plugin-entry key that is neither a host framework key
243
274
  * nor the single lint-specific `configFile` key. Rule, format, and plugin
@@ -61,28 +61,32 @@ export interface ITtscLintFormat {
61
61
  * Pad the inside of single-line braces with one space. Mirrors Prettier's
62
62
  * `bracketSpacing`. `true` (the default) gives `{ x: 1 }`, `import { foo }`;
63
63
  * `false` gives `{x: 1}`, `import {foo}`. Applies to object literals, object
64
- * destructuring patterns, named imports/exports, and type literals; block,
65
- * class, interface, and enum braces are unaffected.
64
+ * destructuring patterns, named imports/exports, type literals, mapped types,
65
+ * and import attributes; block, class, interface, and enum braces are
66
+ * unaffected.
66
67
  *
67
68
  * @default true
68
69
  */
69
70
  bracketSpacing?: boolean;
70
71
 
71
72
  /**
72
- * Quoting policy for object-literal property keys. Mirrors Prettier's
73
- * `quoteProps`. `"as-needed"` (the default) removes quotes from a key that is
74
- * a valid identifier (`{ "foo": 1 }` becomes `{ foo: 1 }`), keeping them on
75
- * non-identifier or numeric keys (`"bar-baz"`, `"123"`). `"consistent"` keeps
76
- * every key quoted when any one of them requires quotes. `"preserve"` never
77
- * changes quoting.
73
+ * Quoting policy for object-literal keys plus class-method and type-member
74
+ * names. Mirrors Prettier's `quoteProps`. `"as-needed"` (the default) removes
75
+ * quotes from a key that is a valid identifier (`{ "foo": 1 }` becomes `{
76
+ * foo: 1 }`), keeping them on non-identifier or numeric keys (`"bar-baz"`,
77
+ * `"123"`). `"consistent"` quotes every object-literal identifier key when a
78
+ * sibling requires quotes. `"preserve"` never changes quoting. ttsc keeps
79
+ * `"__proto__"` and non-ASCII identifier keys quoted because unquoting can
80
+ * change runtime semantics or exceed its conservative identifier policy.
78
81
  *
79
82
  * @default "as-needed"
80
83
  */
81
84
  quoteProps?: "as-needed" | "consistent" | "preserve";
82
85
 
83
86
  /**
84
- * Trailing-comma policy. Mirrors Prettier's `trailingComma`. The `"none"`
85
- * mode disables the rule's edits.
87
+ * Trailing-comma policy. Mirrors Prettier's `trailingComma`. `"none"` removes
88
+ * an existing governed trailing comma, `"es5"` keeps it on ES5-level lists,
89
+ * and `"all"` also keeps it on calls and parameter lists.
86
90
  *
87
91
  * @default "all"
88
92
  */
@@ -103,7 +103,8 @@ export interface ITtscLintRegexpRules {
103
103
  /**
104
104
  * Reject regex flags that the literal does not exercise — `i` on a pattern
105
105
  * with no case-variable character, `m` on a pattern with no `^`/`$`
106
- * assertion.
106
+ * assertion. Autofixable: the diagnostic names the dead flags and the fix
107
+ * deletes exactly those, leaving the live ones in place.
107
108
  *
108
109
  * Cleans up flag combos that suggest behavior the pattern can never trigger.
109
110
  * A character is case-variable wherever it sits, so `/[a-z]/i` keeps its flag
@@ -121,12 +122,18 @@ export interface ITtscLintRegexpRules {
121
122
  * (`/a{1}/`), `?` on patterns already matching the empty string
122
123
  * (`/(?:a+|b*)?/`), and quantifiers on non-consuming atoms (`/(?:\b)+/`).
123
124
  *
125
+ * Autofixable for the constant-one count, except where the next character
126
+ * would change the meaning of the deletion: a lazy marker (`/a{1}?/` is
127
+ * "exactly one", not optional) or a digit (`/\1{1}2/` would fuse into
128
+ * backreference twelve).
129
+ *
124
130
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-quantifier.html
125
131
  */
126
132
  "regexp/no-useless-quantifier"?: TtscLintRuleSetting;
127
133
 
128
134
  /**
129
135
  * Reject equal min/max quantifiers (`/a{2,2}/`) in favor of `/a{2}/`.
136
+ * Autofixable.
130
137
  *
131
138
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-two-nums-quantifier.html
132
139
  */
@@ -136,42 +143,45 @@ export interface ITtscLintRegexpRules {
136
143
  * Reject zero-repeat quantifiers (`/a{0}/`, `/a{0,0}/`) — the atom never
137
144
  * matches, so the quantifier is either dead code or a typo for `{1,…}`.
138
145
  *
139
- * The fix is normally to delete the atom or correct the upper bound.
146
+ * Diagnostic-only: the correction is to delete the atom or repair the bound,
147
+ * and which one was meant is not recoverable from the source.
140
148
  *
141
149
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-zero-quantifier.html
142
150
  */
143
151
  "regexp/no-zero-quantifier"?: TtscLintRuleSetting;
144
152
 
145
153
  /**
146
- * Prefer `\d` over `[0-9]` in regex literals.
154
+ * Prefer `\d` over `[0-9]` in regex literals. Autofixable.
147
155
  *
148
156
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html
149
157
  */
150
158
  "regexp/prefer-d"?: TtscLintRuleSetting;
151
159
 
152
160
  /**
153
- * Prefer `+` over `{1,}` in regex literals.
161
+ * Prefer `+` over `{1,}` in regex literals. Autofixable.
154
162
  *
155
163
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html
156
164
  */
157
165
  "regexp/prefer-plus-quantifier"?: TtscLintRuleSetting;
158
166
 
159
167
  /**
160
- * Prefer `?` over `{0,1}` in regex literals.
168
+ * Prefer `?` over `{0,1}` in regex literals. Autofixable; a lazy `{0,1}`
169
+ * correctly becomes `??`.
161
170
  *
162
171
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-question-quantifier.html
163
172
  */
164
173
  "regexp/prefer-question-quantifier"?: TtscLintRuleSetting;
165
174
 
166
175
  /**
167
- * Prefer `*` over `{0,}` in regex literals.
176
+ * Prefer `*` over `{0,}` in regex literals. Autofixable.
168
177
  *
169
178
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-star-quantifier.html
170
179
  */
171
180
  "regexp/prefer-star-quantifier"?: TtscLintRuleSetting;
172
181
 
173
182
  /**
174
- * Prefer `\w` over `[A-Za-z0-9_]` in regex literals.
183
+ * Prefer `\w` over `[A-Za-z0-9_]` in regex literals. Autofixable for both
184
+ * accepted spellings.
175
185
  *
176
186
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-w.html
177
187
  */
@@ -181,6 +191,9 @@ export interface ITtscLintRegexpRules {
181
191
  * Require regex literals to use the `u` or `v` flag, so Unicode-property
182
192
  * escapes and surrogate-pair handling stay predictable.
183
193
  *
194
+ * Offers `u` and `v` as editor suggestions rather than an automatic fix: both
195
+ * satisfy the rule, and both change what the pattern matches.
196
+ *
184
197
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-regexp.html
185
198
  */
186
199
  "regexp/require-unicode-regexp"?: TtscLintRuleSetting;
@@ -191,7 +204,8 @@ export interface ITtscLintRegexpRules {
191
204
  * stricter escape rules on top of `u`.
192
205
  *
193
206
  * Choose this over `require-unicode-regexp` only on engines that ship
194
- * ES2024-era regex.
207
+ * ES2024-era regex. Offers one editor suggestion, which replaces an existing
208
+ * `u` because the two flags are mutually exclusive.
195
209
  *
196
210
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-sets-regexp.html
197
211
  */
@@ -201,7 +215,8 @@ export interface ITtscLintRegexpRules {
201
215
  * Require regex flags to appear in canonical alphabetical order (`dgimsuvy`).
202
216
  *
203
217
  * Stable ordering keeps diffs small and lets readers compare flag sets at a
204
- * glance.
218
+ * glance. Autofixable: a permutation of a flag run cannot change what the
219
+ * literal matches.
205
220
  *
206
221
  * @reference https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-flags.html
207
222
  */
@@ -67,6 +67,13 @@ export interface ITtscLintSecurityRules {
67
67
  * Detect `new Buffer(input)` constructions with non-literal input —
68
68
  * historical source of allocation-disclosure bugs.
69
69
  *
70
+ * The three successors — `Buffer.from`, `Buffer.alloc`, `Buffer.allocUnsafe`
71
+ * — are offered as editor suggestions and none is applied automatically:
72
+ * which one is correct depends on the argument, which this rule fires
73
+ * precisely because it cannot read. The rule stays untagged because its
74
+ * upstream-compatible name match also admits a user-defined constructor named
75
+ * `Buffer`.
76
+ *
70
77
  * @reference https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-new-buffer.md
71
78
  */
72
79
  "security/detect-new-buffer"?: TtscLintRuleSetting;
@@ -138,6 +145,17 @@ export interface ITtscLintSecurityRules {
138
145
  * Tokens, session ids, and key material must use `crypto.randomBytes` (or Web
139
146
  * Crypto's `getRandomValues`) instead.
140
147
  *
148
+ * Type-aware via the Checker, which resolves the object at the use site so an
149
+ * automatic rewrite is never applied to a shadowed binding. Enabling this
150
+ * rule therefore puts the whole run on the checker path. The diagnostic
151
+ * itself stays name-based, so it still reports without one.
152
+ *
153
+ * The member name alone is rewritten to `randomBytes`. The edit is automatic
154
+ * when the object is proven to be an import or require of Node `crypto`, and
155
+ * an editor suggestion otherwise, because a local application object can also
156
+ * be named `crypto`. That name-based diagnostic surface also keeps the rule
157
+ * untagged.
158
+ *
141
159
  * @reference https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-pseudoRandomBytes.md
142
160
  */
143
161
  "security/detect-pseudoRandomBytes"?: TtscLintRuleSetting;
@@ -31,8 +31,14 @@ export interface ITtscLintSolidRules {
31
31
 
32
32
  /**
33
33
  * Route each Solid export to the correct entry point (`solid-js`,
34
- * `solid-js/web`, or `solid-js/store`) and merge duplicate imports from the
35
- * same entry.
34
+ * `solid-js/web`, or `solid-js/store`) and relocate a misrouted one to where
35
+ * it belongs, joining an existing import from that entry when the file
36
+ * already has one. The diagnostic names the symbol and its entry point.
37
+ *
38
+ * Autofixable. A specifier that stands alone has its declaration's module
39
+ * specifier rewritten; one with siblings, or one beside a default binding, is
40
+ * cut out and relocated. A type-only declaration relocates into a type-only
41
+ * one, never into a value import.
36
42
  *
37
43
  * @reference https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/imports.md
38
44
  */
@@ -103,6 +109,15 @@ export interface ITtscLintSolidRules {
103
109
  * Reject React-style dependency arrays in Solid tracked scopes
104
110
  * (`createEffect(() => ..., [deps])`).
105
111
  *
112
+ * Type-aware via the Checker. The tag below claims the array is dead, so the
113
+ * callee has to be the Solid primitive itself rather than a same-named local
114
+ * or a shadowing parameter, and only symbol resolution can say that. Enabling
115
+ * this rule therefore puts the whole run on the checker path.
116
+ *
117
+ * Tagged `Unnecessary`: the reported range is the array literal alone, and
118
+ * Solid tracks dependencies automatically, so deleting it is the whole
119
+ * resolution and an editor greys it out.
120
+ *
106
121
  * @reference https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-react-deps.md
107
122
  */
108
123
  "solid/no-react-deps"?: TtscLintRuleSetting;
@@ -111,6 +126,11 @@ export interface ITtscLintSolidRules {
111
126
  * Reject React-specific JSX props such as `className` and `htmlFor` — Solid
112
127
  * uses `class` and `for`.
113
128
  *
129
+ * The two renames are autofixed by rewriting the name token alone, so the
130
+ * value survives untouched. The `key` arm stays diagnostic-only: a Solid DOM
131
+ * element does not consume `key`, so its resolution is a deletion that has to
132
+ * take the surrounding whitespace with it.
133
+ *
114
134
  * @reference https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-react-specific-props.md
115
135
  */
116
136
  "solid/no-react-specific-props"?: TtscLintRuleSetting;