@ttsc/lint 0.18.4 → 0.19.1

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 +125 -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 +47 -8
  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 +159 -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 +231 -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 +206 -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,13 +1,38 @@
1
- // MVP: registers the rule so the typed-key parity test passes; the
2
- // upstream behavior needs scope analysis (track each destructured
3
- // binding through subsequent member-access sites in the same scope),
4
- // which is out of scope for this AST-only port.
1
+ // unicorn/consistent-destructuring: once a property has been destructured
2
+ // from an object with `const {a} = foo`, later reads of `foo.a` in the same
3
+ // or a nested scope must use the destructured binding instead.
5
4
  //
6
- // unicorn/consistent-destructuring:
5
+ // The port mirrors the current upstream implementation: only `const`
6
+ // declarations whose initializer is a bare identifier or `this` are tracked;
7
+ // member reads are matched to the most recent preceding declaration through
8
+ // checker binding identity so shadowed roots, shadowed bindings, and
9
+ // same-name-different-variable pairs never collude. Reads are exempt when
10
+ // the root variable or the same member was written between the declaration
11
+ // and the read (conservatively including writes from other function scopes,
12
+ // which may execute in between through calls or closures), when the read
13
+ // sits in a positive `'prop' in obj` guarded branch (TypeScript narrowing),
14
+ // or when the member expression is itself written, called, tagged, or
15
+ // computed. Replacements ship as editor suggestions — never autofixes —
16
+ // exactly like upstream's `hasSuggestions` contract; nested member chains
17
+ // report without a suggestion.
18
+ //
19
+ // eslint-scope concepts translate as follows: variable identity is the
20
+ // checker symbol at the identifier (export/merge-normalized on both sides of
21
+ // every comparison), scope containment is AST ancestry of the declaration's
22
+ // nearest scope-creating node, and `variableScope` is the nearest
23
+ // function-like boundary (source file, function, accessor, static block,
24
+ // class-field initializer, namespace, or enum body).
7
25
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-destructuring.md
8
26
  package linthost
9
27
 
10
- import shimast "github.com/microsoft/typescript-go/shim/ast"
28
+ import (
29
+ "fmt"
30
+
31
+ shimast "github.com/microsoft/typescript-go/shim/ast"
32
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
33
+ )
34
+
35
+ const unicornConsistentDestructuringMessage = "Use destructured variables over properties."
11
36
 
12
37
  type unicornConsistentDestructuring struct{}
13
38
 
@@ -17,7 +42,977 @@ func (unicornConsistentDestructuring) Name() string {
17
42
  func (unicornConsistentDestructuring) Visits() []shimast.Kind {
18
43
  return []shimast.Kind{shimast.KindSourceFile}
19
44
  }
20
- func (unicornConsistentDestructuring) Check(ctx *Context, node *shimast.Node) {
45
+
46
+ // The checker supplies binding identity: distinguishing a shadowing inner
47
+ // `foo` (or a shadowing inner destructured binding) from the declaration's
48
+ // root is exactly the resolution eslint-scope performs upstream.
49
+ func (unicornConsistentDestructuring) NeedsTypeChecker() bool { return true }
50
+
51
+ // unicornConsistentDestructuringDeclaration is one tracked
52
+ // `const {…} = foo` / `const {…} = this` declarator.
53
+ type unicornConsistentDestructuringDeclaration struct {
54
+ // object is the unwrapped initializer node (Identifier or `this`).
55
+ object *shimast.Node
56
+ // objectEnd is the initializer's end offset; upstream orders every
57
+ // declaration/read/write comparison against it.
58
+ objectEnd int
59
+ // key is the raw source text of the initializer ("foo" / "this").
60
+ key string
61
+ // rootName is the cooked identifier name; empty for `this`.
62
+ rootName string
63
+ // rootSymbol is the checker binding of the initializer identifier; nil
64
+ // for `this` or an unresolvable global.
65
+ rootSymbol *shimast.Symbol
66
+ // thisBoundary is the enclosing `this` boundary for `this` initializers.
67
+ thisBoundary *shimast.Node
68
+ // pattern is the ObjectBindingPattern being declared.
69
+ pattern *shimast.Node
70
+ // scope is the nearest scope-creating ancestor of the declarator; a read
71
+ // matches only when it sits inside this scope.
72
+ scope *shimast.Node
73
+ }
74
+
75
+ // unicornConsistentDestructuringMemberWrite is one recorded write through a
76
+ // member expression (`foo.a = 1`, `foo.a++`, `delete foo.a`, destructuring
77
+ // assignment targets, `for (foo.a of …)`).
78
+ type unicornConsistentDestructuringMemberWrite struct {
79
+ property string
80
+ rootName string
81
+ rootSymbol *shimast.Symbol
82
+ thisBoundary *shimast.Node
83
+ start int
84
+ variableScope *shimast.Node
85
+ }
86
+
87
+ // unicornConsistentDestructuringRootWrite is one recorded write to an
88
+ // identifier binding (assignment, update, for-in/of target, declarator init).
89
+ type unicornConsistentDestructuringRootWrite struct {
90
+ symbol *shimast.Symbol
91
+ start int
92
+ variableScope *shimast.Node
93
+ }
94
+
95
+ // unicornConsistentDestructuringState is the per-file collection pass state.
96
+ type unicornConsistentDestructuringState struct {
97
+ ctx *Context
98
+ declarations map[string][]*unicornConsistentDestructuringDeclaration
99
+ members []*shimast.Node
100
+ memberWrites []unicornConsistentDestructuringMemberWrite
101
+ rootWrites []unicornConsistentDestructuringRootWrite
102
+ memberWriteSeen map[*shimast.Node]struct{}
103
+ rootWriteSeen map[*shimast.Node]struct{}
104
+ }
105
+
106
+ func (unicornConsistentDestructuring) Check(ctx *Context, root *shimast.Node) {
107
+ if ctx == nil || ctx.File == nil || root == nil {
108
+ return
109
+ }
110
+ state := &unicornConsistentDestructuringState{
111
+ ctx: ctx,
112
+ declarations: make(map[string][]*unicornConsistentDestructuringDeclaration),
113
+ memberWriteSeen: make(map[*shimast.Node]struct{}),
114
+ rootWriteSeen: make(map[*shimast.Node]struct{}),
115
+ }
116
+ walkDescendants(root, state.collect)
117
+ for _, member := range state.members {
118
+ state.checkMember(member)
119
+ }
120
+ }
121
+
122
+ // collect records declarations, member reads, member writes, and root-binding
123
+ // writes in one source-order pass, mirroring upstream's listener set.
124
+ func (s *unicornConsistentDestructuringState) collect(node *shimast.Node) {
125
+ switch node.Kind {
126
+ case shimast.KindVariableDeclaration:
127
+ s.collectDeclaration(node)
128
+ s.collectDeclaratorRootWrites(node)
129
+ case shimast.KindBinaryExpression:
130
+ expression := node.AsBinaryExpression()
131
+ if expression != nil && expression.OperatorToken != nil &&
132
+ isAssignmentOperator(expression.OperatorToken.Kind) {
133
+ s.collectMemberWritesFromTarget(expression.Left)
134
+ s.collectRootWrites(node)
135
+ }
136
+ case shimast.KindPrefixUnaryExpression:
137
+ expression := node.AsPrefixUnaryExpression()
138
+ if expression != nil &&
139
+ (expression.Operator == shimast.KindPlusPlusToken || expression.Operator == shimast.KindMinusMinusToken) {
140
+ s.addDirectMemberWrite(stripParens(expression.Operand))
141
+ s.collectRootWrites(node)
142
+ }
143
+ case shimast.KindPostfixUnaryExpression:
144
+ expression := node.AsPostfixUnaryExpression()
145
+ if expression != nil &&
146
+ (expression.Operator == shimast.KindPlusPlusToken || expression.Operator == shimast.KindMinusMinusToken) {
147
+ s.addDirectMemberWrite(stripParens(expression.Operand))
148
+ s.collectRootWrites(node)
149
+ }
150
+ case shimast.KindDeleteExpression:
151
+ expression := node.AsDeleteExpression()
152
+ if expression != nil {
153
+ s.addDirectMemberWrite(stripParens(expression.Expression))
154
+ }
155
+ case shimast.KindForInStatement, shimast.KindForOfStatement:
156
+ statement := node.AsForInOrOfStatement()
157
+ if statement == nil || statement.Initializer == nil {
158
+ return
159
+ }
160
+ if statement.Initializer.Kind == shimast.KindVariableDeclarationList {
161
+ s.collectForHeadDeclarationRootWrites(statement.Initializer)
162
+ return
163
+ }
164
+ s.collectMemberWritesFromTarget(statement.Initializer)
165
+ s.collectRootWrites(node)
166
+ case shimast.KindPropertyAccessExpression:
167
+ if !unicornConsistentDestructuringIsJsxTagName(node) {
168
+ s.members = append(s.members, node)
169
+ }
170
+ }
171
+ }
172
+
173
+ // collectDeclaration tracks `const {…} = <identifier|this>` declarators.
174
+ // Upstream requires the `const` kind, an object pattern, and a "simple"
175
+ // initializer (identifier or `this`); parentheses are unwrapped because the
176
+ // ESTree oracle never materializes them, while TypeScript assertion wrappers
177
+ // intentionally disqualify the initializer exactly as upstream does.
178
+ func (s *unicornConsistentDestructuringState) collectDeclaration(node *shimast.Node) {
179
+ declaration := node.AsVariableDeclaration()
180
+ if declaration == nil || declaration.Initializer == nil {
181
+ return
182
+ }
183
+ name := declaration.Name()
184
+ if name == nil || name.Kind != shimast.KindObjectBindingPattern {
185
+ return
186
+ }
187
+ if node.Parent == nil || node.Parent.Kind != shimast.KindVariableDeclarationList ||
188
+ !shimast.IsConst(node) {
189
+ return
190
+ }
191
+ object := stripParens(declaration.Initializer)
192
+ if object == nil ||
193
+ (object.Kind != shimast.KindIdentifier && object.Kind != shimast.KindThisKeyword) {
194
+ return
195
+ }
196
+ entry := &unicornConsistentDestructuringDeclaration{
197
+ object: object,
198
+ objectEnd: object.End(),
199
+ key: nodeText(s.ctx.File, object),
200
+ pattern: name,
201
+ scope: unicornConsistentDestructuringNearestScope(node),
202
+ }
203
+ if object.Kind == shimast.KindIdentifier {
204
+ entry.rootName = identifierText(object)
205
+ entry.rootSymbol = canonicalValueSymbol(s.ctx, object)
206
+ } else {
207
+ entry.thisBoundary = unicornConsistentDestructuringThisBoundary(object)
208
+ }
209
+ if entry.key == "" {
210
+ return
211
+ }
212
+ s.declarations[entry.key] = append(s.declarations[entry.key], entry)
213
+ }
214
+
215
+ // collectMemberWritesFromTarget descends one assignment target the way
216
+ // upstream's addMemberExpressionWrites walks ESTree patterns. TypeScript-Go
217
+ // parses assignment destructuring targets as array/object literal
218
+ // expressions; property keys and default-value right sides are reads and are
219
+ // therefore not descended into.
220
+ func (s *unicornConsistentDestructuringState) collectMemberWritesFromTarget(node *shimast.Node) {
221
+ if node == nil {
222
+ return
223
+ }
224
+ switch node.Kind {
225
+ case shimast.KindPropertyAccessExpression:
226
+ s.addDirectMemberWrite(node)
227
+ case shimast.KindParenthesizedExpression:
228
+ s.collectMemberWritesFromTarget(stripParens(node))
229
+ case shimast.KindBinaryExpression:
230
+ // A default inside a pattern (`[foo.a = 1] = …`) parses as an `=`
231
+ // binary expression; only its left side is written.
232
+ expression := node.AsBinaryExpression()
233
+ if expression != nil && expression.OperatorToken != nil &&
234
+ expression.OperatorToken.Kind == shimast.KindEqualsToken {
235
+ s.collectMemberWritesFromTarget(expression.Left)
236
+ }
237
+ case shimast.KindArrayLiteralExpression:
238
+ array := node.AsArrayLiteralExpression()
239
+ if array != nil && array.Elements != nil {
240
+ for _, element := range array.Elements.Nodes {
241
+ s.collectMemberWritesFromTarget(element)
242
+ }
243
+ }
244
+ case shimast.KindObjectLiteralExpression:
245
+ object := node.AsObjectLiteralExpression()
246
+ if object == nil || object.Properties == nil {
247
+ return
248
+ }
249
+ for _, property := range object.Properties.Nodes {
250
+ switch property.Kind {
251
+ case shimast.KindPropertyAssignment:
252
+ if assignment := property.AsPropertyAssignment(); assignment != nil {
253
+ s.collectMemberWritesFromTarget(assignment.Initializer)
254
+ }
255
+ case shimast.KindSpreadAssignment:
256
+ if spread := property.AsSpreadAssignment(); spread != nil {
257
+ s.collectMemberWritesFromTarget(spread.Expression)
258
+ }
259
+ }
260
+ }
261
+ case shimast.KindSpreadElement:
262
+ if spread := node.AsSpreadElement(); spread != nil {
263
+ s.collectMemberWritesFromTarget(spread.Expression)
264
+ }
265
+ }
266
+ }
267
+
268
+ // addDirectMemberWrite records one `<identifier|this>.<name>` write target.
269
+ // Computed properties and private names are skipped, mirroring upstream's
270
+ // identifier-property requirement; the object is unwrapped through parens
271
+ // and TypeScript assertion wrappers so `foo!.a = 1` still shields `foo.a`.
272
+ func (s *unicornConsistentDestructuringState) addDirectMemberWrite(node *shimast.Node) {
273
+ if node == nil || node.Kind != shimast.KindPropertyAccessExpression {
274
+ return
275
+ }
276
+ access := node.AsPropertyAccessExpression()
277
+ if access == nil {
278
+ return
279
+ }
280
+ name := access.Name()
281
+ if name == nil || name.Kind != shimast.KindIdentifier {
282
+ return
283
+ }
284
+ object := unicornConsistentDestructuringUnwrapObject(access.Expression)
285
+ if object == nil ||
286
+ (object.Kind != shimast.KindIdentifier && object.Kind != shimast.KindThisKeyword) {
287
+ return
288
+ }
289
+ if _, duplicate := s.memberWriteSeen[node]; duplicate {
290
+ return
291
+ }
292
+ s.memberWriteSeen[node] = struct{}{}
293
+ write := unicornConsistentDestructuringMemberWrite{
294
+ property: identifierText(name),
295
+ start: shimscanner.SkipTrivia(s.ctx.File.Text(), node.Pos()),
296
+ variableScope: unicornConsistentDestructuringVariableScope(node),
297
+ }
298
+ if object.Kind == shimast.KindIdentifier {
299
+ write.rootName = identifierText(object)
300
+ write.rootSymbol = canonicalValueSymbol(s.ctx, object)
301
+ } else {
302
+ write.thisBoundary = unicornConsistentDestructuringThisBoundary(object)
303
+ }
304
+ s.memberWrites = append(s.memberWrites, write)
305
+ }
306
+
307
+ // collectRootWrites records identifier write targets of one assignment,
308
+ // update, or for-in/of head — the same references eslint-scope marks
309
+ // isWrite() on the root variable.
310
+ func (s *unicornConsistentDestructuringState) collectRootWrites(node *shimast.Node) {
311
+ for _, identifier := range writeTargetIdentifiers(node) {
312
+ s.addRootWrite(identifier)
313
+ }
314
+ }
315
+
316
+ // collectDeclaratorRootWrites records the initialized bindings of one
317
+ // declarator. eslint-scope counts a declarator's init as a write reference,
318
+ // which matters when a `var` redeclaration re-binds the tracked root.
319
+ func (s *unicornConsistentDestructuringState) collectDeclaratorRootWrites(node *shimast.Node) {
320
+ declaration := node.AsVariableDeclaration()
321
+ if declaration == nil || declaration.Initializer == nil {
322
+ return
323
+ }
324
+ s.addBindingNameWrites(declaration.Name())
325
+ }
326
+
327
+ // collectForHeadDeclarationRootWrites records `for (const x of …)` style
328
+ // heads: the declared names are written on every iteration even though the
329
+ // declarator itself has no initializer.
330
+ func (s *unicornConsistentDestructuringState) collectForHeadDeclarationRootWrites(list *shimast.Node) {
331
+ declarationList := list.AsVariableDeclarationList()
332
+ if declarationList == nil || declarationList.Declarations == nil {
333
+ return
334
+ }
335
+ for _, declaration := range declarationList.Declarations.Nodes {
336
+ if variable := declaration.AsVariableDeclaration(); variable != nil {
337
+ s.addBindingNameWrites(variable.Name())
338
+ }
339
+ }
340
+ }
341
+
342
+ // addBindingNameWrites walks a binding name (identifier or nested pattern)
343
+ // and records each bound identifier as a write.
344
+ func (s *unicornConsistentDestructuringState) addBindingNameWrites(name *shimast.Node) {
345
+ if name == nil {
346
+ return
347
+ }
348
+ switch name.Kind {
349
+ case shimast.KindIdentifier:
350
+ s.addRootWrite(name)
351
+ case shimast.KindObjectBindingPattern, shimast.KindArrayBindingPattern:
352
+ pattern := name.AsBindingPattern()
353
+ if pattern == nil || pattern.Elements == nil {
354
+ return
355
+ }
356
+ for _, element := range pattern.Elements.Nodes {
357
+ if binding := element.AsBindingElement(); binding != nil {
358
+ s.addBindingNameWrites(binding.Name())
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ func (s *unicornConsistentDestructuringState) addRootWrite(identifier *shimast.Node) {
365
+ if identifier == nil || identifier.Kind != shimast.KindIdentifier {
366
+ return
367
+ }
368
+ if _, duplicate := s.rootWriteSeen[identifier]; duplicate {
369
+ return
370
+ }
371
+ s.rootWriteSeen[identifier] = struct{}{}
372
+ symbol := canonicalValueSymbol(s.ctx, identifier)
373
+ if symbol == nil {
374
+ return
375
+ }
376
+ s.rootWrites = append(s.rootWrites, unicornConsistentDestructuringRootWrite{
377
+ symbol: symbol,
378
+ start: shimscanner.SkipTrivia(s.ctx.File.Text(), identifier.Pos()),
379
+ variableScope: unicornConsistentDestructuringVariableScope(identifier),
380
+ })
381
+ }
382
+
383
+ // checkMember mirrors upstream's getProblem for one collected member read.
384
+ func (s *unicornConsistentDestructuringState) checkMember(node *shimast.Node) {
385
+ access := node.AsPropertyAccessExpression()
386
+ if access == nil || access.Name() == nil {
387
+ return
388
+ }
389
+ if s.shouldIgnoreMember(node) {
390
+ return
391
+ }
392
+ object := unicornConsistentDestructuringUnwrapObject(access.Expression)
393
+ if object == nil ||
394
+ (object.Kind != shimast.KindIdentifier && object.Kind != shimast.KindThisKeyword) {
395
+ return
396
+ }
397
+ matching := s.declarations[nodeText(s.ctx.File, object)]
398
+ if len(matching) == 0 {
399
+ return
400
+ }
401
+ memberStart := shimscanner.SkipTrivia(s.ctx.File.Text(), node.Pos())
402
+ memberVariableScope := unicornConsistentDestructuringVariableScope(node)
403
+ var memberRootName string
404
+ var memberRootSymbol *shimast.Symbol
405
+ var memberThisBoundary *shimast.Node
406
+ if object.Kind == shimast.KindIdentifier {
407
+ memberRootName = identifierText(object)
408
+ memberRootSymbol = canonicalValueSymbol(s.ctx, object)
409
+ } else {
410
+ memberThisBoundary = unicornConsistentDestructuringThisBoundary(object)
411
+ }
412
+ // Upstream matches the destructured key against the raw property text
413
+ // (`sourceCode.getText(node.property)`), while write shielding compares
414
+ // cooked names (`node.property.name`). Keep both projections.
415
+ memberRaw := nodeText(s.ctx.File, access.Name())
416
+ propertyCooked := identifierText(access.Name())
417
+
418
+ var destructured *shimast.Node
419
+ for index := len(matching) - 1; index >= 0; index-- {
420
+ declaration := matching[index]
421
+ if !s.declarationMatches(
422
+ declaration,
423
+ node,
424
+ memberStart,
425
+ memberVariableScope,
426
+ memberRootName,
427
+ memberRootSymbol,
428
+ memberThisBoundary,
429
+ propertyCooked,
430
+ ) {
431
+ continue
432
+ }
433
+ destructured = s.availableDestructuredMember(declaration, memberRaw, node)
434
+ if destructured != nil {
435
+ break
436
+ }
437
+ }
438
+ if destructured == nil {
439
+ return
440
+ }
441
+ if s.inTypeGuardedBranch(node) {
442
+ return
443
+ }
444
+ if unicornConsistentDestructuringHasMemberParent(node) {
445
+ // Upstream never offers to rewrite a nested member chain.
446
+ s.ctx.Report(node, unicornConsistentDestructuringMessage)
447
+ return
448
+ }
449
+ replacement := identifierText(destructured)
450
+ s.ctx.ReportSuggestion(
451
+ node,
452
+ unicornConsistentDestructuringMessage,
453
+ fmt.Sprintf(
454
+ "Replace `%s` with destructured property `%s`.",
455
+ nodeText(s.ctx.File, node),
456
+ replacement,
457
+ ),
458
+ TextEdit{Pos: memberStart, End: node.End(), Text: replacement},
459
+ )
460
+ }
461
+
462
+ // declarationMatches ports upstream's isMatchingDeclaration: order, shadowed
463
+ // roots, `this` boundaries, root reassignment, member reassignment, and
464
+ // scope containment.
465
+ func (s *unicornConsistentDestructuringState) declarationMatches(
466
+ declaration *unicornConsistentDestructuringDeclaration,
467
+ member *shimast.Node,
468
+ memberStart int,
469
+ memberVariableScope *shimast.Node,
470
+ memberRootName string,
471
+ memberRootSymbol *shimast.Symbol,
472
+ memberThisBoundary *shimast.Node,
473
+ propertyCooked string,
474
+ ) bool {
475
+ if declaration.objectEnd >= memberStart {
476
+ return false
477
+ }
478
+ if declaration.rootName != "" && memberRootName == declaration.rootName &&
479
+ memberRootSymbol != declaration.rootSymbol {
480
+ return false
481
+ }
482
+ if declaration.thisBoundary != nil && memberThisBoundary != declaration.thisBoundary {
483
+ return false
484
+ }
485
+ if s.rootVariableReassigned(declaration, memberStart, memberVariableScope) {
486
+ return false
487
+ }
488
+ if s.memberExpressionReassigned(
489
+ declaration,
490
+ memberStart,
491
+ memberVariableScope,
492
+ propertyCooked,
493
+ memberRootName,
494
+ memberRootSymbol,
495
+ memberThisBoundary,
496
+ ) {
497
+ return false
498
+ }
499
+ return unicornConsistentDestructuringIsAncestor(declaration.scope, member)
500
+ }
501
+
502
+ // rootVariableReassigned reports whether the declaration's root binding was
503
+ // written after the declaration and before the read — conservatively
504
+ // treating writes from other function scopes as always intervening, since a
505
+ // call or closure may run them between the two points.
506
+ func (s *unicornConsistentDestructuringState) rootVariableReassigned(
507
+ declaration *unicornConsistentDestructuringDeclaration,
508
+ memberStart int,
509
+ memberVariableScope *shimast.Node,
510
+ ) bool {
511
+ if declaration.rootSymbol == nil {
512
+ return false
513
+ }
514
+ for _, write := range s.rootWrites {
515
+ if write.symbol != declaration.rootSymbol {
516
+ continue
517
+ }
518
+ if write.start < declaration.objectEnd {
519
+ continue
520
+ }
521
+ if write.variableScope != memberVariableScope {
522
+ return true
523
+ }
524
+ if write.start <= memberStart {
525
+ return true
526
+ }
527
+ }
528
+ return false
529
+ }
530
+
531
+ // memberExpressionReassigned reports whether the same `<root>.<property>`
532
+ // member was written between the declaration and the read, with the same
533
+ // cross-function-scope conservatism as root reassignment.
534
+ func (s *unicornConsistentDestructuringState) memberExpressionReassigned(
535
+ declaration *unicornConsistentDestructuringDeclaration,
536
+ memberStart int,
537
+ memberVariableScope *shimast.Node,
538
+ propertyCooked string,
539
+ memberRootName string,
540
+ memberRootSymbol *shimast.Symbol,
541
+ memberThisBoundary *shimast.Node,
542
+ ) bool {
543
+ for _, write := range s.memberWrites {
544
+ if write.property != propertyCooked ||
545
+ write.rootName != memberRootName ||
546
+ write.rootSymbol != memberRootSymbol ||
547
+ write.thisBoundary != memberThisBoundary {
548
+ continue
549
+ }
550
+ if write.variableScope != memberVariableScope {
551
+ return true
552
+ }
553
+ if write.start >= declaration.objectEnd && write.start <= memberStart {
554
+ return true
555
+ }
556
+ }
557
+ return false
558
+ }
559
+
560
+ // availableDestructuredMember finds the declaration's binding for the read
561
+ // property: a non-rest element with a plain identifier key, a plain
562
+ // identifier value, and no default, whose binding is still what the name
563
+ // resolves to at the read site (not shadowed in between).
564
+ func (s *unicornConsistentDestructuringState) availableDestructuredMember(
565
+ declaration *unicornConsistentDestructuringDeclaration,
566
+ memberRaw string,
567
+ member *shimast.Node,
568
+ ) *shimast.Node {
569
+ pattern := declaration.pattern.AsBindingPattern()
570
+ if pattern == nil || pattern.Elements == nil {
571
+ return nil
572
+ }
573
+ for _, element := range pattern.Elements.Nodes {
574
+ binding := element.AsBindingElement()
575
+ if binding == nil || binding.DotDotDotToken != nil || binding.Initializer != nil {
576
+ continue
577
+ }
578
+ value := binding.Name()
579
+ if value == nil || value.Kind != shimast.KindIdentifier {
580
+ continue
581
+ }
582
+ key := binding.PropertyName
583
+ if key == nil {
584
+ key = value
585
+ }
586
+ if key.Kind != shimast.KindIdentifier || identifierText(key) != memberRaw {
587
+ continue
588
+ }
589
+ bindingSymbol := canonicalValueSymbol(s.ctx, value)
590
+ if bindingSymbol == nil {
591
+ continue
592
+ }
593
+ if s.resolveValueSymbol(identifierText(value), member) != bindingSymbol {
594
+ continue
595
+ }
596
+ return value
597
+ }
598
+ return nil
599
+ }
600
+
601
+ // resolveValueSymbol resolves a name from the member read's lexical position
602
+ // — the checker equivalent of upstream's findVariable(memberScope, name) —
603
+ // normalized the same way canonicalValueSymbol normalizes declarations.
604
+ func (s *unicornConsistentDestructuringState) resolveValueSymbol(
605
+ name string,
606
+ location *shimast.Node,
607
+ ) *shimast.Symbol {
608
+ if s.ctx == nil || s.ctx.Checker == nil || name == "" || location == nil {
609
+ return nil
610
+ }
611
+ symbol := s.ctx.Checker.ResolveName(name, location, shimast.SymbolFlagsValue, false /*excludeGlobals*/)
612
+ if symbol == nil {
613
+ return nil
614
+ }
615
+ if symbol.Flags&shimast.SymbolFlagsExportValue != 0 && symbol.ExportSymbol != nil {
616
+ symbol = symbol.ExportSymbol
617
+ }
618
+ return s.ctx.Checker.GetMergedSymbol(symbol)
619
+ }
620
+
621
+ // shouldIgnoreMember ports upstream's shouldIgnoreMemberExpression plus its
622
+ // isLeftHandSide util. Element access never reaches here (only property
623
+ // accesses are collected), which subsumes the `computed` arm. Parentheses
624
+ // are transparent in the ESTree oracle, so relations are checked against the
625
+ // outermost paren wrapper.
626
+ func (s *unicornConsistentDestructuringState) shouldIgnoreMember(node *shimast.Node) bool {
627
+ wrapped := skipParents(node)
628
+ parent := wrapped.Parent
629
+ if parent != nil {
630
+ switch parent.Kind {
631
+ case shimast.KindCallExpression:
632
+ if call := parent.AsCallExpression(); call != nil && call.Expression == wrapped {
633
+ return true
634
+ }
635
+ case shimast.KindNewExpression:
636
+ if newExpression := parent.AsNewExpression(); newExpression != nil && newExpression.Expression == wrapped {
637
+ return true
638
+ }
639
+ case shimast.KindTaggedTemplateExpression:
640
+ // Replacing the tag would change the `this` binding, like a method
641
+ // call.
642
+ if tagged := parent.AsTaggedTemplateExpression(); tagged != nil && tagged.Tag == wrapped {
643
+ return true
644
+ }
645
+ case shimast.KindBinaryExpression:
646
+ binary := parent.AsBinaryExpression()
647
+ if binary != nil && binary.OperatorToken != nil &&
648
+ isAssignmentOperator(binary.OperatorToken.Kind) && binary.Left == wrapped {
649
+ return true
650
+ }
651
+ case shimast.KindPrefixUnaryExpression:
652
+ prefix := parent.AsPrefixUnaryExpression()
653
+ if prefix != nil &&
654
+ (prefix.Operator == shimast.KindPlusPlusToken || prefix.Operator == shimast.KindMinusMinusToken) &&
655
+ prefix.Operand == wrapped {
656
+ return true
657
+ }
658
+ case shimast.KindPostfixUnaryExpression:
659
+ postfix := parent.AsPostfixUnaryExpression()
660
+ if postfix != nil &&
661
+ (postfix.Operator == shimast.KindPlusPlusToken || postfix.Operator == shimast.KindMinusMinusToken) &&
662
+ postfix.Operand == wrapped {
663
+ return true
664
+ }
665
+ case shimast.KindDeleteExpression:
666
+ if deleted := parent.AsDeleteExpression(); deleted != nil && deleted.Expression == wrapped {
667
+ return true
668
+ }
669
+ case shimast.KindArrayLiteralExpression:
670
+ // ESTree's ArrayPattern-element arm: the read is a write slot only
671
+ // when the containing literal is itself a destructuring target.
672
+ return unicornConsistentDestructuringIsAssignmentPattern(parent)
673
+ case shimast.KindSpreadElement:
674
+ // ESTree's RestElement-argument arm; SpreadElement in expression
675
+ // position (call arguments, plain array literals) is a read.
676
+ if spread := parent.AsSpreadElement(); spread != nil && spread.Expression == wrapped {
677
+ return unicornConsistentDestructuringIsAssignmentPattern(parent)
678
+ }
679
+ case shimast.KindSpreadAssignment:
680
+ // Object-rest form of the RestElement-argument arm.
681
+ if spread := parent.AsSpreadAssignment(); spread != nil && spread.Expression == wrapped {
682
+ return unicornConsistentDestructuringIsAssignmentPattern(parent)
683
+ }
684
+ case shimast.KindPropertyAssignment:
685
+ // ESTree's Property-value-in-ObjectPattern arm; the property name is
686
+ // a read, and a value in a plain object literal is a read.
687
+ if property := parent.AsPropertyAssignment(); property != nil && property.Initializer == wrapped {
688
+ return unicornConsistentDestructuringIsAssignmentPattern(parent)
689
+ }
690
+ }
691
+ }
692
+ return false
693
+ }
694
+
695
+ // unicornConsistentDestructuringIsAssignmentPattern reports whether node (a
696
+ // pattern slot: array/object literal, spread, or property assignment) sits in
697
+ // a destructuring-assignment target position. This is the TypeScript-AST
698
+ // equivalent of ESTree spelling the containing literal as ArrayPattern /
699
+ // ObjectPattern: the walk ascends only through pattern edges (literal
700
+ // elements, property values, spreads, `=`-default lefts) and stops at
701
+ // anything else, so a literal in a computed index, call argument, or
702
+ // default-value position never counts.
703
+ func unicornConsistentDestructuringIsAssignmentPattern(node *shimast.Node) bool {
704
+ for node != nil {
705
+ parent := node.Parent
706
+ if parent == nil {
707
+ return false
708
+ }
709
+ switch parent.Kind {
710
+ case shimast.KindBinaryExpression:
711
+ // `[…] = value` anywhere is a destructuring assignment; `x = […]`
712
+ // and pattern defaults keep the literal on the right, a read.
713
+ binary := parent.AsBinaryExpression()
714
+ return binary != nil && binary.OperatorToken != nil &&
715
+ binary.OperatorToken.Kind == shimast.KindEqualsToken && binary.Left == node
716
+ case shimast.KindForInStatement, shimast.KindForOfStatement:
717
+ statement := parent.AsForInOrOfStatement()
718
+ return statement != nil && statement.Initializer == node
719
+ case shimast.KindArrayLiteralExpression, shimast.KindSpreadElement,
720
+ shimast.KindSpreadAssignment, shimast.KindObjectLiteralExpression:
721
+ node = parent
722
+ case shimast.KindPropertyAssignment:
723
+ property := parent.AsPropertyAssignment()
724
+ if property == nil || property.Initializer != node {
725
+ return false
726
+ }
727
+ node = parent
728
+ default:
729
+ return false
730
+ }
731
+ }
732
+ return false
733
+ }
734
+
735
+ // inTypeGuardedBranch ports upstream's isInTypeGuardedBranch: a read inside
736
+ // the positive branch of a `'prop' in obj` test narrows the type, so the
737
+ // destructured binding is not equivalent. Class member boundaries stop the
738
+ // upward walk exactly as upstream's boundary list does.
739
+ func (s *unicornConsistentDestructuringState) inTypeGuardedBranch(node *shimast.Node) bool {
740
+ child := node
741
+ for parent := node.Parent; parent != nil; parent = parent.Parent {
742
+ if unicornConsistentDestructuringIsGuardBoundary(parent) {
743
+ return false
744
+ }
745
+ if s.inPositiveGuardBranch(parent, child, node) {
746
+ return true
747
+ }
748
+ child = parent
749
+ }
750
+ return false
751
+ }
752
+
753
+ func (s *unicornConsistentDestructuringState) inPositiveGuardBranch(
754
+ parent *shimast.Node,
755
+ child *shimast.Node,
756
+ member *shimast.Node,
757
+ ) bool {
758
+ switch parent.Kind {
759
+ case shimast.KindConditionalExpression:
760
+ conditional := parent.AsConditionalExpression()
761
+ return conditional != nil && conditional.WhenTrue == child &&
762
+ s.hasMatchingInExpression(conditional.Condition, member)
763
+ case shimast.KindBinaryExpression:
764
+ binary := parent.AsBinaryExpression()
765
+ return binary != nil && binary.OperatorToken != nil &&
766
+ binary.OperatorToken.Kind == shimast.KindAmpersandAmpersandToken &&
767
+ binary.Right == child &&
768
+ s.hasMatchingInExpression(binary.Left, member)
769
+ case shimast.KindIfStatement:
770
+ ifStatement := parent.AsIfStatement()
771
+ return ifStatement != nil && ifStatement.ThenStatement == child &&
772
+ s.hasMatchingInExpression(ifStatement.Expression, member)
773
+ }
774
+ return false
775
+ }
776
+
777
+ func (s *unicornConsistentDestructuringState) hasMatchingInExpression(
778
+ node *shimast.Node,
779
+ member *shimast.Node,
780
+ ) bool {
781
+ node = stripParens(node)
782
+ if node == nil {
783
+ return false
784
+ }
785
+ if s.matchesInExpression(node, member) {
786
+ return true
787
+ }
788
+ if node.Kind != shimast.KindBinaryExpression {
789
+ return false
790
+ }
791
+ binary := node.AsBinaryExpression()
792
+ if binary == nil || binary.OperatorToken == nil ||
793
+ binary.OperatorToken.Kind != shimast.KindAmpersandAmpersandToken {
794
+ return false
795
+ }
796
+ return s.hasMatchingInExpression(binary.Left, member) ||
797
+ s.hasMatchingInExpression(binary.Right, member)
798
+ }
799
+
800
+ // matchesInExpression recognizes `'name' in <object>` where name is the read
801
+ // property and the right side spells the same source text as the read's
802
+ // object (paren-stripped on both sides, since the ESTree oracle has no paren
803
+ // nodes).
804
+ func (s *unicornConsistentDestructuringState) matchesInExpression(
805
+ node *shimast.Node,
806
+ member *shimast.Node,
807
+ ) bool {
808
+ if node.Kind != shimast.KindBinaryExpression {
809
+ return false
810
+ }
811
+ binary := node.AsBinaryExpression()
812
+ if binary == nil || binary.OperatorToken == nil ||
813
+ binary.OperatorToken.Kind != shimast.KindInKeyword {
814
+ return false
815
+ }
816
+ left := stripParens(binary.Left)
817
+ if left == nil || left.Kind != shimast.KindStringLiteral {
818
+ return false
819
+ }
820
+ access := member.AsPropertyAccessExpression()
821
+ if access == nil {
822
+ return false
823
+ }
824
+ property := access.Name()
825
+ if property == nil || property.Kind != shimast.KindIdentifier {
826
+ return false
827
+ }
828
+ if stringLiteralText(left) != identifierText(property) {
829
+ return false
830
+ }
831
+ return nodeText(s.ctx.File, stripParens(binary.Right)) ==
832
+ nodeText(s.ctx.File, stripParens(access.Expression))
833
+ }
834
+
835
+ // unicornConsistentDestructuringIsGuardBoundary mirrors upstream's
836
+ // isInTypeGuardBoundary set: function declarations, class fields (including
837
+ // accessor fields), and class methods. Object-literal methods intentionally
838
+ // pass through — ESTree walks their FunctionExpression, which is not a
839
+ // boundary.
840
+ func unicornConsistentDestructuringIsGuardBoundary(node *shimast.Node) bool {
841
+ switch node.Kind {
842
+ case shimast.KindFunctionDeclaration, shimast.KindPropertyDeclaration:
843
+ return true
844
+ case shimast.KindMethodDeclaration, shimast.KindGetAccessor,
845
+ shimast.KindSetAccessor, shimast.KindConstructor:
846
+ return node.Parent != nil &&
847
+ (node.Parent.Kind == shimast.KindClassDeclaration ||
848
+ node.Parent.Kind == shimast.KindClassExpression)
849
+ }
850
+ return false
851
+ }
852
+
853
+ // unicornConsistentDestructuringHasMemberParent decides whether the ESTree
854
+ // parent of the read would itself be a MemberExpression, in which case
855
+ // upstream reports without a suggestion. Parentheses do not exist in ESTree,
856
+ // so they are skipped — except that a parenthesized (or computed-argument)
857
+ // optional chain is capped by a ChainExpression wrapper upstream and
858
+ // therefore does get a suggestion.
859
+ func unicornConsistentDestructuringHasMemberParent(node *shimast.Node) bool {
860
+ wrapped := node
861
+ passedParens := false
862
+ for wrapped.Parent != nil && wrapped.Parent.Kind == shimast.KindParenthesizedExpression {
863
+ wrapped = wrapped.Parent
864
+ passedParens = true
865
+ }
866
+ parent := wrapped.Parent
867
+ if parent == nil {
868
+ return false
869
+ }
870
+ objectPosition := false
871
+ switch parent.Kind {
872
+ case shimast.KindPropertyAccessExpression:
873
+ access := parent.AsPropertyAccessExpression()
874
+ if access == nil || access.Expression != wrapped {
875
+ return false
876
+ }
877
+ objectPosition = true
878
+ case shimast.KindElementAccessExpression:
879
+ access := parent.AsElementAccessExpression()
880
+ if access == nil {
881
+ return false
882
+ }
883
+ objectPosition = access.Expression == wrapped
884
+ if !objectPosition && access.ArgumentExpression != wrapped {
885
+ return false
886
+ }
887
+ default:
888
+ return false
889
+ }
890
+ if node.Flags&shimast.NodeFlagsOptionalChain != 0 {
891
+ // A ChainExpression caps the optional chain unless the outer access
892
+ // continues the same chain in object position.
893
+ if passedParens || !objectPosition {
894
+ return false
895
+ }
896
+ return parent.Flags&shimast.NodeFlagsOptionalChain != 0
897
+ }
898
+ return true
899
+ }
900
+
901
+ // unicornConsistentDestructuringUnwrapObject unwraps parentheses and
902
+ // TypeScript assertion wrappers, the union of ESTree's paren transparency
903
+ // and upstream's unwrapTypeScriptExpression.
904
+ func unicornConsistentDestructuringUnwrapObject(node *shimast.Node) *shimast.Node {
905
+ for node != nil {
906
+ switch node.Kind {
907
+ case shimast.KindParenthesizedExpression:
908
+ node = node.AsParenthesizedExpression().Expression
909
+ case shimast.KindAsExpression:
910
+ node = node.AsAsExpression().Expression
911
+ case shimast.KindSatisfiesExpression:
912
+ node = node.AsSatisfiesExpression().Expression
913
+ case shimast.KindNonNullExpression:
914
+ node = node.AsNonNullExpression().Expression
915
+ case shimast.KindTypeAssertionExpression:
916
+ node = node.AsTypeAssertion().Expression
917
+ default:
918
+ return node
919
+ }
920
+ }
921
+ return nil
922
+ }
923
+
924
+ // unicornConsistentDestructuringIsJsxTagName reports whether the property
925
+ // access spells (part of) a JSX element tag name. ESTree types those as
926
+ // JSXMemberExpression, which upstream's MemberExpression listener never
927
+ // sees.
928
+ func unicornConsistentDestructuringIsJsxTagName(node *shimast.Node) bool {
929
+ current := node
930
+ for current.Parent != nil && current.Parent.Kind == shimast.KindPropertyAccessExpression {
931
+ access := current.Parent.AsPropertyAccessExpression()
932
+ if access == nil || access.Expression != current {
933
+ return false
934
+ }
935
+ current = current.Parent
936
+ }
937
+ parent := current.Parent
938
+ if parent == nil {
939
+ return false
940
+ }
941
+ switch parent.Kind {
942
+ case shimast.KindJsxOpeningElement, shimast.KindJsxClosingElement,
943
+ shimast.KindJsxSelfClosingElement:
944
+ return true
945
+ }
946
+ return false
947
+ }
948
+
949
+ // unicornConsistentDestructuringNearestScope returns the nearest
950
+ // scope-creating ancestor. A read matches a declaration only when the read
951
+ // sits inside this node, which is the AST equivalent of eslint-scope's
952
+ // child-scope chain containment.
953
+ func unicornConsistentDestructuringNearestScope(node *shimast.Node) *shimast.Node {
954
+ for current := node.Parent; current != nil; current = current.Parent {
955
+ switch current.Kind {
956
+ case shimast.KindSourceFile, shimast.KindBlock, shimast.KindModuleBlock,
957
+ shimast.KindCaseBlock, shimast.KindCatchClause,
958
+ shimast.KindForStatement, shimast.KindForInStatement, shimast.KindForOfStatement,
959
+ shimast.KindFunctionDeclaration, shimast.KindFunctionExpression, shimast.KindArrowFunction,
960
+ shimast.KindMethodDeclaration, shimast.KindGetAccessor, shimast.KindSetAccessor,
961
+ shimast.KindConstructor, shimast.KindClassStaticBlockDeclaration,
962
+ shimast.KindClassDeclaration, shimast.KindClassExpression,
963
+ shimast.KindPropertyDeclaration, shimast.KindEnumDeclaration, shimast.KindModuleDeclaration:
964
+ return current
965
+ }
966
+ }
967
+ return nil
968
+ }
969
+
970
+ // unicornConsistentDestructuringVariableScope returns the nearest
971
+ // function-like scope — eslint-scope's variableScope — used by the
972
+ // conservative cross-scope write checks.
973
+ func unicornConsistentDestructuringVariableScope(node *shimast.Node) *shimast.Node {
974
+ for current := node.Parent; current != nil; current = current.Parent {
975
+ switch current.Kind {
976
+ case shimast.KindSourceFile,
977
+ shimast.KindFunctionDeclaration, shimast.KindFunctionExpression, shimast.KindArrowFunction,
978
+ shimast.KindMethodDeclaration, shimast.KindGetAccessor, shimast.KindSetAccessor,
979
+ shimast.KindConstructor, shimast.KindClassStaticBlockDeclaration,
980
+ shimast.KindPropertyDeclaration, shimast.KindEnumDeclaration, shimast.KindModuleDeclaration:
981
+ return current
982
+ }
983
+ }
984
+ return nil
985
+ }
986
+
987
+ // unicornConsistentDestructuringThisBoundary returns the node that owns the
988
+ // `this` binding at `node`. Arrow functions inherit `this` and are
989
+ // deliberately absent, matching upstream's boundary list.
990
+ func unicornConsistentDestructuringThisBoundary(node *shimast.Node) *shimast.Node {
991
+ for current := node.Parent; current != nil; current = current.Parent {
992
+ switch current.Kind {
993
+ case shimast.KindSourceFile,
994
+ shimast.KindFunctionDeclaration, shimast.KindFunctionExpression,
995
+ shimast.KindMethodDeclaration, shimast.KindGetAccessor, shimast.KindSetAccessor,
996
+ shimast.KindConstructor, shimast.KindPropertyDeclaration,
997
+ shimast.KindClassStaticBlockDeclaration:
998
+ return current
999
+ }
1000
+ }
1001
+ return nil
1002
+ }
1003
+
1004
+ // unicornConsistentDestructuringIsAncestor reports whether scope is an
1005
+ // ancestor of node.
1006
+ func unicornConsistentDestructuringIsAncestor(scope *shimast.Node, node *shimast.Node) bool {
1007
+ if scope == nil || node == nil {
1008
+ return false
1009
+ }
1010
+ for current := node.Parent; current != nil; current = current.Parent {
1011
+ if current == scope {
1012
+ return true
1013
+ }
1014
+ }
1015
+ return false
21
1016
  }
22
1017
 
23
1018
  func init() {