@ttsc/lint 0.10.2 → 0.11.0-dev.20260518

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 (113) hide show
  1. package/README.md +172 -33
  2. package/lib/defineConfig.d.ts +55 -0
  3. package/lib/defineConfig.js +39 -0
  4. package/lib/defineConfig.js.map +1 -0
  5. package/lib/index.d.ts +25 -1
  6. package/lib/index.js +622 -2
  7. package/lib/index.js.map +1 -1
  8. package/lib/structures/ITtscLintPlugin.d.ts +53 -0
  9. package/lib/structures/ITtscLintPlugin.js +3 -0
  10. package/lib/structures/ITtscLintPlugin.js.map +1 -0
  11. package/lib/structures/ITtscLintPluginConfig.d.ts +82 -5
  12. package/lib/structures/ITtscLintPluginMeta.d.ts +12 -0
  13. package/lib/structures/ITtscLintPluginMeta.js +3 -0
  14. package/lib/structures/ITtscLintPluginMeta.js.map +1 -0
  15. package/lib/structures/PluginRuleNames.d.ts +14 -0
  16. package/lib/structures/PluginRuleNames.js +3 -0
  17. package/lib/structures/PluginRuleNames.js.map +1 -0
  18. package/lib/structures/TtscLintConfig.d.ts +24 -8
  19. package/lib/structures/TtscLintConfigEntry.d.ts +39 -0
  20. package/lib/structures/TtscLintConfigEntry.js +3 -0
  21. package/lib/structures/TtscLintConfigEntry.js.map +1 -0
  22. package/lib/structures/TtscLintFormatConfig.d.ts +154 -0
  23. package/lib/structures/TtscLintFormatConfig.js +3 -0
  24. package/lib/structures/TtscLintFormatConfig.js.map +1 -0
  25. package/lib/structures/TtscLintPlugins.d.ts +9 -0
  26. package/lib/structures/TtscLintPlugins.js +3 -0
  27. package/lib/structures/TtscLintPlugins.js.map +1 -0
  28. package/lib/structures/TtscLintRule.d.ts +1 -1
  29. package/lib/structures/TtscLintRuleEntry.d.ts +22 -0
  30. package/lib/structures/TtscLintRuleEntry.js +3 -0
  31. package/lib/structures/TtscLintRuleEntry.js.map +1 -0
  32. package/lib/structures/TtscLintRuleMap.d.ts +44 -0
  33. package/lib/structures/TtscLintRuleMap.js +3 -0
  34. package/lib/structures/TtscLintRuleMap.js.map +1 -0
  35. package/lib/structures/TtscLintRuleOptions.d.ts +149 -0
  36. package/lib/structures/TtscLintRuleOptions.js +3 -0
  37. package/lib/structures/TtscLintRuleOptions.js.map +1 -0
  38. package/lib/structures/defaultFormat.d.ts +31 -0
  39. package/lib/structures/defaultFormat.js +35 -0
  40. package/lib/structures/defaultFormat.js.map +1 -0
  41. package/lib/structures/index.d.ts +10 -0
  42. package/lib/structures/index.js +10 -0
  43. package/lib/structures/index.js.map +1 -1
  44. package/{plugin → linthost}/ast_helpers.go +92 -1
  45. package/{plugin → linthost}/compile.go +1 -1
  46. package/{plugin → linthost}/config.go +583 -66
  47. package/linthost/config_format.go +360 -0
  48. package/linthost/contrib_adapter.go +134 -0
  49. package/{plugin → linthost}/directives.go +1 -1
  50. package/linthost/dispatch.go +65 -0
  51. package/{plugin → linthost}/engine.go +156 -11
  52. package/{plugin → linthost}/eslint_runtime.go +76 -2
  53. package/linthost/fix.go +249 -0
  54. package/linthost/format.go +107 -0
  55. package/{plugin → linthost}/host.go +1 -1
  56. package/linthost/print_dispatch.go +118 -0
  57. package/linthost/print_doc.go +168 -0
  58. package/linthost/print_engine.go +343 -0
  59. package/linthost/print_nodes_array.go +46 -0
  60. package/linthost/print_nodes_call.go +180 -0
  61. package/linthost/print_nodes_imports.go +171 -0
  62. package/linthost/print_nodes_list.go +64 -0
  63. package/linthost/print_nodes_object.go +51 -0
  64. package/{plugin → linthost}/rules_arrays.go +1 -1
  65. package/{plugin → linthost}/rules_console.go +1 -1
  66. package/{plugin → linthost}/rules_debugger.go +1 -1
  67. package/{plugin → linthost}/rules_dupes.go +1 -1
  68. package/{plugin → linthost}/rules_empty.go +1 -1
  69. package/linthost/rules_escape.go +238 -0
  70. package/{plugin → linthost}/rules_eval.go +1 -1
  71. package/{plugin → linthost}/rules_finally.go +1 -1
  72. package/linthost/rules_format_jsdoc.go +230 -0
  73. package/linthost/rules_format_print_width.go +332 -0
  74. package/linthost/rules_format_quotes.go +219 -0
  75. package/linthost/rules_format_semi.go +190 -0
  76. package/linthost/rules_format_sort_imports.go +462 -0
  77. package/linthost/rules_format_trailing_comma.go +460 -0
  78. package/{plugin → linthost}/rules_gap.go +101 -6
  79. package/linthost/rules_imports.go +101 -0
  80. package/{plugin → linthost}/rules_logic.go +55 -3
  81. package/{plugin → linthost}/rules_loops.go +1 -1
  82. package/{plugin → linthost}/rules_misc.go +1 -1
  83. package/linthost/rules_params.go +64 -0
  84. package/{plugin → linthost}/rules_problems.go +10 -2
  85. package/linthost/rules_promise.go +111 -0
  86. package/{plugin → linthost}/rules_protos.go +1 -1
  87. package/{plugin → linthost}/rules_self.go +1 -1
  88. package/{plugin → linthost}/rules_strings.go +1 -1
  89. package/{plugin → linthost}/rules_suggestions.go +51 -9
  90. package/{plugin → linthost}/rules_throw.go +1 -1
  91. package/{plugin → linthost}/rules_ts.go +12 -2
  92. package/{plugin → linthost}/rules_ts_extra.go +90 -4
  93. package/{plugin → linthost}/rules_var.go +36 -5
  94. package/package.json +8 -6
  95. package/plugin/main.go +14 -27
  96. package/rule/astutil/astutil.go +148 -0
  97. package/rule/rule.go +291 -0
  98. package/src/defineConfig.ts +69 -0
  99. package/src/index.ts +807 -3
  100. package/src/structures/ITtscLintPlugin.ts +56 -0
  101. package/src/structures/ITtscLintPluginConfig.ts +86 -5
  102. package/src/structures/ITtscLintPluginMeta.ts +14 -0
  103. package/src/structures/PluginRuleNames.ts +19 -0
  104. package/src/structures/TtscLintConfig.ts +27 -8
  105. package/src/structures/TtscLintConfigEntry.ts +50 -0
  106. package/src/structures/TtscLintFormatConfig.ts +169 -0
  107. package/src/structures/TtscLintPlugins.ts +10 -0
  108. package/src/structures/TtscLintRule.ts +14 -1
  109. package/src/structures/TtscLintRuleEntry.ts +28 -0
  110. package/src/structures/TtscLintRuleMap.ts +52 -0
  111. package/src/structures/TtscLintRuleOptions.ts +162 -0
  112. package/src/structures/defaultFormat.ts +33 -0
  113. 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
+ }
@@ -1,4 +1,4 @@
1
- package main
1
+ package linthost
2
2
 
3
3
  import (
4
4
  "strings"
@@ -0,0 +1,65 @@
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.
15
+ // Overridden at link time via
16
+ // `-ldflags "-X github.com/samchon/ttsc/packages/lint/linthost.Version=..."`.
17
+ // Defaults to `"dev"` so local Go builds and `go test` runs print a
18
+ // distinguishable value without depending on the release pipeline.
19
+ var Version = "dev"
20
+
21
+ // Main dispatches the lint plugin subcommands. `args` is the argv tail after
22
+ // the binary name (i.e. `os.Args[1:]`). The return value is the exit code the
23
+ // caller should propagate to the OS (`os.Exit`) or the host (`Plugin.Run`).
24
+ //
25
+ // Recognized verbs: `version` / `-v` / `--version`, `check`, `fix`, `format`,
26
+ // `build`, `transform`. Anything else is a usage error (exit code 2).
27
+ func Main(args []string) int {
28
+ return run(args)
29
+ }
30
+
31
+ // run is the package-local dispatcher invoked by Main and by the in-tree
32
+ // test/command corpus, which exercises end-to-end subcommand routing through
33
+ // the same entry point the CLI uses.
34
+ func run(args []string) int {
35
+ if len(args) == 0 {
36
+ fmt.Fprintln(os.Stderr, "@ttsc/lint: command required (expected check|fix|format|build|transform|version)")
37
+ return 2
38
+ }
39
+ switch args[0] {
40
+ case "-v", "--version", "version":
41
+ // Don't pay contributor-registration cost for the version banner.
42
+ fmt.Fprintf(os.Stdout, "@ttsc/lint %s\n", Version)
43
+ return 0
44
+ case "check", "fix", "format", "build", "transform":
45
+ default:
46
+ fmt.Fprintf(os.Stderr, "@ttsc/lint: unknown command %q\n", args[0])
47
+ return 2
48
+ }
49
+ // Wire contributor rules into the engine's dispatch table after every
50
+ // package init has settled. See contrib_adapter.go for the rationale.
51
+ registerContributors()
52
+ switch args[0] {
53
+ case "check":
54
+ return RunCheck(args[1:])
55
+ case "fix":
56
+ return RunFix(args[1:])
57
+ case "format":
58
+ return RunFormat(args[1:])
59
+ case "build":
60
+ return RunBuild(args[1:])
61
+ case "transform":
62
+ return RunTransform(args[1:])
63
+ }
64
+ return 2
65
+ }