@ttsc/lint 0.12.3 → 0.12.4

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 (62) 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 +10 -4
  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/ITtscLintPluginMeta.d.ts +9 -1
  9. package/lib/structures/TtscLintRule.d.ts +10 -1
  10. package/lib/structures/TtscLintRuleMap.d.ts +2 -2
  11. package/linthost/ast_helpers.go +17 -0
  12. package/linthost/compile.go +37 -0
  13. package/linthost/config.go +184 -35
  14. package/linthost/config_format.go +257 -244
  15. package/linthost/directives.go +72 -0
  16. package/linthost/dispatch.go +33 -33
  17. package/linthost/engine.go +4 -0
  18. package/linthost/eslint_runtime.go +31 -0
  19. package/linthost/fix.go +29 -0
  20. package/linthost/format.go +23 -0
  21. package/linthost/host.go +11 -0
  22. package/linthost/print_dispatch.go +7 -5
  23. package/linthost/print_doc.go +4 -0
  24. package/linthost/print_nodes_call.go +9 -0
  25. package/linthost/rules_arrays.go +5 -2
  26. package/linthost/rules_debugger.go +3 -2
  27. package/linthost/rules_dupes.go +7 -4
  28. package/linthost/rules_empty.go +3 -2
  29. package/linthost/rules_escape.go +15 -0
  30. package/linthost/rules_eval.go +3 -0
  31. package/linthost/rules_finally.go +11 -0
  32. package/linthost/rules_format_jsdoc.go +7 -0
  33. package/linthost/rules_format_print_width.go +3 -0
  34. package/linthost/rules_format_quotes.go +3 -0
  35. package/linthost/rules_format_sort_imports.go +4 -0
  36. package/linthost/rules_gap.go +20 -0
  37. package/linthost/rules_imports.go +5 -7
  38. package/linthost/rules_logic.go +11 -0
  39. package/linthost/rules_loops.go +4 -0
  40. package/linthost/rules_misc.go +4 -2
  41. package/linthost/rules_params.go +7 -11
  42. package/linthost/rules_problems.go +24 -1
  43. package/linthost/rules_promise.go +12 -0
  44. package/linthost/rules_protos.go +3 -2
  45. package/linthost/rules_self.go +6 -0
  46. package/linthost/rules_strings.go +10 -0
  47. package/linthost/rules_suggestions.go +14 -4
  48. package/linthost/rules_throw.go +2 -0
  49. package/linthost/rules_ts.go +13 -0
  50. package/linthost/rules_ts_extra.go +25 -8
  51. package/linthost/rules_var.go +14 -1
  52. package/package.json +3 -3
  53. package/plugin/main.go +4 -4
  54. package/rule/astutil/astutil.go +12 -0
  55. package/rule/rule.go +123 -123
  56. package/src/defaultFormat.ts +10 -11
  57. package/src/index.ts +16 -4
  58. package/src/structures/ITtscLintConfig.ts +2 -2
  59. package/src/structures/ITtscLintFormatConfig.ts +53 -55
  60. package/src/structures/ITtscLintPluginMeta.ts +9 -1
  61. package/src/structures/TtscLintRule.ts +10 -1
  62. package/src/structures/TtscLintRuleMap.ts +17 -18
@@ -25,8 +25,11 @@ func (noSparseArrays) Check(ctx *Context, node *shimast.Node) {
25
25
  }
26
26
 
27
27
  // no-array-constructor: forbid `new Array(0)` / `Array(1, 2, 3)` (use
28
- // array literals). The 1-arg numeric form is also banned because its
29
- // behavior depends on the runtime see ESLint defaults.
28
+ // array literals). The single-argument form is intentionally excluded from
29
+ // the ban: `Array(n)` is commonly used to pre-allocate a sparse array by
30
+ // length, and banning it would generate noise on existing idiomatic code.
31
+ // The typed form `new Array<string>()` is also excluded — a type argument
32
+ // signals intentional use and the caller takes responsibility.
30
33
  // https://eslint.org/docs/latest/rules/no-array-constructor
31
34
  type noArrayConstructor struct{}
32
35
 
@@ -12,8 +12,9 @@ func (noDebugger) Check(ctx *Context, node *shimast.Node) {
12
12
  ctx.Report(node, "Unexpected `debugger` statement.")
13
13
  }
14
14
 
15
- // no-with: forbid `with` statements (already disallowed in strict mode,
16
- // but lint catches it before the parse error).
15
+ // no-with: forbid `with` statements. `with` is disallowed in strict mode
16
+ // at the parser level, but TypeScript source files may not use strict mode
17
+ // explicitly; the lint rule catches it uniformly regardless of mode.
17
18
  // https://eslint.org/docs/latest/rules/no-with
18
19
  type noWith struct{}
19
20
 
@@ -49,18 +49,17 @@ func (noDupeKeys) Check(ctx *Context, node *shimast.Node) {
49
49
  if obj == nil || obj.Properties == nil {
50
50
  return
51
51
  }
52
- seen := make(map[string]*shimast.Node, len(obj.Properties.Nodes))
52
+ seen := make(map[string]bool, len(obj.Properties.Nodes))
53
53
  for _, prop := range obj.Properties.Nodes {
54
54
  key := propertyKey(ctx.File, prop)
55
55
  if key == "" {
56
56
  continue
57
57
  }
58
- if first, ok := seen[key]; ok {
58
+ if seen[key] {
59
59
  ctx.Report(prop, "Duplicate key '"+key+"'.")
60
- _ = first
61
60
  continue
62
61
  }
63
- seen[key] = prop
62
+ seen[key] = true
64
63
  }
65
64
  }
66
65
 
@@ -153,6 +152,10 @@ func propertyKey(file *shimast.SourceFile, prop *shimast.Node) string {
153
152
  return ""
154
153
  }
155
154
 
155
+ // staticPropertyKey extracts a comparable string key from a property name node.
156
+ // Identifiers, string/numeric literals, and computed names with a literal
157
+ // payload all produce a stable string. Other computed names (dynamic
158
+ // expressions) return "" so the caller skips them without false positives.
156
159
  func staticPropertyKey(file *shimast.SourceFile, name *shimast.Node) string {
157
160
  if name == nil {
158
161
  return ""
@@ -83,8 +83,9 @@ func (noEmptyPattern) Check(ctx *Context, node *shimast.Node) {
83
83
  }
84
84
  }
85
85
 
86
- // isFunctionLikeKind reports whether the node represents a function-like
87
- // host whose body is the relevant scope for the empty check.
86
+ // isFunctionLikeKind reports whether n represents a function-like AST node
87
+ // (declaration, expression, arrow, method, accessor, or constructor). Used
88
+ // to detect scope boundaries by rules_empty, rules_finally, and others.
88
89
  func isFunctionLikeKind(n *shimast.Node) bool {
89
90
  if n == nil {
90
91
  return false
@@ -73,6 +73,12 @@ const templateValidEscapes = "`'\"\\bfnrtv0xuU$\n\r"
73
73
  // — it never deletes a backslash whose meaning could be context-sensitive).
74
74
  const regexValidEscapes = "^$\\.*+?()[]{}|/-\n\r"
75
75
 
76
+ // reportStringEscapes walks the raw source bytes of a string or template
77
+ // literal and reports each backslash whose following character is not in
78
+ // `whitelist`. `base` is the source offset of `raw[0]` so reported ranges
79
+ // translate to absolute file positions. The function issues an autofix
80
+ // (delete the backslash) for ASCII escapes; multi-byte sequences are
81
+ // reported without a fix to avoid corrupting UTF-8.
76
82
  func reportStringEscapes(ctx *Context, raw string, base int, whitelist string) {
77
83
  if len(raw) < 2 {
78
84
  return
@@ -117,6 +123,9 @@ func reportStringEscapes(ctx *Context, raw string, base int, whitelist string) {
117
123
  }
118
124
  }
119
125
 
126
+ // reportRegexEscapes walks the pattern body of a regex literal and reports
127
+ // backslashes that escape non-special characters. `base` is the source offset
128
+ // of `raw[0]`. Character-class context (`[…]`) widens the legal set slightly.
120
129
  func reportRegexEscapes(ctx *Context, raw string, base int) {
121
130
  if len(raw) < 3 || raw[0] != '/' {
122
131
  return
@@ -166,6 +175,9 @@ func reportRegexEscapes(ctx *Context, raw string, base int) {
166
175
  }
167
176
  }
168
177
 
178
+ // isUselessStringEscape reports whether a backslash before `ch` is redundant
179
+ // inside a string or template literal. The `whitelist` contains the characters
180
+ // that are valid escape targets for the specific literal kind (string vs template).
169
181
  func isUselessStringEscape(ch byte, whitelist string) bool {
170
182
  // Whitespace + control chars are escape sequences too.
171
183
  if ch < 0x20 {
@@ -181,6 +193,9 @@ func isUselessStringEscape(ch byte, whitelist string) bool {
181
193
  return true
182
194
  }
183
195
 
196
+ // isUselessRegexEscape reports whether a backslash before `ch` is redundant
197
+ // in a regex pattern. `inClass` is true when the escape occurs inside a `[…]`
198
+ // character class, which widens the set of meaningful escapes.
184
199
  func isUselessRegexEscape(ch byte, inClass bool) bool {
185
200
  if ch < 0x20 {
186
201
  return false
@@ -36,6 +36,9 @@ func (noScriptURL) Check(ctx *Context, node *shimast.Node) {
36
36
  }
37
37
  }
38
38
 
39
+ // isJavaScriptURL reports whether text starts with the "javascript:" scheme
40
+ // (case-insensitively, ASCII only). The manual loop avoids importing strings
41
+ // just for strings.ToLower and keeps the hot path allocation-free.
39
42
  func isJavaScriptURL(text string) bool {
40
43
  const prefix = "javascript:"
41
44
  if len(text) < len(prefix) {
@@ -25,6 +25,15 @@ func (noUnsafeFinally) Check(ctx *Context, node *shimast.Node) {
25
25
  ctx.Report(node, "Unsafe usage of "+keyword+".")
26
26
  }
27
27
 
28
+ // walkToFinally walks the parent chain from node upward looking for a
29
+ // `finally` block. It returns the Block node that IS the finally clause when
30
+ // found, or nil when the search exits through a function boundary (making any
31
+ // control-flow transfer target something outside the finally block) or when no
32
+ // finally block is found at all.
33
+ //
34
+ // A `break` or `continue` that targets an inner loop or switch INSIDE the
35
+ // finally block is safe — it does not escape the finally — so the walk stops
36
+ // early and returns nil in that case.
28
37
  func walkToFinally(node *shimast.Node) *shimast.Node {
29
38
  cur := node.Parent
30
39
  for cur != nil {
@@ -58,6 +67,8 @@ func walkToFinally(node *shimast.Node) *shimast.Node {
58
67
  return nil
59
68
  }
60
69
 
70
+ // keywordOfControl returns the control-flow keyword string for the given
71
+ // statement node, used to build the diagnostic message text.
61
72
  func keywordOfControl(node *shimast.Node) string {
62
73
  switch node.Kind {
63
74
  case shimast.KindReturnStatement:
@@ -136,6 +136,10 @@ func findJSDocBlocks(src string) []jsdocBlock {
136
136
  return out
137
137
  }
138
138
 
139
+ // rewriteJSDocTags scans one JSDoc block and emits a fix for each tag that has
140
+ // a canonical synonym. Tags preceded by a byte other than `*`, whitespace, or a
141
+ // newline are treated as inline `@foo` references (not top-level tags) and are
142
+ // left alone. Tags inside `@example` bodies are also skipped.
139
143
  func rewriteJSDocTags(ctx *Context, src string, block jsdocBlock, synonyms map[string]string) {
140
144
  for i := block.bodyStart; i < block.bodyEnd; i++ {
141
145
  if src[i] != '@' {
@@ -205,6 +209,9 @@ func endOfJSDocExampleBody(src string, block jsdocBlock, start int) int {
205
209
  return block.bodyEnd
206
210
  }
207
211
 
212
+ // isJSDocTagByte reports whether `b` is an ASCII letter that may appear in a
213
+ // JSDoc tag name. Tags are purely alphabetic: digits, hyphens, and underscores
214
+ // terminate a tag name.
208
215
  func isJSDocTagByte(b byte) bool {
209
216
  return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
210
217
  }
@@ -256,6 +256,9 @@ func hasReflowAncestor(node *shimast.Node) bool {
256
256
  return false
257
257
  }
258
258
 
259
+ // isReflowKind reports whether `k` is one of the node kinds the
260
+ // format/print-width rule visits. Kept in sync with Visits() so
261
+ // hasReflowAncestor does not need to call Visits() at runtime.
259
262
  func isReflowKind(k shimast.Kind) bool {
260
263
  switch k {
261
264
  case shimast.KindObjectLiteralExpression,
@@ -25,6 +25,9 @@ import (
25
25
  // also intentionally out of scope.
26
26
  type formatQuotes struct{}
27
27
 
28
+ // formatQuotesOptions mirrors `TtscLintRuleOptions.Quotes`. Prefer accepts
29
+ // `"double"` (the default, enforces double quotes) or `"single"` (enforces
30
+ // single quotes). Any other value is treated as the default.
28
31
  type formatQuotesOptions struct {
29
32
  Prefer string `json:"prefer"`
30
33
  }
@@ -173,6 +173,8 @@ func loadSortImportsOptions(ctx *Context) resolvedSortImportsOptions {
173
173
  }
174
174
  }
175
175
 
176
+ // hasThirdPartyGroup reports whether at least one group in the list is the
177
+ // third-party catchall. Used to decide whether a sentinel needs to be appended.
176
178
  func hasThirdPartyGroup(groups []sortImportsGroup) bool {
177
179
  for _, g := range groups {
178
180
  if g.thirdParty {
@@ -449,6 +451,8 @@ func reportNamedSpecifierSort(ctx *Context, decl *shimast.Node, caseInsensitive
449
451
  )
450
452
  }
451
453
 
454
+ // joinSortedSpecifiers returns the specifier texts joined with ", " in their
455
+ // already-sorted order. The caller owns the sort; this is a formatting step only.
452
456
  func joinSortedSpecifiers(entries []specifierEntry) string {
453
457
  texts := make([]string, len(entries))
454
458
  for i, e := range entries {
@@ -1,3 +1,11 @@
1
+ // Miscellaneous AST-only rules that cover the gap between the core ESLint
2
+ // "Possible Problems" list and the @typescript-eslint strict/stylistic
3
+ // presets: empty static blocks, setter returns, unused labels, dynamic
4
+ // delete, nullish-coalescing with non-null assertions, unnecessary type
5
+ // constraints, unsafe built-in types, wrapper object types, useless
6
+ // constructors, non-literal enum members, type-assertion style, type
7
+ // definition style, dot notation, and unsafe declaration merging.
8
+ // Each rule is registered in init() at the bottom of the file.
1
9
  package linthost
2
10
 
3
11
  import shimast "github.com/microsoft/typescript-go/shim/ast"
@@ -35,6 +43,9 @@ func (noSetterReturn) Check(ctx *Context, node *shimast.Node) {
35
43
  ctx.Report(node, "Setter should not return a value.")
36
44
  }
37
45
 
46
+ // isInsideDirectSetter reports whether node is lexically nested inside a
47
+ // SetAccessor without an intervening function boundary. "Direct" means the
48
+ // return value would actually be a setter return, not a nested callback's.
38
49
  func isInsideDirectSetter(node *shimast.Node) bool {
39
50
  for p := node.Parent; p != nil; p = p.Parent {
40
51
  if p.Kind == shimast.KindSetAccessor {
@@ -99,6 +110,9 @@ func (noDynamicDelete) Check(ctx *Context, node *shimast.Node) {
99
110
  ctx.Report(node, "Do not delete dynamically computed property keys.")
100
111
  }
101
112
 
113
+ // isStaticPropertyKey reports whether node is a literal key that can
114
+ // appear in a delete expression without triggering no-dynamic-delete:
115
+ // string literals, no-substitution template literals, and numeric literals.
102
116
  func isStaticPropertyKey(node *shimast.Node) bool {
103
117
  node = stripParens(node)
104
118
  if node == nil {
@@ -263,6 +277,9 @@ func fileShadowsWrapperName(file *shimast.SourceFile, name string) bool {
263
277
  return false
264
278
  }
265
279
 
280
+ // wrapperPrimitive maps a boxed object type name (e.g. "String") to its
281
+ // primitive keyword equivalent (e.g. "string"). Returns ("", false) for
282
+ // names that are not recognized wrapper types.
266
283
  func wrapperPrimitive(name string) (string, bool) {
267
284
  switch name {
268
285
  case "String":
@@ -365,6 +382,9 @@ func (dotNotation) Check(ctx *Context, node *shimast.Node) {
365
382
  ctx.Report(node, "Use dot notation instead of a string literal property access.")
366
383
  }
367
384
 
385
+ // isSimpleIdentifierName reports whether value is a valid bare identifier
386
+ // (ASCII letters, digits, underscore, dollar sign; digits not first). Used
387
+ // by dot-notation to decide whether bracket access can become dot access.
368
388
  func isSimpleIdentifierName(value string) bool {
369
389
  if value == "" {
370
390
  return false
@@ -67,13 +67,11 @@ func (noImportTypeSideEffects) Check(ctx *Context, node *shimast.Node) {
67
67
  }
68
68
  src := ctx.File.Text()
69
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.
70
+ // Locate the `type` keyword token at the head of each specifier by
71
+ // skipping leading trivia (whitespace + comments). A naive
72
+ // findKeyword scan would risk matching `type` inside a block comment
73
+ // such as `/* type alias */`, corrupting the source. SkipTrivia
74
+ // anchors the scan at the actual first token byte.
77
75
  typePos := shimscanner.SkipTrivia(src, spec.Pos())
78
76
  if typePos < 0 || typePos+len("type") > len(src) {
79
77
  continue
@@ -1,3 +1,8 @@
1
+ // Correctness and equality rules — a focused set from ESLint's "Possible
2
+ // Problems" and "Suggestions" categories that catch logic errors rather than
3
+ // style issues: redundant boolean casts, unsafe negations, loose equality,
4
+ // NaN comparisons, constant conditions, and assignment-in-condition.
5
+ // AST-only, no scope analysis.
1
6
  package linthost
2
7
 
3
8
  import shimast "github.com/microsoft/typescript-go/shim/ast"
@@ -71,6 +76,12 @@ func isInBooleanContext(node *shimast.Node) bool {
71
76
  return false
72
77
  }
73
78
 
79
+ // skipParents walks up through any wrapping ParenthesizedExpression nodes and
80
+ // returns the outermost parenthesized wrapper. This is the inverse of
81
+ // stripParens: where stripParens descends into the canonical inner expression,
82
+ // skipParents climbs up to the outermost paren so that structural parent
83
+ // comparisons (e.g. "is this expression the test of an if?") resolve against
84
+ // the node the parser actually attached as a child, not the inner form.
74
85
  func skipParents(node *shimast.Node) *shimast.Node {
75
86
  for node != nil && node.Parent != nil && node.Parent.Kind == shimast.KindParenthesizedExpression {
76
87
  node = node.Parent
@@ -57,6 +57,10 @@ func (forDirection) Check(ctx *Context, node *shimast.Node) {
57
57
  }
58
58
  }
59
59
 
60
+ // updateDirection returns the direction (+1 for incrementing, -1 for
61
+ // decrementing, 0 for unknown) that the incrementor expression moves the
62
+ // named counter variable. Only simple patterns are recognised: i++, i--,
63
+ // ++i, --i, i += N, and i -= N. Compound or computed expressions return 0.
60
64
  func updateDirection(node *shimast.Node, counter string) int {
61
65
  if node == nil || counter == "" {
62
66
  return 0
@@ -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
@@ -110,6 +110,8 @@ func (noEmptyCharacterClass) Check(ctx *Context, node *shimast.Node) {
110
110
  }
111
111
  }
112
112
 
113
+ // hasEmptyCharClass reports whether the regex source text src contains an
114
+ // empty character class (`[]` or `[^]`). Respects backslash escapes.
113
115
  func hasEmptyCharClass(src string) bool {
114
116
  // Walk the regex literal source manually, respecting escapes.
115
117
  for i := 0; i < len(src); i++ {
@@ -143,6 +145,10 @@ func (noMisleadingCharacterClass) Check(ctx *Context, node *shimast.Node) {
143
145
  }
144
146
  }
145
147
 
148
+ // regexHasSurrogatePair reports whether the regex source text src contains
149
+ // a non-BMP character (code point >= U+10000) inside a character class
150
+ // without the `u` flag. Such characters are stored as surrogate pairs in
151
+ // the source and the class will only match one half of the pair.
146
152
  func regexHasSurrogatePair(src string) bool {
147
153
  // Strip the trailing flags so we don't misread the `u` flag — it
148
154
  // suppresses this rule.
@@ -188,6 +194,10 @@ func (noLossOfPrecision) Check(ctx *Context, node *shimast.Node) {
188
194
  }
189
195
  }
190
196
 
197
+ // numericLiteralLosesPrecision reports whether the decimal integer literal
198
+ // text exceeds Number.MAX_SAFE_INTEGER (2^53 - 1). Non-decimal literals
199
+ // (hex, octal, binary) and literals with exponents or decimal points are
200
+ // exempt because their precision loss is caller-visible and intentional.
191
201
  func numericLiteralLosesPrecision(text string) bool {
192
202
  // Strip underscore separators, exponents, decimal/hex/oct/binary
193
203
  // markers — for the simple-base-10 integer case the round-trip
@@ -204,7 +214,8 @@ func numericLiteralLosesPrecision(text string) bool {
204
214
  if trimmed == "" {
205
215
  return false
206
216
  }
207
- // 2^53 = 9007199254740992; anything larger as an integer literal loses precision.
217
+ // Number.MAX_SAFE_INTEGER is 2^53-1 = 9007199254740991; 2^53 itself
218
+ // (9007199254740992) is the first integer that float64 cannot round-trip.
208
219
  const maxSafe = "9007199254740992"
209
220
  if len(trimmed) < len(maxSafe) {
210
221
  return false
@@ -351,6 +362,9 @@ func (noControlRegex) Check(ctx *Context, node *shimast.Node) {
351
362
  }
352
363
  }
353
364
 
365
+ // regexContainsControl reports whether the regex source text src contains a
366
+ // literal control character (U+0000–U+001F, excluding \t, \n, \r) or a
367
+ // \xNN / \uNNNN escape that resolves to a control character.
354
368
  func regexContainsControl(src string) bool {
355
369
  for i := 0; i < len(src); i++ {
356
370
  c := src[i]
@@ -382,6 +396,8 @@ func regexContainsControl(src string) bool {
382
396
  return false
383
397
  }
384
398
 
399
+ // hexDigit converts an ASCII hex byte ('0'-'9', 'a'-'f', 'A'-'F') to its
400
+ // integer value. Returns -1 for non-hex bytes.
385
401
  func hexDigit(b byte) int {
386
402
  switch {
387
403
  case b >= '0' && b <= '9':
@@ -412,6 +428,10 @@ func (noIrregularWhitespace) Check(ctx *Context, node *shimast.Node) {
412
428
  }
413
429
  }
414
430
 
431
+ // isIrregularWhitespace reports whether rune r is a non-standard whitespace
432
+ // character that the TypeScript parser accepts but is almost certainly a
433
+ // copy-paste artifact: vertical tab, form feed, non-breaking space, and the
434
+ // various Unicode space and line separator code points.
415
435
  func isIrregularWhitespace(r rune) bool {
416
436
  switch r {
417
437
  case '\v', '\f',
@@ -458,6 +478,9 @@ func (noFallthrough) Check(ctx *Context, node *shimast.Node) {
458
478
  }
459
479
  }
460
480
 
481
+ // isTerminating reports whether stmt is a statement that unconditionally
482
+ // transfers control out of the current block: break, continue, return,
483
+ // throw, or a block whose last statement is terminating.
461
484
  func isTerminating(stmt *shimast.Node) bool {
462
485
  if stmt == nil {
463
486
  return false
@@ -64,6 +64,15 @@ func (awaitThenable) Check(ctx *Context, node *shimast.Node) {
64
64
  // reference to globalPromise, and `GetPropertyOfType` filters `then` as
65
65
  // a partial member, so without iterating constituents the rule would
66
66
  // fire on legitimate code.
67
+ // isAwaitable reports whether t is safe to await. A type is awaitable when:
68
+ // - its flags include Any, Unknown, or Never (these escape static strictness);
69
+ // - it is a Promise (GetPromisedTypeOfPromise returns non-nil); or
70
+ // - it is thenable (has a callable `then` property).
71
+ //
72
+ // For union and intersection types the function recurses into constituents: if
73
+ // ANY constituent is awaitable the whole type is considered awaitable. This is
74
+ // necessary because GetPromisedTypeOfPromise returns nil on composite types
75
+ // like `Promise<X> | number` even though the expression can legally be awaited.
67
76
  func isAwaitable(checker *shimchecker.Checker, t *shimchecker.Type) bool {
68
77
  if checker == nil || t == nil {
69
78
  return false
@@ -91,6 +100,9 @@ func isAwaitable(checker *shimchecker.Checker, t *shimchecker.Type) bool {
91
100
  return isThenableType(checker, t)
92
101
  }
93
102
 
103
+ // isThenableType reports whether t has a callable `then` property, which is
104
+ // the runtime-observable contract for "thenable" in the ES spec. The check
105
+ // intentionally mirrors what the JS engine uses at await-time.
94
106
  func isThenableType(checker *shimchecker.Checker, t *shimchecker.Type) bool {
95
107
  if checker == nil || t == nil {
96
108
  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() {
@@ -253,10 +253,15 @@ func (noLoneBlocks) Check(ctx *Context, node *shimast.Node) {
253
253
  switch parent.Kind {
254
254
  case shimast.KindBlock, shimast.KindSourceFile, shimast.KindModuleBlock:
255
255
  default:
256
+ // Block is the body of a control-flow statement (if/for/while/…) —
257
+ // those braces are not lone; only report blocks nested inside another
258
+ // statement list (another Block, SourceFile, or ModuleBlock).
256
259
  return
257
260
  }
258
- // Skip blocks that are themselves a function/method body — those
259
- // are tracked by isFunctionLikeKind on the parent.
261
+ // isFunctionLikeKind returns false for all three parent kinds above
262
+ // (Block/SourceFile/ModuleBlock are never function-like), so this guard
263
+ // is a no-op. It is left in place to document intent: if the switch were
264
+ // ever widened to admit function-body containers, this guard would fire.
260
265
  if isFunctionLikeKind(parent) {
261
266
  return
262
267
  }
@@ -783,8 +788,13 @@ func isProductiveExpression(node *shimast.Node) bool {
783
788
  shimast.KindPrefixUnaryExpression,
784
789
  shimast.KindPostfixUnaryExpression,
785
790
  shimast.KindTaggedTemplateExpression:
786
- // These can have side effects. The narrower checks
787
- // (no-cond-assign, no-bitwise) handle the suspicious shapes.
791
+ // Most of these kinds are unconditionally productive (call, new, await,
792
+ // yield, delete, tagged template). The inner switch narrows the two kinds
793
+ // that can be non-productive: a BinaryExpression is only productive when
794
+ // it is an assignment, and a PrefixUnaryExpression is only productive when
795
+ // it is ++ or --. KindPostfixUnaryExpression is always productive (++ and
796
+ // -- are the only postfix operators). All un-matched cases reach the
797
+ // outer `return true` below.
788
798
  switch expr.Kind {
789
799
  case shimast.KindBinaryExpression:
790
800
  bin := expr.AsBinaryExpression()
@@ -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