@ttsc/lint 0.18.3 → 0.19.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 (188) hide show
  1. package/README.md +95 -43
  2. package/go.mod +5 -4
  3. package/internal/rulecode/rulecode.go +88 -0
  4. package/lib/index.d.ts +10 -1
  5. package/lib/index.js +45 -17
  6. package/lib/index.js.map +1 -1
  7. package/lib/structures/ITtscLintConfig.d.ts +3 -0
  8. package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
  9. package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
  10. package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
  11. package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
  12. package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
  13. package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
  14. package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
  15. package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
  16. package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
  17. package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
  18. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
  19. package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
  20. package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
  21. package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
  22. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
  23. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
  24. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
  25. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
  26. package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
  27. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
  28. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
  29. package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
  30. package/lib/structures/rules/index.d.ts +3 -0
  31. package/lib/structures/rules/index.js +3 -0
  32. package/lib/structures/rules/index.js.map +1 -1
  33. package/linthost/ast_helpers.go +521 -23
  34. package/linthost/ast_selector_matcher.go +1628 -0
  35. package/linthost/ast_selector_parser.go +628 -0
  36. package/linthost/comment_scan.go +154 -0
  37. package/linthost/compile.go +129 -79
  38. package/linthost/config.go +352 -88
  39. package/linthost/config_format.go +15 -1
  40. package/linthost/contrib_adapter.go +129 -25
  41. package/linthost/declaration_rules.go +12 -11
  42. package/linthost/directives.go +9 -67
  43. package/linthost/dispatch.go +37 -3
  44. package/linthost/engine.go +368 -53
  45. package/linthost/escape_string.go +77 -0
  46. package/linthost/fix.go +133 -23
  47. package/linthost/flags_gen.go +18 -17
  48. package/linthost/format.go +73 -167
  49. package/linthost/format_editor_settings.go +134 -26
  50. package/linthost/grapheme_tables_gen.go +1920 -0
  51. package/linthost/host.go +160 -61
  52. package/linthost/literal_escape_scan.go +117 -0
  53. package/linthost/lsp.go +545 -98
  54. package/linthost/numeric_literal_precision.go +228 -0
  55. package/linthost/polyfill_browserslist.go +1439 -0
  56. package/linthost/polyfill_browserslist_config.go +603 -0
  57. package/linthost/polyfill_corejs.go +750 -0
  58. package/linthost/polyfill_data.go +113 -0
  59. package/linthost/polyfill_data_gen.json +39868 -0
  60. package/linthost/print_nodes_array.go +7 -1
  61. package/linthost/print_nodes_object.go +7 -1
  62. package/linthost/project_engine.go +151 -0
  63. package/linthost/project_rules.go +78 -0
  64. package/linthost/regex_clean.go +191 -0
  65. package/linthost/regex_tree.go +1544 -0
  66. package/linthost/regex_tree_optimizer.go +1685 -0
  67. package/linthost/regexp_cache.go +44 -0
  68. package/linthost/rule_codes.go +105 -0
  69. package/linthost/rule_codes.json +745 -0
  70. package/linthost/rules_ban_ts_comment.go +312 -0
  71. package/linthost/rules_boundaries.go +128 -43
  72. package/linthost/rules_boundaries_dependencies.go +1013 -0
  73. package/linthost/rules_core_extra.go +62 -92
  74. package/linthost/rules_cypress.go +1 -1
  75. package/linthost/rules_default_case.go +87 -11
  76. package/linthost/rules_dupes.go +38 -13
  77. package/linthost/rules_empty.go +188 -20
  78. package/linthost/rules_escape.go +62 -48
  79. package/linthost/rules_format_arrow_parens.go +19 -5
  80. package/linthost/rules_format_bracket_spacing.go +1 -1
  81. package/linthost/rules_format_clause_join.go +1 -1
  82. package/linthost/rules_format_declaration_header.go +31 -19
  83. package/linthost/rules_format_indent.go +1 -1
  84. package/linthost/rules_format_jsdoc.go +1 -1
  85. package/linthost/rules_format_orphan_semi.go +1 -1
  86. package/linthost/rules_format_parameter_properties.go +17 -7
  87. package/linthost/rules_format_print_width.go +1 -1
  88. package/linthost/rules_format_quote_props.go +1 -1
  89. package/linthost/rules_format_quotes.go +1 -1
  90. package/linthost/rules_format_semi.go +106 -79
  91. package/linthost/rules_format_sort_imports.go +79 -45
  92. package/linthost/rules_format_statement_split.go +1 -1
  93. package/linthost/rules_format_ternary_nullish_parens.go +1 -1
  94. package/linthost/rules_format_trailing_comma.go +64 -1
  95. package/linthost/rules_format_whitespace.go +1 -1
  96. package/linthost/rules_functional.go +21 -22
  97. package/linthost/rules_gap.go +80 -3
  98. package/linthost/rules_grouped_accessor_pairs.go +84 -25
  99. package/linthost/rules_guard_for_in.go +64 -147
  100. package/linthost/rules_jsx_a11y.go +232 -22
  101. package/linthost/rules_logic.go +84 -38
  102. package/linthost/rules_nextjs.go +105 -16
  103. package/linthost/rules_no_class_assign.go +70 -0
  104. package/linthost/rules_no_duplicate_imports.go +266 -0
  105. package/linthost/rules_no_else_return.go +156 -38
  106. package/linthost/rules_no_extend_native.go +113 -17
  107. package/linthost/rules_no_fallthrough.go +1013 -0
  108. package/linthost/rules_no_func_assign.go +70 -0
  109. package/linthost/rules_no_import_assign.go +278 -86
  110. package/linthost/rules_no_inner_declarations.go +186 -0
  111. package/linthost/rules_no_loop_func.go +438 -53
  112. package/linthost/rules_no_mixed_operators.go +208 -52
  113. package/linthost/rules_no_param_reassign.go +376 -61
  114. package/linthost/rules_no_promise_executor_return.go +156 -0
  115. package/linthost/rules_no_restricted_imports.go +1013 -57
  116. package/linthost/rules_no_restricted_syntax.go +190 -26
  117. package/linthost/rules_no_useless_constructor.go +6 -36
  118. package/linthost/rules_problems.go +154 -311
  119. package/linthost/rules_promise.go +2578 -109
  120. package/linthost/rules_react_extras.go +71 -45
  121. package/linthost/rules_react_hooks.go +18 -2
  122. package/linthost/rules_react_perf.go +1 -0
  123. package/linthost/rules_react_refresh.go +1 -1
  124. package/linthost/rules_regexp.go +230 -14
  125. package/linthost/rules_security.go +45 -19
  126. package/linthost/rules_self.go +21 -1
  127. package/linthost/rules_solid.go +13 -7
  128. package/linthost/rules_storybook.go +1 -1
  129. package/linthost/rules_suggestions.go +490 -84
  130. package/linthost/rules_testing_library.go +12 -1
  131. package/linthost/rules_ts.go +227 -54
  132. package/linthost/rules_ts_async.go +0 -131
  133. package/linthost/rules_ts_no_base_to_string.go +3 -4
  134. package/linthost/rules_ts_no_misused_promises.go +817 -0
  135. package/linthost/rules_ts_no_restricted_types.go +320 -34
  136. package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
  137. package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
  138. package/linthost/rules_ts_require_array_sort_compare.go +5 -5
  139. package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
  140. package/linthost/rules_unicorn_better_regex.go +224 -16
  141. package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
  142. package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
  143. package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
  144. package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
  145. package/linthost/rules_unicorn_escape_case.go +30 -19
  146. package/linthost/rules_unicorn_filename_case.go +763 -16
  147. package/linthost/rules_unicorn_import_style.go +783 -17
  148. package/linthost/rules_unicorn_isolated_functions.go +934 -14
  149. package/linthost/rules_unicorn_no_hex_escape.go +26 -11
  150. package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
  151. package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
  152. package/linthost/rules_unicorn_no_unused_properties.go +756 -19
  153. package/linthost/rules_unicorn_number_literal_case.go +58 -39
  154. package/linthost/rules_unicorn_prefer_at.go +7 -3
  155. package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
  156. package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
  157. package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
  158. package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
  159. package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
  160. package/linthost/rules_unicorn_string_content.go +611 -15
  161. package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
  162. package/linthost/rules_unicorn_template_indent.go +923 -18
  163. package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
  164. package/linthost/rules_unicorn_throw_new_error.go +218 -26
  165. package/linthost/rules_var.go +829 -142
  166. package/linthost/string_length.go +222 -0
  167. package/package.json +4 -3
  168. package/rule/project.go +145 -0
  169. package/rule/rule.go +58 -1
  170. package/src/index.ts +58 -17
  171. package/src/structures/ITtscLintConfig.ts +3 -0
  172. package/src/structures/TtscLintRuleSetting.ts +3 -2
  173. package/src/structures/format/ITtscLintFormat.ts +3 -2
  174. package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
  175. package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
  176. package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
  177. package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
  178. package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
  179. package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
  180. package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
  181. package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
  182. package/src/structures/rules/ITtscLintRules.ts +7 -2
  183. package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
  184. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
  185. package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
  186. package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
  187. package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
  188. package/src/structures/rules/index.ts +3 -0
@@ -4,42 +4,66 @@ import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
5
5
  // noVar: ban `var` declarations. ESLint canonical:
6
6
  // https://eslint.org/docs/latest/rules/no-var
7
+ //
8
+ // The rule registers KindVariableDeclarationList, not KindVariableStatement:
9
+ // the grammar puts a `var` declaration list either inside a VariableStatement
10
+ // or directly in a `for` / `for...in` / `for...of` header, and only the list
11
+ // node is common to all four shapes. Registering the statement kind alone
12
+ // left every loop-header `var` invisible (issue #409). Each list node occurs
13
+ // exactly once in the tree, so every shape reports exactly once and no shape
14
+ // can double-report.
7
15
  type noVar struct{}
8
16
 
9
- func (noVar) Name() string { return "no-var" }
10
- func (noVar) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindVariableStatement} }
17
+ func (noVar) Name() string { return "no-var" }
18
+ func (noVar) Visits() []shimast.Kind {
19
+ return []shimast.Kind{shimast.KindVariableDeclarationList}
20
+ }
11
21
  func (noVar) Check(ctx *Context, node *shimast.Node) {
12
- stmt := node.AsVariableStatement()
13
- if stmt == nil || stmt.DeclarationList == nil {
22
+ if node.AsVariableDeclarationList() == nil || !shimast.IsVar(node) {
14
23
  return
15
24
  }
16
25
  if ctx.File != nil && ctx.File.IsDeclarationFile {
17
26
  return
18
27
  }
19
- if node.ModifierFlags()&shimast.ModifierFlagsAmbient != 0 {
28
+ owner := node.Parent
29
+ ownedByStatement := owner != nil && owner.Kind == shimast.KindVariableStatement
30
+ // `declare var x` describes an existing binding instead of creating one;
31
+ // like ESLint, the rule leaves ambient declarations alone. Only a
32
+ // VariableStatement can carry the modifier — loop headers cannot be
33
+ // ambient outside declaration files, which returned above.
34
+ if ownedByStatement && owner.ModifierFlags()&shimast.ModifierFlagsAmbient != 0 {
20
35
  return
21
36
  }
22
- if shimast.IsVar(stmt.DeclarationList) {
23
- const message = "Unexpected var, use let or const instead."
24
- start := keywordStart(ctx.File, stmt.DeclarationList, "var")
25
- if start >= 0 && isNoVarAutoFixSafe(ctx, node) {
26
- ctx.ReportRangeFix(
27
- start,
28
- start+len("var"),
29
- message,
30
- TextEdit{Pos: start, End: start + len("var"), Text: "let"},
31
- )
32
- return
33
- }
34
- ctx.Report(node, message)
37
+ const message = "Unexpected var, use let or const instead."
38
+ start := keywordStart(ctx.File, node, "var")
39
+ if start >= 0 && isNoVarAutoFixSafe(ctx, node) {
40
+ ctx.ReportRangeFix(
41
+ start,
42
+ start+len("var"),
43
+ message,
44
+ TextEdit{Pos: start, End: start + len("var"), Text: "let"},
45
+ )
46
+ return
47
+ }
48
+ if ownedByStatement {
49
+ // Fixless statement reports keep the whole-statement range (through the
50
+ // terminating semicolon) the rule has always rendered.
51
+ ctx.Report(owner, message)
52
+ return
35
53
  }
54
+ // A loop-header list has no wrapping statement of its own; the list node
55
+ // (`var i = 0`) is the natural diagnostic range.
56
+ ctx.Report(node, message)
36
57
  }
37
58
 
38
- // isNoVarAutoFixSafe reports whether rewriting a `var` statement's keyword to
39
- // `let` is safe using only AST-local information (no scope/data-flow engine).
40
- // It mirrors the conservative posture of isEqeqeqAutoFixSafe: over-declining
41
- // is fine, corrupting source is not. Blindly turning every `var` into `let`
42
- // breaks two real shapes that `var` hoisting tolerates but `let` does not:
59
+ // isNoVarAutoFixSafe reports whether rewriting a `var` declaration list's
60
+ // keyword to `let` is safe using only AST-local information (no
61
+ // scope/data-flow engine). `listNode` is the KindVariableDeclarationList the
62
+ // rule visited; its parent is a VariableStatement or a `for` / `for...in` /
63
+ // `for...of` header. It mirrors the conservative posture of
64
+ // isEqeqeqAutoFixSafe: over-declining is fine, corrupting source is not.
65
+ // Blindly turning every `var` into `let` breaks two real shapes that `var`
66
+ // hoisting tolerates but `let` does not:
43
67
  // - redeclaration (`var x=1; var x=2;`) → two `let x` is a SyntaxError;
44
68
  // - use-before-declaration (a binding referenced above its own line) →
45
69
  // `let` makes that reference a TDZ ReferenceError.
@@ -47,8 +71,8 @@ func (noVar) Check(ctx *Context, node *shimast.Node) {
47
71
  // Five corruption holes were patched piecemeal here before (var-vs-var,
48
72
  // for-header var, function/class redeclaration, mixed destructuring sibling,
49
73
  // object-literal shorthand, use-before-declaration). That whack-a-mole is
50
- // replaced by one conservative rule with two preconditions; the fix is
51
- // emitted only if BOTH hold:
74
+ // replaced by one conservative rule with five preconditions; the fix is
75
+ // emitted only if ALL hold:
52
76
  //
53
77
  // 1. Single binding in the whole file. The declared name is introduced by
54
78
  // EXACTLY ONE binding position anywhere in the source — counting every
@@ -60,50 +84,163 @@ func (noVar) Check(ctx *Context, node *shimast.Node) {
60
84
  // destructure siblings, for-header var, …). It over-declines harmless
61
85
  // cross-scope same-name bindings, which never corrupts.
62
86
  // 2. No use-before-declaration / TDZ. The declared name is not referenced as
63
- // a VALUE before the statement's Pos(). A non-reference occurrence of the
87
+ // a VALUE before the list's Pos(). A non-reference occurrence of the
64
88
  // same text — a member name (`o.x`), an object-literal key (`{x:1}`), a
65
89
  // statement label (`x:`), or a type reference (`: x`) — binds no value and
66
90
  // must not force a decline; isValueReferenceIdentifier classifies these.
91
+ // 3. No block-scope escape. `var` is function/global-scoped while `let` is
92
+ // block-scoped, so a `var` declared inside a block and read after the
93
+ // block (`if (c) { var x = 1; } log(x);`) would stop compiling under
94
+ // `let`. For a statement list the statement's parent must be a position
95
+ // where a lexical declaration is legal AND which forms the `let`'s block
96
+ // scope — a Block (plain, function-body, loop-body, …), a ModuleBlock, a
97
+ // switch Case/DefaultClause, or the SourceFile — and every value
98
+ // reference to the name must lie inside that parent's [Pos, End) span. A
99
+ // non-scope parent (`if (c) var x = 1;` — a single-statement body, where
100
+ // `let` is a SyntaxError outright) declines unconditionally. For a
101
+ // loop-header list the loop statement itself is the scope: a header
102
+ // `let` is always grammatically legal and scopes to the whole loop
103
+ // (header plus body), so references must stay inside the loop's span
104
+ // (`for (var i = 0; …) {}` followed by `log(i);` declines). Given
105
+ // precondition 1 (one binding file-wide), positional containment is
106
+ // exact: no other binding can shadow the name, so a reference outside
107
+ // the span really does resolve to this binding. Using the CaseClause
108
+ // (not the whole switch CaseBlock) as the scope over-declines cross-case
109
+ // references, which under `let` would flip from `undefined` reads to
110
+ // runtime TDZ throws — declining is the safe side. Mirrors ESLint
111
+ // no-var's isUsedFromOutsideOf.
112
+ // 4. No loop-closure capture. When the declaration sits inside a loop — a
113
+ // statement in a loop body OR the loop's own header — and the name is
114
+ // referenced from a function or arrow nested within that loop
115
+ // (`for (const k of keys) { var last = k; fns.push(() => last); }`,
116
+ // `for (var i = 0; i < n; i++) { fns.push(() => i); }`), every closure
117
+ // shares ONE `var` binding but would capture a FRESH per-iteration `let`
118
+ // binding — the rewrite silently changes runtime results. Mirrors ESLint
119
+ // no-var's isReferencedInClosure loop check.
120
+ // 5. Not declared under a `with` statement. `var` hoists PAST the with body
121
+ // to the function scope, so references inside the body resolve through
122
+ // the with object first (`o.x` shadows the var when present); `let`
123
+ // stays inside the body's block scope, where it shadows the with object
124
+ // instead. The rewrite can flip which binding every reference hits, so
125
+ // any var with a WithStatement ancestor below the nearest function
126
+ // boundary declines.
67
127
  //
68
- // The var statement being fixed must itself be a single plain identifier
128
+ // Two loop-header-only grammar/TDZ hazards also decline:
129
+ // - a `for...in` / `for...of` declarator with an initializer (Annex B
130
+ // tolerates `for (var i = 0 in o)`; `for (let i = 0 in o)` is a
131
+ // SyntaxError), and
132
+ // - a value reference to the name inside the `for...in` / `for...of` head
133
+ // expression (`for (var x of x)` reads the hoisted undefined `var`;
134
+ // under `let` the head expression evaluates inside the binding's TDZ
135
+ // and throws a ReferenceError).
136
+ //
137
+ // The var list being fixed must itself be a single plain identifier
69
138
  // declarator so the keyword rewrite has a simple `let x` rename target; a
70
- // destructuring var (`var {a}=o`) or a multi-binding list is declined here
71
- // even though its names might each bind only once.
72
- func isNoVarAutoFixSafe(ctx *Context, node *shimast.Node) bool {
139
+ // destructuring var (`var {a}=o`, `for (var [a] of …)`) or a multi-binding
140
+ // list is declined here even though its names might each bind only once.
141
+ func isNoVarAutoFixSafe(ctx *Context, listNode *shimast.Node) bool {
73
142
  if ctx == nil || ctx.File == nil {
74
143
  return false
75
144
  }
76
- // Precondition: the var being fixed is a single plain identifier declarator.
77
- // variableStatementBindingNames returns plain identifier names only, so a
78
- // destructuring declarator yields zero names; requiring exactly one name AND
79
- // exactly one VariableDeclaration node rejects both multi-binding lists and
80
- // any destructured (or destructured-sibling) declarator.
81
- names := variableStatementBindingNames(node)
82
- if len(names) != 1 {
145
+ owner := listNode.Parent
146
+ if owner == nil {
147
+ return false
148
+ }
149
+ // Precondition: the var being fixed is a single plain identifier
150
+ // declarator. identifierText returns "" for a destructuring pattern, so
151
+ // requiring exactly one VariableDeclaration node AND a non-empty plain
152
+ // name rejects both multi-binding lists and destructured declarators.
153
+ list := listNode.AsVariableDeclarationList()
154
+ if list == nil || list.Declarations == nil || len(list.Declarations.Nodes) != 1 {
155
+ return false
156
+ }
157
+ decl := list.Declarations.Nodes[0].AsVariableDeclaration()
158
+ if decl == nil {
159
+ return false
160
+ }
161
+ target := identifierText(decl.Name())
162
+ if target == "" {
83
163
  return false
84
164
  }
85
- if list := node.AsVariableStatement().DeclarationList.AsVariableDeclarationList(); list == nil ||
86
- list.Declarations == nil || len(list.Declarations.Nodes) != 1 {
165
+
166
+ // A name `var` tolerates but `let` cannot redeclare: `let let = 1;` is a
167
+ // SyntaxError everywhere, and `let static = 1;` is one in strict mode
168
+ // (modules, classes) — while `var let` / `var static` parse in sloppy
169
+ // scripts. Mirrors upstream ESLint no-var's DISALLOWED_LET_NAMES.
170
+ if target == "let" || target == "static" {
87
171
  return false
88
172
  }
89
- target := names[0]
90
173
 
91
- declPos := node.Pos()
174
+ // Precondition 3 setup: resolve the node that will delimit the rewritten
175
+ // `let` binding's block scope, per owner shape.
176
+ var scopeNode *shimast.Node
177
+ switch owner.Kind {
178
+ case shimast.KindVariableStatement:
179
+ // The statement's parent must both allow a lexical declaration and act
180
+ // as the `let`'s block scope. Any other parent kind is a
181
+ // single-statement body (`if (c) var x = 1;`, `while (c) var x = 1;`,
182
+ // `label: var x = 1;`, `with (o) var x = 1;`) where `let` is a plain
183
+ // SyntaxError, so the fix declines before any reference is examined.
184
+ scopeNode = owner.Parent
185
+ if scopeNode == nil || !isBlockScopeContainer(scopeNode.Kind) {
186
+ return false
187
+ }
188
+ case shimast.KindForStatement:
189
+ scopeNode = owner
190
+ case shimast.KindForInStatement, shimast.KindForOfStatement:
191
+ // Annex B parses `for (var i = 0 in o)` in sloppy scripts; the same
192
+ // header with `let` is a SyntaxError everywhere, so any initializer on
193
+ // a for-in/for-of declarator declines outright.
194
+ if decl.Initializer != nil {
195
+ return false
196
+ }
197
+ scopeNode = owner
198
+ default:
199
+ // A declaration list can only be owned by the four shapes above; an
200
+ // unrecognized owner keeps the diagnostic but never offers a rewrite.
201
+ return false
202
+ }
203
+ scopeStart, scopeEnd := scopeNode.Pos(), scopeNode.End()
204
+
205
+ // Precondition 5: a `var` declared under a `with` statement resolves its
206
+ // references through the with object; the block-scoped `let` would shadow
207
+ // that object instead, so the rewrite declines outright.
208
+ if isDeclaredInsideWithStatement(listNode) {
209
+ return false
210
+ }
211
+
212
+ // Precondition 4 setup: the outermost loop enclosing the declaration
213
+ // without an intervening function boundary. A loop-header list's first
214
+ // ancestor IS its loop, so header declarations always run the
215
+ // closure-capture check. nil when the declaration is not loop-local,
216
+ // which disables the check.
217
+ enclosingLoop := enclosingLoopWithinFunction(listNode)
218
+
219
+ declPos := listNode.Pos()
92
220
  // The single declarator's initializer subtree. A value reference to `target`
93
221
  // inside this range is a self-reference that runs while `target` is still in
94
222
  // its temporal dead zone under `let` (`var x = typeof x;`, `var x = x;`,
95
223
  // `var x = (() => x)();`), so it must also force a decline even though its
96
- // Pos() is AFTER the statement's start. initStart/initEnd are -1 when the
224
+ // Pos() is AFTER the list's start. initStart/initEnd are -1 when the
97
225
  // declarator has no initializer, which disables the range check.
98
226
  initStart, initEnd := -1, -1
99
- if list := node.AsVariableStatement().DeclarationList.AsVariableDeclarationList(); list != nil &&
100
- list.Declarations != nil && len(list.Declarations.Nodes) == 1 {
101
- if decl := list.Declarations.Nodes[0].AsVariableDeclaration(); decl != nil && decl.Initializer != nil {
102
- initStart, initEnd = decl.Initializer.Pos(), decl.Initializer.End()
227
+ if decl.Initializer != nil {
228
+ initStart, initEnd = decl.Initializer.Pos(), decl.Initializer.End()
229
+ }
230
+ // The for-in/for-of head expression evaluates while a `let` loop binding
231
+ // is still in its TDZ, so a self-reference there (`for (var x of x)`)
232
+ // must decline exactly like an initializer self-reference. -1 for the
233
+ // other owner shapes, which have no head expression.
234
+ exprStart, exprEnd := -1, -1
235
+ if owner.Kind == shimast.KindForInStatement || owner.Kind == shimast.KindForOfStatement {
236
+ if stmt := owner.AsForInOrOfStatement(); stmt != nil && stmt.Expression != nil {
237
+ exprStart, exprEnd = stmt.Expression.Pos(), stmt.Expression.End()
103
238
  }
104
239
  }
105
240
  bindingCount := 0
106
241
  referencedBefore := false
242
+ escapesScope := false
243
+ capturedInLoop := false
107
244
  walkDescendants(ctx.File.AsNode(), func(child *shimast.Node) {
108
245
  // Binding count: every position that introduces the name `target` as a
109
246
  // binding (a declaration), not a value reference. Two or more positions
@@ -115,28 +252,167 @@ func isNoVarAutoFixSafe(ctx *Context, node *shimast.Node) bool {
115
252
  }
116
253
  // TDZ: a value reference to `target` turns into a ReferenceError under
117
254
  // `let` when it executes while `target` is still in its temporal dead
118
- // zone. Two cases decline:
119
- // - a reference BEFORE the var's own position (`log(x); var x = 1;`);
255
+ // zone. Three cases decline:
256
+ // - a reference BEFORE the list's own position (`log(x); var x = 1;`);
120
257
  // - a self-reference WITHIN the declarator's own initializer range
121
258
  // (`var x = typeof x;`). Conservatively, any value reference inside
122
259
  // the initializer declines — including a deferred read in a nested
123
260
  // closure (`var f = () => f;`) that is actually safe — because the
124
261
  // AST-local gate does not track whether that closure runs during
125
- // initialization. Over-declining never corrupts source.
262
+ // initialization. Over-declining never corrupts source;
263
+ // - a self-reference within a for-in/for-of head expression
264
+ // (`for (var x of x)`), which evaluates inside the `let` TDZ.
126
265
  if child.Kind == shimast.KindIdentifier && identifierText(child) == target &&
127
266
  isValueReferenceIdentifier(child) {
128
267
  pos := child.Pos()
129
- if pos < declPos || (initStart >= 0 && pos >= initStart && pos < initEnd) {
268
+ if pos < declPos ||
269
+ (initStart >= 0 && pos >= initStart && pos < initEnd) ||
270
+ (exprStart >= 0 && pos >= exprStart && pos < exprEnd) {
130
271
  referencedBefore = true
131
272
  }
273
+ // Scope escape: a value reference outside the enclosing block-scope
274
+ // node's span stops resolving (or flips to a TDZ throw across switch
275
+ // cases) once the binding becomes block-scoped `let`. Nested blocks
276
+ // WITHIN the span stay fixable: containment, not clause equality.
277
+ if pos < scopeStart || pos >= scopeEnd {
278
+ escapesScope = true
279
+ }
280
+ // Loop-closure capture: a reference reached only by crossing a
281
+ // function/arrow boundary between itself and the enclosing loop would
282
+ // switch from one shared `var` binding to a fresh per-iteration `let`
283
+ // binding.
284
+ if enclosingLoop != nil && isCapturedInLoopClosure(child, enclosingLoop) {
285
+ capturedInLoop = true
286
+ }
132
287
  }
133
288
  })
134
- if referencedBefore {
289
+ if referencedBefore || escapesScope || capturedInLoop {
135
290
  return false
136
291
  }
137
292
  return bindingCount == 1
138
293
  }
139
294
 
295
+ // isBlockScopeContainer reports whether a node kind is a legal parent for a
296
+ // lexical (`let`) declaration statement AND the node that delimits the
297
+ // resulting binding's block scope: a Block (plain, function body, loop body,
298
+ // try/catch/finally, labeled block, class static block body — all
299
+ // KindBlock), a namespace's ModuleBlock, a switch clause, or the SourceFile
300
+ // itself. Every other statement position (an unbraced if/else/loop/with body
301
+ // or a directly-labeled statement) rejects lexical declarations at the
302
+ // grammar level, so the keyword rewrite is never legal there.
303
+ //
304
+ // A CaseClause/DefaultClause is used as the scope rather than the enclosing
305
+ // CaseBlock even though `let` technically hoists to the whole switch block:
306
+ // a reference in a LATER clause compiles under `let` but changes an
307
+ // `undefined` read into a runtime TDZ ReferenceError when the declaring
308
+ // clause did not execute first. Bounding the scope at the clause declines
309
+ // that shape; over-declining never corrupts.
310
+ func isBlockScopeContainer(kind shimast.Kind) bool {
311
+ switch kind {
312
+ case shimast.KindBlock,
313
+ shimast.KindModuleBlock,
314
+ shimast.KindCaseClause,
315
+ shimast.KindDefaultClause,
316
+ shimast.KindSourceFile:
317
+ return true
318
+ }
319
+ return false
320
+ }
321
+
322
+ // isFunctionCaptureBoundary reports whether a node creates a new function
323
+ // scope whose body captures outer bindings by closure: function
324
+ // declarations/expressions, arrows, methods, constructors, accessors, class
325
+ // static blocks, and class property declarations (an instance field
326
+ // initializer runs at construction time and closes over the class
327
+ // definition's environment exactly like a method body; escope models it as
328
+ // its own variable scope). A PropertyDeclaration's computed NAME evaluates
329
+ // immediately rather than deferred, so classifying the whole declaration
330
+ // over-declines that rare shape — which never corrupts. Used both to stop
331
+ // the enclosing-loop walk (a `var` inside a function nested in a loop is
332
+ // per-call regardless of the loop) and to detect references that reach the
333
+ // loop only through deferred code.
334
+ func isFunctionCaptureBoundary(node *shimast.Node) bool {
335
+ switch node.Kind {
336
+ case shimast.KindFunctionDeclaration,
337
+ shimast.KindFunctionExpression,
338
+ shimast.KindArrowFunction,
339
+ shimast.KindMethodDeclaration,
340
+ shimast.KindConstructor,
341
+ shimast.KindGetAccessor,
342
+ shimast.KindSetAccessor,
343
+ shimast.KindClassStaticBlockDeclaration,
344
+ shimast.KindPropertyDeclaration:
345
+ return true
346
+ }
347
+ return false
348
+ }
349
+
350
+ // isDeclaredInsideWithStatement reports whether the declaration (a
351
+ // statement's or loop header's `var` list) has a WithStatement ancestor
352
+ // below the nearest function boundary. Under `var`
353
+ // the binding hoists past the with body to the function scope, so a
354
+ // same-name property on the with target intercepts every reference; under
355
+ // `let` the binding lives inside the body's block and shadows the with
356
+ // object instead. A function boundary resets the risk: a `var` inside a
357
+ // function nested in the with body binds tighter than the with object
358
+ // either way.
359
+ func isDeclaredInsideWithStatement(node *shimast.Node) bool {
360
+ for ancestor := node.Parent; ancestor != nil; ancestor = ancestor.Parent {
361
+ if isFunctionCaptureBoundary(ancestor) {
362
+ return false
363
+ }
364
+ if ancestor.Kind == shimast.KindWithStatement {
365
+ return true
366
+ }
367
+ }
368
+ return false
369
+ }
370
+
371
+ // enclosingLoopWithinFunction returns the OUTERMOST loop statement enclosing
372
+ // `node` without an intervening function boundary, or nil when no such loop
373
+ // exists. The walk stops at the nearest function-like ancestor because a
374
+ // `var` declared inside a function nested in a loop already gets a fresh
375
+ // binding per call — the loop outside the function cannot make `var` and
376
+ // `let` capture semantics diverge for it.
377
+ func enclosingLoopWithinFunction(node *shimast.Node) *shimast.Node {
378
+ var loop *shimast.Node
379
+ for ancestor := node.Parent; ancestor != nil; ancestor = ancestor.Parent {
380
+ if isFunctionCaptureBoundary(ancestor) {
381
+ break
382
+ }
383
+ switch ancestor.Kind {
384
+ case shimast.KindForStatement,
385
+ shimast.KindForInStatement,
386
+ shimast.KindForOfStatement,
387
+ shimast.KindWhileStatement,
388
+ shimast.KindDoStatement:
389
+ loop = ancestor
390
+ }
391
+ }
392
+ return loop
393
+ }
394
+
395
+ // isCapturedInLoopClosure reports whether a value-reference identifier
396
+ // reaches `loop` on its ancestor chain only after crossing a function
397
+ // boundary — i.e. the reference lives in a closure created inside the loop.
398
+ // Under `var` every iteration's closure shares one binding; under `let` each
399
+ // iteration captures a fresh binding, so such a reference makes the keyword
400
+ // rewrite change observable behavior. A reference whose chain never meets
401
+ // `loop` lies outside the loop entirely and is left to the scope-containment
402
+ // check.
403
+ func isCapturedInLoopClosure(ref, loop *shimast.Node) bool {
404
+ crossedFunction := false
405
+ for ancestor := ref.Parent; ancestor != nil; ancestor = ancestor.Parent {
406
+ if ancestor == loop {
407
+ return crossedFunction
408
+ }
409
+ if isFunctionCaptureBoundary(ancestor) {
410
+ crossedFunction = true
411
+ }
412
+ }
413
+ return false
414
+ }
415
+
140
416
  // bindingNamesIntroducedBy returns every plain identifier name that `node`
141
417
  // introduces as a binding in its OWN slot — never the names bound by its
142
418
  // descendants, which the file-wide walk visits independently. The classifier
@@ -286,142 +562,553 @@ func isValueReferenceIdentifier(node *shimast.Node) bool {
286
562
  return true
287
563
  }
288
564
 
289
- // preferConst: flag `let` declarations whose binding is never reassigned.
290
- // This follows ESLint's core rule for the common AST-local cases. It is
291
- // intentionally conservative: destructuring and declaration-only `let`
292
- // variables (those without an initializer and not in a for-of/for-in
293
- // header) are skipped until the lint host grows full scope/data-flow state.
294
- // ESLint canonical: https://eslint.org/docs/latest/rules/prefer-const
295
- type preferConst struct{}
565
+ // preferConst follows ESLint's binding semantics rather than comparing
566
+ // identifier text. TypeScript's checker resolves every declaration and write
567
+ // target to its lexical symbol, so same-spelled bindings in sibling, nested,
568
+ // and shadowing scopes remain independent. The rule records the declaration's
569
+ // initialization plus subsequent writes, then reports bindings with exactly
570
+ // one effective initialization.
571
+ //
572
+ // Declaration-only bindings are diagnostic-only. Rewriting `let value;` plus
573
+ // a later `value = expression` requires moving comments and changing statement
574
+ // structure, so the existing keyword edit stays limited to an initialized,
575
+ // single-declarator list. ESLint canonical:
576
+ // https://eslint.org/docs/latest/rules/prefer-const
577
+ type preferConst struct{ optionsRule }
578
+
579
+ type preferConstOptions struct {
580
+ Destructuring string `json:"destructuring"`
581
+ IgnoreReadBeforeAssign bool `json:"ignoreReadBeforeAssign"`
582
+ }
583
+
584
+ type preferConstWriteKind uint8
585
+
586
+ const (
587
+ preferConstSimpleAssignment preferConstWriteKind = iota
588
+ preferConstReassignment
589
+ )
590
+
591
+ type preferConstWrite struct {
592
+ target *shimast.Node
593
+ assignment *shimast.Node
594
+ kind preferConstWriteKind
595
+ }
596
+
597
+ type preferConstCandidate struct {
598
+ symbol *shimast.Symbol
599
+ nameNode *shimast.Node
600
+ declaration *shimast.Node
601
+ listNode *shimast.Node
602
+ scope *shimast.Node
603
+ declarationGroup *shimast.Node
604
+ initialized bool
605
+ readBeforeAssign bool
606
+ invalid bool
607
+ writes []preferConstWrite
608
+ }
296
609
 
297
- func (preferConst) Name() string { return "prefer-const" }
610
+ func (preferConst) Name() string { return "prefer-const" }
611
+ func (preferConst) NeedsTypeChecker() bool {
612
+ return true
613
+ }
298
614
  func (preferConst) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
299
615
  func (preferConst) Check(ctx *Context, node *shimast.Node) {
300
- type candidate struct {
301
- name string
302
- node *shimast.Node
303
- listNode *shimast.Node
616
+ if ctx.Checker == nil {
617
+ return
618
+ }
619
+
620
+ options := preferConstOptions{Destructuring: "any"}
621
+ _ = ctx.DecodeOptions(&options)
622
+ if options.Destructuring != "all" {
623
+ options.Destructuring = "any"
304
624
  }
305
- var candidates []candidate
306
- assigned := map[string]bool{}
625
+
626
+ candidates := make([]*preferConstCandidate, 0)
627
+ bySymbol := make(map[*shimast.Symbol]*preferConstCandidate)
628
+ declarationNames := make(map[*shimast.Node]struct{})
629
+ candidateNames := make(map[string]struct{})
630
+ groups := make(map[*shimast.Node][]*preferConstCandidate)
307
631
 
308
632
  walkDescendants(node, func(child *shimast.Node) {
309
- switch child.Kind {
310
- case shimast.KindVariableDeclaration:
311
- decl := child.AsVariableDeclaration()
312
- if decl == nil || child.Parent == nil || child.Parent.Kind != shimast.KindVariableDeclarationList {
313
- return
633
+ if child.Kind != shimast.KindVariableDeclaration {
634
+ return
635
+ }
636
+ decl := child.AsVariableDeclaration()
637
+ if decl == nil || child.Parent == nil || child.Parent.Kind != shimast.KindVariableDeclarationList {
638
+ return
639
+ }
640
+ listNode := child.Parent
641
+ if !shimast.IsLet(listNode) || preferConstIsForStatementInitializer(listNode) {
642
+ return
643
+ }
644
+
645
+ names := bindingIdentifierNodes(decl.Name())
646
+ if len(names) == 0 {
647
+ return
648
+ }
649
+ initialized := decl.Initializer != nil || preferConstIsForInOrOfInitializer(listNode)
650
+ destructuring := decl.Name() != nil && decl.Name().Kind != shimast.KindIdentifier
651
+ for _, nameNode := range names {
652
+ symbol := ctx.Checker.GetSymbolAtLocation(nameNode)
653
+ candidate := &preferConstCandidate{
654
+ symbol: symbol,
655
+ nameNode: nameNode,
656
+ declaration: child,
657
+ listNode: listNode,
658
+ scope: preferConstLexicalScope(nameNode),
659
+ initialized: initialized,
314
660
  }
315
- listNode := child.Parent
316
- if !shimast.IsLet(listNode) {
317
- return
661
+ if destructuring {
662
+ candidate.declarationGroup = child
663
+ groups[child] = append(groups[child], candidate)
318
664
  }
319
- name := identifierText(decl.Name())
320
- if name == "" {
321
- return
665
+ candidates = append(candidates, candidate)
666
+ declarationNames[nameNode] = struct{}{}
667
+ if name := identifierText(nameNode); name != "" {
668
+ candidateNames[name] = struct{}{}
322
669
  }
323
- if !isConstEligibleLetDeclaration(child, decl) {
324
- return
670
+ if symbol == nil {
671
+ candidate.invalid = true
672
+ continue
325
673
  }
326
- candidates = append(candidates, candidate{name: name, node: child, listNode: listNode})
674
+ if prior := bySymbol[symbol]; prior != nil {
675
+ // Duplicate lexical declarations are already a TypeScript error. Do
676
+ // not add a secondary const suggestion to either declaration.
677
+ prior.invalid = true
678
+ candidate.invalid = true
679
+ continue
680
+ }
681
+ bySymbol[symbol] = candidate
682
+ }
683
+ })
684
+
685
+ writeTargets := make(map[*shimast.Node]struct{})
686
+ walkDescendants(node, func(child *shimast.Node) {
687
+ switch child.Kind {
327
688
  case shimast.KindBinaryExpression:
328
689
  expr := child.AsBinaryExpression()
329
- if expr == nil || expr.OperatorToken == nil || !isAssignmentOperator(expr.OperatorToken.Kind) {
690
+ if expr == nil || expr.OperatorToken == nil || !isAssignmentOperator(expr.OperatorToken.Kind) ||
691
+ isDestructuringDefaultAssignment(child) {
330
692
  return
331
693
  }
332
- for _, name := range assignmentTargetNames(expr.Left) {
333
- assigned[name] = true
694
+ kind := preferConstReassignment
695
+ if expr.OperatorToken.Kind == shimast.KindEqualsToken {
696
+ kind = preferConstSimpleAssignment
697
+ }
698
+ targets := assignmentTargetIdentifiers(expr.Left)
699
+ for _, target := range targets {
700
+ preferConstRecordWrite(ctx, bySymbol, writeTargets, target, child, kind)
701
+ }
702
+ if kind == preferConstSimpleAssignment && len(targets) > 1 {
703
+ for _, target := range targets {
704
+ symbol := valueSymbolAtIdentifier(ctx, target)
705
+ if candidate := bySymbol[symbol]; candidate != nil {
706
+ groups[child] = appendPreferConstCandidate(groups[child], candidate)
707
+ }
708
+ }
334
709
  }
335
710
  case shimast.KindPrefixUnaryExpression:
336
711
  expr := child.AsPrefixUnaryExpression()
337
- if expr == nil {
338
- return
339
- }
340
- if expr.Operator == shimast.KindPlusPlusToken || expr.Operator == shimast.KindMinusMinusToken {
341
- if name := identifierText(expr.Operand); name != "" {
342
- assigned[name] = true
712
+ if expr != nil && (expr.Operator == shimast.KindPlusPlusToken || expr.Operator == shimast.KindMinusMinusToken) {
713
+ for _, target := range assignmentTargetIdentifiers(expr.Operand) {
714
+ preferConstRecordWrite(ctx, bySymbol, writeTargets, target, nil, preferConstReassignment)
343
715
  }
344
716
  }
345
717
  case shimast.KindPostfixUnaryExpression:
346
718
  expr := child.AsPostfixUnaryExpression()
347
- if expr == nil {
348
- return
349
- }
350
- if expr.Operator == shimast.KindPlusPlusToken || expr.Operator == shimast.KindMinusMinusToken {
351
- if name := identifierText(expr.Operand); name != "" {
352
- assigned[name] = true
719
+ if expr != nil && (expr.Operator == shimast.KindPlusPlusToken || expr.Operator == shimast.KindMinusMinusToken) {
720
+ for _, target := range assignmentTargetIdentifiers(expr.Operand) {
721
+ preferConstRecordWrite(ctx, bySymbol, writeTargets, target, nil, preferConstReassignment)
353
722
  }
354
723
  }
355
724
  case shimast.KindForOfStatement, shimast.KindForInStatement:
356
- // `for (x of …)` / `for (x in …)` reassigns the existing binding `x`
357
- // on every iteration. When the initializer IS a
358
- // VariableDeclarationList (`for (const y of …)`) it declares a fresh
359
- // binding instead, so only a non-declaration initializer counts as a
360
- // reassignment target. Missing this lets a pre-existing `let` be
361
- // rewritten to `const` that the loop then assigns to — a TS error and
362
- // runtime TypeError.
363
725
  stmt := child.AsForInOrOfStatement()
364
- if stmt == nil || stmt.Initializer == nil {
726
+ if stmt == nil || stmt.Initializer == nil || stmt.Initializer.Kind == shimast.KindVariableDeclarationList {
365
727
  return
366
728
  }
367
- if stmt.Initializer.Kind == shimast.KindVariableDeclarationList {
368
- return
729
+ for _, target := range assignmentTargetIdentifiers(stmt.Initializer) {
730
+ preferConstRecordWrite(ctx, bySymbol, writeTargets, target, nil, preferConstReassignment)
369
731
  }
370
- for _, name := range assignmentTargetNames(stmt.Initializer) {
371
- assigned[name] = true
732
+ }
733
+ })
734
+
735
+ // Only declaration-only candidates need read history. A read preceding the
736
+ // sole assignment changes the diagnostic location under the default option,
737
+ // and suppresses the diagnostic when ignoreReadBeforeAssign is enabled.
738
+ walkDescendants(node, func(child *shimast.Node) {
739
+ if child.Kind != shimast.KindIdentifier {
740
+ return
741
+ }
742
+ if _, ok := declarationNames[child]; ok {
743
+ return
744
+ }
745
+ if _, ok := writeTargets[child]; ok {
746
+ return
747
+ }
748
+ if _, ok := candidateNames[identifierText(child)]; !ok {
749
+ return
750
+ }
751
+ symbol := valueSymbolAtIdentifier(ctx, child)
752
+ candidate := bySymbol[symbol]
753
+ if candidate == nil || candidate.initialized || len(candidate.writes) != 1 {
754
+ return
755
+ }
756
+ if child.Pos() < candidate.writes[0].target.Pos() {
757
+ candidate.readBeforeAssign = true
758
+ }
759
+ })
760
+
761
+ eligible := make(map[*preferConstCandidate]bool, len(candidates))
762
+ for _, candidate := range candidates {
763
+ eligible[candidate] = preferConstCandidateIsEligible(ctx, candidate, bySymbol, options)
764
+ }
765
+
766
+ for _, candidate := range candidates {
767
+ if !eligible[candidate] {
768
+ continue
769
+ }
770
+ group := candidate.declarationGroup
771
+ if group == nil && !candidate.initialized && len(candidate.writes) == 1 {
772
+ assignment := candidate.writes[0].assignment
773
+ if len(groups[assignment]) > 1 {
774
+ group = assignment
372
775
  }
373
776
  }
777
+ if options.Destructuring == "all" && group != nil && !preferConstGroupIsEligible(groups[group], eligible) {
778
+ continue
779
+ }
780
+
781
+ reportNode := candidate.nameNode
782
+ if !candidate.initialized && !candidate.readBeforeAssign && len(candidate.writes) == 1 {
783
+ reportNode = candidate.writes[0].target
784
+ }
785
+
786
+ start := -1
787
+ if candidate.initialized && isSingleDeclarationList(candidate.listNode) {
788
+ declarationGroup := groups[candidate.declaration]
789
+ if len(declarationGroup) == 0 || preferConstGroupIsEligible(declarationGroup, eligible) {
790
+ start = keywordStart(ctx.File, candidate.listNode, "let")
791
+ }
792
+ }
793
+ if start >= 0 {
794
+ ctx.ReportFix(
795
+ reportNode,
796
+ "Use const instead of let.",
797
+ TextEdit{Pos: start, End: start + len("let"), Text: "const"},
798
+ )
799
+ } else {
800
+ ctx.Report(reportNode, "Use const instead of let.")
801
+ }
802
+ }
803
+ }
804
+
805
+ func preferConstRecordWrite(
806
+ ctx *Context,
807
+ bySymbol map[*shimast.Symbol]*preferConstCandidate,
808
+ writeTargets map[*shimast.Node]struct{},
809
+ target *shimast.Node,
810
+ assignment *shimast.Node,
811
+ kind preferConstWriteKind,
812
+ ) {
813
+ if target == nil || target.Kind != shimast.KindIdentifier {
814
+ return
815
+ }
816
+ symbol := valueSymbolAtIdentifier(ctx, target)
817
+ candidate := bySymbol[symbol]
818
+ if candidate == nil {
819
+ return
820
+ }
821
+ candidate.writes = append(candidate.writes, preferConstWrite{
822
+ target: target,
823
+ assignment: assignment,
824
+ kind: kind,
374
825
  })
826
+ writeTargets[target] = struct{}{}
827
+ }
375
828
 
376
- for _, c := range candidates {
377
- if !assigned[c.name] {
378
- start := -1
379
- if isSingleDeclarationList(c.listNode) {
380
- start = keywordStart(ctx.File, c.listNode, "let")
829
+ func preferConstCandidateIsEligible(
830
+ ctx *Context,
831
+ candidate *preferConstCandidate,
832
+ bySymbol map[*shimast.Symbol]*preferConstCandidate,
833
+ options preferConstOptions,
834
+ ) bool {
835
+ if candidate == nil || candidate.invalid || candidate.symbol == nil {
836
+ return false
837
+ }
838
+ if candidate.initialized {
839
+ return len(candidate.writes) == 0
840
+ }
841
+ if len(candidate.writes) != 1 || candidate.writes[0].kind != preferConstSimpleAssignment {
842
+ return false
843
+ }
844
+ if options.IgnoreReadBeforeAssign && candidate.readBeforeAssign {
845
+ return false
846
+ }
847
+ return preferConstAssignmentCanInitialize(ctx, candidate, candidate.writes[0], bySymbol)
848
+ }
849
+
850
+ func preferConstAssignmentCanInitialize(
851
+ ctx *Context,
852
+ candidate *preferConstCandidate,
853
+ write preferConstWrite,
854
+ bySymbol map[*shimast.Symbol]*preferConstCandidate,
855
+ ) bool {
856
+ assignment := write.assignment
857
+ if assignment == nil || assignment.Kind != shimast.KindBinaryExpression ||
858
+ candidate.scope == nil || candidate.scope != preferConstLexicalScope(write.target) {
859
+ return false
860
+ }
861
+ expr := assignment.AsBinaryExpression()
862
+ if expr == nil || expr.OperatorToken == nil || expr.OperatorToken.Kind != shimast.KindEqualsToken {
863
+ return false
864
+ }
865
+
866
+ statementOwner := assignment
867
+ for statementOwner.Parent != nil && statementOwner.Parent.Kind == shimast.KindParenthesizedExpression {
868
+ statementOwner = statementOwner.Parent
869
+ }
870
+ if statementOwner.Parent == nil || statementOwner.Parent.Kind != shimast.KindExpressionStatement {
871
+ return false
872
+ }
873
+ container := statementOwner.Parent.Parent
874
+ if container == nil {
875
+ return false
876
+ }
877
+ switch container.Kind {
878
+ case shimast.KindSourceFile,
879
+ shimast.KindBlock,
880
+ shimast.KindModuleBlock,
881
+ shimast.KindCaseClause,
882
+ shimast.KindDefaultClause:
883
+ default:
884
+ return false
885
+ }
886
+
887
+ if preferConstAssignmentTargetHasMember(expr.Left) {
888
+ return false
889
+ }
890
+ targets := assignmentTargetIdentifiers(expr.Left)
891
+ if len(targets) == 0 {
892
+ return false
893
+ }
894
+ for _, target := range targets {
895
+ symbol := valueSymbolAtIdentifier(ctx, target)
896
+ grouped := bySymbol[symbol]
897
+ if symbol == nil || !preferConstSymbolIsLocalToScope(symbol, candidate.scope) ||
898
+ (grouped != nil && grouped.invalid) {
899
+ return false
900
+ }
901
+ }
902
+ return true
903
+ }
904
+
905
+ func preferConstSymbolIsLocalToScope(symbol *shimast.Symbol, scope *shimast.Node) bool {
906
+ if symbol == nil || scope == nil {
907
+ return false
908
+ }
909
+ for _, declaration := range symbol.Declarations {
910
+ if preferConstDeclarationScope(declaration) == scope {
911
+ return true
912
+ }
913
+ }
914
+ return false
915
+ }
916
+
917
+ // preferConstDeclarationScope returns the scope that owns a sibling target in
918
+ // a destructuring assignment. Parameters cannot be folded into a declaration,
919
+ // and `var` declarations hoist past ordinary blocks to their function-like
920
+ // owner; every other declaration uses lexical scope.
921
+ func preferConstDeclarationScope(declaration *shimast.Node) *shimast.Node {
922
+ if declaration == nil {
923
+ return nil
924
+ }
925
+ for ancestor := declaration; ancestor != nil; ancestor = ancestor.Parent {
926
+ if ancestor.Kind == shimast.KindParameter {
927
+ return nil
928
+ }
929
+ if isFunctionLikeKind(ancestor) {
930
+ break
931
+ }
932
+ }
933
+ for ancestor := declaration.Parent; ancestor != nil; ancestor = ancestor.Parent {
934
+ if ancestor.Kind != shimast.KindVariableDeclarationList {
935
+ continue
936
+ }
937
+ if !shimast.IsVar(ancestor) {
938
+ break
939
+ }
940
+ for scope := ancestor.Parent; scope != nil; scope = scope.Parent {
941
+ if scope.Kind == shimast.KindSourceFile || scope.Kind == shimast.KindModuleBlock ||
942
+ scope.Kind == shimast.KindClassStaticBlockDeclaration || isFunctionLikeKind(scope) {
943
+ return scope
381
944
  }
382
- if start >= 0 {
383
- ctx.ReportFix(
384
- c.node,
385
- "Use const instead of let.",
386
- TextEdit{Pos: start, End: start + len("let"), Text: "const"},
387
- )
388
- } else {
389
- ctx.Report(c.node, "Use const instead of let.")
945
+ }
946
+ return nil
947
+ }
948
+ return preferConstLexicalScope(declaration)
949
+ }
950
+
951
+ func preferConstAssignmentTargetHasMember(node *shimast.Node) bool {
952
+ if node == nil {
953
+ return false
954
+ }
955
+ switch node.Kind {
956
+ case shimast.KindPropertyAccessExpression, shimast.KindElementAccessExpression:
957
+ return true
958
+ case shimast.KindParenthesizedExpression:
959
+ return preferConstAssignmentTargetHasMember(stripParens(node))
960
+ case shimast.KindNonNullExpression:
961
+ if expression := node.AsNonNullExpression(); expression != nil {
962
+ return preferConstAssignmentTargetHasMember(expression.Expression)
963
+ }
964
+ case shimast.KindAsExpression:
965
+ if expression := node.AsAsExpression(); expression != nil {
966
+ return preferConstAssignmentTargetHasMember(expression.Expression)
967
+ }
968
+ case shimast.KindTypeAssertionExpression:
969
+ if expression := node.AsTypeAssertion(); expression != nil {
970
+ return preferConstAssignmentTargetHasMember(expression.Expression)
971
+ }
972
+ case shimast.KindSatisfiesExpression:
973
+ if expression := node.AsSatisfiesExpression(); expression != nil {
974
+ return preferConstAssignmentTargetHasMember(expression.Expression)
975
+ }
976
+ case shimast.KindArrayLiteralExpression:
977
+ if array := node.AsArrayLiteralExpression(); array != nil && array.Elements != nil {
978
+ for _, element := range array.Elements.Nodes {
979
+ if preferConstAssignmentTargetHasMember(element) {
980
+ return true
981
+ }
982
+ }
983
+ }
984
+ case shimast.KindObjectLiteralExpression:
985
+ if object := node.AsObjectLiteralExpression(); object != nil && object.Properties != nil {
986
+ for _, property := range object.Properties.Nodes {
987
+ if preferConstAssignmentTargetHasMember(property) {
988
+ return true
989
+ }
390
990
  }
391
991
  }
992
+ case shimast.KindSpreadElement:
993
+ if spread := node.AsSpreadElement(); spread != nil {
994
+ return preferConstAssignmentTargetHasMember(spread.Expression)
995
+ }
996
+ case shimast.KindSpreadAssignment:
997
+ if spread := node.AsSpreadAssignment(); spread != nil {
998
+ return preferConstAssignmentTargetHasMember(spread.Expression)
999
+ }
1000
+ case shimast.KindPropertyAssignment:
1001
+ if property := node.AsPropertyAssignment(); property != nil {
1002
+ return preferConstAssignmentTargetHasMember(property.Initializer)
1003
+ }
1004
+ case shimast.KindBinaryExpression:
1005
+ if expression := node.AsBinaryExpression(); expression != nil && expression.OperatorToken != nil &&
1006
+ expression.OperatorToken.Kind == shimast.KindEqualsToken {
1007
+ return preferConstAssignmentTargetHasMember(expression.Left)
1008
+ }
1009
+ // A shorthand property assignment writes its name. Its optional object
1010
+ // assignment initializer is a default-value read, so member access there
1011
+ // does not make the destructuring target unsafe.
1012
+ case shimast.KindShorthandPropertyAssignment, shimast.KindIdentifier:
1013
+ return false
392
1014
  }
1015
+ return false
393
1016
  }
394
1017
 
395
- // isSingleDeclarationList reports whether the VariableDeclarationList node
396
- // declares exactly one binding, which is required before the `let` keyword
397
- // can safely be rewritten to `const` (a multi-binding list shares a single
398
- // keyword, so replacing just one binding's keyword is not valid).
399
- func isSingleDeclarationList(node *shimast.Node) bool {
1018
+ func preferConstGroupIsEligible(group []*preferConstCandidate, eligible map[*preferConstCandidate]bool) bool {
1019
+ if len(group) == 0 {
1020
+ return false
1021
+ }
1022
+ for _, candidate := range group {
1023
+ if !eligible[candidate] {
1024
+ return false
1025
+ }
1026
+ }
1027
+ return true
1028
+ }
1029
+
1030
+ func appendPreferConstCandidate(group []*preferConstCandidate, candidate *preferConstCandidate) []*preferConstCandidate {
1031
+ for _, existing := range group {
1032
+ if existing == candidate {
1033
+ return group
1034
+ }
1035
+ }
1036
+ return append(group, candidate)
1037
+ }
1038
+
1039
+ func bindingIdentifierNodes(node *shimast.Node) []*shimast.Node {
400
1040
  if node == nil {
1041
+ return nil
1042
+ }
1043
+ if node.Kind == shimast.KindIdentifier {
1044
+ return []*shimast.Node{node}
1045
+ }
1046
+ pattern := node.AsBindingPattern()
1047
+ if pattern == nil || pattern.Elements == nil {
1048
+ return nil
1049
+ }
1050
+ var identifiers []*shimast.Node
1051
+ for _, elementNode := range pattern.Elements.Nodes {
1052
+ element := elementNode.AsBindingElement()
1053
+ if element != nil {
1054
+ identifiers = append(identifiers, bindingIdentifierNodes(element.Name())...)
1055
+ }
1056
+ }
1057
+ return identifiers
1058
+ }
1059
+
1060
+ func preferConstIsForStatementInitializer(listNode *shimast.Node) bool {
1061
+ return listNode != nil && listNode.Parent != nil && listNode.Parent.Kind == shimast.KindForStatement
1062
+ }
1063
+
1064
+ func preferConstIsForInOrOfInitializer(listNode *shimast.Node) bool {
1065
+ if listNode == nil || listNode.Parent == nil {
401
1066
  return false
402
1067
  }
403
- list := node.AsVariableDeclarationList()
404
- return list != nil && list.Declarations != nil && len(list.Declarations.Nodes) == 1
1068
+ return listNode.Parent.Kind == shimast.KindForInStatement || listNode.Parent.Kind == shimast.KindForOfStatement
405
1069
  }
406
1070
 
407
- // isConstEligibleLetDeclaration reports whether a `let` VariableDeclaration
408
- // node is eligible for preferConst analysis. A declaration is eligible when:
409
- // - it has an initializer (the value is set immediately), or
410
- // - it is the loop variable of a for-in or for-of statement (e.g. `for (let k of m)`).
411
- //
412
- // For-statement initializers (`for (let i = 0; …)`) are eligible only when
413
- // the declaration list is a single binding; the loop index is a well-known
414
- // reassignment target so multi-binding for-statement lists are excluded.
415
- func isConstEligibleLetDeclaration(node *shimast.Node, decl *shimast.VariableDeclaration) bool {
416
- if decl.Initializer != nil {
417
- if node.Parent != nil && node.Parent.Parent != nil && node.Parent.Parent.Kind == shimast.KindForStatement {
418
- list := node.Parent.AsVariableDeclarationList()
419
- return list == nil || list.Declarations == nil || len(list.Declarations.Nodes) == 1
1071
+ func preferConstLexicalScope(node *shimast.Node) *shimast.Node {
1072
+ if node == nil {
1073
+ return nil
1074
+ }
1075
+ for ancestor := node.Parent; ancestor != nil; ancestor = ancestor.Parent {
1076
+ if isFunctionLikeKind(ancestor) {
1077
+ return ancestor
1078
+ }
1079
+ switch ancestor.Kind {
1080
+ case shimast.KindSourceFile,
1081
+ shimast.KindModuleBlock,
1082
+ shimast.KindCaseBlock,
1083
+ shimast.KindForStatement,
1084
+ shimast.KindForInStatement,
1085
+ shimast.KindForOfStatement,
1086
+ shimast.KindCatchClause,
1087
+ shimast.KindClassStaticBlockDeclaration,
1088
+ shimast.KindPropertyDeclaration:
1089
+ return ancestor
1090
+ case shimast.KindBlock:
1091
+ if ancestor.Parent != nil && (isFunctionLikeKind(ancestor.Parent) ||
1092
+ ancestor.Parent.Kind == shimast.KindClassStaticBlockDeclaration) {
1093
+ return ancestor.Parent
1094
+ }
1095
+ return ancestor
420
1096
  }
421
- return true
422
1097
  }
423
- return node.Parent != nil && node.Parent.Parent != nil &&
424
- (node.Parent.Parent.Kind == shimast.KindForInStatement || node.Parent.Parent.Kind == shimast.KindForOfStatement)
1098
+ return nil
1099
+ }
1100
+
1101
+ // isSingleDeclarationList reports whether the VariableDeclarationList contains
1102
+ // exactly one declarator. Multiple comma-separated declarators share one
1103
+ // keyword, so replacing it for only one declarator is not valid. One
1104
+ // destructuring declarator may still contain several bindings; preferConst
1105
+ // separately requires every affected binding to be eligible before fixing it.
1106
+ func isSingleDeclarationList(node *shimast.Node) bool {
1107
+ if node == nil {
1108
+ return false
1109
+ }
1110
+ list := node.AsVariableDeclarationList()
1111
+ return list != nil && list.Declarations != nil && len(list.Declarations.Nodes) == 1
425
1112
  }
426
1113
 
427
1114
  // noUndefInit: forbid `let x = undefined` and `var x = undefined`.