@ttsc/lint 0.10.1 → 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.
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 +62 -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,460 @@
1
+ package linthost
2
+
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ )
6
+
7
+ // format/trailing-comma adds trailing commas to multi-line lists. Mirrors
8
+ // prettier's `trailingComma: "all"` default — *not* a tunable.
9
+ //
10
+ // Scope (intentionally narrower than the closing-brace surface of TS):
11
+ //
12
+ // - ArrayLiteralExpression `[a, b]`
13
+ // - ObjectLiteralExpression `{ a: 1 }`
14
+ // - CallExpression / NewExpression `foo(a, b)` / `new Foo(a, b)`
15
+ // - NamedImports / NamedExports `import { a, b } from "x"`
16
+ // - TupleType `[A, B]` at the type level
17
+ // - Function parameter lists `function foo(a, b) {}` etc.,
18
+ // including interface call/construct/method signatures and
19
+ // `(a, b) => …` / `new (a, b) => …` function/constructor type
20
+ // literals.
21
+ // - Type-parameter declaration lists `<T, U>` at the declaration site
22
+ // (prettier omits trailing commas on type-argument call sites such
23
+ // as `foo<A, B>(…)`; see prettier PR #10353).
24
+ //
25
+ // Out of scope on purpose:
26
+ //
27
+ // - Destructuring binding patterns. The last element may be a rest
28
+ // pattern, where a trailing comma is a syntax error.
29
+ // - JSX attribute lists. Prettier does not apply trailing commas there
30
+ // either.
31
+ //
32
+ // Rest parameters (`...rest`) are explicitly skipped: ECMAScript forbids
33
+ // a trailing comma after a rest element (TS1013), so the rule must not
34
+ // insert one even when the rest parameter is the multi-line list's last
35
+ // element. The same restriction applies to rest binding patterns, which
36
+ // the rule does not visit at all.
37
+ //
38
+ // Unparenthesized arrow parameters (`a => …`) are also skipped: there is
39
+ // no parameter-list paren to anchor the comma against, and ECMAScript has
40
+ // no place to insert one. `findCloseTokenAfter` bails on the first
41
+ // non-trivia byte after the parameter's `End()` (the `=>` token), so the
42
+ // rule abstains without emitting an edit.
43
+ type formatTrailingComma struct{}
44
+
45
+ // formatTrailingCommaOptions mirrors `TtscLintRuleOptions.TrailingComma`.
46
+ type formatTrailingCommaOptions struct {
47
+ Mode string `json:"mode"`
48
+ }
49
+
50
+ func (formatTrailingComma) Name() string { return "format/trailing-comma" }
51
+ func (formatTrailingComma) IsFormat() bool { return true }
52
+
53
+ func (formatTrailingComma) Visits() []shimast.Kind {
54
+ return []shimast.Kind{
55
+ shimast.KindArrayLiteralExpression,
56
+ shimast.KindObjectLiteralExpression,
57
+ shimast.KindCallExpression,
58
+ shimast.KindNewExpression,
59
+ shimast.KindNamedImports,
60
+ shimast.KindNamedExports,
61
+ shimast.KindTupleType,
62
+ shimast.KindFunctionDeclaration,
63
+ shimast.KindFunctionExpression,
64
+ shimast.KindArrowFunction,
65
+ shimast.KindMethodDeclaration,
66
+ shimast.KindConstructor,
67
+ shimast.KindGetAccessor,
68
+ shimast.KindSetAccessor,
69
+ shimast.KindMethodSignature,
70
+ shimast.KindCallSignature,
71
+ shimast.KindConstructSignature,
72
+ shimast.KindFunctionType,
73
+ shimast.KindConstructorType,
74
+ shimast.KindTypeParameter,
75
+ }
76
+ }
77
+
78
+ func (formatTrailingComma) Check(ctx *Context, node *shimast.Node) {
79
+ if ctx == nil || ctx.File == nil || node == nil {
80
+ return
81
+ }
82
+ var opts formatTrailingCommaOptions
83
+ _ = ctx.DecodeOptions(&opts)
84
+ mode := opts.Mode
85
+ if mode == "" {
86
+ mode = "all"
87
+ }
88
+ if mode == "none" {
89
+ return
90
+ }
91
+ switch node.Kind {
92
+ case shimast.KindArrayLiteralExpression:
93
+ arr := node.AsArrayLiteralExpression()
94
+ if arr == nil {
95
+ return
96
+ }
97
+ considerTrailingComma(ctx, arr.Elements, node.End()-1)
98
+ case shimast.KindObjectLiteralExpression:
99
+ obj := node.AsObjectLiteralExpression()
100
+ if obj == nil {
101
+ return
102
+ }
103
+ considerTrailingComma(ctx, obj.Properties, node.End()-1)
104
+ case shimast.KindCallExpression:
105
+ if mode == "es5" {
106
+ return // prettier's es5 mode skips call arguments
107
+ }
108
+ call := node.AsCallExpression()
109
+ if call == nil {
110
+ return
111
+ }
112
+ considerTrailingComma(ctx, call.Arguments, node.End()-1)
113
+ case shimast.KindNewExpression:
114
+ if mode == "es5" {
115
+ return
116
+ }
117
+ ne := node.AsNewExpression()
118
+ if ne == nil || ne.Arguments == nil {
119
+ return
120
+ }
121
+ considerTrailingComma(ctx, ne.Arguments, node.End()-1)
122
+ case shimast.KindNamedImports:
123
+ named := node.AsNamedImports()
124
+ if named == nil {
125
+ return
126
+ }
127
+ considerTrailingComma(ctx, named.Elements, node.End()-1)
128
+ case shimast.KindNamedExports:
129
+ named := node.AsNamedExports()
130
+ if named == nil {
131
+ return
132
+ }
133
+ considerTrailingComma(ctx, named.Elements, node.End()-1)
134
+ case shimast.KindTupleType:
135
+ if mode == "es5" {
136
+ return // tuple types are type-level; ES5 mode is runtime-only
137
+ }
138
+ tup := node.AsTupleTypeNode()
139
+ if tup == nil {
140
+ return
141
+ }
142
+ considerTrailingComma(ctx, tup.Elements, node.End()-1)
143
+ case shimast.KindFunctionDeclaration:
144
+ if mode == "es5" {
145
+ return
146
+ }
147
+ fn := node.AsFunctionDeclaration()
148
+ if fn == nil {
149
+ return
150
+ }
151
+ considerFunctionParameterComma(ctx, fn.Parameters)
152
+ case shimast.KindFunctionExpression:
153
+ if mode == "es5" {
154
+ return
155
+ }
156
+ fn := node.AsFunctionExpression()
157
+ if fn == nil {
158
+ return
159
+ }
160
+ considerFunctionParameterComma(ctx, fn.Parameters)
161
+ case shimast.KindArrowFunction:
162
+ if mode == "es5" {
163
+ return
164
+ }
165
+ fn := node.AsArrowFunction()
166
+ if fn == nil {
167
+ return
168
+ }
169
+ considerFunctionParameterComma(ctx, fn.Parameters)
170
+ case shimast.KindMethodDeclaration:
171
+ if mode == "es5" {
172
+ return
173
+ }
174
+ fn := node.AsMethodDeclaration()
175
+ if fn == nil {
176
+ return
177
+ }
178
+ considerFunctionParameterComma(ctx, fn.Parameters)
179
+ case shimast.KindConstructor:
180
+ if mode == "es5" {
181
+ return
182
+ }
183
+ fn := node.AsConstructorDeclaration()
184
+ if fn == nil {
185
+ return
186
+ }
187
+ considerFunctionParameterComma(ctx, fn.Parameters)
188
+ case shimast.KindGetAccessor:
189
+ if mode == "es5" {
190
+ return
191
+ }
192
+ fn := node.AsGetAccessorDeclaration()
193
+ if fn == nil {
194
+ return
195
+ }
196
+ considerFunctionParameterComma(ctx, fn.Parameters)
197
+ case shimast.KindSetAccessor:
198
+ if mode == "es5" {
199
+ return
200
+ }
201
+ fn := node.AsSetAccessorDeclaration()
202
+ if fn == nil {
203
+ return
204
+ }
205
+ considerFunctionParameterComma(ctx, fn.Parameters)
206
+ case shimast.KindMethodSignature:
207
+ if mode == "es5" {
208
+ return
209
+ }
210
+ sig := node.AsMethodSignatureDeclaration()
211
+ if sig == nil {
212
+ return
213
+ }
214
+ considerFunctionParameterComma(ctx, sig.Parameters)
215
+ case shimast.KindCallSignature:
216
+ if mode == "es5" {
217
+ return
218
+ }
219
+ sig := node.AsCallSignatureDeclaration()
220
+ if sig == nil {
221
+ return
222
+ }
223
+ considerFunctionParameterComma(ctx, sig.Parameters)
224
+ case shimast.KindConstructSignature:
225
+ if mode == "es5" {
226
+ return
227
+ }
228
+ sig := node.AsConstructSignatureDeclaration()
229
+ if sig == nil {
230
+ return
231
+ }
232
+ considerFunctionParameterComma(ctx, sig.Parameters)
233
+ case shimast.KindFunctionType:
234
+ if mode == "es5" {
235
+ return
236
+ }
237
+ ft := node.AsFunctionTypeNode()
238
+ if ft == nil {
239
+ return
240
+ }
241
+ considerFunctionParameterComma(ctx, ft.Parameters)
242
+ case shimast.KindConstructorType:
243
+ if mode == "es5" {
244
+ return
245
+ }
246
+ ct := node.AsConstructorTypeNode()
247
+ if ct == nil {
248
+ return
249
+ }
250
+ considerFunctionParameterComma(ctx, ct.Parameters)
251
+ case shimast.KindTypeParameter:
252
+ if mode == "es5" {
253
+ return // type parameters postdate ES5; prettier es5 skips them
254
+ }
255
+ if node.Parent == nil {
256
+ return
257
+ }
258
+ list := node.Parent.TypeParameterList()
259
+ if list == nil || len(list.Nodes) == 0 || list.Nodes[len(list.Nodes)-1] != node {
260
+ return
261
+ }
262
+ src := ctx.File.Text()
263
+ closePos := findCloseTokenAfter(src, list.End(), '>')
264
+ if closePos < 0 {
265
+ return
266
+ }
267
+ considerTrailingComma(ctx, list, closePos)
268
+ }
269
+ }
270
+
271
+ // considerTrailingComma reports a fix when the bracket-delimited list is
272
+ // multi-line and missing its trailing comma. `closeBracketPos` points at
273
+ // the closing punctuation byte itself (e.g. the `]` of an array literal).
274
+ //
275
+ // "Multi-line" means the close bracket sits on a different line from the
276
+ // last element's end — not just "the list contains a newline somewhere".
277
+ // Prettier's `trailingComma: "all"` omits the comma whenever the close
278
+ // bracket is adjacent to the last element on the same line, even if the
279
+ // element itself is internally multi-line. The canonical shape is
280
+ // `JSON.stringify({ ... })` where the object argument spans many lines
281
+ // but `}` and `)` collapse onto one line; inserting `,)` there would be
282
+ // stylistically wrong and, for parameter rest-paths nearby, can shift
283
+ // later diagnostics.
284
+ func considerTrailingComma(ctx *Context, list *shimast.NodeList, closeBracketPos int) {
285
+ if list == nil || len(list.Nodes) == 0 {
286
+ return
287
+ }
288
+ last := list.Nodes[len(list.Nodes)-1]
289
+ if last == nil {
290
+ return
291
+ }
292
+ src := ctx.File.Text()
293
+ if closeBracketPos < 0 || closeBracketPos >= len(src) {
294
+ return
295
+ }
296
+ if !rangeSpansMultipleLines(src, last.End(), closeBracketPos) {
297
+ return
298
+ }
299
+ if rangeHasTrailingComma(src, last.End(), closeBracketPos) {
300
+ return
301
+ }
302
+ ctx.ReportRangeFix(
303
+ last.End()-1,
304
+ last.End(),
305
+ "Missing trailing comma.",
306
+ TextEdit{Pos: last.End(), End: last.End(), Text: ","},
307
+ )
308
+ }
309
+
310
+ // considerFunctionParameterComma reuses the same trailing-comma logic for
311
+ // a parameter list. The closing `)` lives between the parameter list's
312
+ // End() and the next non-trivia byte; rather than carry token positions
313
+ // around, the scanner walks forward in source until it finds the close
314
+ // paren.
315
+ //
316
+ // Rest parameters short-circuit the insert: `function f(a, ...rest,)` is
317
+ // a TS1013 syntax error. The rule peeks the last element's
318
+ // DotDotDotToken and bails when set, since the rest must remain the
319
+ // terminal element with no following comma.
320
+ func considerFunctionParameterComma(ctx *Context, list *shimast.NodeList) {
321
+ if list == nil || len(list.Nodes) == 0 {
322
+ return
323
+ }
324
+ if lastParameterIsRest(list) {
325
+ return
326
+ }
327
+ src := ctx.File.Text()
328
+ closePos := findCloseTokenAfter(src, list.End(), ')')
329
+ if closePos < 0 {
330
+ return
331
+ }
332
+ considerTrailingComma(ctx, list, closePos)
333
+ }
334
+
335
+ // lastParameterIsRest reports whether the parameter list ends with a
336
+ // rest element (`...rest`). ECMAScript syntax disallows a trailing
337
+ // comma after a rest element; the rule must not insert one.
338
+ func lastParameterIsRest(list *shimast.NodeList) bool {
339
+ last := list.Nodes[len(list.Nodes)-1]
340
+ if last == nil {
341
+ return false
342
+ }
343
+ param := last.AsParameterDeclaration()
344
+ if param == nil {
345
+ return false
346
+ }
347
+ return param.DotDotDotToken != nil
348
+ }
349
+
350
+ // findCloseTokenAfter returns the byte offset of the first `target`
351
+ // punctuation byte immediately after `start`, allowing only whitespace
352
+ // and comments in between. Any other byte means the caller's expected
353
+ // close token is not the next non-trivia token, and the function returns
354
+ // -1.
355
+ //
356
+ // The strict "trivia-only" contract is load-bearing for the sole caller
357
+ // `considerFunctionParameterComma`. An unparenthesized arrow parameter
358
+ // `a => …` has no opening paren, so a looser scanner would walk past
359
+ // `=>` and the body and land on some unrelated `)` (e.g. the close of
360
+ // an outer call), causing the rule to insert a spurious `,` after the
361
+ // parameter and produce a syntax error like `a, =>`. Bailing on any
362
+ // non-trivia byte cleanly suppresses the rule in those cases.
363
+ func findCloseTokenAfter(src string, start int, target byte) int {
364
+ for i := start; i < len(src); i++ {
365
+ c := src[i]
366
+ if c == target {
367
+ return i
368
+ }
369
+ if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
370
+ continue
371
+ }
372
+ if c == '/' && i+1 < len(src) {
373
+ if src[i+1] == '/' {
374
+ for i < len(src) && src[i] != '\n' {
375
+ i++
376
+ }
377
+ continue
378
+ }
379
+ if src[i+1] == '*' {
380
+ i += 2
381
+ for i+1 < len(src) && !(src[i] == '*' && src[i+1] == '/') {
382
+ i++
383
+ }
384
+ if i+1 < len(src) {
385
+ i++ // step past '*/'
386
+ }
387
+ continue
388
+ }
389
+ }
390
+ return -1
391
+ }
392
+ return -1
393
+ }
394
+
395
+ // rangeSpansMultipleLines reports whether the source between two byte
396
+ // offsets contains at least one `\n`.
397
+ func rangeSpansMultipleLines(src string, a, b int) bool {
398
+ if a > b {
399
+ a, b = b, a
400
+ }
401
+ if a < 0 {
402
+ a = 0
403
+ }
404
+ if b > len(src) {
405
+ b = len(src)
406
+ }
407
+ for i := a; i < b; i++ {
408
+ if src[i] == '\n' {
409
+ return true
410
+ }
411
+ }
412
+ return false
413
+ }
414
+
415
+ // rangeHasTrailingComma scans the source between the last item's end and
416
+ // the close bracket. Returns true if a `,` is the first non-whitespace,
417
+ // non-comment byte. Comments after the trailing comma still count as
418
+ // "comma present".
419
+ func rangeHasTrailingComma(src string, start, end int) bool {
420
+ if start < 0 {
421
+ start = 0
422
+ }
423
+ if end > len(src) {
424
+ end = len(src)
425
+ }
426
+ for i := start; i < end; {
427
+ c := src[i]
428
+ if c == ',' {
429
+ return true
430
+ }
431
+ if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
432
+ i++
433
+ continue
434
+ }
435
+ if c == '/' && i+1 < end {
436
+ if src[i+1] == '/' {
437
+ for i < end && src[i] != '\n' {
438
+ i++
439
+ }
440
+ continue
441
+ }
442
+ if src[i+1] == '*' {
443
+ i += 2
444
+ for i+1 < end && !(src[i] == '*' && src[i+1] == '/') {
445
+ i++
446
+ }
447
+ if i+1 < end {
448
+ i += 2
449
+ }
450
+ continue
451
+ }
452
+ }
453
+ return false
454
+ }
455
+ return false
456
+ }
457
+
458
+ func init() {
459
+ Register(formatTrailingComma{})
460
+ }
@@ -1,4 +1,4 @@
1
- package main
1
+ package linthost
2
2
 
3
3
  import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
@@ -149,9 +149,22 @@ func (noUnnecessaryTypeConstraint) Check(ctx *Context, node *shimast.Node) {
149
149
  if param == nil || param.Constraint == nil {
150
150
  return
151
151
  }
152
- if param.Constraint.Kind == shimast.KindAnyKeyword || param.Constraint.Kind == shimast.KindUnknownKeyword {
153
- ctx.Report(param.Constraint, "Constraining a type parameter to any or unknown is unnecessary.")
152
+ if param.Constraint.Kind != shimast.KindAnyKeyword && param.Constraint.Kind != shimast.KindUnknownKeyword {
153
+ return
154
+ }
155
+ message := "Constraining a type parameter to any or unknown is unnecessary."
156
+ // Delete the entire ` extends any|unknown` clause by spanning from the
157
+ // end of the parameter's name to the end of the constraint node.
158
+ name := param.Name()
159
+ if name == nil {
160
+ ctx.Report(param.Constraint, message)
161
+ return
154
162
  }
163
+ ctx.ReportFix(
164
+ param.Constraint,
165
+ message,
166
+ TextEdit{Pos: name.End(), End: param.Constraint.End(), Text: ""},
167
+ )
155
168
  }
156
169
 
157
170
  // no-unsafe-function-type: `Function` accepts any callable shape.
@@ -180,10 +193,92 @@ func (noWrapperObjectTypes) Check(ctx *Context, node *shimast.Node) {
180
193
  if ref == nil {
181
194
  return
182
195
  }
183
- switch identifierText(ref.TypeName) {
184
- case "String", "Number", "Boolean", "Symbol", "BigInt", "Object":
185
- ctx.Report(node, "Use primitive type keywords instead of wrapper object types.")
196
+ name := identifierText(ref.TypeName)
197
+ primitive, ok := wrapperPrimitive(name)
198
+ if !ok {
199
+ return
200
+ }
201
+ message := "Use primitive type keywords instead of wrapper object types."
202
+ // Shadow guard: if the user has declared `type String = …`,
203
+ // `interface String { … }`, or `class String { … }` at file scope,
204
+ // their `String` is NOT the global wrapper and a rewrite to `string`
205
+ // would change the type. Pre-existing detection issue, but the fix
206
+ // would silently corrupt the type — bail to detection-only when a
207
+ // shadowing declaration exists in the same file.
208
+ if fileShadowsWrapperName(ctx.File, name) {
209
+ return
210
+ }
211
+ // `Object` maps to `object` in ESLint's fix, but the semantics shift
212
+ // enough (boxed object vs lower-case object type) that we surface the
213
+ // diagnostic without a fix and leave the rewrite to the author.
214
+ if name == "Object" {
215
+ ctx.Report(node, message)
216
+ return
217
+ }
218
+ pos, end := tokenRange(ctx.File, ref.TypeName)
219
+ if pos < 0 {
220
+ ctx.Report(node, message)
221
+ return
222
+ }
223
+ ctx.ReportFix(
224
+ node,
225
+ message,
226
+ TextEdit{Pos: pos, End: end, Text: primitive},
227
+ )
228
+ }
229
+
230
+ // fileShadowsWrapperName reports whether the source file declares its own
231
+ // `String`/`Number`/`Boolean`/`Symbol`/`BigInt`/`Object` at top level via
232
+ // `type`, `interface`, or `class`. Walks SourceFile.Statements once; no
233
+ // memoization because the rule already runs once per TypeReference and
234
+ // the average statement count per file dominates the cost over the inner
235
+ // loop.
236
+ func fileShadowsWrapperName(file *shimast.SourceFile, name string) bool {
237
+ if file == nil || file.Statements == nil {
238
+ return false
239
+ }
240
+ for _, stmt := range file.Statements.Nodes {
241
+ if stmt == nil {
242
+ continue
243
+ }
244
+ var declName *shimast.Node
245
+ switch stmt.Kind {
246
+ case shimast.KindTypeAliasDeclaration:
247
+ if alias := stmt.AsTypeAliasDeclaration(); alias != nil {
248
+ declName = alias.Name()
249
+ }
250
+ case shimast.KindInterfaceDeclaration:
251
+ if iface := stmt.AsInterfaceDeclaration(); iface != nil {
252
+ declName = iface.Name()
253
+ }
254
+ case shimast.KindClassDeclaration:
255
+ if cls := stmt.AsClassDeclaration(); cls != nil {
256
+ declName = cls.Name()
257
+ }
258
+ }
259
+ if declName != nil && identifierText(declName) == name {
260
+ return true
261
+ }
262
+ }
263
+ return false
264
+ }
265
+
266
+ func wrapperPrimitive(name string) (string, bool) {
267
+ switch name {
268
+ case "String":
269
+ return "string", true
270
+ case "Number":
271
+ return "number", true
272
+ case "Boolean":
273
+ return "boolean", true
274
+ case "Symbol":
275
+ return "symbol", true
276
+ case "BigInt":
277
+ return "bigint", true
278
+ case "Object":
279
+ return "object", true
186
280
  }
281
+ return "", false
187
282
  }
188
283
 
189
284
  // no-useless-constructor: an empty constructor with no parameters is noise.
@@ -0,0 +1,101 @@
1
+ package linthost
2
+
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
6
+ )
7
+
8
+ // no-import-type-side-effects: an import whose every named specifier
9
+ // uses the inline `type` modifier is morally a type-only import; hoist
10
+ // the `type` to the import clause to make that intent explicit and let
11
+ // `verbatimModuleSyntax` elide the import entirely. typescript-eslint
12
+ // stylistic: https://typescript-eslint.io/rules/no-import-type-side-effects/
13
+ type noImportTypeSideEffects struct{}
14
+
15
+ func (noImportTypeSideEffects) Name() string { return "no-import-type-side-effects" }
16
+ func (noImportTypeSideEffects) Visits() []shimast.Kind {
17
+ return []shimast.Kind{shimast.KindImportDeclaration}
18
+ }
19
+ func (noImportTypeSideEffects) Check(ctx *Context, node *shimast.Node) {
20
+ decl := node.AsImportDeclaration()
21
+ if decl == nil || decl.ImportClause == nil {
22
+ return
23
+ }
24
+ clause := decl.ImportClause.AsImportClause()
25
+ if clause == nil {
26
+ return
27
+ }
28
+ // `import type { … }` already hoists the modifier; nothing to do.
29
+ if clause.PhaseModifier == shimast.KindTypeKeyword {
30
+ return
31
+ }
32
+ // The rule only applies to named imports; default imports cannot
33
+ // carry an inline `type` modifier.
34
+ if clause.NamedBindings == nil || clause.NamedBindings.Kind != shimast.KindNamedImports {
35
+ return
36
+ }
37
+ // A clause with a default binding alongside named imports is a
38
+ // mixed import; hoisting `type` would change semantics for the
39
+ // default. Skip.
40
+ if clause.Name() != nil {
41
+ return
42
+ }
43
+ named := clause.NamedBindings.AsNamedImports()
44
+ if named == nil || named.Elements == nil || len(named.Elements.Nodes) == 0 {
45
+ return
46
+ }
47
+ for _, spec := range named.Elements.Nodes {
48
+ s := spec.AsImportSpecifier()
49
+ if s == nil || !s.IsTypeOnly {
50
+ return
51
+ }
52
+ }
53
+ message := "Use top-level `import type` instead of marking every specifier individually."
54
+ // Compute edits: insert ` type` after the leading `import` keyword,
55
+ // then delete each specifier's `type ` prefix. All edits must be
56
+ // non-overlapping; the keyword insertion is at offset 6 (after
57
+ // `import`), and each specifier-level delete is strictly inside the
58
+ // braces, so they never overlap.
59
+ importKwEnd := findKeyword(ctx.File, node.Pos(), node.End(), "import")
60
+ if importKwEnd < 0 {
61
+ ctx.Report(node, message)
62
+ return
63
+ }
64
+ importKwEnd += len("import")
65
+ edits := []TextEdit{
66
+ {Pos: importKwEnd, End: importKwEnd, Text: " type"},
67
+ }
68
+ src := ctx.File.Text()
69
+ for _, spec := range named.Elements.Nodes {
70
+ // Locate the `type` keyword token at the head of the specifier by
71
+ // first skipping leading trivia (whitespace + comments). A naive
72
+ // `findKeyword` would search the byte range linearly and could
73
+ // match `type` *inside* a leading block comment such as
74
+ // `/* type alias */`, deleting the comment text and corrupting
75
+ // the source. SkipTrivia honors the lexer's notion of trivia, so
76
+ // the post-skip position is the actual first token byte.
77
+ typePos := shimscanner.SkipTrivia(src, spec.Pos())
78
+ if typePos < 0 || typePos+len("type") > len(src) {
79
+ continue
80
+ }
81
+ if src[typePos:typePos+len("type")] != "type" {
82
+ continue
83
+ }
84
+ after := typePos + len("type")
85
+ // Defensive: ensure the keyword is followed by a non-identifier byte
86
+ // — otherwise we'd be matching an identifier prefix like `typeOf`.
87
+ if after < len(src) && isIdentifierPart(src[after]) {
88
+ continue
89
+ }
90
+ deleteEnd := after
91
+ if deleteEnd < len(src) && (src[deleteEnd] == ' ' || src[deleteEnd] == '\t') {
92
+ deleteEnd++
93
+ }
94
+ edits = append(edits, TextEdit{Pos: typePos, End: deleteEnd, Text: ""})
95
+ }
96
+ ctx.ReportFix(node, message, edits...)
97
+ }
98
+
99
+ func init() {
100
+ Register(noImportTypeSideEffects{})
101
+ }