@ttsc/lint 0.11.0-dev.20260518-2 → 0.12.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 (57) hide show
  1. package/README.md +67 -141
  2. package/lib/{structures/defaultFormat.d.ts → defaultFormat.d.ts} +3 -3
  3. package/lib/{structures/defaultFormat.js → defaultFormat.js} +3 -3
  4. package/lib/defaultFormat.js.map +1 -0
  5. package/lib/index.d.ts +1 -38
  6. package/lib/index.js +7 -5
  7. package/lib/index.js.map +1 -1
  8. package/lib/structures/ITtscLintConfig.d.ts +27 -0
  9. package/lib/structures/{PluginRuleNames.js → ITtscLintConfig.js} +1 -1
  10. package/lib/structures/ITtscLintConfig.js.map +1 -0
  11. package/lib/structures/{TtscLintFormatConfig.d.ts → ITtscLintFormatConfig.d.ts} +17 -16
  12. package/lib/structures/{TtscLintConfig.js → ITtscLintFormatConfig.js} +1 -1
  13. package/lib/structures/ITtscLintFormatConfig.js.map +1 -0
  14. package/lib/structures/ITtscLintPluginConfig.d.ts +6 -6
  15. package/lib/structures/TtscLintRuleMap.d.ts +11 -9
  16. package/lib/structures/TtscLintRuleOptions.d.ts +118 -144
  17. package/lib/structures/index.d.ts +2 -7
  18. package/lib/structures/index.js +2 -7
  19. package/lib/structures/index.js.map +1 -1
  20. package/linthost/config.go +1 -6
  21. package/linthost/config_format.go +253 -298
  22. package/linthost/format.go +91 -1
  23. package/package.json +3 -3
  24. package/rule/rule.go +124 -124
  25. package/src/{structures/defaultFormat.ts → defaultFormat.ts} +5 -5
  26. package/src/index.ts +7 -5
  27. package/src/structures/ITtscLintConfig.ts +33 -0
  28. package/src/structures/{TtscLintFormatConfig.ts → ITtscLintFormatConfig.ts} +17 -16
  29. package/src/structures/ITtscLintPluginConfig.ts +6 -6
  30. package/src/structures/TtscLintRuleMap.ts +17 -13
  31. package/src/structures/TtscLintRuleOptions.ts +118 -144
  32. package/src/structures/index.ts +2 -7
  33. package/lib/defineConfig.d.ts +0 -55
  34. package/lib/defineConfig.js +0 -39
  35. package/lib/defineConfig.js.map +0 -1
  36. package/lib/structures/PluginRuleNames.d.ts +0 -14
  37. package/lib/structures/PluginRuleNames.js.map +0 -1
  38. package/lib/structures/TtscLintConfig.d.ts +0 -27
  39. package/lib/structures/TtscLintConfig.js.map +0 -1
  40. package/lib/structures/TtscLintConfigEntry.d.ts +0 -39
  41. package/lib/structures/TtscLintConfigEntry.js +0 -3
  42. package/lib/structures/TtscLintConfigEntry.js.map +0 -1
  43. package/lib/structures/TtscLintFormatConfig.js +0 -3
  44. package/lib/structures/TtscLintFormatConfig.js.map +0 -1
  45. package/lib/structures/TtscLintPlugins.d.ts +0 -9
  46. package/lib/structures/TtscLintPlugins.js +0 -3
  47. package/lib/structures/TtscLintPlugins.js.map +0 -1
  48. package/lib/structures/TtscLintRuleEntry.d.ts +0 -22
  49. package/lib/structures/TtscLintRuleEntry.js +0 -3
  50. package/lib/structures/TtscLintRuleEntry.js.map +0 -1
  51. package/lib/structures/defaultFormat.js.map +0 -1
  52. package/src/defineConfig.ts +0 -69
  53. package/src/structures/PluginRuleNames.ts +0 -19
  54. package/src/structures/TtscLintConfig.ts +0 -31
  55. package/src/structures/TtscLintConfigEntry.ts +0 -50
  56. package/src/structures/TtscLintPlugins.ts +0 -10
  57. package/src/structures/TtscLintRuleEntry.ts +0 -28
@@ -1,19 +1,24 @@
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
9
9
  // per-rule severity + options shape the existing rule parsers consume.
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
- // `"format/semi": ["warning", {"prefer": "always"}]` — so the caller
12
+ // `"format/semi": ["off", {"prefer": "always"}]` — so the caller
13
13
  // can route it through either `ParseRulesWithOptions` (inline path) or
14
14
  // `parseExternalRuleMapInto` (flat-config path) without duplicating
15
15
  // option-decoding logic.
16
16
  //
17
+ // The block's default severity is off. That keeps check/build diagnostics
18
+ // independent from formatting policy unless the user explicitly sets
19
+ // `format.severity`; the options still exist so `ttsc format` can apply the
20
+ // formatter from the same config block.
21
+ //
17
22
  // The block's enablement matrix:
18
23
  //
19
24
  // - `format/semi` — always on. `semi: false` flips to `prefer: "never"`.
@@ -23,193 +28,194 @@ import (
23
28
  // - `format/sort-imports` — opt-in by setting `importOrder`.
24
29
  // - `format/jsdoc` — opt-in by setting `jsdoc` truthy.
25
30
  //
26
- // `severity` controls the per-finding severity for every emitted entry.
27
- // Default is "warning". Setting it to "off" returns an empty map so the
28
- // rest of the loader treats the block as if it were absent.
29
- //
30
31
  // The returned map is the raw form rules parsers expect. Callers MUST
31
32
  // merge any user-supplied `rules` map on top of this one (rules-wins
32
33
  // semantics) before invoking the existing parsers — `mergeRuleMaps`
33
34
  // below performs the merge with the right precedence.
34
35
  func expandFormatBlock(raw map[string]any) (map[string]any, error) {
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 := "warning"
42
- if s, ok := raw["severity"]; ok && s != nil {
43
- resolved, err := parseFormatSeverity(s)
44
- if err != nil {
45
- return nil, err
46
- }
47
- severity = resolved
48
- }
49
- if severityIsOff(severity) {
50
- // `severity: "off"` collapses the whole block to a no-op so the
51
- // engine never registers any of the format rules.
52
- return map[string]any{}, nil
53
- }
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{}
54
50
 
55
- out := map[string]any{}
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})
56
63
 
57
- // format/semi
58
- semiPrefer := "always"
59
- if v, ok := raw["semi"]; ok {
60
- b, err := asBool("format.semi", v)
61
- if err != nil {
62
- return nil, err
63
- }
64
- if !b {
65
- semiPrefer = "never"
66
- }
67
- }
68
- out["format/semi"] = []any{severity, map[string]any{"prefer": semiPrefer}}
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})
69
76
 
70
- // format/quotes
71
- quotesPrefer := "double"
72
- if v, ok := raw["singleQuote"]; ok {
73
- b, err := asBool("format.singleQuote", v)
74
- if err != nil {
75
- return nil, err
76
- }
77
- if b {
78
- quotesPrefer = "single"
79
- }
80
- }
81
- out["format/quotes"] = []any{severity, map[string]any{"prefer": quotesPrefer}}
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})
82
92
 
83
- // format/trailing-comma
84
- tcMode := "all"
85
- if v, ok := raw["trailingComma"]; ok {
86
- s, err := asString("format.trailingComma", v)
87
- if err != nil {
88
- return nil, err
89
- }
90
- switch s {
91
- case "all", "es5", "none":
92
- tcMode = s
93
- default:
94
- return nil, fmt.Errorf("@ttsc/lint: format.trailingComma must be \"all\", \"es5\", or \"none\"; got %q", s)
95
- }
96
- }
97
- out["format/trailing-comma"] = []any{severity, map[string]any{"mode": tcMode}}
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)
98
127
 
99
- // format/print-width
100
- pwOpts := map[string]any{}
101
- if v, ok := raw["printWidth"]; ok {
102
- n, err := asInt("format.printWidth", v)
103
- if err != nil {
104
- return nil, err
105
- }
106
- pwOpts["printWidth"] = n
107
- }
108
- if v, ok := raw["tabWidth"]; ok {
109
- n, err := asInt("format.tabWidth", v)
110
- if err != nil {
111
- return nil, err
112
- }
113
- pwOpts["tabWidth"] = n
114
- }
115
- if v, ok := raw["useTabs"]; ok {
116
- b, err := asBool("format.useTabs", v)
117
- if err != nil {
118
- return nil, err
119
- }
120
- pwOpts["useTabs"] = b
121
- }
122
- if v, ok := raw["endOfLine"]; ok {
123
- s, err := asString("format.endOfLine", v)
124
- if err != nil {
125
- return nil, err
126
- }
127
- if s != "lf" && s != "crlf" {
128
- return nil, fmt.Errorf("@ttsc/lint: format.endOfLine must be \"lf\" or \"crlf\"; got %q", s)
129
- }
130
- pwOpts["endOfLine"] = s
131
- }
132
- out["format/print-width"] = []any{severity, pwOpts}
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
+ }
133
162
 
134
- // format/sort-imports — opt-in by `importOrder`.
135
- if v, ok := raw["importOrder"]; ok {
136
- siOpts := map[string]any{}
137
- order, err := asStringSlice("format.importOrder", v)
138
- if err != nil {
139
- return nil, err
140
- }
141
- if len(order) == 0 {
142
- return nil, fmt.Errorf("@ttsc/lint: format.importOrder must contain at least one entry; omit the field to keep format/sort-imports off")
143
- }
144
- siOpts["importOrder"] = order
145
- if x, ok := raw["importOrderSeparation"]; ok {
146
- b, err := asBool("format.importOrderSeparation", x)
147
- if err != nil {
148
- return nil, err
149
- }
150
- siOpts["importOrderSeparation"] = b
151
- }
152
- if x, ok := raw["importOrderSortSpecifiers"]; ok {
153
- b, err := asBool("format.importOrderSortSpecifiers", x)
154
- if err != nil {
155
- return nil, err
156
- }
157
- siOpts["importOrderSortSpecifiers"] = b
158
- }
159
- if x, ok := raw["importOrderCaseInsensitive"]; ok {
160
- b, err := asBool("format.importOrderCaseInsensitive", x)
161
- if err != nil {
162
- return nil, err
163
- }
164
- siOpts["importOrderCaseInsensitive"] = b
165
- }
166
- out["format/sort-imports"] = []any{severity, siOpts}
167
- }
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
+ }
168
205
 
169
- // format/jsdoc — opt-in by `jsdoc` truthy (boolean or object).
170
- if v, ok := raw["jsdoc"]; ok && v != nil {
171
- jdOpts := map[string]any{}
172
- enabled := false
173
- switch j := v.(type) {
174
- case bool:
175
- enabled = j
176
- case map[string]any:
177
- enabled = true
178
- for key, val := range j {
179
- switch key {
180
- case "tagSynonyms":
181
- ts, ok := val.(map[string]any)
182
- if !ok {
183
- return nil, fmt.Errorf("@ttsc/lint: format.jsdoc.tagSynonyms must be an object, got %T", val)
184
- }
185
- // Element values must be strings; surface
186
- // typos early instead of after a downstream
187
- // JSON-decode failure.
188
- for k, v := range ts {
189
- if _, ok := v.(string); !ok {
190
- return nil, fmt.Errorf("@ttsc/lint: format.jsdoc.tagSynonyms[%q] must be a string, got %T", k, v)
191
- }
192
- }
193
- jdOpts["tagSynonyms"] = ts
194
- case "sortTags":
195
- b, err := asBool("format.jsdoc.sortTags", val)
196
- if err != nil {
197
- return nil, err
198
- }
199
- jdOpts["sortTags"] = b
200
- default:
201
- return nil, fmt.Errorf("@ttsc/lint: format.jsdoc unknown key %q (allowed: tagSynonyms, sortTags)", key)
202
- }
203
- }
204
- default:
205
- return nil, fmt.Errorf("@ttsc/lint: format.jsdoc must be a boolean or object, got %T", v)
206
- }
207
- if enabled {
208
- out["format/jsdoc"] = []any{severity, jdOpts}
209
- }
210
- }
206
+ return out, nil
207
+ }
211
208
 
212
- return out, nil
209
+ 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
213
219
  }
214
220
 
215
221
  // mergeRuleMaps overlays `overrides` on `base` and returns the merged
@@ -219,142 +225,91 @@ func expandFormatBlock(raw map[string]any) (map[string]any, error) {
219
225
  // `format/*` rule fully replaces the corresponding entry expanded
220
226
  // from the `format` block.
221
227
  func mergeRuleMaps(base, overrides map[string]any) map[string]any {
222
- out := make(map[string]any, len(base)+len(overrides))
223
- for k, v := range base {
224
- out[k] = v
225
- }
226
- for k, v := range overrides {
227
- out[k] = v
228
- }
229
- return out
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
230
236
  }
231
237
 
232
238
  func asBool(field string, v any) (bool, error) {
233
- if b, ok := v.(bool); ok {
234
- return b, nil
235
- }
236
- return false, fmt.Errorf("@ttsc/lint: %s must be a boolean, got %T", field, v)
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)
237
243
  }
238
244
 
239
245
  func asString(field string, v any) (string, error) {
240
- if s, ok := v.(string); ok {
241
- return s, nil
242
- }
243
- return "", fmt.Errorf("@ttsc/lint: %s must be a string, got %T", field, v)
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)
244
250
  }
245
251
 
246
252
  func asInt(field string, v any) (int, error) {
247
- switch n := v.(type) {
248
- case int:
249
- return n, nil
250
- case int32:
251
- return int(n), nil
252
- case int64:
253
- return int(n), nil
254
- case float64:
255
- // JSON numbers decode as float64; coerce when integer-valued.
256
- if float64(int(n)) == n {
257
- return int(n), nil
258
- }
259
- case json.Number:
260
- i, err := n.Int64()
261
- if err == nil {
262
- return int(i), nil
263
- }
264
- }
265
- return 0, fmt.Errorf("@ttsc/lint: %s must be an integer, got %T", field, v)
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)
266
272
  }
267
273
 
268
274
  func asStringSlice(field string, v any) ([]string, error) {
269
- arr, ok := v.([]any)
270
- if !ok {
271
- return nil, fmt.Errorf("@ttsc/lint: %s must be an array of strings, got %T", field, v)
272
- }
273
- out := make([]string, 0, len(arr))
274
- for i, item := range arr {
275
- s, ok := item.(string)
276
- if !ok {
277
- return nil, fmt.Errorf("@ttsc/lint: %s[%d] must be a string, got %T", field, i, item)
278
- }
279
- out = append(out, s)
280
- }
281
- return out, nil
282
- }
283
-
284
- func severityIsOff(s string) bool {
285
- switch s {
286
- case "off":
287
- return true
288
- case "0":
289
- return true
290
- }
291
- return false
292
- }
293
-
294
- // parseFormatSeverity validates a `format.severity` value (string or
295
- // numeric) against the documented allow-list and returns the
296
- // canonical string form. The strict allow-list catches typos
297
- // (`"warm"`, `"fatal"`) at the format block's boundary instead of
298
- // letting them leak through into per-rule entries where the eventual
299
- // error message blames the rule by name.
300
- func parseFormatSeverity(v any) (string, error) {
301
- switch t := v.(type) {
302
- case string:
303
- switch t {
304
- case "off", "warn", "warning", "error":
305
- return t, nil
306
- }
307
- return "", fmt.Errorf("@ttsc/lint: format.severity must be one of \"off\", \"warn\", \"warning\", \"error\"; got %q", t)
308
- case float64:
309
- if t != float64(int(t)) {
310
- return "", fmt.Errorf("@ttsc/lint: format.severity numeric form must be 0, 1, or 2; got %v", t)
311
- }
312
- switch int(t) {
313
- case 0:
314
- return "off", nil
315
- case 1:
316
- return "warn", nil
317
- case 2:
318
- return "error", nil
319
- }
320
- return "", fmt.Errorf("@ttsc/lint: format.severity numeric form must be 0, 1, or 2; got %d", int(t))
321
- case int:
322
- switch t {
323
- case 0:
324
- return "off", nil
325
- case 1:
326
- return "warn", nil
327
- case 2:
328
- return "error", nil
329
- }
330
- return "", fmt.Errorf("@ttsc/lint: format.severity numeric form must be 0, 1, or 2; got %d", t)
331
- }
332
- return "", fmt.Errorf("@ttsc/lint: format.severity must be a string or number, got %T", v)
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
333
288
  }
334
289
 
335
290
  // rejectUnknownFormatKeys surfaces typos in top-level format-block
336
291
  // keys at the boundary rather than silently ignoring them. The
337
- // allow-list mirrors `TtscLintFormatConfig` exactly.
292
+ // key set mirrors `ITtscLintFormatConfig` exactly.
338
293
  func rejectUnknownFormatKeys(raw map[string]any) error {
339
- allowed := map[string]struct{}{
340
- "severity": {},
341
- "semi": {},
342
- "singleQuote": {},
343
- "trailingComma": {},
344
- "printWidth": {},
345
- "tabWidth": {},
346
- "useTabs": {},
347
- "endOfLine": {},
348
- "importOrder": {},
349
- "importOrderSeparation": {},
350
- "importOrderSortSpecifiers": {},
351
- "importOrderCaseInsensitive": {},
352
- "jsdoc": {},
353
- }
354
- for key := range raw {
355
- if _, ok := allowed[key]; !ok {
356
- return fmt.Errorf("@ttsc/lint: format unknown key %q; see TtscLintFormatConfig for the allowed surface", key)
357
- }
358
- }
359
- return nil
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
360
315
  }