@ttsc/lint 0.12.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/lib/defaultFormat.d.ts +10 -11
  2. package/lib/defaultFormat.js +10 -11
  3. package/lib/defaultFormat.js.map +1 -1
  4. package/lib/index.js +233 -135
  5. package/lib/index.js.map +1 -1
  6. package/lib/structures/ITtscLintConfig.d.ts +2 -2
  7. package/lib/structures/ITtscLintFormatConfig.d.ts +53 -55
  8. package/lib/structures/ITtscLintPluginConfig.d.ts +10 -77
  9. package/lib/structures/ITtscLintPluginMeta.d.ts +9 -1
  10. package/lib/structures/TtscLintRule.d.ts +10 -1
  11. package/lib/structures/TtscLintRuleMap.d.ts +2 -2
  12. package/lib/structures/TtscLintRuleOptions.d.ts +14 -0
  13. package/linthost/ast_helpers.go +80 -11
  14. package/linthost/compile.go +116 -112
  15. package/linthost/config.go +647 -687
  16. package/linthost/config_format.go +272 -247
  17. package/linthost/contrib_adapter.go +7 -0
  18. package/linthost/directives.go +116 -0
  19. package/linthost/dispatch.go +33 -33
  20. package/linthost/engine.go +156 -43
  21. package/linthost/fix.go +47 -27
  22. package/linthost/flags_gen.go +31 -0
  23. package/linthost/format.go +119 -3
  24. package/linthost/host.go +115 -5
  25. package/linthost/print_dispatch.go +128 -28
  26. package/linthost/print_doc.go +23 -0
  27. package/linthost/print_engine.go +168 -4
  28. package/linthost/print_nodes_array.go +17 -7
  29. package/linthost/print_nodes_call.go +138 -20
  30. package/linthost/print_nodes_function.go +353 -0
  31. package/linthost/print_nodes_imports.go +46 -29
  32. package/linthost/print_nodes_list.go +86 -5
  33. package/linthost/print_nodes_object.go +56 -11
  34. package/linthost/rules_arrays.go +5 -2
  35. package/linthost/rules_debugger.go +3 -2
  36. package/linthost/rules_dupes.go +7 -4
  37. package/linthost/rules_empty.go +3 -2
  38. package/linthost/rules_escape.go +35 -3
  39. package/linthost/rules_eval.go +3 -0
  40. package/linthost/rules_finally.go +11 -0
  41. package/linthost/rules_format_jsdoc.go +7 -0
  42. package/linthost/rules_format_print_width.go +270 -15
  43. package/linthost/rules_format_quotes.go +3 -0
  44. package/linthost/rules_format_sort_imports.go +4 -0
  45. package/linthost/rules_gap.go +75 -3
  46. package/linthost/rules_imports.go +5 -7
  47. package/linthost/rules_logic.go +75 -5
  48. package/linthost/rules_loops.go +4 -0
  49. package/linthost/rules_misc.go +4 -2
  50. package/linthost/rules_params.go +7 -11
  51. package/linthost/rules_problems.go +89 -22
  52. package/linthost/rules_promise.go +15 -0
  53. package/linthost/rules_protos.go +3 -2
  54. package/linthost/rules_self.go +6 -0
  55. package/linthost/rules_strings.go +10 -0
  56. package/linthost/rules_suggestions.go +174 -9
  57. package/linthost/rules_throw.go +2 -0
  58. package/linthost/rules_ts.go +13 -0
  59. package/linthost/rules_ts_extra.go +25 -8
  60. package/linthost/rules_var.go +15 -2
  61. package/package.json +3 -3
  62. package/plugin/main.go +4 -4
  63. package/rule/astutil/astutil.go +12 -0
  64. package/rule/rule.go +123 -123
  65. package/src/defaultFormat.ts +10 -11
  66. package/src/index.ts +257 -168
  67. package/src/structures/ITtscLintConfig.ts +2 -2
  68. package/src/structures/ITtscLintFormatConfig.ts +53 -55
  69. package/src/structures/ITtscLintPluginConfig.ts +10 -83
  70. package/src/structures/ITtscLintPluginMeta.ts +9 -1
  71. package/src/structures/TtscLintRule.ts +10 -1
  72. package/src/structures/TtscLintRuleMap.ts +17 -18
  73. package/src/structures/TtscLintRuleOptions.ts +15 -0
  74. package/linthost/eslint_runtime.go +0 -320
@@ -2,8 +2,10 @@ package linthost
2
2
 
3
3
  import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
5
- // radix: `parseInt(x)` without an explicit radix can hit the legacy
6
- // "leading 0 means octal" trap. ESLint default mode requires the radix.
5
+ // radix: `parseInt(x)` without an explicit radix argument can trigger
6
+ // implementation-defined octal parsing in older environments. ESLint's
7
+ // default "always" mode requires the second argument and rejects values
8
+ // outside the valid bases {2, 8, 10, 16}.
7
9
  // https://eslint.org/docs/latest/rules/radix
8
10
  type radix struct{}
9
11
 
@@ -24,9 +24,12 @@ func (defaultParamLast) Check(ctx *Context, node *shimast.Node) {
24
24
  if len(params) == 0 {
25
25
  return
26
26
  }
27
- // Walk right-to-left; once we see a parameter without an Initializer
28
- // (and not a rest parameter), every earlier parameter that DOES have
29
- // an Initializer is misordered.
27
+ // Walk right-to-left: the first non-default-like parameter sets the
28
+ // boundary; any default-like parameter to its LEFT is mis-ordered.
29
+ // "Default-like" includes both initializer-bearing params (`a = 1`)
30
+ // and optional params (`a?: T`): both let callers elide the argument,
31
+ // so placing them before a required param forces callers to write
32
+ // `undefined` explicitly — which defeats the point of both forms.
30
33
  sawNonDefaultAfter := false
31
34
  for i := len(params) - 1; i >= 0; i-- {
32
35
  p := params[i]
@@ -38,16 +41,9 @@ func (defaultParamLast) Check(ctx *Context, node *shimast.Node) {
38
41
  continue
39
42
  }
40
43
  if decl.DotDotDotToken != nil {
41
- // Rest parameters are always last by grammar; they don't
42
- // participate in the default-position check.
44
+ // Rest parameters must be last by grammar; skip them.
43
45
  continue
44
46
  }
45
- // typescript-eslint canonical treats both default-initialized and
46
- // optional (`a?: T`) parameters as default-like for ordering: both
47
- // permit callers to elide the argument, so a non-optional / non-
48
- // default parameter after either one forces the caller to spell
49
- // `undefined`. The round-1 implementation only checked Initializer;
50
- // round 2 adds the QuestionToken branch.
51
47
  isDefaultLike := decl.Initializer != nil || decl.QuestionToken != nil
52
48
  if !isDefaultLike {
53
49
  sawNonDefaultAfter = true
@@ -74,7 +74,9 @@ func (noExAssign) Check(ctx *Context, node *shimast.Node) {
74
74
  }
75
75
 
76
76
  // walkAssignments invokes `report` on every `<name> = ...` shape inside
77
- // `root`. Used by no-ex-assign and friends.
77
+ // `root`. Used by no-ex-assign to scan a single catch block; the
78
+ // file-wide no-func-assign / no-class-assign scans go through
79
+ // reportAssignmentsToDeclarations instead.
78
80
  func walkAssignments(root *shimast.Node, name string, report func(*shimast.Node)) {
79
81
  if root == nil {
80
82
  return
@@ -110,6 +112,8 @@ func (noEmptyCharacterClass) Check(ctx *Context, node *shimast.Node) {
110
112
  }
111
113
  }
112
114
 
115
+ // hasEmptyCharClass reports whether the regex source text src contains an
116
+ // empty character class (`[]` or `[^]`). Respects backslash escapes.
113
117
  func hasEmptyCharClass(src string) bool {
114
118
  // Walk the regex literal source manually, respecting escapes.
115
119
  for i := 0; i < len(src); i++ {
@@ -143,6 +147,10 @@ func (noMisleadingCharacterClass) Check(ctx *Context, node *shimast.Node) {
143
147
  }
144
148
  }
145
149
 
150
+ // regexHasSurrogatePair reports whether the regex source text src contains
151
+ // a non-BMP character (code point >= U+10000) inside a character class
152
+ // without the `u` flag. Such characters are stored as surrogate pairs in
153
+ // the source and the class will only match one half of the pair.
146
154
  func regexHasSurrogatePair(src string) bool {
147
155
  // Strip the trailing flags so we don't misread the `u` flag — it
148
156
  // suppresses this rule.
@@ -188,6 +196,10 @@ func (noLossOfPrecision) Check(ctx *Context, node *shimast.Node) {
188
196
  }
189
197
  }
190
198
 
199
+ // numericLiteralLosesPrecision reports whether the decimal integer literal
200
+ // text exceeds Number.MAX_SAFE_INTEGER (2^53 - 1). Non-decimal literals
201
+ // (hex, octal, binary) and literals with exponents or decimal points are
202
+ // exempt because their precision loss is caller-visible and intentional.
191
203
  func numericLiteralLosesPrecision(text string) bool {
192
204
  // Strip underscore separators, exponents, decimal/hex/oct/binary
193
205
  // markers — for the simple-base-10 integer case the round-trip
@@ -204,7 +216,8 @@ func numericLiteralLosesPrecision(text string) bool {
204
216
  if trimmed == "" {
205
217
  return false
206
218
  }
207
- // 2^53 = 9007199254740992; anything larger as an integer literal loses precision.
219
+ // Number.MAX_SAFE_INTEGER is 2^53-1 = 9007199254740991; 2^53 itself
220
+ // (9007199254740992) is the first integer that float64 cannot round-trip.
208
221
  const maxSafe = "9007199254740992"
209
222
  if len(trimmed) < len(maxSafe) {
210
223
  return false
@@ -219,38 +232,80 @@ func numericLiteralLosesPrecision(text string) bool {
219
232
  type noClassAssign struct{}
220
233
 
221
234
  func (noClassAssign) Name() string { return "no-class-assign" }
222
- func (noClassAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindClassDeclaration} }
235
+ func (noClassAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
223
236
  func (noClassAssign) Check(ctx *Context, node *shimast.Node) {
224
- decl := node.AsClassDeclaration()
225
- if decl == nil || decl.Name() == nil {
226
- return
227
- }
228
- name := identifierText(decl.Name())
229
- if name == "" {
230
- return
231
- }
232
- walkAssignments(ctx.File.AsNode(), name, func(target *shimast.Node) {
233
- ctx.Report(target, "'"+name+"' is a class.")
234
- })
237
+ reportAssignmentsToDeclarations(ctx, node, shimast.KindClassDeclaration, "is a class.")
235
238
  }
236
239
 
237
240
  // no-func-assign: same idea, but for function declarations.
238
241
  type noFuncAssign struct{}
239
242
 
240
243
  func (noFuncAssign) Name() string { return "no-func-assign" }
241
- func (noFuncAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindFunctionDeclaration} }
244
+ func (noFuncAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
242
245
  func (noFuncAssign) Check(ctx *Context, node *shimast.Node) {
243
- decl := node.AsFunctionDeclaration()
244
- if decl == nil || decl.Name() == nil {
246
+ reportAssignmentsToDeclarations(ctx, node, shimast.KindFunctionDeclaration, "is a function.")
247
+ }
248
+
249
+ // reportAssignmentsToDeclarations flags every `<name> = …` assignment whose
250
+ // target identifier names a `declKind` declaration found anywhere in the
251
+ // file. It walks the file exactly once — gathering declared names and
252
+ // assignment targets in the same pass — so the cost is linear in file size.
253
+ //
254
+ // The earlier shape registered for `declKind` directly and, on every
255
+ // declaration, re-scanned the whole file for assignments: O(declarations ×
256
+ // file size), which blows up quadratically on a file with many top-level
257
+ // functions. Visiting `KindSourceFile` once and cross-referencing afterward
258
+ // keeps the same findings without the repeated scans.
259
+ func reportAssignmentsToDeclarations(
260
+ ctx *Context,
261
+ file *shimast.Node,
262
+ declKind shimast.Kind,
263
+ noun string,
264
+ ) {
265
+ if ctx == nil || file == nil {
245
266
  return
246
267
  }
247
- name := identifierText(decl.Name())
248
- if name == "" {
268
+ declared := map[string]struct{}{}
269
+ var targets []*shimast.Node
270
+ walkDescendants(file, func(n *shimast.Node) {
271
+ switch n.Kind {
272
+ case declKind:
273
+ if name := declarationName(n); name != "" {
274
+ declared[name] = struct{}{}
275
+ }
276
+ case shimast.KindBinaryExpression:
277
+ if expr := n.AsBinaryExpression(); expr != nil &&
278
+ expr.OperatorToken != nil && isAssignmentOperator(expr.OperatorToken.Kind) &&
279
+ expr.Left != nil && expr.Left.Kind == shimast.KindIdentifier {
280
+ targets = append(targets, expr.Left)
281
+ }
282
+ }
283
+ })
284
+ if len(declared) == 0 || len(targets) == 0 {
249
285
  return
250
286
  }
251
- walkAssignments(ctx.File.AsNode(), name, func(target *shimast.Node) {
252
- ctx.Report(target, "'"+name+"' is a function.")
253
- })
287
+ for _, target := range targets {
288
+ name := identifierText(target)
289
+ if _, ok := declared[name]; ok {
290
+ ctx.Report(target, "'"+name+"' "+noun)
291
+ }
292
+ }
293
+ }
294
+
295
+ // declarationName returns the bound name of a class or function declaration
296
+ // node, or "" when the node is neither (or is anonymous).
297
+ func declarationName(n *shimast.Node) string {
298
+ switch n.Kind {
299
+ case shimast.KindFunctionDeclaration:
300
+ if d := n.AsFunctionDeclaration(); d != nil {
301
+ return identifierText(d.Name())
302
+ }
303
+ case shimast.KindClassDeclaration:
304
+ if d := n.AsClassDeclaration(); d != nil {
305
+ return identifierText(d.Name())
306
+ }
307
+ }
308
+ return ""
254
309
  }
255
310
 
256
311
  // no-prototype-builtins: `obj.hasOwnProperty(x)` — should be
@@ -351,6 +406,9 @@ func (noControlRegex) Check(ctx *Context, node *shimast.Node) {
351
406
  }
352
407
  }
353
408
 
409
+ // regexContainsControl reports whether the regex source text src contains a
410
+ // literal control character (U+0000–U+001F, excluding \t, \n, \r) or a
411
+ // \xNN / \uNNNN escape that resolves to a control character.
354
412
  func regexContainsControl(src string) bool {
355
413
  for i := 0; i < len(src); i++ {
356
414
  c := src[i]
@@ -382,6 +440,8 @@ func regexContainsControl(src string) bool {
382
440
  return false
383
441
  }
384
442
 
443
+ // hexDigit converts an ASCII hex byte ('0'-'9', 'a'-'f', 'A'-'F') to its
444
+ // integer value. Returns -1 for non-hex bytes.
385
445
  func hexDigit(b byte) int {
386
446
  switch {
387
447
  case b >= '0' && b <= '9':
@@ -412,6 +472,10 @@ func (noIrregularWhitespace) Check(ctx *Context, node *shimast.Node) {
412
472
  }
413
473
  }
414
474
 
475
+ // isIrregularWhitespace reports whether rune r is a non-standard whitespace
476
+ // character that the TypeScript parser accepts but is almost certainly a
477
+ // copy-paste artifact: vertical tab, form feed, non-breaking space, and the
478
+ // various Unicode space and line separator code points.
415
479
  func isIrregularWhitespace(r rune) bool {
416
480
  switch r {
417
481
  case '\v', '\f',
@@ -458,6 +522,9 @@ func (noFallthrough) Check(ctx *Context, node *shimast.Node) {
458
522
  }
459
523
  }
460
524
 
525
+ // isTerminating reports whether stmt is a statement that unconditionally
526
+ // transfers control out of the current block: break, continue, return,
527
+ // throw, or a block whose last statement is terminating.
461
528
  func isTerminating(stmt *shimast.Node) bool {
462
529
  if stmt == nil {
463
530
  return false
@@ -19,6 +19,9 @@ import (
19
19
  type awaitThenable struct{}
20
20
 
21
21
  func (awaitThenable) Name() string { return "await-thenable" }
22
+ func (awaitThenable) NeedsTypeChecker() bool {
23
+ return true
24
+ }
22
25
  func (awaitThenable) Visits() []shimast.Kind {
23
26
  return []shimast.Kind{shimast.KindAwaitExpression}
24
27
  }
@@ -64,6 +67,15 @@ func (awaitThenable) Check(ctx *Context, node *shimast.Node) {
64
67
  // reference to globalPromise, and `GetPropertyOfType` filters `then` as
65
68
  // a partial member, so without iterating constituents the rule would
66
69
  // fire on legitimate code.
70
+ // isAwaitable reports whether t is safe to await. A type is awaitable when:
71
+ // - its flags include Any, Unknown, or Never (these escape static strictness);
72
+ // - it is a Promise (GetPromisedTypeOfPromise returns non-nil); or
73
+ // - it is thenable (has a callable `then` property).
74
+ //
75
+ // For union and intersection types the function recurses into constituents: if
76
+ // ANY constituent is awaitable the whole type is considered awaitable. This is
77
+ // necessary because GetPromisedTypeOfPromise returns nil on composite types
78
+ // like `Promise<X> | number` even though the expression can legally be awaited.
67
79
  func isAwaitable(checker *shimchecker.Checker, t *shimchecker.Type) bool {
68
80
  if checker == nil || t == nil {
69
81
  return false
@@ -91,6 +103,9 @@ func isAwaitable(checker *shimchecker.Checker, t *shimchecker.Type) bool {
91
103
  return isThenableType(checker, t)
92
104
  }
93
105
 
106
+ // isThenableType reports whether t has a callable `then` property, which is
107
+ // the runtime-observable contract for "thenable" in the ES spec. The check
108
+ // intentionally mirrors what the JS engine uses at await-time.
94
109
  func isThenableType(checker *shimchecker.Checker, t *shimchecker.Type) bool {
95
110
  if checker == nil || t == nil {
96
111
  return false
@@ -2,8 +2,9 @@ package linthost
2
2
 
3
3
  import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
5
- // no-iterator: `obj.__iterator__` is a non-standard SpiderMonkey-era
6
- // access. Use `Symbol.iterator` instead.
5
+ // no-iterator: accessing `obj.__iterator__` is a non-standard SpiderMonkey-era
6
+ // extension that predates `Symbol.iterator`. Modern code should use
7
+ // `Symbol.iterator` and the iterable protocol instead.
7
8
  // https://eslint.org/docs/latest/rules/no-iterator
8
9
  type noIterator struct{}
9
10
 
@@ -31,6 +31,10 @@ func (noSelfAssign) Check(ctx *Context, node *shimast.Node) {
31
31
  }
32
32
  }
33
33
 
34
+ // isAssignableLeftHand reports whether node can appear as the left-hand side
35
+ // of a plain assignment expression. Only simple identifier and member-access
36
+ // shapes are checked here; complex destructuring patterns are excluded because
37
+ // a textual equality test would produce too many false negatives.
34
38
  func isAssignableLeftHand(node *shimast.Node) bool {
35
39
  if node == nil {
36
40
  return false
@@ -67,6 +71,8 @@ func (noSelfCompare) Check(ctx *Context, node *shimast.Node) {
67
71
  }
68
72
  }
69
73
 
74
+ // isComparisonOperator reports whether kind is one of the eight standard
75
+ // comparison operators: ==, ===, !=, !==, <, >, <=, >=.
70
76
  func isComparisonOperator(kind shimast.Kind) bool {
71
77
  switch kind {
72
78
  case
@@ -22,6 +22,9 @@ func (noTemplateCurlyInString) Check(ctx *Context, node *shimast.Node) {
22
22
  }
23
23
  }
24
24
 
25
+ // hasTemplatePlaceholder reports whether `text` contains a `${...}`
26
+ // sequence — i.e. at least one `${` followed by a closing `}`. A lone
27
+ // `${` with no matching brace is not flagged.
25
28
  func hasTemplatePlaceholder(text string) bool {
26
29
  if !strings.Contains(text, "${") {
27
30
  return false
@@ -57,6 +60,9 @@ func (noMultiStr) Check(ctx *Context, node *shimast.Node) {
57
60
  }
58
61
  }
59
62
 
63
+ // hasBackslashLineContinuation reports whether `src` contains a backslash
64
+ // immediately followed by a newline (`\n` or `\r`), which is the raw-source
65
+ // signature of a backslash line-continuation inside a string literal.
60
66
  func hasBackslashLineContinuation(src string) bool {
61
67
  for i := 0; i < len(src)-1; i++ {
62
68
  if src[i] == '\\' {
@@ -89,6 +95,9 @@ func (noUselessConcat) Check(ctx *Context, node *shimast.Node) {
89
95
  }
90
96
  }
91
97
 
98
+ // isStringLikeLiteral reports whether `node` is a string literal or a
99
+ // no-substitution template literal — the two kinds that can be trivially
100
+ // concatenated with `+`.
92
101
  func isStringLikeLiteral(node *shimast.Node) bool {
93
102
  if node == nil {
94
103
  return false
@@ -115,6 +124,7 @@ func (noOctal) Check(ctx *Context, node *shimast.Node) {
115
124
  }
116
125
  }
117
126
 
127
+ // isAsciiDigit reports whether b is an ASCII decimal digit (0–9).
118
128
  func isAsciiDigit(b byte) bool { return b >= '0' && b <= '9' }
119
129
 
120
130
  func init() {
@@ -7,6 +7,7 @@ import (
7
7
  "strings"
8
8
 
9
9
  shimast "github.com/microsoft/typescript-go/shim/ast"
10
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
10
11
  )
11
12
 
12
13
  // no-alert: `alert()` / `confirm()` / `prompt()`. Rarely the right
@@ -253,10 +254,15 @@ func (noLoneBlocks) Check(ctx *Context, node *shimast.Node) {
253
254
  switch parent.Kind {
254
255
  case shimast.KindBlock, shimast.KindSourceFile, shimast.KindModuleBlock:
255
256
  default:
257
+ // Block is the body of a control-flow statement (if/for/while/…) —
258
+ // those braces are not lone; only report blocks nested inside another
259
+ // statement list (another Block, SourceFile, or ModuleBlock).
256
260
  return
257
261
  }
258
- // Skip blocks that are themselves a function/method body — those
259
- // are tracked by isFunctionLikeKind on the parent.
262
+ // isFunctionLikeKind returns false for all three parent kinds above
263
+ // (Block/SourceFile/ModuleBlock are never function-like), so this guard
264
+ // is a no-op. It is left in place to document intent: if the switch were
265
+ // ever widened to admit function-body containers, this guard would fire.
260
266
  if isFunctionLikeKind(parent) {
261
267
  return
262
268
  }
@@ -737,16 +743,72 @@ func (noUnneededTernary) Visits() []shimast.Kind {
737
743
  }
738
744
  func (noUnneededTernary) Check(ctx *Context, node *shimast.Node) {
739
745
  cond := node.AsConditionalExpression()
740
- if cond == nil {
746
+ if cond == nil || cond.Condition == nil {
741
747
  return
742
748
  }
743
749
  t := stripParens(cond.WhenTrue)
744
750
  f := stripParens(cond.WhenFalse)
745
751
  tBool, tOk := isLiteralBoolean(t)
746
752
  fBool, fOk := isLiteralBoolean(f)
747
- if tOk && fOk && tBool != fBool {
748
- ctx.Report(node, "Unnecessary use of conditional expression for boolean.")
753
+ if !(tOk && fOk && tBool != fBool) {
754
+ return
755
+ }
756
+ message := "Unnecessary use of conditional expression for boolean."
757
+ src := ctx.File.Text()
758
+ condStart := shimscanner.SkipTrivia(src, cond.Condition.Pos())
759
+ if condStart < 0 || condStart >= cond.Condition.End() {
760
+ ctx.Report(node, message)
761
+ return
762
+ }
763
+ condText := src[condStart:cond.Condition.End()]
764
+ var replacement string
765
+ if tBool {
766
+ // `cond ? true : false` → `Boolean(cond)`
767
+ replacement = "Boolean(" + condText + ")"
768
+ } else {
769
+ // `cond ? false : true` → `!cond`. Wrap the condition in parentheses
770
+ // when it is not already a primary expression so operator precedence
771
+ // does not flip the meaning (e.g. `a || b` must become `!(a || b)`).
772
+ if needsParensForUnaryNegation(cond.Condition) {
773
+ replacement = "!(" + condText + ")"
774
+ } else {
775
+ replacement = "!" + condText
776
+ }
777
+ }
778
+ editPos := shimscanner.SkipTrivia(src, node.Pos())
779
+ if editPos < 0 || editPos >= node.End() {
780
+ ctx.Report(node, message)
781
+ return
782
+ }
783
+ ctx.ReportFix(
784
+ node,
785
+ message,
786
+ TextEdit{Pos: editPos, End: node.End(), Text: replacement},
787
+ )
788
+ }
789
+
790
+ // needsParensForUnaryNegation reports whether `cond` must be wrapped in
791
+ // parentheses before prefixing with `!`. Anything looser than a unary /
792
+ // member / primary expression flips precedence when negated. Mirrors
793
+ // ESLint's `no-unneeded-ternary` autofix safety check.
794
+ func needsParensForUnaryNegation(node *shimast.Node) bool {
795
+ inner := stripParens(node)
796
+ if inner == nil {
797
+ return false
798
+ }
799
+ switch inner.Kind {
800
+ case shimast.KindBinaryExpression,
801
+ shimast.KindConditionalExpression,
802
+ shimast.KindYieldExpression,
803
+ shimast.KindAwaitExpression,
804
+ shimast.KindArrowFunction,
805
+ shimast.KindFunctionExpression,
806
+ shimast.KindAsExpression,
807
+ shimast.KindSatisfiesExpression,
808
+ shimast.KindTypeAssertionExpression:
809
+ return true
749
810
  }
811
+ return false
750
812
  }
751
813
 
752
814
  // no-unused-expressions: an expression statement whose value isn't used.
@@ -783,8 +845,13 @@ func isProductiveExpression(node *shimast.Node) bool {
783
845
  shimast.KindPrefixUnaryExpression,
784
846
  shimast.KindPostfixUnaryExpression,
785
847
  shimast.KindTaggedTemplateExpression:
786
- // These can have side effects. The narrower checks
787
- // (no-cond-assign, no-bitwise) handle the suspicious shapes.
848
+ // Most of these kinds are unconditionally productive (call, new, await,
849
+ // yield, delete, tagged template). The inner switch narrows the two kinds
850
+ // that can be non-productive: a BinaryExpression is only productive when
851
+ // it is an assignment, and a PrefixUnaryExpression is only productive when
852
+ // it is ++ or --. KindPostfixUnaryExpression is always productive (++ and
853
+ // -- are the only postfix operators). All un-matched cases reach the
854
+ // outer `return true` below.
788
855
  switch expr.Kind {
789
856
  case shimast.KindBinaryExpression:
790
857
  bin := expr.AsBinaryExpression()
@@ -1089,9 +1156,27 @@ func (preferTemplate) Check(ctx *Context, node *shimast.Node) {
1089
1156
  }
1090
1157
  }
1091
1158
  hasString, hasOther := concatChainShape(node)
1092
- if hasString && hasOther {
1093
- ctx.Report(node, "Unexpected string concatenation.")
1159
+ if !(hasString && hasOther) {
1160
+ return
1161
+ }
1162
+ message := "Unexpected string concatenation."
1163
+ src := ctx.File.Text()
1164
+ operands := flattenConcatOperands(node)
1165
+ template, ok := renderConcatAsTemplate(src, operands)
1166
+ if !ok {
1167
+ ctx.Report(node, message)
1168
+ return
1094
1169
  }
1170
+ editPos := shimscanner.SkipTrivia(src, node.Pos())
1171
+ if editPos < 0 || editPos >= node.End() {
1172
+ ctx.Report(node, message)
1173
+ return
1174
+ }
1175
+ ctx.ReportFix(
1176
+ node,
1177
+ message,
1178
+ TextEdit{Pos: editPos, End: node.End(), Text: template},
1179
+ )
1095
1180
  }
1096
1181
 
1097
1182
  func concatChainShape(node *shimast.Node) (hasString bool, hasOther bool) {
@@ -1112,6 +1197,86 @@ func concatChainShape(node *shimast.Node) (hasString bool, hasOther bool) {
1112
1197
  return false, true
1113
1198
  }
1114
1199
 
1200
+ // flattenConcatOperands walks a `+` chain left-to-right and returns each
1201
+ // leaf operand in source order. Parenthesized sub-expressions are kept as
1202
+ // a single operand so the rendered template literal does not lose their
1203
+ // grouping. Mirrors ESLint's behavior of treating `("a" + b)` as one
1204
+ // expression slot when it appears inside a larger chain.
1205
+ func flattenConcatOperands(node *shimast.Node) []*shimast.Node {
1206
+ if node == nil {
1207
+ return nil
1208
+ }
1209
+ if node.Kind == shimast.KindBinaryExpression {
1210
+ bin := node.AsBinaryExpression()
1211
+ if bin != nil && bin.OperatorToken != nil && bin.OperatorToken.Kind == shimast.KindPlusToken {
1212
+ out := flattenConcatOperands(bin.Left)
1213
+ out = append(out, flattenConcatOperands(bin.Right)...)
1214
+ return out
1215
+ }
1216
+ }
1217
+ return []*shimast.Node{node}
1218
+ }
1219
+
1220
+ // renderConcatAsTemplate renders the flattened concat operands as a single
1221
+ // backtick template literal. String-like literals contribute their value
1222
+ // directly (with template-specific escaping); any other expression becomes
1223
+ // a `${…}` placeholder copied verbatim from the source text. Returns ok=false
1224
+ // when an operand cannot be rendered (typically because its source range is
1225
+ // unavailable), so the caller falls back to detection-only.
1226
+ func renderConcatAsTemplate(src string, operands []*shimast.Node) (string, bool) {
1227
+ if len(operands) == 0 {
1228
+ return "", false
1229
+ }
1230
+ var sb strings.Builder
1231
+ sb.WriteByte('`')
1232
+ for _, operand := range operands {
1233
+ if operand == nil {
1234
+ return "", false
1235
+ }
1236
+ inner := stripParens(operand)
1237
+ if isStringLikeLiteral(inner) {
1238
+ sb.WriteString(escapeTemplateLiteralBody(stringLiteralText(inner)))
1239
+ continue
1240
+ }
1241
+ pos := shimscanner.SkipTrivia(src, operand.Pos())
1242
+ end := operand.End()
1243
+ if pos < 0 || pos >= end || end > len(src) {
1244
+ return "", false
1245
+ }
1246
+ sb.WriteString("${")
1247
+ sb.WriteString(src[pos:end])
1248
+ sb.WriteByte('}')
1249
+ }
1250
+ sb.WriteByte('`')
1251
+ return sb.String(), true
1252
+ }
1253
+
1254
+ // escapeTemplateLiteralBody escapes the characters that would otherwise
1255
+ // terminate or interpolate a template literal body: backslash, backtick,
1256
+ // and the `${` sequence. Matches the canonical ESLint `prefer-template`
1257
+ // fixer escape set.
1258
+ func escapeTemplateLiteralBody(text string) string {
1259
+ var sb strings.Builder
1260
+ sb.Grow(len(text))
1261
+ for i := 0; i < len(text); i++ {
1262
+ ch := text[i]
1263
+ switch ch {
1264
+ case '\\', '`':
1265
+ sb.WriteByte('\\')
1266
+ sb.WriteByte(ch)
1267
+ case '$':
1268
+ if i+1 < len(text) && text[i+1] == '{' {
1269
+ sb.WriteString("\\$")
1270
+ } else {
1271
+ sb.WriteByte('$')
1272
+ }
1273
+ default:
1274
+ sb.WriteByte(ch)
1275
+ }
1276
+ }
1277
+ return sb.String()
1278
+ }
1279
+
1115
1280
  // require-yield: `function* gen() { return 1; }` — generators that
1116
1281
  // never yield are usually unintended.
1117
1282
  type requireYield struct{}
@@ -21,6 +21,8 @@ func (noThrowLiteral) Check(ctx *Context, node *shimast.Node) {
21
21
  ctx.Report(throw.Expression, "Expected an error object to be thrown.")
22
22
  return
23
23
  }
24
+ // `undefined` can appear as either KindUndefinedKeyword (handled above) or
25
+ // as a plain KindIdentifier whose text is "undefined" — check both forms.
24
26
  if id := identifierText(expr); id == "undefined" {
25
27
  ctx.Report(throw.Expression, "Expected an error object to be thrown.")
26
28
  }
@@ -1,3 +1,7 @@
1
+ // Core TypeScript-specific lint rules: direct ports of the most commonly
2
+ // enabled @typescript-eslint/recommended and @typescript-eslint/stylistic
3
+ // rules that require only AST inspection (no checker or scope analysis).
4
+ // Each rule is registered in the package init function at the bottom.
1
5
  package linthost
2
6
 
3
7
  import shimast "github.com/microsoft/typescript-go/shim/ast"
@@ -83,6 +87,10 @@ func (noInferrableTypes) Check(ctx *Context, node *shimast.Node) {
83
87
  ctx.Report(typeNode, "Type annotation here is unnecessary.")
84
88
  }
85
89
 
90
+ // isInferrablePair reports whether typeNode is a type annotation that
91
+ // TypeScript would have inferred automatically from the initializer init.
92
+ // Only covers the scalar literal kinds: string, number, boolean, bigint,
93
+ // null, and undefined.
86
94
  func isInferrablePair(typeNode, init *shimast.Node) bool {
87
95
  switch typeNode.Kind {
88
96
  case shimast.KindStringKeyword:
@@ -101,6 +109,8 @@ func isInferrablePair(typeNode, init *shimast.Node) bool {
101
109
  return false
102
110
  }
103
111
 
112
+ // isUnaryNumeric reports whether node is a unary +/- applied to a numeric
113
+ // literal (e.g. `-1`, `+0`). Used by isInferrablePair for the number case.
104
114
  func isUnaryNumeric(node *shimast.Node) bool {
105
115
  if node == nil || node.Kind != shimast.KindPrefixUnaryExpression {
106
116
  return false
@@ -202,6 +212,9 @@ func (preferAsConst) Check(ctx *Context, node *shimast.Node) {
202
212
  )
203
213
  }
204
214
 
215
+ // literalsMatchSourceText reports whether lhs and rhs are both literal
216
+ // expressions whose source text is identical. Used by prefer-as-const to
217
+ // detect `x as "foo"` where "foo" matches x's literal value.
205
218
  func literalsMatchSourceText(file *shimast.SourceFile, lhs, rhs *shimast.Node) bool {
206
219
  if lhs == nil || rhs == nil {
207
220
  return false