@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,28 +1,765 @@
1
- // unicorn/no-unused-properties: flags object-literal properties that
2
- // are never read after the object is constructed. The signal is "this
3
- // key sits in a literal that nothing reaches into" — analogous to
4
- // `no-unused-vars` but for one level deeper inside object data.
1
+ // unicorn/no-unused-properties: report properties of object literals and
2
+ // inline type literals that no reference ever reads.
5
3
  //
6
- // Escape-hatch / no-op port: implementing this faithfully requires
7
- // per-object reachability and read tracking across the file (and across
8
- // destructuring patterns, spread, computed access, and exports). That
9
- // is a scope-and-flow problem the AST-only engine in this package
10
- // cannot yet model without false positives every time a property is
11
- // read through dynamic access. Registered so the typed-key/Go-registry
12
- // parity check passes and so users can configure the rule, but it
13
- // intentionally reports nothing until the analysis lands in a
14
- // follow-up.
15
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-unused-properties.md
4
+ // Port of the current upstream analysis: for every single-definition
5
+ // variable that binds an object literal (or carries an inline `{...}` type
6
+ // annotation, including annotated parameters), walk the variable's
7
+ // references and keep only the ones that can reach each property — a member
8
+ // access with the same key, a dynamic/computed access, a member call or
9
+ // assignment, a destructuring that names the key or uses rest, or any
10
+ // escape (alias, argument, return, export, spread). A property whose
11
+ // reference list filters down to nothing is dead data and gets reported;
12
+ // references that survive drive the same analysis one level deeper into
13
+ // nested object/type literals.
14
+ //
15
+ // Escape analysis stays conservative exactly where upstream is: one
16
+ // unpredictable use (passed, returned, aliased, exported, computed access,
17
+ // mutated) marks every property as used. Variables in a script file's
18
+ // global scope are skipped, mirroring upstream's global-scope exclusion.
19
+ // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/rules/no-unused-properties.js
16
20
  package linthost
17
21
 
18
- import shimast "github.com/microsoft/typescript-go/shim/ast"
22
+ import (
23
+ "math/big"
24
+
25
+ shimast "github.com/microsoft/typescript-go/shim/ast"
26
+ )
19
27
 
20
28
  type unicornNoUnusedProperties struct{}
21
29
 
22
- func (unicornNoUnusedProperties) Name() string { return "unicorn/no-unused-properties" }
23
- func (unicornNoUnusedProperties) Visits() []shimast.Kind { return nil }
24
- func (unicornNoUnusedProperties) Check(_ *Context, _ *shimast.Node) {
25
- // Intentionally empty — see file header for the escape-hatch rationale.
30
+ func (unicornNoUnusedProperties) Name() string { return "unicorn/no-unused-properties" }
31
+ func (unicornNoUnusedProperties) NeedsTypeChecker() bool {
32
+ return true
33
+ }
34
+ func (unicornNoUnusedProperties) Visits() []shimast.Kind {
35
+ return []shimast.Kind{shimast.KindSourceFile}
36
+ }
37
+
38
+ // unusedPropertiesReference is one use of the analyzed variable. At the top
39
+ // level `node` is the referencing Identifier; recursion into nested
40
+ // containers replaces it with the syntax that consumed the previous key
41
+ // (member expression, matched destructuring, exported declaration) so the
42
+ // next level can inspect that node's parent. `init` marks the write at the
43
+ // variable's own declarator.
44
+ type unusedPropertiesReference struct {
45
+ node *shimast.Node
46
+ init bool
47
+ }
48
+
49
+ // unusedPropertiesCandidate is one analyzable variable: a binding identifier
50
+ // plus the property container its declaration supplies.
51
+ type unusedPropertiesCandidate struct {
52
+ nameNode *shimast.Node
53
+ container *shimast.Node
54
+ declaration *shimast.Node
55
+ references []unusedPropertiesReference
56
+ }
57
+
58
+ // unusedPropertiesKey is the comparable identity of a property key or member
59
+ // access. `class` separates runtime value kinds (string names and string
60
+ // literals share one class; numbers, bigints, booleans and null each keep
61
+ // their own) so `foo[0]` matches `{0: 1}` but not `{"0": 1}`, mirroring the
62
+ // strict-equality comparison upstream performs on key values.
63
+ type unusedPropertiesKey struct {
64
+ class string
65
+ text string
66
+ ok bool
67
+ }
68
+
69
+ func (unicornNoUnusedProperties) Check(ctx *Context, node *shimast.Node) {
70
+ if ctx == nil || ctx.Checker == nil || ctx.File == nil ||
71
+ node == nil || node.Kind != shimast.KindSourceFile {
72
+ return
73
+ }
74
+ candidates := unusedPropertiesCollectCandidates(ctx, node)
75
+ if len(candidates) == 0 {
76
+ return
77
+ }
78
+ unusedPropertiesCollectReferences(ctx, node, candidates)
79
+ pureWrites := unusedPropertiesCollectPureWriteTargets(node)
80
+ for _, candidate := range candidates {
81
+ if !unusedPropertiesHasReadReference(candidate, pureWrites) {
82
+ continue
83
+ }
84
+ unusedPropertiesReportContainer(ctx, candidate.container, candidate.references)
85
+ }
86
+ }
87
+
88
+ // unusedPropertiesCollectCandidates walks the file for variables the rule can
89
+ // analyze: variable declarations whose initializer (through TS assertion
90
+ // wrappers) is an object literal or whose annotated type is an inline type
91
+ // literal, and identifier parameters annotated with an inline type literal.
92
+ // Mirroring upstream's scope walk, script-file globals are excluded; only a
93
+ // module's top level is analyzable.
94
+ func unusedPropertiesCollectCandidates(
95
+ ctx *Context,
96
+ root *shimast.Node,
97
+ ) []*unusedPropertiesCandidate {
98
+ var candidates []*unusedPropertiesCandidate
99
+ walkDescendants(root, func(child *shimast.Node) {
100
+ switch child.Kind {
101
+ case shimast.KindVariableDeclaration:
102
+ declaration := child.AsVariableDeclaration()
103
+ if declaration == nil || declaration.Initializer == nil {
104
+ return
105
+ }
106
+ container := unusedPropertiesDeclarationContainer(declaration)
107
+ if container == nil || unusedPropertiesDeclaredInGlobalScope(ctx.File, child) {
108
+ return
109
+ }
110
+ for _, nameNode := range bindingIdentifierNodes(declaration.Name()) {
111
+ candidates = append(candidates, &unusedPropertiesCandidate{
112
+ nameNode: nameNode,
113
+ container: container,
114
+ declaration: child,
115
+ })
116
+ }
117
+ case shimast.KindParameter:
118
+ parameter := child.AsParameterDeclaration()
119
+ if parameter == nil || parameter.Type == nil ||
120
+ parameter.Type.Kind != shimast.KindTypeLiteral {
121
+ return
122
+ }
123
+ nameNode := parameter.Name()
124
+ if nameNode == nil || nameNode.Kind != shimast.KindIdentifier {
125
+ return
126
+ }
127
+ candidates = append(candidates, &unusedPropertiesCandidate{
128
+ nameNode: nameNode,
129
+ container: parameter.Type,
130
+ declaration: child,
131
+ })
132
+ }
133
+ })
134
+ return candidates
135
+ }
136
+
137
+ // unusedPropertiesDeclarationContainer resolves the property container a
138
+ // variable declaration supplies: the object literal it is initialized with
139
+ // (TS assertion wrappers are transparent), else the inline type literal it
140
+ // is annotated with. Upstream only consults the annotation when an
141
+ // initializer exists (`declare const x: {...}` stays out of scope); the
142
+ // caller has already required one.
143
+ func unusedPropertiesDeclarationContainer(
144
+ declaration *shimast.VariableDeclaration,
145
+ ) *shimast.Node {
146
+ initializer := unwrapReferenceExpression(declaration.Initializer)
147
+ if initializer != nil && initializer.Kind == shimast.KindObjectLiteralExpression {
148
+ return initializer
149
+ }
150
+ if declaration.Type != nil && declaration.Type.Kind == shimast.KindTypeLiteral {
151
+ return declaration.Type
152
+ }
153
+ return nil
154
+ }
155
+
156
+ // unusedPropertiesDeclaredInGlobalScope reports whether a variable
157
+ // declaration lands in the global scope, which only exists at the top level
158
+ // of a script file (no import/export). `var` hoists past blocks, so it is
159
+ // global unless a function-like body, class static block, or namespace body
160
+ // intervenes; `let`/`const`/`using` are global only as a direct top-level
161
+ // statement.
162
+ func unusedPropertiesDeclaredInGlobalScope(
163
+ file *shimast.SourceFile,
164
+ declaration *shimast.Node,
165
+ ) bool {
166
+ if file == nil || file.ExternalModuleIndicator != nil {
167
+ return false
168
+ }
169
+ if shimast.GetCombinedNodeFlags(declaration)&shimast.NodeFlagsBlockScoped != 0 {
170
+ list := declaration.Parent
171
+ if list == nil || list.Parent == nil ||
172
+ list.Parent.Kind != shimast.KindVariableStatement {
173
+ return false
174
+ }
175
+ statement := list.Parent
176
+ return statement.Parent != nil && statement.Parent.Kind == shimast.KindSourceFile
177
+ }
178
+ for ancestor := declaration.Parent; ancestor != nil; ancestor = ancestor.Parent {
179
+ if isFunctionLikeKind(ancestor) {
180
+ return false
181
+ }
182
+ switch ancestor.Kind {
183
+ case shimast.KindClassStaticBlockDeclaration,
184
+ shimast.KindModuleBlock,
185
+ shimast.KindModuleDeclaration:
186
+ return false
187
+ case shimast.KindSourceFile:
188
+ return true
189
+ }
190
+ }
191
+ return false
192
+ }
193
+
194
+ // unusedPropertiesCollectReferences fills each candidate's reference list by
195
+ // resolving every same-named identifier in the file to its checker symbol.
196
+ // The candidate's own binding identifier becomes the `init` reference when
197
+ // the declarator has an initializer; parameters bind without one. Candidates
198
+ // whose symbol cannot be resolved to exactly one declaration lose their
199
+ // container, which excludes them from analysis.
200
+ func unusedPropertiesCollectReferences(
201
+ ctx *Context,
202
+ root *shimast.Node,
203
+ candidates []*unusedPropertiesCandidate,
204
+ ) {
205
+ bySymbol := make(map[*shimast.Symbol]*unusedPropertiesCandidate, len(candidates))
206
+ names := make(map[string]struct{}, len(candidates))
207
+ for _, candidate := range candidates {
208
+ symbol := unusedPropertiesDeclaredSymbol(ctx, candidate)
209
+ if symbol == nil || len(symbol.Declarations) != 1 || bySymbol[symbol] != nil {
210
+ candidate.container = nil
211
+ continue
212
+ }
213
+ bySymbol[symbol] = candidate
214
+ names[identifierText(candidate.nameNode)] = struct{}{}
215
+ }
216
+ if len(bySymbol) == 0 {
217
+ return
218
+ }
219
+ walkDescendants(root, func(child *shimast.Node) {
220
+ if child.Kind != shimast.KindIdentifier {
221
+ return
222
+ }
223
+ if _, possible := names[identifierText(child)]; !possible {
224
+ return
225
+ }
226
+ candidate, ok := bySymbol[unusedPropertiesSymbolAtIdentifier(ctx, child)]
227
+ if !ok {
228
+ return
229
+ }
230
+ if child == candidate.nameNode {
231
+ if candidate.declaration.Kind == shimast.KindVariableDeclaration {
232
+ candidate.references = append(candidate.references, unusedPropertiesReference{
233
+ node: child,
234
+ init: true,
235
+ })
236
+ }
237
+ return
238
+ }
239
+ candidate.references = append(candidate.references, unusedPropertiesReference{node: child})
240
+ })
241
+ }
242
+
243
+ // unusedPropertiesDeclaredSymbol resolves a candidate's binding identifier to
244
+ // its canonical symbol. A TypeScript parameter property carries two symbols
245
+ // at one declaration name — the class member and the constructor-local
246
+ // parameter; body references bind to the latter, so the constructor's locals
247
+ // table is the canonical lookup (mirrors noParamReassignParameterSymbol).
248
+ func unusedPropertiesDeclaredSymbol(
249
+ ctx *Context,
250
+ candidate *unusedPropertiesCandidate,
251
+ ) *shimast.Symbol {
252
+ declaration := candidate.declaration
253
+ if declaration.Kind == shimast.KindParameter && declaration.Parent != nil &&
254
+ declaration.Parent.Kind == shimast.KindConstructor && isParameterProperty(declaration) {
255
+ symbol := declaration.Parent.Locals()[identifierText(candidate.nameNode)]
256
+ if symbol == nil {
257
+ return nil
258
+ }
259
+ return ctx.Checker.GetMergedSymbol(symbol)
260
+ }
261
+ return unusedPropertiesSymbolAtIdentifier(ctx, candidate.nameNode)
262
+ }
263
+
264
+ // unusedPropertiesSymbolAtIdentifier resolves an identifier in value position
265
+ // to its canonical binding symbol. Shorthand property values and export
266
+ // specifiers need their dedicated checker queries: `({foo} = x)` resolves the
267
+ // written binding and `export { foo }` resolves the aliased local rather than
268
+ // the specifier's own alias symbol, matching how the checker's own
269
+ // reference-finding treats those positions.
270
+ func unusedPropertiesSymbolAtIdentifier(
271
+ ctx *Context,
272
+ identifier *shimast.Node,
273
+ ) *shimast.Symbol {
274
+ if identifier == nil || identifier.Kind != shimast.KindIdentifier {
275
+ return nil
276
+ }
277
+ symbol := valueSymbolAtIdentifier(ctx, identifier)
278
+ if parent := identifier.Parent; parent != nil && parent.Kind == shimast.KindExportSpecifier {
279
+ if specifier := parent.AsExportSpecifier(); specifier != nil &&
280
+ unusedPropertiesIsExportSpecifierAlias(identifier, specifier) {
281
+ if local := ctx.Checker.GetExportSpecifierLocalTargetSymbol(parent); local != nil {
282
+ symbol = local
283
+ }
284
+ }
285
+ }
286
+ if symbol == nil {
287
+ return nil
288
+ }
289
+ if symbol.Flags&shimast.SymbolFlagsExportValue != 0 && symbol.ExportSymbol != nil {
290
+ symbol = symbol.ExportSymbol
291
+ }
292
+ return ctx.Checker.GetMergedSymbol(symbol)
293
+ }
294
+
295
+ // unusedPropertiesIsExportSpecifierAlias reports whether the identifier is
296
+ // the side of an export specifier that references a local binding: the
297
+ // property name of `export { foo as bar }`, or the single name of a
298
+ // non-re-export `export { foo }`.
299
+ func unusedPropertiesIsExportSpecifierAlias(
300
+ identifier *shimast.Node,
301
+ specifier *shimast.ExportSpecifier,
302
+ ) bool {
303
+ if specifier.PropertyName != nil {
304
+ return specifier.PropertyName == identifier
305
+ }
306
+ declaration := specifier.AsNode().Parent
307
+ if declaration != nil {
308
+ declaration = declaration.Parent
309
+ }
310
+ return declaration == nil || declaration.ModuleSpecifier() == nil
311
+ }
312
+
313
+ // unusedPropertiesCollectPureWriteTargets gathers every identifier written by
314
+ // a plain `=` assignment (including destructuring targets) or a for-in/of
315
+ // head. Those references never read the variable, so a variable whose uses
316
+ // are all pure writes is left to no-unused-vars, exactly like upstream's
317
+ // read-reference check. Compound assignments and updates read before writing
318
+ // and are intentionally absent.
319
+ func unusedPropertiesCollectPureWriteTargets(
320
+ root *shimast.Node,
321
+ ) map[*shimast.Node]struct{} {
322
+ targets := make(map[*shimast.Node]struct{})
323
+ walkDescendants(root, func(child *shimast.Node) {
324
+ switch child.Kind {
325
+ case shimast.KindBinaryExpression:
326
+ expression := child.AsBinaryExpression()
327
+ if expression == nil || expression.OperatorToken == nil ||
328
+ expression.OperatorToken.Kind != shimast.KindEqualsToken ||
329
+ isDestructuringDefaultAssignment(child) {
330
+ return
331
+ }
332
+ for _, target := range assignmentTargetIdentifiers(expression.Left) {
333
+ targets[target] = struct{}{}
334
+ }
335
+ case shimast.KindForInStatement, shimast.KindForOfStatement:
336
+ statement := child.AsForInOrOfStatement()
337
+ if statement == nil || statement.Initializer == nil ||
338
+ statement.Initializer.Kind == shimast.KindVariableDeclarationList {
339
+ return
340
+ }
341
+ for _, target := range assignmentTargetIdentifiers(statement.Initializer) {
342
+ targets[target] = struct{}{}
343
+ }
344
+ }
345
+ })
346
+ return targets
347
+ }
348
+
349
+ // unusedPropertiesHasReadReference reports whether the candidate is read at
350
+ // least once. A candidate stripped of its container (unresolvable symbol) or
351
+ // with write-only references is skipped entirely.
352
+ func unusedPropertiesHasReadReference(
353
+ candidate *unusedPropertiesCandidate,
354
+ pureWrites map[*shimast.Node]struct{},
355
+ ) bool {
356
+ if candidate.container == nil {
357
+ return false
358
+ }
359
+ for _, reference := range candidate.references {
360
+ if reference.init {
361
+ continue
362
+ }
363
+ if _, writeOnly := pureWrites[reference.node]; writeOnly {
364
+ continue
365
+ }
366
+ return true
367
+ }
368
+ return false
369
+ }
370
+
371
+ // unusedPropertiesReportContainer checks every named property of an object or
372
+ // type literal against the reference list, reporting the ones nothing can
373
+ // read and recursing into nested containers with the references that reached
374
+ // their property.
375
+ func unusedPropertiesReportContainer(
376
+ ctx *Context,
377
+ container *shimast.Node,
378
+ references []unusedPropertiesReference,
379
+ ) {
380
+ for _, property := range unusedPropertiesContainerProperties(container) {
381
+ keyNode := property.Name()
382
+ if keyNode == nil {
383
+ continue
384
+ }
385
+ key := unusedPropertiesKeyOf(keyNode)
386
+ if key.ok && key.class == "string" && key.text == "__proto__" {
387
+ continue
388
+ }
389
+ next := unusedPropertiesFilterReferences(references, key)
390
+ if len(next) == 0 {
391
+ ctx.Report(
392
+ property,
393
+ "Property `"+unusedPropertiesDisplayName(ctx.File, keyNode, key)+"` is defined but never used.",
394
+ )
395
+ continue
396
+ }
397
+ if nested := unusedPropertiesPropertyContainer(property); nested != nil {
398
+ unusedPropertiesReportContainer(ctx, nested, next)
399
+ }
400
+ }
401
+ }
402
+
403
+ // unusedPropertiesContainerProperties returns the analyzable members of a
404
+ // property container: every keyed property of an object literal (spreads
405
+ // have no key and are skipped by the caller's nil check), or the property
406
+ // signatures of a type literal. Index, call, construct, method, and accessor
407
+ // signatures carry no analyzable data slot, matching upstream's
408
+ // TSPropertySignature filter.
409
+ func unusedPropertiesContainerProperties(container *shimast.Node) []*shimast.Node {
410
+ switch container.Kind {
411
+ case shimast.KindObjectLiteralExpression:
412
+ literal := container.AsObjectLiteralExpression()
413
+ if literal == nil || literal.Properties == nil {
414
+ return nil
415
+ }
416
+ properties := make([]*shimast.Node, 0, len(literal.Properties.Nodes))
417
+ for _, property := range literal.Properties.Nodes {
418
+ if property == nil || property.Kind == shimast.KindSpreadAssignment {
419
+ continue
420
+ }
421
+ properties = append(properties, property)
422
+ }
423
+ return properties
424
+ case shimast.KindTypeLiteral:
425
+ literal := container.AsTypeLiteralNode()
426
+ if literal == nil || literal.Members == nil {
427
+ return nil
428
+ }
429
+ var members []*shimast.Node
430
+ for _, member := range literal.Members.Nodes {
431
+ if member != nil && member.Kind == shimast.KindPropertySignature {
432
+ members = append(members, member)
433
+ }
434
+ }
435
+ return members
436
+ }
437
+ return nil
438
+ }
439
+
440
+ // unusedPropertiesPropertyContainer resolves the nested container a surviving
441
+ // property recurses into: an object-literal initializer (through TS
442
+ // assertion wrappers) or a property signature's inline type literal.
443
+ func unusedPropertiesPropertyContainer(property *shimast.Node) *shimast.Node {
444
+ switch property.Kind {
445
+ case shimast.KindPropertyAssignment:
446
+ assignment := property.AsPropertyAssignment()
447
+ if assignment == nil {
448
+ return nil
449
+ }
450
+ value := unwrapReferenceExpression(assignment.Initializer)
451
+ if value != nil && value.Kind == shimast.KindObjectLiteralExpression {
452
+ return value
453
+ }
454
+ case shimast.KindPropertySignature:
455
+ signature := property.AsPropertySignatureDeclaration()
456
+ if signature != nil && signature.Type != nil &&
457
+ signature.Type.Kind == shimast.KindTypeLiteral {
458
+ return signature.Type
459
+ }
460
+ }
461
+ return nil
462
+ }
463
+
464
+ // unusedPropertiesFilterReferences maps the reference list for one property
465
+ // key, mirroring upstream's per-key reference walk:
466
+ //
467
+ // - the declarator's own init write only survives for `export const`,
468
+ // where it stands in for the unseen importers;
469
+ // - a member access survives when it is assigned, called, dynamically
470
+ // computed, or names this key — and is dropped when it names a
471
+ // different known key;
472
+ // - destructuring (declaration or assignment form) survives when the
473
+ // pattern names this key or uses a rest element;
474
+ // - anything else — call argument, return, alias, spread, export — is an
475
+ // escape and survives untouched.
476
+ func unusedPropertiesFilterReferences(
477
+ references []unusedPropertiesReference,
478
+ key unusedPropertiesKey,
479
+ ) []unusedPropertiesReference {
480
+ var next []unusedPropertiesReference
481
+ for _, reference := range references {
482
+ parent := unusedPropertiesReferenceParent(reference.node)
483
+ if parent == nil {
484
+ next = append(next, reference)
485
+ continue
486
+ }
487
+ if reference.init {
488
+ if parent.Kind == shimast.KindVariableDeclaration &&
489
+ unusedPropertiesDeclarationIsExported(parent) {
490
+ next = append(next, unusedPropertiesReference{node: parent})
491
+ }
492
+ continue
493
+ }
494
+ switch parent.Kind {
495
+ case shimast.KindPropertyAccessExpression, shimast.KindElementAccessExpression:
496
+ if unusedPropertiesMemberAccessKeeps(parent, key) {
497
+ next = append(next, unusedPropertiesReference{node: parent})
498
+ }
499
+ continue
500
+ case shimast.KindVariableDeclaration:
501
+ declaration := parent.AsVariableDeclaration()
502
+ if declaration != nil && declaration.Name() != nil &&
503
+ declaration.Name().Kind == shimast.KindObjectBindingPattern {
504
+ if unusedPropertiesBindingPatternMatches(declaration.Name(), key) {
505
+ next = append(next, unusedPropertiesReference{node: parent})
506
+ }
507
+ continue
508
+ }
509
+ case shimast.KindBinaryExpression:
510
+ expression := parent.AsBinaryExpression()
511
+ if expression != nil && expression.OperatorToken != nil &&
512
+ isAssignmentOperator(expression.OperatorToken.Kind) &&
513
+ expression.Left != nil &&
514
+ expression.Left.Kind == shimast.KindObjectLiteralExpression &&
515
+ !isDestructuringDefaultAssignment(parent) {
516
+ if unusedPropertiesAssignmentPatternMatches(expression.Left, key) {
517
+ next = append(next, unusedPropertiesReference{node: parent})
518
+ }
519
+ continue
520
+ }
521
+ }
522
+ next = append(next, reference)
523
+ }
524
+ return next
525
+ }
526
+
527
+ // unusedPropertiesReferenceParent returns the node that consumes a reference,
528
+ // looking through parentheses and TypeScript assertion wrappers (`as`,
529
+ // `satisfies`, `!`, angle-bracket assertions) whenever the reference is the
530
+ // wrapped expression, so `(foo as Foo).a` behaves like `foo.a`.
531
+ func unusedPropertiesReferenceParent(node *shimast.Node) *shimast.Node {
532
+ for node != nil && node.Parent != nil {
533
+ parent := node.Parent
534
+ switch parent.Kind {
535
+ case shimast.KindParenthesizedExpression,
536
+ shimast.KindAsExpression,
537
+ shimast.KindSatisfiesExpression,
538
+ shimast.KindNonNullExpression,
539
+ shimast.KindTypeAssertionExpression:
540
+ if parent.Expression() == node {
541
+ node = parent
542
+ continue
543
+ }
544
+ }
545
+ return parent
546
+ }
547
+ return nil
548
+ }
549
+
550
+ // unusedPropertiesDeclarationIsExported reports whether a variable
551
+ // declaration belongs to an `export const/let/var` statement.
552
+ func unusedPropertiesDeclarationIsExported(declaration *shimast.Node) bool {
553
+ list := declaration.Parent
554
+ if list == nil || list.Parent == nil ||
555
+ list.Parent.Kind != shimast.KindVariableStatement {
556
+ return false
557
+ }
558
+ return hasModifier(list.Parent, shimast.KindExportKeyword)
559
+ }
560
+
561
+ // unusedPropertiesMemberAccessKeeps decides whether a member access on the
562
+ // analyzed value can reach the property key. Assignments (either side, like
563
+ // upstream), calls through the member, and dynamic indexes are conservative
564
+ // keeps; a static access survives only when its key equals the property's.
565
+ //
566
+ // The consumer of the member is found through parentheses only — `(foo.a)()`
567
+ // is a member call, exactly as in upstream's paren-free ESTree — while
568
+ // TypeScript wrappers stay opaque: upstream inspects the member's direct
569
+ // parent, so `(foo.a as any)()` is not treated as a call.
570
+ func unusedPropertiesMemberAccessKeeps(member *shimast.Node, key unusedPropertiesKey) bool {
571
+ wrapped := member
572
+ for wrapped.Parent != nil && wrapped.Parent.Kind == shimast.KindParenthesizedExpression &&
573
+ wrapped.Parent.Expression() == wrapped {
574
+ wrapped = wrapped.Parent
575
+ }
576
+ if grand := wrapped.Parent; grand != nil {
577
+ if grand.Kind == shimast.KindBinaryExpression {
578
+ expression := grand.AsBinaryExpression()
579
+ if expression != nil && expression.OperatorToken != nil &&
580
+ isAssignmentOperator(expression.OperatorToken.Kind) &&
581
+ !isDestructuringDefaultAssignment(grand) {
582
+ return true
583
+ }
584
+ }
585
+ if grand.Kind == shimast.KindCallExpression {
586
+ call := grand.AsCallExpression()
587
+ if call != nil && call.Expression == wrapped {
588
+ return true
589
+ }
590
+ }
591
+ }
592
+ if member.Kind == shimast.KindElementAccessExpression {
593
+ access := member.AsElementAccessExpression()
594
+ if access == nil {
595
+ return false
596
+ }
597
+ index := stripParens(access.ArgumentExpression)
598
+ if !unusedPropertiesPredictableIndex(index) {
599
+ return true
600
+ }
601
+ return unusedPropertiesKeysEqual(unusedPropertiesKeyOf(index), key)
602
+ }
603
+ access := member.AsPropertyAccessExpression()
604
+ if access == nil {
605
+ return false
606
+ }
607
+ return unusedPropertiesKeysEqual(unusedPropertiesKeyOf(access.Name()), key)
608
+ }
609
+
610
+ // unusedPropertiesPredictableIndex reports whether an element-access index is
611
+ // a literal whose value is knowable without evaluation. Everything else —
612
+ // identifiers, templates, expressions — is a dynamic access that could reach
613
+ // any property.
614
+ func unusedPropertiesPredictableIndex(index *shimast.Node) bool {
615
+ if index == nil {
616
+ return false
617
+ }
618
+ switch index.Kind {
619
+ case shimast.KindStringLiteral,
620
+ shimast.KindNumericLiteral,
621
+ shimast.KindBigIntLiteral,
622
+ shimast.KindTrueKeyword,
623
+ shimast.KindFalseKeyword,
624
+ shimast.KindNullKeyword,
625
+ shimast.KindRegularExpressionLiteral:
626
+ return true
627
+ }
628
+ return false
629
+ }
630
+
631
+ // unusedPropertiesBindingPatternMatches reports whether an object binding
632
+ // pattern (`const {a, ...rest} = value`) can extract the property key.
633
+ func unusedPropertiesBindingPatternMatches(pattern *shimast.Node, key unusedPropertiesKey) bool {
634
+ binding := pattern.AsBindingPattern()
635
+ if binding == nil || binding.Elements == nil {
636
+ return false
637
+ }
638
+ for _, elementNode := range binding.Elements.Nodes {
639
+ element := elementNode.AsBindingElement()
640
+ if element == nil {
641
+ continue
642
+ }
643
+ if element.DotDotDotToken != nil {
644
+ return true
645
+ }
646
+ keyNode := element.PropertyName
647
+ if keyNode == nil {
648
+ keyNode = element.Name()
649
+ }
650
+ if unusedPropertiesKeysEqual(unusedPropertiesKeyOf(keyNode), key) {
651
+ return true
652
+ }
653
+ }
654
+ return false
655
+ }
656
+
657
+ // unusedPropertiesAssignmentPatternMatches reports whether the object pattern
658
+ // of a destructuring assignment (`({a, ...rest} = value)`) can extract the
659
+ // property key. The pattern parses as an object literal in TS-go's AST.
660
+ func unusedPropertiesAssignmentPatternMatches(pattern *shimast.Node, key unusedPropertiesKey) bool {
661
+ literal := pattern.AsObjectLiteralExpression()
662
+ if literal == nil || literal.Properties == nil {
663
+ return false
664
+ }
665
+ for _, property := range literal.Properties.Nodes {
666
+ if property == nil {
667
+ continue
668
+ }
669
+ if property.Kind == shimast.KindSpreadAssignment {
670
+ return true
671
+ }
672
+ switch property.Kind {
673
+ case shimast.KindPropertyAssignment, shimast.KindShorthandPropertyAssignment:
674
+ if unusedPropertiesKeysEqual(unusedPropertiesKeyOf(property.Name()), key) {
675
+ return true
676
+ }
677
+ }
678
+ }
679
+ return false
680
+ }
681
+
682
+ // unusedPropertiesKeyOf normalizes a property name or index expression to a
683
+ // comparable key. Identifiers compare as strings (so a computed `[a]` key
684
+ // matches `.a` access, as upstream's name-based comparison does), string
685
+ // literals by value, numeric literals by their canonical numeric text (the
686
+ // parser already normalizes `0xFF`/`1e2`/`1_000`), bigint literals by their
687
+ // canonical decimal value, and boolean/null literals within their own
688
+ // classes. Parentheses inside a computed name are transparent because
689
+ // ESTree has no parenthesized-expression node. Anything else has no
690
+ // predictable name.
691
+ func unusedPropertiesKeyOf(node *shimast.Node) unusedPropertiesKey {
692
+ if node == nil {
693
+ return unusedPropertiesKey{}
694
+ }
695
+ switch node.Kind {
696
+ case shimast.KindIdentifier:
697
+ return unusedPropertiesKey{class: "string", text: identifierText(node), ok: true}
698
+ case shimast.KindStringLiteral:
699
+ return unusedPropertiesKey{class: "string", text: stringLiteralText(node), ok: true}
700
+ case shimast.KindNumericLiteral:
701
+ return unusedPropertiesKey{class: "number", text: numericLiteralText(node), ok: true}
702
+ case shimast.KindBigIntLiteral:
703
+ return unusedPropertiesKey{
704
+ class: "bigint",
705
+ text: unusedPropertiesBigIntText(numericLiteralText(node)),
706
+ ok: true,
707
+ }
708
+ case shimast.KindTrueKeyword:
709
+ return unusedPropertiesKey{class: "boolean", text: "true", ok: true}
710
+ case shimast.KindFalseKeyword:
711
+ return unusedPropertiesKey{class: "boolean", text: "false", ok: true}
712
+ case shimast.KindNullKeyword:
713
+ return unusedPropertiesKey{class: "null", text: "null", ok: true}
714
+ case shimast.KindComputedPropertyName:
715
+ computed := node.AsComputedPropertyName()
716
+ if computed == nil || computed.Expression == nil {
717
+ return unusedPropertiesKey{}
718
+ }
719
+ return unusedPropertiesKeyOf(stripParens(computed.Expression))
720
+ }
721
+ return unusedPropertiesKey{}
722
+ }
723
+
724
+ // unusedPropertiesBigIntText converts a bigint literal's source text (which
725
+ // the parser keeps verbatim, unlike normalized number literals) into its
726
+ // canonical decimal digits, so `0x10n` and `16n` compare equal the way
727
+ // upstream's BigInt value comparison does. Legacy octal (`0123n`) is a
728
+ // syntax error in JavaScript, so base auto-detection over the `0x`/`0o`/`0b`
729
+ // prefixes is unambiguous; unparsable text falls back to itself.
730
+ func unusedPropertiesBigIntText(text string) string {
731
+ digits := text
732
+ if len(digits) > 0 && digits[len(digits)-1] == 'n' {
733
+ digits = digits[:len(digits)-1]
734
+ }
735
+ value, ok := new(big.Int).SetString(digits, 0)
736
+ if !ok {
737
+ return digits
738
+ }
739
+ return value.String()
740
+ }
741
+
742
+ func unusedPropertiesKeysEqual(a, b unusedPropertiesKey) bool {
743
+ return a.ok && b.ok && a.class == b.class && a.text == b.text
744
+ }
745
+
746
+ // unusedPropertiesDisplayName renders the property key for the diagnostic
747
+ // message: resolved key text when the key is predictable, otherwise the
748
+ // source text of the (computed) key expression.
749
+ func unusedPropertiesDisplayName(
750
+ file *shimast.SourceFile,
751
+ keyNode *shimast.Node,
752
+ key unusedPropertiesKey,
753
+ ) string {
754
+ if key.ok {
755
+ return key.text
756
+ }
757
+ if keyNode.Kind == shimast.KindComputedPropertyName {
758
+ if computed := keyNode.AsComputedPropertyName(); computed != nil && computed.Expression != nil {
759
+ return nodeText(file, computed.Expression)
760
+ }
761
+ }
762
+ return nodeText(file, keyNode)
26
763
  }
27
764
 
28
765
  func init() {