@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
@@ -1,8 +1,8 @@
1
1
  package linthost
2
2
 
3
3
  import (
4
- "encoding/json"
5
- "fmt"
4
+ "encoding/json"
5
+ "fmt"
6
6
  )
7
7
 
8
8
  // expandFormatBlock translates a Prettier-style `format` block into the
@@ -10,9 +10,8 @@ import (
10
10
  // The result is a `map[string]any` that mirrors what a user would
11
11
  // have written under `rules` directly — entries like
12
12
  // `"format/semi": ["off", {"prefer": "always"}]` — so the caller
13
- // can route it through either `ParseRulesWithOptions` (inline path) or
14
- // `parseExternalRuleMapInto` (flat-config path) without duplicating
15
- // option-decoding logic.
13
+ // can route it through either `ParseRulesWithOptions` or
14
+ // `parseExternalRuleMapInto` without duplicating option-decoding logic.
16
15
  //
17
16
  // The block's default severity is off. That keeps check/build diagnostics
18
17
  // independent from formatting policy unless the user explicitly sets
@@ -33,189 +32,206 @@ import (
33
32
  // semantics) before invoking the existing parsers — `mergeRuleMaps`
34
33
  // below performs the merge with the right precedence.
35
34
  func expandFormatBlock(raw map[string]any) (map[string]any, error) {
36
- if raw == nil {
37
- return map[string]any{}, nil
38
- }
39
- if err := rejectUnknownFormatKeys(raw); err != nil {
40
- return nil, err
41
- }
42
- severity, err := formatBlockSeverity(raw)
43
- if err != nil {
44
- return nil, err
45
- }
46
- ruleEntry := func(options map[string]any) []any {
47
- return []any{severity.String(), options}
48
- }
49
- out := map[string]any{}
35
+ if raw == nil {
36
+ return map[string]any{}, nil
37
+ }
38
+ if err := rejectUnknownFormatKeys(raw); err != nil {
39
+ return nil, err
40
+ }
41
+ severity, err := formatBlockSeverity(raw)
42
+ if err != nil {
43
+ return nil, err
44
+ }
45
+ ruleEntry := func(options map[string]any) []any {
46
+ return []any{severity.String(), options}
47
+ }
48
+ out := map[string]any{}
50
49
 
51
- // format/semi
52
- semiPrefer := "always"
53
- if v, ok := raw["semi"]; ok {
54
- b, err := asBool("format.semi", v)
55
- if err != nil {
56
- return nil, err
57
- }
58
- if !b {
59
- semiPrefer = "never"
60
- }
61
- }
62
- out["format/semi"] = ruleEntry(map[string]any{"prefer": semiPrefer})
50
+ // format/semi
51
+ semiPrefer := "always"
52
+ if v, ok := raw["semi"]; ok {
53
+ b, err := asBool("format.semi", v)
54
+ if err != nil {
55
+ return nil, err
56
+ }
57
+ if !b {
58
+ semiPrefer = "never"
59
+ }
60
+ }
61
+ out["format/semi"] = ruleEntry(map[string]any{"prefer": semiPrefer})
63
62
 
64
- // format/quotes
65
- quotesPrefer := "double"
66
- if v, ok := raw["singleQuote"]; ok {
67
- b, err := asBool("format.singleQuote", v)
68
- if err != nil {
69
- return nil, err
70
- }
71
- if b {
72
- quotesPrefer = "single"
73
- }
74
- }
75
- out["format/quotes"] = ruleEntry(map[string]any{"prefer": quotesPrefer})
63
+ // format/quotes
64
+ quotesPrefer := "double"
65
+ if v, ok := raw["singleQuote"]; ok {
66
+ b, err := asBool("format.singleQuote", v)
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
+ if b {
71
+ quotesPrefer = "single"
72
+ }
73
+ }
74
+ out["format/quotes"] = ruleEntry(map[string]any{"prefer": quotesPrefer})
76
75
 
77
- // format/trailing-comma
78
- tcMode := "all"
79
- if v, ok := raw["trailingComma"]; ok {
80
- s, err := asString("format.trailingComma", v)
81
- if err != nil {
82
- return nil, err
83
- }
84
- switch s {
85
- case "all", "es5", "none":
86
- tcMode = s
87
- default:
88
- return nil, fmt.Errorf("@ttsc/lint: format.trailingComma must be \"all\", \"es5\", or \"none\"; got %q", s)
89
- }
90
- }
91
- out["format/trailing-comma"] = ruleEntry(map[string]any{"mode": tcMode})
76
+ // format/trailing-comma
77
+ tcMode := "all"
78
+ if v, ok := raw["trailingComma"]; ok {
79
+ s, err := asString("format.trailingComma", v)
80
+ if err != nil {
81
+ return nil, err
82
+ }
83
+ switch s {
84
+ case "all", "es5", "none":
85
+ tcMode = s
86
+ default:
87
+ return nil, fmt.Errorf("@ttsc/lint: format.trailingComma must be \"all\", \"es5\", or \"none\"; got %q", s)
88
+ }
89
+ }
90
+ out["format/trailing-comma"] = ruleEntry(map[string]any{"mode": tcMode})
92
91
 
93
- // format/print-width
94
- pwOpts := map[string]any{}
95
- if v, ok := raw["printWidth"]; ok {
96
- n, err := asInt("format.printWidth", v)
97
- if err != nil {
98
- return nil, err
99
- }
100
- pwOpts["printWidth"] = n
101
- }
102
- if v, ok := raw["tabWidth"]; ok {
103
- n, err := asInt("format.tabWidth", v)
104
- if err != nil {
105
- return nil, err
106
- }
107
- pwOpts["tabWidth"] = n
108
- }
109
- if v, ok := raw["useTabs"]; ok {
110
- b, err := asBool("format.useTabs", v)
111
- if err != nil {
112
- return nil, err
113
- }
114
- pwOpts["useTabs"] = b
115
- }
116
- if v, ok := raw["endOfLine"]; ok {
117
- s, err := asString("format.endOfLine", v)
118
- if err != nil {
119
- return nil, err
120
- }
121
- if s != "lf" && s != "crlf" {
122
- return nil, fmt.Errorf("@ttsc/lint: format.endOfLine must be \"lf\" or \"crlf\"; got %q", s)
123
- }
124
- pwOpts["endOfLine"] = s
125
- }
126
- out["format/print-width"] = ruleEntry(pwOpts)
92
+ // format/print-width
93
+ //
94
+ // `trailingComma` is mirrored into the print-width rule's options so
95
+ // the printer's broken-list reflow emits the same trailing-comma
96
+ // shape `format/trailing-comma` does. Without the mirror the two
97
+ // rules disagree on `es5` / `none` projects and oscillate on every
98
+ // cascade pass — the trailing-comma rule says "no comma" while the
99
+ // printer adds one back. See `printArgList` in print_nodes_call.go.
100
+ pwOpts := map[string]any{"trailingComma": tcMode}
101
+ if v, ok := raw["printWidth"]; ok {
102
+ n, err := asInt("format.printWidth", v)
103
+ if err != nil {
104
+ return nil, err
105
+ }
106
+ if n < 1 {
107
+ return nil, fmt.Errorf("@ttsc/lint: format.printWidth must be a positive integer; got %d", n)
108
+ }
109
+ pwOpts["printWidth"] = n
110
+ }
111
+ if v, ok := raw["tabWidth"]; ok {
112
+ n, err := asInt("format.tabWidth", v)
113
+ if err != nil {
114
+ return nil, err
115
+ }
116
+ if n < 1 {
117
+ return nil, fmt.Errorf("@ttsc/lint: format.tabWidth must be a positive integer; got %d", n)
118
+ }
119
+ pwOpts["tabWidth"] = n
120
+ }
121
+ if v, ok := raw["useTabs"]; ok {
122
+ b, err := asBool("format.useTabs", v)
123
+ if err != nil {
124
+ return nil, err
125
+ }
126
+ pwOpts["useTabs"] = b
127
+ }
128
+ if v, ok := raw["endOfLine"]; ok {
129
+ s, err := asString("format.endOfLine", v)
130
+ if err != nil {
131
+ return nil, err
132
+ }
133
+ if s != "lf" && s != "crlf" {
134
+ return nil, fmt.Errorf("@ttsc/lint: format.endOfLine must be \"lf\" or \"crlf\"; got %q", s)
135
+ }
136
+ pwOpts["endOfLine"] = s
137
+ }
138
+ out["format/print-width"] = ruleEntry(pwOpts)
127
139
 
128
- // format/sort-imports — opt-in by `importOrder`.
129
- if v, ok := raw["importOrder"]; ok {
130
- siOpts := map[string]any{}
131
- order, err := asStringSlice("format.importOrder", v)
132
- if err != nil {
133
- return nil, err
134
- }
135
- if len(order) == 0 {
136
- return nil, fmt.Errorf("@ttsc/lint: format.importOrder must contain at least one entry; omit the field to keep format/sort-imports off")
137
- }
138
- siOpts["importOrder"] = order
139
- if x, ok := raw["importOrderSeparation"]; ok {
140
- b, err := asBool("format.importOrderSeparation", x)
141
- if err != nil {
142
- return nil, err
143
- }
144
- siOpts["importOrderSeparation"] = b
145
- }
146
- if x, ok := raw["importOrderSortSpecifiers"]; ok {
147
- b, err := asBool("format.importOrderSortSpecifiers", x)
148
- if err != nil {
149
- return nil, err
150
- }
151
- siOpts["importOrderSortSpecifiers"] = b
152
- }
153
- if x, ok := raw["importOrderCaseInsensitive"]; ok {
154
- b, err := asBool("format.importOrderCaseInsensitive", x)
155
- if err != nil {
156
- return nil, err
157
- }
158
- siOpts["importOrderCaseInsensitive"] = b
159
- }
160
- out["format/sort-imports"] = ruleEntry(siOpts)
161
- }
140
+ // format/sort-imports — opt-in by `importOrder`.
141
+ if v, ok := raw["importOrder"]; ok {
142
+ siOpts := map[string]any{}
143
+ order, err := asStringSlice("format.importOrder", v)
144
+ if err != nil {
145
+ return nil, err
146
+ }
147
+ if len(order) == 0 {
148
+ return nil, fmt.Errorf("@ttsc/lint: format.importOrder must contain at least one entry; omit the field to keep format/sort-imports off")
149
+ }
150
+ siOpts["importOrder"] = order
151
+ if x, ok := raw["importOrderSeparation"]; ok {
152
+ b, err := asBool("format.importOrderSeparation", x)
153
+ if err != nil {
154
+ return nil, err
155
+ }
156
+ siOpts["importOrderSeparation"] = b
157
+ }
158
+ if x, ok := raw["importOrderSortSpecifiers"]; ok {
159
+ b, err := asBool("format.importOrderSortSpecifiers", x)
160
+ if err != nil {
161
+ return nil, err
162
+ }
163
+ siOpts["importOrderSortSpecifiers"] = b
164
+ }
165
+ if x, ok := raw["importOrderCaseInsensitive"]; ok {
166
+ b, err := asBool("format.importOrderCaseInsensitive", x)
167
+ if err != nil {
168
+ return nil, err
169
+ }
170
+ siOpts["importOrderCaseInsensitive"] = b
171
+ }
172
+ out["format/sort-imports"] = ruleEntry(siOpts)
173
+ }
162
174
 
163
- // format/jsdoc — opt-in by `jsdoc` truthy (boolean or object).
164
- if v, ok := raw["jsdoc"]; ok && v != nil {
165
- jdOpts := map[string]any{}
166
- enabled := false
167
- switch j := v.(type) {
168
- case bool:
169
- enabled = j
170
- case map[string]any:
171
- enabled = true
172
- for key, val := range j {
173
- switch key {
174
- case "tagSynonyms":
175
- ts, ok := val.(map[string]any)
176
- if !ok {
177
- return nil, fmt.Errorf("@ttsc/lint: format.jsdoc.tagSynonyms must be an object, got %T", val)
178
- }
179
- // Element values must be strings; surface
180
- // typos early instead of after a downstream
181
- // JSON-decode failure.
182
- for k, v := range ts {
183
- if _, ok := v.(string); !ok {
184
- return nil, fmt.Errorf("@ttsc/lint: format.jsdoc.tagSynonyms[%q] must be a string, got %T", k, v)
185
- }
186
- }
187
- jdOpts["tagSynonyms"] = ts
188
- case "sortTags":
189
- b, err := asBool("format.jsdoc.sortTags", val)
190
- if err != nil {
191
- return nil, err
192
- }
193
- jdOpts["sortTags"] = b
194
- default:
195
- return nil, fmt.Errorf("@ttsc/lint: format.jsdoc unknown key %q (allowed: tagSynonyms, sortTags)", key)
196
- }
197
- }
198
- default:
199
- return nil, fmt.Errorf("@ttsc/lint: format.jsdoc must be a boolean or object, got %T", v)
200
- }
201
- if enabled {
202
- out["format/jsdoc"] = ruleEntry(jdOpts)
203
- }
204
- }
175
+ // format/jsdoc — opt-in by `jsdoc` truthy (boolean or object).
176
+ if v, ok := raw["jsdoc"]; ok && v != nil {
177
+ jdOpts := map[string]any{}
178
+ enabled := false
179
+ switch j := v.(type) {
180
+ case bool:
181
+ enabled = j
182
+ case map[string]any:
183
+ enabled = true
184
+ for key, val := range j {
185
+ switch key {
186
+ case "tagSynonyms":
187
+ ts, ok := val.(map[string]any)
188
+ if !ok {
189
+ return nil, fmt.Errorf("@ttsc/lint: format.jsdoc.tagSynonyms must be an object, got %T", val)
190
+ }
191
+ // Element values must be strings; surface
192
+ // typos early instead of after a downstream
193
+ // JSON-decode failure.
194
+ for k, v := range ts {
195
+ if _, ok := v.(string); !ok {
196
+ return nil, fmt.Errorf("@ttsc/lint: format.jsdoc.tagSynonyms[%q] must be a string, got %T", k, v)
197
+ }
198
+ }
199
+ jdOpts["tagSynonyms"] = ts
200
+ case "sortTags":
201
+ b, err := asBool("format.jsdoc.sortTags", val)
202
+ if err != nil {
203
+ return nil, err
204
+ }
205
+ jdOpts["sortTags"] = b
206
+ default:
207
+ return nil, fmt.Errorf("@ttsc/lint: format.jsdoc unknown key %q (allowed: tagSynonyms, sortTags)", key)
208
+ }
209
+ }
210
+ default:
211
+ return nil, fmt.Errorf("@ttsc/lint: format.jsdoc must be a boolean or object, got %T", v)
212
+ }
213
+ if enabled {
214
+ out["format/jsdoc"] = ruleEntry(jdOpts)
215
+ }
216
+ }
205
217
 
206
- return out, nil
218
+ return out, nil
207
219
  }
208
220
 
221
+ // formatBlockSeverity extracts the optional `severity` field from a format
222
+ // block. Defaults to SeverityOff so that format rules never produce check/build
223
+ // diagnostics unless the user explicitly opts in. SeverityOff still allows
224
+ // `ttsc format` to apply formatting; it only suppresses error/warning output.
209
225
  func formatBlockSeverity(raw map[string]any) (Severity, error) {
210
- value, ok := raw["severity"]
211
- if !ok || value == nil {
212
- return SeverityOff, nil
213
- }
214
- severity, err := parseSeverity(value)
215
- if err != nil {
216
- return SeverityOff, fmt.Errorf("@ttsc/lint: format.severity: %w", err)
217
- }
218
- return severity, nil
226
+ value, ok := raw["severity"]
227
+ if !ok || value == nil {
228
+ return SeverityOff, nil
229
+ }
230
+ severity, err := parseSeverity(value)
231
+ if err != nil {
232
+ return SeverityOff, fmt.Errorf("@ttsc/lint: format.severity: %w", err)
233
+ }
234
+ return severity, nil
219
235
  }
220
236
 
221
237
  // mergeRuleMaps overlays `overrides` on `base` and returns the merged
@@ -225,91 +241,100 @@ func formatBlockSeverity(raw map[string]any) (Severity, error) {
225
241
  // `format/*` rule fully replaces the corresponding entry expanded
226
242
  // from the `format` block.
227
243
  func mergeRuleMaps(base, overrides map[string]any) map[string]any {
228
- out := make(map[string]any, len(base)+len(overrides))
229
- for k, v := range base {
230
- out[k] = v
231
- }
232
- for k, v := range overrides {
233
- out[k] = v
234
- }
235
- return out
244
+ out := make(map[string]any, len(base)+len(overrides))
245
+ for k, v := range base {
246
+ out[k] = v
247
+ }
248
+ for k, v := range overrides {
249
+ out[k] = v
250
+ }
251
+ return out
236
252
  }
237
253
 
254
+ // asBool coerces a raw config value to a bool, returning a typed error on
255
+ // failure. Used by expandFormatBlock to validate boolean format fields.
238
256
  func asBool(field string, v any) (bool, error) {
239
- if b, ok := v.(bool); ok {
240
- return b, nil
241
- }
242
- return false, fmt.Errorf("@ttsc/lint: %s must be a boolean, got %T", field, v)
257
+ if b, ok := v.(bool); ok {
258
+ return b, nil
259
+ }
260
+ return false, fmt.Errorf("@ttsc/lint: %s must be a boolean, got %T", field, v)
243
261
  }
244
262
 
263
+ // asString coerces a raw config value to a string, returning a typed error on
264
+ // failure. Used by expandFormatBlock to validate string format fields.
245
265
  func asString(field string, v any) (string, error) {
246
- if s, ok := v.(string); ok {
247
- return s, nil
248
- }
249
- return "", fmt.Errorf("@ttsc/lint: %s must be a string, got %T", field, v)
266
+ if s, ok := v.(string); ok {
267
+ return s, nil
268
+ }
269
+ return "", fmt.Errorf("@ttsc/lint: %s must be a string, got %T", field, v)
250
270
  }
251
271
 
272
+ // asInt coerces a raw config value to an int. Accepts all integer-shaped Go
273
+ // numeric types plus float64 (the default JSON decode type) and json.Number,
274
+ // rejecting fractional float64 values since no format option takes a non-integer.
252
275
  func asInt(field string, v any) (int, error) {
253
- switch n := v.(type) {
254
- case int:
255
- return n, nil
256
- case int32:
257
- return int(n), nil
258
- case int64:
259
- return int(n), nil
260
- case float64:
261
- // JSON numbers decode as float64; coerce when integer-valued.
262
- if float64(int(n)) == n {
263
- return int(n), nil
264
- }
265
- case json.Number:
266
- i, err := n.Int64()
267
- if err == nil {
268
- return int(i), nil
269
- }
270
- }
271
- return 0, fmt.Errorf("@ttsc/lint: %s must be an integer, got %T", field, v)
276
+ switch n := v.(type) {
277
+ case int:
278
+ return n, nil
279
+ case int32:
280
+ return int(n), nil
281
+ case int64:
282
+ return int(n), nil
283
+ case float64:
284
+ // JSON numbers decode as float64; coerce when integer-valued.
285
+ if float64(int(n)) == n {
286
+ return int(n), nil
287
+ }
288
+ case json.Number:
289
+ i, err := n.Int64()
290
+ if err == nil {
291
+ return int(i), nil
292
+ }
293
+ }
294
+ return 0, fmt.Errorf("@ttsc/lint: %s must be an integer, got %T", field, v)
272
295
  }
273
296
 
297
+ // asStringSlice coerces a raw config value to a []string, returning a typed
298
+ // error on failure. Used by expandFormatBlock to validate importOrder.
274
299
  func asStringSlice(field string, v any) ([]string, error) {
275
- arr, ok := v.([]any)
276
- if !ok {
277
- return nil, fmt.Errorf("@ttsc/lint: %s must be an array of strings, got %T", field, v)
278
- }
279
- out := make([]string, 0, len(arr))
280
- for i, item := range arr {
281
- s, ok := item.(string)
282
- if !ok {
283
- return nil, fmt.Errorf("@ttsc/lint: %s[%d] must be a string, got %T", field, i, item)
284
- }
285
- out = append(out, s)
286
- }
287
- return out, nil
300
+ arr, ok := v.([]any)
301
+ if !ok {
302
+ return nil, fmt.Errorf("@ttsc/lint: %s must be an array of strings, got %T", field, v)
303
+ }
304
+ out := make([]string, 0, len(arr))
305
+ for i, item := range arr {
306
+ s, ok := item.(string)
307
+ if !ok {
308
+ return nil, fmt.Errorf("@ttsc/lint: %s[%d] must be a string, got %T", field, i, item)
309
+ }
310
+ out = append(out, s)
311
+ }
312
+ return out, nil
288
313
  }
289
314
 
290
315
  // rejectUnknownFormatKeys surfaces typos in top-level format-block
291
316
  // keys at the boundary rather than silently ignoring them. The
292
317
  // key set mirrors `ITtscLintFormatConfig` exactly.
293
318
  func rejectUnknownFormatKeys(raw map[string]any) error {
294
- allowed := map[string]struct{}{
295
- "severity": {},
296
- "semi": {},
297
- "singleQuote": {},
298
- "trailingComma": {},
299
- "printWidth": {},
300
- "tabWidth": {},
301
- "useTabs": {},
302
- "endOfLine": {},
303
- "importOrder": {},
304
- "importOrderSeparation": {},
305
- "importOrderSortSpecifiers": {},
306
- "importOrderCaseInsensitive": {},
307
- "jsdoc": {},
308
- }
309
- for key := range raw {
310
- if _, ok := allowed[key]; !ok {
311
- return fmt.Errorf("@ttsc/lint: format unknown key %q; see ITtscLintFormatConfig for the allowed surface", key)
312
- }
313
- }
314
- return nil
319
+ allowed := map[string]struct{}{
320
+ "severity": {},
321
+ "semi": {},
322
+ "singleQuote": {},
323
+ "trailingComma": {},
324
+ "printWidth": {},
325
+ "tabWidth": {},
326
+ "useTabs": {},
327
+ "endOfLine": {},
328
+ "importOrder": {},
329
+ "importOrderSeparation": {},
330
+ "importOrderSortSpecifiers": {},
331
+ "importOrderCaseInsensitive": {},
332
+ "jsdoc": {},
333
+ }
334
+ for key := range raw {
335
+ if _, ok := allowed[key]; !ok {
336
+ return fmt.Errorf("@ttsc/lint: format unknown key %q; see ITtscLintFormatConfig for the allowed surface", key)
337
+ }
338
+ }
339
+ return nil
315
340
  }
@@ -68,6 +68,13 @@ type contributorAdapter struct {
68
68
  inner rule.Rule
69
69
  }
70
70
 
71
+ // NeedsTypeChecker keeps contributor rules on the historical checker path.
72
+ // The public rule.Context exposes Checker and has no mandatory marker, so the
73
+ // host cannot safely infer that a third-party rule is AST-only.
74
+ func (a contributorAdapter) NeedsTypeChecker() bool {
75
+ return true
76
+ }
77
+
71
78
  // formatContributorAdapter is the FormatRule-tagged variant of
72
79
  // contributorAdapter. Wrapping the lint-only adapter (rather than
73
80
  // duplicating its method set) keeps the marker addition trivial and