@ttsc/lint 0.10.2 → 0.11.0-dev.20260517
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.
- package/README.md +172 -33
- package/lib/defineConfig.d.ts +55 -0
- package/lib/defineConfig.js +39 -0
- package/lib/defineConfig.js.map +1 -0
- package/lib/index.d.ts +25 -1
- package/lib/index.js +622 -2
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintPlugin.d.ts +53 -0
- package/lib/structures/ITtscLintPlugin.js +3 -0
- package/lib/structures/ITtscLintPlugin.js.map +1 -0
- package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
- package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
- package/lib/structures/ITtscLintPluginMeta.js +3 -0
- package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
- package/lib/structures/PluginRuleNames.d.ts +14 -0
- package/lib/structures/PluginRuleNames.js +3 -0
- package/lib/structures/PluginRuleNames.js.map +1 -0
- package/lib/structures/TtscLintConfig.d.ts +24 -8
- package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
- package/lib/structures/TtscLintConfigEntry.js +3 -0
- package/lib/structures/TtscLintConfigEntry.js.map +1 -0
- package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
- package/lib/structures/TtscLintFormatConfig.js +3 -0
- package/lib/structures/TtscLintFormatConfig.js.map +1 -0
- package/lib/structures/TtscLintPlugins.d.ts +9 -0
- package/lib/structures/TtscLintPlugins.js +3 -0
- package/lib/structures/TtscLintPlugins.js.map +1 -0
- package/lib/structures/TtscLintRule.d.ts +1 -1
- package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
- package/lib/structures/TtscLintRuleEntry.js +3 -0
- package/lib/structures/TtscLintRuleEntry.js.map +1 -0
- package/lib/structures/TtscLintRuleMap.d.ts +44 -0
- package/lib/structures/TtscLintRuleMap.js +3 -0
- package/lib/structures/TtscLintRuleMap.js.map +1 -0
- package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
- package/lib/structures/TtscLintRuleOptions.js +3 -0
- package/lib/structures/TtscLintRuleOptions.js.map +1 -0
- package/lib/structures/defaultFormat.d.ts +31 -0
- package/lib/structures/defaultFormat.js +35 -0
- package/lib/structures/defaultFormat.js.map +1 -0
- package/lib/structures/index.d.ts +10 -0
- package/lib/structures/index.js +10 -0
- package/lib/structures/index.js.map +1 -1
- package/{plugin → linthost}/ast_helpers.go +92 -1
- package/{plugin → linthost}/compile.go +1 -1
- package/{plugin → linthost}/config.go +583 -66
- package/linthost/config_format.go +360 -0
- package/linthost/contrib_adapter.go +134 -0
- package/{plugin → linthost}/directives.go +1 -1
- package/linthost/dispatch.go +62 -0
- package/{plugin → linthost}/engine.go +156 -11
- package/{plugin → linthost}/eslint_runtime.go +76 -2
- package/linthost/fix.go +249 -0
- package/linthost/format.go +107 -0
- package/{plugin → linthost}/host.go +1 -1
- package/linthost/print_dispatch.go +118 -0
- package/linthost/print_doc.go +168 -0
- package/linthost/print_engine.go +343 -0
- package/linthost/print_nodes_array.go +46 -0
- package/linthost/print_nodes_call.go +180 -0
- package/linthost/print_nodes_imports.go +171 -0
- package/linthost/print_nodes_list.go +64 -0
- package/linthost/print_nodes_object.go +51 -0
- package/{plugin → linthost}/rules_arrays.go +1 -1
- package/{plugin → linthost}/rules_console.go +1 -1
- package/{plugin → linthost}/rules_debugger.go +1 -1
- package/{plugin → linthost}/rules_dupes.go +1 -1
- package/{plugin → linthost}/rules_empty.go +1 -1
- package/linthost/rules_escape.go +238 -0
- package/{plugin → linthost}/rules_eval.go +1 -1
- package/{plugin → linthost}/rules_finally.go +1 -1
- package/linthost/rules_format_jsdoc.go +230 -0
- package/linthost/rules_format_print_width.go +332 -0
- package/linthost/rules_format_quotes.go +219 -0
- package/linthost/rules_format_semi.go +190 -0
- package/linthost/rules_format_sort_imports.go +462 -0
- package/linthost/rules_format_trailing_comma.go +460 -0
- package/{plugin → linthost}/rules_gap.go +101 -6
- package/linthost/rules_imports.go +101 -0
- package/{plugin → linthost}/rules_logic.go +55 -3
- package/{plugin → linthost}/rules_loops.go +1 -1
- package/{plugin → linthost}/rules_misc.go +1 -1
- package/linthost/rules_params.go +64 -0
- package/{plugin → linthost}/rules_problems.go +10 -2
- package/linthost/rules_promise.go +111 -0
- package/{plugin → linthost}/rules_protos.go +1 -1
- package/{plugin → linthost}/rules_self.go +1 -1
- package/{plugin → linthost}/rules_strings.go +1 -1
- package/{plugin → linthost}/rules_suggestions.go +51 -9
- package/{plugin → linthost}/rules_throw.go +1 -1
- package/{plugin → linthost}/rules_ts.go +12 -2
- package/{plugin → linthost}/rules_ts_extra.go +90 -4
- package/{plugin → linthost}/rules_var.go +36 -5
- package/package.json +8 -6
- package/plugin/main.go +14 -27
- package/rule/astutil/astutil.go +148 -0
- package/rule/rule.go +291 -0
- package/src/defineConfig.ts +69 -0
- package/src/index.ts +807 -3
- package/src/structures/ITtscLintPlugin.ts +56 -0
- package/src/structures/ITtscLintPluginConfig.ts +86 -5
- package/src/structures/ITtscLintPluginMeta.ts +14 -0
- package/src/structures/PluginRuleNames.ts +19 -0
- package/src/structures/TtscLintConfig.ts +27 -8
- package/src/structures/TtscLintConfigEntry.ts +50 -0
- package/src/structures/TtscLintFormatConfig.ts +169 -0
- package/src/structures/TtscLintPlugins.ts +10 -0
- package/src/structures/TtscLintRule.ts +14 -1
- package/src/structures/TtscLintRuleEntry.ts +28 -0
- package/src/structures/TtscLintRuleMap.ts +52 -0
- package/src/structures/TtscLintRuleOptions.ts +162 -0
- package/src/structures/defaultFormat.ts +33 -0
- package/src/structures/index.ts +10 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
package linthost
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
// expandFormatBlock translates a Prettier-style `format` block into the
|
|
9
|
+
// per-rule severity + options shape the existing rule parsers consume.
|
|
10
|
+
// The result is a `map[string]any` that mirrors what a user would
|
|
11
|
+
// have written under `rules` directly — entries like
|
|
12
|
+
// `"format/semi": ["warning", {"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.
|
|
16
|
+
//
|
|
17
|
+
// The block's enablement matrix:
|
|
18
|
+
//
|
|
19
|
+
// - `format/semi` — always on. `semi: false` flips to `prefer: "never"`.
|
|
20
|
+
// - `format/quotes` — always on. `singleQuote: true` flips to `prefer: "single"`.
|
|
21
|
+
// - `format/trailing-comma` — always on with the requested mode.
|
|
22
|
+
// - `format/print-width` — always on, driven by printWidth/tabWidth/useTabs/endOfLine.
|
|
23
|
+
// - `format/sort-imports` — opt-in by setting `importOrder`.
|
|
24
|
+
// - `format/jsdoc` — opt-in by setting `jsdoc` truthy.
|
|
25
|
+
//
|
|
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
|
+
// The returned map is the raw form rules parsers expect. Callers MUST
|
|
31
|
+
// merge any user-supplied `rules` map on top of this one (rules-wins
|
|
32
|
+
// semantics) before invoking the existing parsers — `mergeRuleMaps`
|
|
33
|
+
// below performs the merge with the right precedence.
|
|
34
|
+
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
|
+
}
|
|
54
|
+
|
|
55
|
+
out := map[string]any{}
|
|
56
|
+
|
|
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}}
|
|
69
|
+
|
|
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}}
|
|
82
|
+
|
|
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}}
|
|
98
|
+
|
|
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}
|
|
133
|
+
|
|
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
|
+
}
|
|
168
|
+
|
|
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
|
+
}
|
|
211
|
+
|
|
212
|
+
return out, nil
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// mergeRuleMaps overlays `overrides` on `base` and returns the merged
|
|
216
|
+
// map. Identical keys in `overrides` replace the entire entry from
|
|
217
|
+
// `base`; option objects are NOT deep-merged, which matches the
|
|
218
|
+
// conflict-resolution policy spec: a `rules` entry that names a
|
|
219
|
+
// `format/*` rule fully replaces the corresponding entry expanded
|
|
220
|
+
// from the `format` block.
|
|
221
|
+
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
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
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)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
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)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
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)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
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)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// rejectUnknownFormatKeys surfaces typos in top-level format-block
|
|
336
|
+
// keys at the boundary rather than silently ignoring them. The
|
|
337
|
+
// allow-list mirrors `TtscLintFormatConfig` exactly.
|
|
338
|
+
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
|
|
360
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Adapter that bridges public `rule.Rule` contributors onto the engine's
|
|
2
|
+
// internal `Rule` interface. Built-in rules live in `package main` and
|
|
3
|
+
// dispatch directly through `Register`; contributors live in sibling
|
|
4
|
+
// packages and call `rule.Register` from their `init()`. This file walks
|
|
5
|
+
// the populated `rule.Registered()` slice and wraps each entry so the
|
|
6
|
+
// engine sees the same surface for built-in and contributor rules.
|
|
7
|
+
//
|
|
8
|
+
// Init order: every contributor package's `init` runs before `package
|
|
9
|
+
// main`'s init (Go spec). But the built-in rules also register from
|
|
10
|
+
// `package main` init functions and the relative order across files in
|
|
11
|
+
// the same package is alphabetical — so we cannot blindly run the
|
|
12
|
+
// contributor wiring from a file-level `init()` and expect built-in
|
|
13
|
+
// registration to have already completed. Instead, `registerContributors`
|
|
14
|
+
// is invoked explicitly from `main.run` after all built-in `init` calls
|
|
15
|
+
// have settled, which makes the collision check meaningful.
|
|
16
|
+
package linthost
|
|
17
|
+
|
|
18
|
+
import (
|
|
19
|
+
"fmt"
|
|
20
|
+
"os"
|
|
21
|
+
"sort"
|
|
22
|
+
|
|
23
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
24
|
+
|
|
25
|
+
"github.com/samchon/ttsc/packages/lint/rule"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// registerContributors wraps every contributor rule registered through
|
|
29
|
+
// the public `rule` package into the engine's internal `Rule` interface
|
|
30
|
+
// and pushes it onto the same dispatch table the built-in rules use.
|
|
31
|
+
//
|
|
32
|
+
// Collision policy: a contributor that shares a name with an existing
|
|
33
|
+
// rule (built-in or another contributor whose init ran first) is dropped
|
|
34
|
+
// with a stderr warning. The host prefers a deterministic, debuggable
|
|
35
|
+
// outcome over panicking inside startup.
|
|
36
|
+
func registerContributors() {
|
|
37
|
+
contributors := rule.Registered()
|
|
38
|
+
sort.SliceStable(contributors, func(i, j int) bool {
|
|
39
|
+
return contributors[i].Name() < contributors[j].Name()
|
|
40
|
+
})
|
|
41
|
+
for _, contributor := range contributors {
|
|
42
|
+
name := contributor.Name()
|
|
43
|
+
if name == "" {
|
|
44
|
+
fmt.Fprintln(os.Stderr, "@ttsc/lint: contributor rule with empty name ignored")
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if LookupRule(name) != nil {
|
|
48
|
+
fmt.Fprintf(os.Stderr,
|
|
49
|
+
"@ttsc/lint: contributor rule %q collides with an existing rule; dropping contributor entry\n",
|
|
50
|
+
name)
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
if fr, ok := contributor.(rule.FormatRule); ok && fr.IsFormat() {
|
|
54
|
+
Register(formatContributorAdapter{contributorAdapter{inner: contributor}})
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
Register(contributorAdapter{inner: contributor})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// contributorAdapter wraps a public `rule.Rule` so the engine's
|
|
62
|
+
// `Register` accepts it. Forward `Name` and `Visits` directly; bridge
|
|
63
|
+
// `Check` by constructing a `rule.Context` whose `Reporter` calls back
|
|
64
|
+
// into the engine's existing `Context.Report` / `ReportRange`. The
|
|
65
|
+
// public `rule.Context` and the engine's internal `Context` share the
|
|
66
|
+
// same shim AST types, so no wrapping / unwrapping of nodes is needed.
|
|
67
|
+
type contributorAdapter struct {
|
|
68
|
+
inner rule.Rule
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// formatContributorAdapter is the FormatRule-tagged variant of
|
|
72
|
+
// contributorAdapter. Wrapping the lint-only adapter (rather than
|
|
73
|
+
// duplicating its method set) keeps the marker addition trivial and
|
|
74
|
+
// guarantees the host's `isFormatRule` check fires through the standard
|
|
75
|
+
// type assertion path.
|
|
76
|
+
type formatContributorAdapter struct {
|
|
77
|
+
contributorAdapter
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func (formatContributorAdapter) IsFormat() bool { return true }
|
|
81
|
+
|
|
82
|
+
func (a contributorAdapter) Name() string { return a.inner.Name() }
|
|
83
|
+
func (a contributorAdapter) Visits() []shimast.Kind { return a.inner.Visits() }
|
|
84
|
+
func (a contributorAdapter) Check(ctx *Context, node *shimast.Node) {
|
|
85
|
+
if ctx == nil {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
pubCtx := rule.NewContext(
|
|
89
|
+
ctx.File,
|
|
90
|
+
ctx.Checker,
|
|
91
|
+
rule.Severity(ctx.Severity),
|
|
92
|
+
ctx.Options,
|
|
93
|
+
contextReporter{ctx: ctx},
|
|
94
|
+
)
|
|
95
|
+
a.inner.Check(pubCtx, node)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// contextReporter forwards `rule.Reporter` calls back to the host's
|
|
99
|
+
// existing collect pipeline. Trivial because the public and internal
|
|
100
|
+
// reporter signatures both speak `*shimast.Node`.
|
|
101
|
+
type contextReporter struct {
|
|
102
|
+
ctx *Context
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func (r contextReporter) Report(node *shimast.Node, message string) {
|
|
106
|
+
r.ctx.Report(node, message)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func (r contextReporter) ReportFix(node *shimast.Node, message string, edits ...rule.TextEdit) {
|
|
110
|
+
r.ctx.ReportFix(node, message, toInternalTextEdits(edits)...)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
func (r contextReporter) ReportRange(pos, end int, message string) {
|
|
114
|
+
r.ctx.ReportRange(pos, end, message)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func (r contextReporter) ReportRangeFix(pos, end int, message string, edits ...rule.TextEdit) {
|
|
118
|
+
r.ctx.ReportRangeFix(pos, end, message, toInternalTextEdits(edits)...)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
func toInternalTextEdits(edits []rule.TextEdit) []TextEdit {
|
|
122
|
+
if len(edits) == 0 {
|
|
123
|
+
return nil
|
|
124
|
+
}
|
|
125
|
+
out := make([]TextEdit, len(edits))
|
|
126
|
+
for i, edit := range edits {
|
|
127
|
+
out[i] = TextEdit{
|
|
128
|
+
Pos: edit.Pos,
|
|
129
|
+
End: edit.End,
|
|
130
|
+
Text: edit.Text,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return out
|
|
134
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Library entry for the @ttsc/lint native engine.
|
|
2
|
+
//
|
|
3
|
+
// The native `@ttsc/lint` CLI lives at `packages/lint/plugin` and is a thin
|
|
4
|
+
// wrapper that calls `linthost.Main(os.Args[1:])`. Other consumers (e.g. the
|
|
5
|
+
// ttsc.dev playground wasm) link `linthost` directly and dispatch through the
|
|
6
|
+
// same entrypoint so the subcommand surface stays in one place.
|
|
7
|
+
package linthost
|
|
8
|
+
|
|
9
|
+
import (
|
|
10
|
+
"fmt"
|
|
11
|
+
"os"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Version is the build banner string the `version` subcommand prints. Keep in
|
|
15
|
+
// sync with `packages/lint/package.json` on release.
|
|
16
|
+
const Version = "0.0.1"
|
|
17
|
+
|
|
18
|
+
// Main dispatches the lint plugin subcommands. `args` is the argv tail after
|
|
19
|
+
// the binary name (i.e. `os.Args[1:]`). The return value is the exit code the
|
|
20
|
+
// caller should propagate to the OS (`os.Exit`) or the host (`Plugin.Run`).
|
|
21
|
+
//
|
|
22
|
+
// Recognized verbs: `version` / `-v` / `--version`, `check`, `fix`, `format`,
|
|
23
|
+
// `build`, `transform`. Anything else is a usage error (exit code 2).
|
|
24
|
+
func Main(args []string) int {
|
|
25
|
+
return run(args)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// run is the package-local dispatcher invoked by Main and by the in-tree
|
|
29
|
+
// test/command corpus, which exercises end-to-end subcommand routing through
|
|
30
|
+
// the same entry point the CLI uses.
|
|
31
|
+
func run(args []string) int {
|
|
32
|
+
if len(args) == 0 {
|
|
33
|
+
fmt.Fprintln(os.Stderr, "@ttsc/lint: command required (expected check|fix|format|build|transform|version)")
|
|
34
|
+
return 2
|
|
35
|
+
}
|
|
36
|
+
switch args[0] {
|
|
37
|
+
case "-v", "--version", "version":
|
|
38
|
+
// Don't pay contributor-registration cost for the version banner.
|
|
39
|
+
fmt.Fprintf(os.Stdout, "@ttsc/lint %s\n", Version)
|
|
40
|
+
return 0
|
|
41
|
+
case "check", "fix", "format", "build", "transform":
|
|
42
|
+
default:
|
|
43
|
+
fmt.Fprintf(os.Stderr, "@ttsc/lint: unknown command %q\n", args[0])
|
|
44
|
+
return 2
|
|
45
|
+
}
|
|
46
|
+
// Wire contributor rules into the engine's dispatch table after every
|
|
47
|
+
// package init has settled. See contrib_adapter.go for the rationale.
|
|
48
|
+
registerContributors()
|
|
49
|
+
switch args[0] {
|
|
50
|
+
case "check":
|
|
51
|
+
return RunCheck(args[1:])
|
|
52
|
+
case "fix":
|
|
53
|
+
return RunFix(args[1:])
|
|
54
|
+
case "format":
|
|
55
|
+
return RunFormat(args[1:])
|
|
56
|
+
case "build":
|
|
57
|
+
return RunBuild(args[1:])
|
|
58
|
+
case "transform":
|
|
59
|
+
return RunTransform(args[1:])
|
|
60
|
+
}
|
|
61
|
+
return 2
|
|
62
|
+
}
|