@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
@@ -1,6 +1,7 @@
1
1
  package linthost
2
2
 
3
3
  import (
4
+ "regexp"
4
5
  "strings"
5
6
 
6
7
  shimast "github.com/microsoft/typescript-go/shim/ast"
@@ -26,6 +27,36 @@ func nodeText(file *shimast.SourceFile, node *shimast.Node) string {
26
27
  return strings.TrimRight(src[pos:end], " \t\r\n")
27
28
  }
28
29
 
30
+ // templateRawNewlinePattern is acorn's TemplateElement raw normalization
31
+ // (`raw.replace(/\r\n?/g, "\n")` in parseTemplateElement).
32
+ var templateRawNewlinePattern = regexp.MustCompile(`\r\n?`)
33
+
34
+ // normalizeTemplateRaw reproduces the raw value ESLint sees for a template
35
+ // quasi: acorn materializes `TemplateElement.value.raw` with CR / CRLF
36
+ // collapsed to LF, so upstream rules both match patterns against and rewrite
37
+ // from the LF-normalized text. TypeScript's scanner performs the same
38
+ // collapse while cooking a template token (ECMA-262 normalizes <CR> and
39
+ // <CRLF> to <LF> in template values), so a raw payload sliced straight out of
40
+ // a CRLF source file must be normalized before it is compared with, or
41
+ // substituted back into, a cooked value.
42
+ func normalizeTemplateRaw(raw string) string {
43
+ return templateRawNewlinePattern.ReplaceAllString(raw, "\n")
44
+ }
45
+
46
+ // isTaggedTemplateQuasi reports whether `node` is the template argument of a
47
+ // TaggedTemplateExpression — the position whose RAW text the tag function
48
+ // observes, and therefore the one where escape spelling is meaningful.
49
+ // A literal that merely appears somewhere inside a tagged template (e.g.
50
+ // within a substitution) or that sits in the tag slot is not the quasi and
51
+ // stays checked, mirroring upstream's isTaggedTemplateLiteral.
52
+ func isTaggedTemplateQuasi(node *shimast.Node) bool {
53
+ if node == nil || node.Parent == nil || node.Parent.Kind != shimast.KindTaggedTemplateExpression {
54
+ return false
55
+ }
56
+ tagged := node.Parent.AsTaggedTemplateExpression()
57
+ return tagged != nil && tagged.Template == node
58
+ }
59
+
29
60
  // keywordStart returns the source offset of a declaration keyword such as
30
61
  // `var` or `let` at the start of a node after leading trivia.
31
62
  func keywordStart(file *shimast.SourceFile, node *shimast.Node, keyword string) int {
@@ -113,6 +144,103 @@ func tokenRange(file *shimast.SourceFile, node *shimast.Node) (int, int) {
113
144
  return pos, end
114
145
  }
115
146
 
147
+ // isTaggedTemplateElement reports whether a template token — a
148
+ // `KindNoSubstitutionTemplateLiteral` or one of the
149
+ // `KindTemplateHead`/`Middle`/`Tail` elements of a `KindTemplateExpression`
150
+ // — belongs to the quasi of a TaggedTemplateExpression. The tag function
151
+ // observes the raw text (`String.raw`, `dedent`, `gql`, `css`, …), so rules
152
+ // that police escape spelling must leave tagged templates alone; this is
153
+ // upstream's `isTaggedTemplateLiteral(node.parent)` guard on its
154
+ // `TemplateElement` handlers.
155
+ //
156
+ // A template that merely appears inside a tagged template — nested in one of
157
+ // its substitutions — is not tagged itself and stays checked, so the walk
158
+ // stops at the element's own enclosing template instead of searching upward
159
+ // for any tag. Template literal *types* can never be tagged and always
160
+ // answer false.
161
+ func isTaggedTemplateElement(node *shimast.Node) bool {
162
+ if node == nil {
163
+ return false
164
+ }
165
+ template := node
166
+ switch node.Kind {
167
+ case shimast.KindNoSubstitutionTemplateLiteral:
168
+ // The literal is the whole template; the tag, if any, is its parent.
169
+ case shimast.KindTemplateHead:
170
+ template = node.Parent
171
+ case shimast.KindTemplateMiddle, shimast.KindTemplateTail:
172
+ if node.Parent == nil {
173
+ return false
174
+ }
175
+ template = node.Parent.Parent
176
+ default:
177
+ return false
178
+ }
179
+ if template == nil || template.Parent == nil ||
180
+ template.Parent.Kind != shimast.KindTaggedTemplateExpression {
181
+ return false
182
+ }
183
+ tagged := template.Parent.AsTaggedTemplateExpression()
184
+ return tagged != nil && tagged.Template == template
185
+ }
186
+
187
+ // isInsideTaggedTemplate reports whether a whole-template node — a
188
+ // `KindNoSubstitutionTemplateLiteral` or a `KindTemplateExpression` — sits
189
+ // anywhere inside a TaggedTemplateExpression, walking up to three ancestors so
190
+ // a template reached through a substitution span also answers true. Unlike the
191
+ // precise `isTaggedTemplateElement`, which asks only whether a template token's
192
+ // OWN enclosing template is tagged, this treats any enclosing tag as tainting.
193
+ // Used by `typescript/no-unnecessary-template-expression`.
194
+ func isInsideTaggedTemplate(node *shimast.Node) bool {
195
+ if node == nil || node.Parent == nil {
196
+ return false
197
+ }
198
+ parent := node.Parent
199
+ if parent.Kind == shimast.KindTaggedTemplateExpression {
200
+ return true
201
+ }
202
+ // Walk up at most two more hops to reach the TaggedTemplateExpression
203
+ // for the spans family.
204
+ for i := 0; i < 2 && parent.Parent != nil; i++ {
205
+ parent = parent.Parent
206
+ if parent.Kind == shimast.KindTaggedTemplateExpression {
207
+ return true
208
+ }
209
+ }
210
+ return false
211
+ }
212
+
213
+ // hasCommentBetween reports whether a comment begins anywhere in the source
214
+ // range [from, to). Fixers whose TextEdit keeps only part of the replaced
215
+ // span use it on the discarded sub-ranges: a comment there would be silently
216
+ // deleted by the edit, so the fix must be declined.
217
+ //
218
+ // The scan alternates SkipTriviaEx (whitespace, with StopAtComments so a
219
+ // comment opener is not consumed) with a two-byte opener check that
220
+ // distinguishes `//` and `/*` from a bare slash token. Non-trivia token
221
+ // bytes are stepped over one at a time, so an opener lookalike inside a
222
+ // string literal within the range would over-detect; callers only ever
223
+ // decline an autofix on a hit, which is the safe direction.
224
+ func hasCommentBetween(src string, from, to int) bool {
225
+ if from < 0 {
226
+ return false
227
+ }
228
+ if to > len(src) {
229
+ to = len(src)
230
+ }
231
+ for pos := from; pos < to; {
232
+ next := shimscanner.SkipTriviaEx(src, pos, &shimscanner.SkipTriviaOptions{StopAtComments: true})
233
+ if next < pos || next >= to {
234
+ return false
235
+ }
236
+ if src[next] == '/' && next+1 < len(src) && (src[next+1] == '/' || src[next+1] == '*') {
237
+ return true
238
+ }
239
+ pos = next + 1
240
+ }
241
+ return false
242
+ }
243
+
116
244
  // isIdentifierPart reports whether `ch` can appear inside a JavaScript
117
245
  // identifier — used as a word-boundary guard by keyword search helpers.
118
246
  // Handles only ASCII; multibyte Unicode identifier parts are treated as
@@ -152,6 +280,184 @@ func stripParens(node *shimast.Node) *shimast.Node {
152
280
  return node
153
281
  }
154
282
 
283
+ // sameReferenceExpression reports whether two expressions denote the same
284
+ // repeatable runtime reference. Parentheses and TypeScript's type-only
285
+ // expression wrappers are transparent; identifiers, primitive literals,
286
+ // `this` / `super`, and property or element-access chains are compared
287
+ // recursively. Calls and other effectful expressions are deliberately rejected
288
+ // even when their source text is identical because evaluating them twice need
289
+ // not produce the same value.
290
+ //
291
+ // Static member keys are normalized to their runtime property key, so `x.y`
292
+ // and `x["y"]` compare equal. Optional and non-optional member accesses remain
293
+ // distinct because optional chaining changes evaluation semantics.
294
+ func sameReferenceExpression(left, right *shimast.Node) bool {
295
+ left = unwrapReferenceExpression(left)
296
+ right = unwrapReferenceExpression(right)
297
+ if left == nil || right == nil {
298
+ return false
299
+ }
300
+
301
+ switch left.Kind {
302
+ case shimast.KindIdentifier, shimast.KindPrivateIdentifier:
303
+ return left.Kind == right.Kind && shimast.NodeText(left) == shimast.NodeText(right)
304
+ case shimast.KindThisKeyword, shimast.KindSuperKeyword:
305
+ return left.Kind == right.Kind
306
+ case shimast.KindPropertyAccessExpression, shimast.KindElementAccessExpression:
307
+ leftMember, ok := referenceMemberParts(left)
308
+ if !ok {
309
+ return false
310
+ }
311
+ rightMember, ok := referenceMemberParts(right)
312
+ if !ok || leftMember.optional != rightMember.optional ||
313
+ !sameReferenceExpression(leftMember.object, rightMember.object) {
314
+ return false
315
+ }
316
+ if leftMember.staticKey != nil || rightMember.staticKey != nil {
317
+ return leftMember.staticKey != nil && rightMember.staticKey != nil &&
318
+ *leftMember.staticKey == *rightMember.staticKey &&
319
+ leftMember.private == rightMember.private
320
+ }
321
+ return sameReferenceExpression(leftMember.dynamicKey, rightMember.dynamicKey)
322
+ default:
323
+ leftValue, leftKind, leftOK := referencePrimitiveValue(left)
324
+ rightValue, rightKind, rightOK := referencePrimitiveValue(right)
325
+ return leftOK && rightOK && leftKind == rightKind && leftValue == rightValue
326
+ }
327
+ }
328
+
329
+ // unwrapReferenceExpression removes syntax that has no runtime evaluation of
330
+ // its own. Keep this list explicit: optional chains, calls, and instantiation
331
+ // expressions are not harmless wrappers and must remain visible to callers.
332
+ func unwrapReferenceExpression(node *shimast.Node) *shimast.Node {
333
+ for node != nil {
334
+ switch node.Kind {
335
+ case shimast.KindParenthesizedExpression,
336
+ shimast.KindAsExpression,
337
+ shimast.KindSatisfiesExpression,
338
+ shimast.KindNonNullExpression,
339
+ shimast.KindTypeAssertionExpression:
340
+ next := node.Expression()
341
+ if next == nil {
342
+ return node
343
+ }
344
+ node = next
345
+ default:
346
+ return node
347
+ }
348
+ }
349
+ return nil
350
+ }
351
+
352
+ type referenceMember struct {
353
+ object *shimast.Node
354
+ staticKey *string
355
+ dynamicKey *shimast.Node
356
+ optional bool
357
+ private bool
358
+ }
359
+
360
+ func referenceMemberParts(node *shimast.Node) (referenceMember, bool) {
361
+ switch node.Kind {
362
+ case shimast.KindPropertyAccessExpression:
363
+ access := node.AsPropertyAccessExpression()
364
+ if access == nil || access.Expression == nil || access.Name() == nil {
365
+ return referenceMember{}, false
366
+ }
367
+ name := access.Name()
368
+ if name.Kind != shimast.KindIdentifier && name.Kind != shimast.KindPrivateIdentifier {
369
+ return referenceMember{}, false
370
+ }
371
+ key := shimast.NodeText(name)
372
+ return referenceMember{
373
+ object: access.Expression,
374
+ staticKey: &key,
375
+ optional: access.QuestionDotToken != nil,
376
+ private: name.Kind == shimast.KindPrivateIdentifier,
377
+ }, true
378
+ case shimast.KindElementAccessExpression:
379
+ access := node.AsElementAccessExpression()
380
+ if access == nil || access.Expression == nil || access.ArgumentExpression == nil {
381
+ return referenceMember{}, false
382
+ }
383
+ member := referenceMember{
384
+ object: access.Expression,
385
+ dynamicKey: access.ArgumentExpression,
386
+ optional: access.QuestionDotToken != nil,
387
+ }
388
+ if key, ok := referenceStaticPropertyKey(access.ArgumentExpression); ok {
389
+ member.staticKey = &key
390
+ member.dynamicKey = nil
391
+ }
392
+ return member, true
393
+ default:
394
+ return referenceMember{}, false
395
+ }
396
+ }
397
+
398
+ func referenceStaticPropertyKey(node *shimast.Node) (string, bool) {
399
+ node = unwrapReferenceExpression(node)
400
+ if node == nil {
401
+ return "", false
402
+ }
403
+ value, kind, ok := referencePrimitiveValue(node)
404
+ if ok {
405
+ if kind == "bigint" {
406
+ value = strings.TrimSuffix(value, "n")
407
+ }
408
+ return value, true
409
+ }
410
+ if node.Kind != shimast.KindPrefixUnaryExpression {
411
+ return "", false
412
+ }
413
+ prefix := node.AsPrefixUnaryExpression()
414
+ if prefix == nil || prefix.Operand == nil {
415
+ return "", false
416
+ }
417
+ value, kind, ok = referencePrimitiveValue(prefix.Operand)
418
+ if !ok || (kind != "number" && kind != "bigint") {
419
+ return "", false
420
+ }
421
+ value = strings.TrimSuffix(value, "n")
422
+ switch prefix.Operator {
423
+ case shimast.KindPlusToken:
424
+ return value, kind == "number"
425
+ case shimast.KindMinusToken:
426
+ if strings.Trim(value, "0.") == "" {
427
+ return "0", true
428
+ }
429
+ return "-" + value, true
430
+ default:
431
+ return "", false
432
+ }
433
+ }
434
+
435
+ // referencePrimitiveValue returns a normalized value plus a runtime-kind tag.
436
+ // The tag keeps numerically similar values such as `1` and `1n` distinct when
437
+ // they are receivers; referenceStaticPropertyKey intentionally collapses them
438
+ // later because JavaScript converts both to the same string property key.
439
+ func referencePrimitiveValue(node *shimast.Node) (value, kind string, ok bool) {
440
+ if node == nil {
441
+ return "", "", false
442
+ }
443
+ switch node.Kind {
444
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
445
+ return stringLiteralText(node), "string", true
446
+ case shimast.KindNumericLiteral:
447
+ return numericLiteralText(node), "number", true
448
+ case shimast.KindBigIntLiteral:
449
+ return numericLiteralText(node), "bigint", true
450
+ case shimast.KindTrueKeyword:
451
+ return "true", "boolean", true
452
+ case shimast.KindFalseKeyword:
453
+ return "false", "boolean", true
454
+ case shimast.KindNullKeyword:
455
+ return "null", "null", true
456
+ default:
457
+ return "", "", false
458
+ }
459
+ }
460
+
155
461
  // isMatchingPropertyAccess reports whether `node` reads the chain
156
462
  // `head.tail[0].tail[1]…`. Useful for detecting `obj.__proto__` or
157
463
  // `console.log` shapes regardless of nesting.
@@ -305,83 +611,275 @@ func (w *descendantsWalker) visitChild(child *shimast.Node) bool {
305
611
  return false
306
612
  }
307
613
 
308
- // assignmentTargetNames collects the identifier names written by an
309
- // assignment's left-hand side. A bare Identifier yields one name. A
614
+ // assignmentTargetIdentifiers collects the Identifier nodes written by an
615
+ // assignment's left-hand side. A bare Identifier yields one node. A
310
616
  // destructuring-assignment target — parsed as an ArrayLiteralExpression
311
617
  // (`[a, b] = …`) or ObjectLiteralExpression (`({a} = …)`) rather than a
312
618
  // binding pattern — is walked so every nested write position is counted:
313
619
  // array elements, object property values, shorthand properties, defaults
314
620
  // (`[a = 1]`, `{a = 1}`), nested patterns, and rest elements.
315
621
  //
316
- // Property names in `{key: target}` are read positions, not writes, so
317
- // only the property value contributes; member-access targets (`obj.x`)
318
- // declare no local binding and are skipped. Returns nil for other shapes.
319
- func assignmentTargetNames(node *shimast.Node) []string {
622
+ // TypeScript assertion wrappers are transparent in reference position, so
623
+ // `as`, angle-bracket assertions, `satisfies`, non-null assertions, and
624
+ // parentheses are unwrapped recursively wherever they appear in a target.
625
+ // Property names in `{key: target}` are read positions, not writes, so only
626
+ // the property value contributes; member-access targets (`obj.x`) declare no
627
+ // local binding and are skipped. Returns nil for other shapes.
628
+ func assignmentTargetIdentifiers(node *shimast.Node) []*shimast.Node {
320
629
  if node == nil {
321
630
  return nil
322
631
  }
323
- if name := identifierText(node); name != "" {
324
- return []string{name}
632
+ if node.Kind == shimast.KindIdentifier {
633
+ return []*shimast.Node{node}
634
+ }
635
+ var identifiers []*shimast.Node
636
+ collectAssignmentTargetIdentifiers(node, &identifiers)
637
+ return identifiers
638
+ }
639
+
640
+ // valueSymbolAtIdentifier returns the checker symbol for an identifier in a
641
+ // value position. A shorthand property assignment needs the checker's
642
+ // dedicated value-symbol query because GetSymbolAtLocation resolves the
643
+ // syntactic property name rather than the binding written by `({name} = value)`.
644
+ func valueSymbolAtIdentifier(ctx *Context, identifier *shimast.Node) *shimast.Symbol {
645
+ if ctx == nil || ctx.Checker == nil || identifier == nil || identifier.Kind != shimast.KindIdentifier {
646
+ return nil
647
+ }
648
+ if parent := identifier.Parent; parent != nil && parent.Kind == shimast.KindShorthandPropertyAssignment {
649
+ if shorthand := parent.AsShorthandPropertyAssignment(); shorthand != nil && shorthand.Name() == identifier {
650
+ return ctx.Checker.GetShorthandAssignmentValueSymbol(parent)
651
+ }
652
+ }
653
+ return ctx.Checker.GetSymbolAtLocation(identifier)
654
+ }
655
+
656
+ // canonicalValueSymbol resolves an identifier in value position and
657
+ // normalizes exported local/export pairs and declaration merges to one binding
658
+ // identity. TypeScript binds an exported declaration to both a local symbol
659
+ // (used by references in the declaring module) and its ExportSymbol (returned
660
+ // for the declaration name), so both sides must converge before comparison.
661
+ // Rules that protect declarations from writes use this for both the
662
+ // declaration name and every modifying reference, including shorthand
663
+ // destructuring targets.
664
+ func canonicalValueSymbol(ctx *Context, identifier *shimast.Node) *shimast.Symbol {
665
+ symbol := valueSymbolAtIdentifier(ctx, identifier)
666
+ if symbol == nil {
667
+ return nil
668
+ }
669
+ if symbol.Flags&shimast.SymbolFlagsExportValue != 0 && symbol.ExportSymbol != nil {
670
+ symbol = symbol.ExportSymbol
671
+ }
672
+ return ctx.Checker.GetMergedSymbol(symbol)
673
+ }
674
+
675
+ // writeTargetIdentifiers returns every identifier written by one reference-
676
+ // modifying syntax node. It recognizes assignment operators, update
677
+ // expressions, and expression initializers of for-in/of statements; nested
678
+ // destructuring and TypeScript assertion wrappers are delegated to the shared
679
+ // assignment-target walker.
680
+ //
681
+ // Walking all nodes can encounter a destructuring default both through its
682
+ // outer pattern and as a nested binary expression. Callers that report a full
683
+ // tree must therefore deduplicate the returned identifier nodes.
684
+ func writeTargetIdentifiers(node *shimast.Node) []*shimast.Node {
685
+ if node == nil {
686
+ return nil
687
+ }
688
+ switch node.Kind {
689
+ case shimast.KindBinaryExpression:
690
+ expression := node.AsBinaryExpression()
691
+ if expression != nil && expression.OperatorToken != nil &&
692
+ isAssignmentOperator(expression.OperatorToken.Kind) {
693
+ return assignmentTargetIdentifiers(expression.Left)
694
+ }
695
+ case shimast.KindPrefixUnaryExpression:
696
+ expression := node.AsPrefixUnaryExpression()
697
+ if expression != nil &&
698
+ (expression.Operator == shimast.KindPlusPlusToken || expression.Operator == shimast.KindMinusMinusToken) {
699
+ return assignmentTargetIdentifiers(expression.Operand)
700
+ }
701
+ case shimast.KindPostfixUnaryExpression:
702
+ expression := node.AsPostfixUnaryExpression()
703
+ if expression != nil &&
704
+ (expression.Operator == shimast.KindPlusPlusToken || expression.Operator == shimast.KindMinusMinusToken) {
705
+ return assignmentTargetIdentifiers(expression.Operand)
706
+ }
707
+ case shimast.KindForInStatement, shimast.KindForOfStatement:
708
+ statement := node.AsForInOrOfStatement()
709
+ if statement != nil && statement.Initializer != nil &&
710
+ statement.Initializer.Kind != shimast.KindVariableDeclarationList {
711
+ return assignmentTargetIdentifiers(statement.Initializer)
712
+ }
713
+ }
714
+ return nil
715
+ }
716
+
717
+ // assignmentTargetNames is the text-only projection used by rules that do not
718
+ // need binding identity.
719
+ func assignmentTargetNames(node *shimast.Node) []string {
720
+ identifiers := assignmentTargetIdentifiers(node)
721
+ if len(identifiers) == 0 {
722
+ return nil
723
+ }
724
+ names := make([]string, 0, len(identifiers))
725
+ for _, identifier := range identifiers {
726
+ if name := identifierText(identifier); name != "" {
727
+ names = append(names, name)
728
+ }
325
729
  }
326
- var names []string
327
- collectAssignmentTargetNames(node, &names)
328
730
  return names
329
731
  }
330
732
 
331
- // collectAssignmentTargetNames appends to `names` every identifier in a
733
+ // isDestructuringAssignmentTarget reports whether node belongs to the left
734
+ // side of a destructuring assignment. TypeScript-Go represents those targets
735
+ // as array/object literal expressions rather than binding-pattern nodes.
736
+ // Property keys, computed names, and default-value expressions are reads and
737
+ // therefore stop the target walk.
738
+ func isDestructuringAssignmentTarget(node *shimast.Node) bool {
739
+ child := node
740
+ for child != nil && child.Parent != nil {
741
+ parent := child.Parent
742
+ switch parent.Kind {
743
+ case shimast.KindBindingElement:
744
+ binding := parent.AsBindingElement()
745
+ if binding != nil &&
746
+ (binding.PropertyName == child || binding.Initializer == child) {
747
+ return false
748
+ }
749
+ case shimast.KindPropertyAssignment:
750
+ property := parent.AsPropertyAssignment()
751
+ if property != nil && property.Name() == child {
752
+ return false
753
+ }
754
+ case shimast.KindShorthandPropertyAssignment:
755
+ property := parent.AsShorthandPropertyAssignment()
756
+ if property != nil && property.ObjectAssignmentInitializer == child {
757
+ return false
758
+ }
759
+ case shimast.KindComputedPropertyName:
760
+ return false
761
+ case shimast.KindForInStatement, shimast.KindForOfStatement:
762
+ statement := parent.AsForInOrOfStatement()
763
+ return statement != nil && statement.Initializer == child
764
+ }
765
+ if parent.Kind == shimast.KindBinaryExpression {
766
+ expression := parent.AsBinaryExpression()
767
+ if expression != nil && expression.OperatorToken != nil &&
768
+ expression.OperatorToken.Kind == shimast.KindEqualsToken {
769
+ if expression.Left == child {
770
+ return true
771
+ }
772
+ if expression.Right == child {
773
+ return false
774
+ }
775
+ }
776
+ }
777
+ child = parent
778
+ }
779
+ return false
780
+ }
781
+
782
+ // collectAssignmentTargetIdentifiers appends to `identifiers` every identifier in a
332
783
  // destructuring-assignment target. It descends only through write-target
333
784
  // positions so reads (object property keys, computed-member expressions)
334
785
  // never count as reassignments.
335
- func collectAssignmentTargetNames(node *shimast.Node, names *[]string) {
786
+ func collectAssignmentTargetIdentifiers(node *shimast.Node, identifiers *[]*shimast.Node) {
336
787
  if node == nil {
337
788
  return
338
789
  }
339
790
  switch node.Kind {
340
791
  case shimast.KindIdentifier:
341
- if name := identifierText(node); name != "" {
342
- *names = append(*names, name)
343
- }
792
+ *identifiers = append(*identifiers, node)
344
793
  case shimast.KindParenthesizedExpression:
345
- collectAssignmentTargetNames(stripParens(node), names)
794
+ collectAssignmentTargetIdentifiers(stripParens(node), identifiers)
795
+ case shimast.KindNonNullExpression:
796
+ if expression := node.AsNonNullExpression(); expression != nil {
797
+ collectAssignmentTargetIdentifiers(expression.Expression, identifiers)
798
+ }
799
+ case shimast.KindAsExpression:
800
+ if expression := node.AsAsExpression(); expression != nil {
801
+ collectAssignmentTargetIdentifiers(expression.Expression, identifiers)
802
+ }
803
+ case shimast.KindTypeAssertionExpression:
804
+ if expression := node.AsTypeAssertion(); expression != nil {
805
+ collectAssignmentTargetIdentifiers(expression.Expression, identifiers)
806
+ }
807
+ case shimast.KindSatisfiesExpression:
808
+ if expression := node.AsSatisfiesExpression(); expression != nil {
809
+ collectAssignmentTargetIdentifiers(expression.Expression, identifiers)
810
+ }
346
811
  case shimast.KindArrayLiteralExpression:
347
812
  if arr := node.AsArrayLiteralExpression(); arr != nil && arr.Elements != nil {
348
813
  for _, el := range arr.Elements.Nodes {
349
- collectAssignmentTargetNames(el, names)
814
+ collectAssignmentTargetIdentifiers(el, identifiers)
350
815
  }
351
816
  }
352
817
  case shimast.KindObjectLiteralExpression:
353
818
  if obj := node.AsObjectLiteralExpression(); obj != nil && obj.Properties != nil {
354
819
  for _, prop := range obj.Properties.Nodes {
355
- collectAssignmentTargetNames(prop, names)
820
+ collectAssignmentTargetIdentifiers(prop, identifiers)
356
821
  }
357
822
  }
358
823
  case shimast.KindSpreadElement:
359
824
  if spread := node.AsSpreadElement(); spread != nil {
360
- collectAssignmentTargetNames(spread.Expression, names)
825
+ collectAssignmentTargetIdentifiers(spread.Expression, identifiers)
361
826
  }
362
827
  case shimast.KindSpreadAssignment:
363
828
  if spread := node.AsSpreadAssignment(); spread != nil {
364
- collectAssignmentTargetNames(spread.Expression, names)
829
+ collectAssignmentTargetIdentifiers(spread.Expression, identifiers)
365
830
  }
366
831
  case shimast.KindShorthandPropertyAssignment:
367
832
  // `{a}` and `{a = 1}` — the property name is the write target; any
368
833
  // ObjectAssignmentInitializer is a default value, not a target.
369
834
  if short := node.AsShorthandPropertyAssignment(); short != nil {
370
- collectAssignmentTargetNames(short.Name(), names)
835
+ collectAssignmentTargetIdentifiers(short.Name(), identifiers)
371
836
  }
372
837
  case shimast.KindPropertyAssignment:
373
838
  // `{key: target}` — only the value (initializer) is written to.
374
839
  if assignment := node.AsPropertyAssignment(); assignment != nil {
375
- collectAssignmentTargetNames(assignment.Initializer, names)
840
+ collectAssignmentTargetIdentifiers(assignment.Initializer, identifiers)
376
841
  }
377
842
  case shimast.KindBinaryExpression:
378
843
  // A default inside a pattern (`[a = 1]`, `{key: a = 1}`) parses as an
379
844
  // `=` BinaryExpression; only its left side is the write target.
380
845
  if expr := node.AsBinaryExpression(); expr != nil &&
381
846
  expr.OperatorToken != nil && expr.OperatorToken.Kind == shimast.KindEqualsToken {
382
- collectAssignmentTargetNames(expr.Left, names)
847
+ collectAssignmentTargetIdentifiers(expr.Left, identifiers)
848
+ }
849
+ }
850
+ }
851
+
852
+ // isDestructuringDefaultAssignment distinguishes the `a = 1` syntax inside
853
+ // `[a = 1]` or `{a = 1}` from a second assignment. A real assignment nested
854
+ // in the default expression's right side leaves the outer target path and is
855
+ // therefore not classified as a default.
856
+ func isDestructuringDefaultAssignment(node *shimast.Node) bool {
857
+ if node == nil {
858
+ return false
859
+ }
860
+ current := node
861
+ for parent := node.Parent; parent != nil; parent = parent.Parent {
862
+ switch parent.Kind {
863
+ case shimast.KindParenthesizedExpression,
864
+ shimast.KindArrayLiteralExpression,
865
+ shimast.KindObjectLiteralExpression,
866
+ shimast.KindPropertyAssignment,
867
+ shimast.KindSpreadElement,
868
+ shimast.KindSpreadAssignment:
869
+ current = parent
870
+ continue
871
+ case shimast.KindShorthandPropertyAssignment:
872
+ return false
873
+ case shimast.KindBinaryExpression:
874
+ expression := parent.AsBinaryExpression()
875
+ return expression != nil && expression.OperatorToken != nil &&
876
+ isAssignmentOperator(expression.OperatorToken.Kind) &&
877
+ current.Pos() >= expression.Left.Pos() && current.End() <= expression.Left.End()
878
+ default:
879
+ return false
383
880
  }
384
881
  }
882
+ return false
385
883
  }
386
884
 
387
885
  // isLiteralLike reports whether `node` (after stripping parentheses) is a