@ttsc/lint 0.18.4 → 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 +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 +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
@@ -9,7 +9,11 @@
9
9
  package linthost
10
10
 
11
11
  import (
12
+ "path/filepath"
13
+ "strings"
14
+
12
15
  shimast "github.com/microsoft/typescript-go/shim/ast"
16
+ shimcore "github.com/microsoft/typescript-go/shim/core"
13
17
  shimscanner "github.com/microsoft/typescript-go/shim/scanner"
14
18
  )
15
19
 
@@ -27,8 +31,11 @@ func (noEmptyStaticBlock) Check(ctx *Context, node *shimast.Node) {
27
31
  return
28
32
  }
29
33
  body := block.Body.AsBlock()
30
- if body == nil || body.Statements == nil || len(body.Statements.Nodes) == 0 {
31
- ctx.Report(node, "Unexpected empty static block.")
34
+ if body == nil || body.Statements != nil && len(body.Statements.Nodes) != 0 {
35
+ return
36
+ }
37
+ if !emptyBracedBodyHasComment(ctx.File, block.Body) {
38
+ ctx.Report(block.Body, "Unexpected empty static block.")
32
39
  }
33
40
  }
34
41
 
@@ -177,13 +184,48 @@ func (noUnnecessaryTypeConstraint) Check(ctx *Context, node *shimast.Node) {
177
184
  ctx.Report(param.Constraint, message)
178
185
  return
179
186
  }
187
+ replacement := ""
188
+ if requiresGenericArrowDisambiguation(ctx, node, param) {
189
+ replacement = ","
190
+ }
180
191
  ctx.ReportFix(
181
192
  param.Constraint,
182
193
  message,
183
- TextEdit{Pos: name.End(), End: param.Constraint.End(), Text: ""},
194
+ TextEdit{Pos: name.End(), End: param.Constraint.End(), Text: replacement},
184
195
  )
185
196
  }
186
197
 
198
+ // requiresGenericArrowDisambiguation reports whether deleting a redundant
199
+ // constraint would leave a single generic arrow looking like JSX or another
200
+ // reserved generic declaration. TypeScript-ESLint uses this same extension
201
+ // boundary for TSX and the explicit Node module source modes.
202
+ func requiresGenericArrowDisambiguation(
203
+ ctx *Context,
204
+ node *shimast.Node,
205
+ param *shimast.TypeParameterDeclaration,
206
+ ) bool {
207
+ if ctx == nil || ctx.File == nil || node == nil || param == nil ||
208
+ node.Parent == nil || node.Parent.Kind != shimast.KindArrowFunction {
209
+ return false
210
+ }
211
+ requiresDisambiguation := ctx.File.ScriptKind == shimcore.ScriptKindTSX
212
+ if !requiresDisambiguation {
213
+ switch strings.ToLower(filepath.Ext(ctx.File.FileName())) {
214
+ case ".mts", ".cts":
215
+ requiresDisambiguation = true
216
+ }
217
+ }
218
+ if !requiresDisambiguation {
219
+ return false
220
+ }
221
+ typeParameters := node.Parent.TypeParameterList()
222
+ return typeParameters != nil &&
223
+ len(typeParameters.Nodes) == 1 &&
224
+ typeParameters.Nodes[0] == node &&
225
+ !typeParameters.HasTrailingComma() &&
226
+ param.DefaultType == nil
227
+ }
228
+
187
229
  // noUnsafeFunctionType: `Function` accepts any callable shape.
188
230
  // typescript-eslint recommended: https://typescript-eslint.io/rules/no-unsafe-function-type/
189
231
  type noUnsafeFunctionType struct{}
@@ -518,11 +560,28 @@ func (dotNotation) Check(ctx *Context, node *shimast.Node) {
518
560
  } else {
519
561
  replaceFrom = access.Expression.End()
520
562
  replacement = "." + key
563
+ // A bare decimal-integer receiver lexes an immediately following `.` as
564
+ // the number's own decimal point, so `5["toString"]` → `5.toString` is a
565
+ // syntax error (TS1351). Emit a separating space (`5 .toString`) exactly
566
+ // where ESLint's dot-notation does. Parenthesized `(5)`, hex/octal/binary,
567
+ // float, exponent, and bigint receivers all take `.` as member access
568
+ // cleanly, so they keep the dot tight.
569
+ if access.Expression.Kind == shimast.KindNumericLiteral &&
570
+ isDecimalIntegerLiteralText(nodeText(ctx.File, access.Expression)) {
571
+ replacement = " " + replacement
572
+ }
521
573
  }
522
574
  if replaceFrom < 0 || replaceFrom >= node.End() {
523
575
  ctx.Report(node, message)
524
576
  return
525
577
  }
578
+ // A comment inside the replaced `[…]` tail (`p1 /* keep */ ["foo"]`) would be
579
+ // silently deleted by the splice. Decline to a report-only diagnostic,
580
+ // matching ESLint dot-notation's `commentsExistBetween` guard.
581
+ if hasCommentBetween(src, replaceFrom, node.End()) {
582
+ ctx.Report(node, message)
583
+ return
584
+ }
526
585
  ctx.ReportFix(
527
586
  node,
528
587
  message,
@@ -530,6 +589,24 @@ func (dotNotation) Check(ctx *Context, node *shimast.Node) {
530
589
  )
531
590
  }
532
591
 
592
+ // isDecimalIntegerLiteralText reports whether raw is a numeric literal written
593
+ // as a plain decimal integer — ASCII digits with optional `_` separators and
594
+ // no radix prefix, decimal point, exponent, or bigint `n` suffix. A `.`
595
+ // spliced directly after such a receiver is lexed as the number's own decimal
596
+ // point (`5["x"]` → `5.x` is a SyntaxError), so dot-notation inserts a
597
+ // separating space. Mirrors ESLint astUtils.isDecimalInteger.
598
+ func isDecimalIntegerLiteralText(raw string) bool {
599
+ if raw == "" {
600
+ return false
601
+ }
602
+ for i := 0; i < len(raw); i++ {
603
+ if c := raw[i]; (c < '0' || c > '9') && c != '_' {
604
+ return false
605
+ }
606
+ }
607
+ return true
608
+ }
609
+
533
610
  // isReservedWord reports whether `value` is an ECMAScript reserved word. The
534
611
  // `dot-notation` autofix uses this to skip the rewrite even though modern
535
612
  // parsers accept reserved-word member names: leaving bracket access matches
@@ -1,31 +1,42 @@
1
1
  // groupedAccessorPairs: a `get` and `set` accessor that share the same
2
- // property name should be declared next to each other in the class
3
- // body. When the pair is split apart by unrelated members, a reader
4
- // scanning the class has to chase the read and write halves separately
5
- // — and patches to one half are easy to make without noticing the
6
- // other.
2
+ // property name should be declared next to each other, whether they sit
3
+ // in a class body or an object literal. When the pair is split apart by
4
+ // unrelated members, a reader scanning the declaration has to chase the
5
+ // read and write halves separately — and patches to one half are easy to
6
+ // make without noticing the other.
7
7
  //
8
- // AST-only: walk every class declaration/expression, group members by
9
- // (name, static-ness), and for any group that contains both a getter
10
- // and a setter check that their member indices are adjacent. The
11
- // second-encountered half is the one reported, because the first half
12
- // reads correctly until the reader hits the second declaration.
8
+ // AST-only: walk every class declaration/expression and object literal,
9
+ // group members by (name, static-ness), and for any group that contains
10
+ // both a getter and a setter check that their member indices are
11
+ // adjacent. The second-encountered half is the one reported, because the
12
+ // first half reads correctly until the reader hits the second
13
+ // declaration.
14
+ //
15
+ // The optional order option (`anyOrder` (the default), `getBeforeSet`, or
16
+ // `setBeforeGet`) additionally pins the relative order of an already
17
+ // adjacent pair, mirroring upstream's `invalidOrder` diagnostic.
13
18
  // https://eslint.org/docs/latest/rules/grouped-accessor-pairs
14
19
  package linthost
15
20
 
16
21
  import shimast "github.com/microsoft/typescript-go/shim/ast"
17
22
 
18
- type groupedAccessorPairs struct{}
23
+ type groupedAccessorPairs struct{ optionsRule }
19
24
 
20
25
  func (groupedAccessorPairs) Name() string { return "grouped-accessor-pairs" }
21
26
  func (groupedAccessorPairs) Visits() []shimast.Kind {
22
- return []shimast.Kind{shimast.KindClassDeclaration, shimast.KindClassExpression}
27
+ return []shimast.Kind{
28
+ shimast.KindClassDeclaration,
29
+ shimast.KindClassExpression,
30
+ shimast.KindObjectLiteralExpression,
31
+ }
23
32
  }
24
33
  func (groupedAccessorPairs) Check(ctx *Context, node *shimast.Node) {
25
- members := classMembers(node)
34
+ members := groupedAccessorMembers(node)
26
35
  if len(members) < 2 {
27
36
  return
28
37
  }
38
+ order := groupedAccessorOrder(ctx)
39
+
29
40
  type slot struct {
30
41
  name string
31
42
  static bool
@@ -35,7 +46,10 @@ func (groupedAccessorPairs) Check(ctx *Context, node *shimast.Node) {
35
46
  kind shimast.Kind
36
47
  node *shimast.Node
37
48
  }
38
- groups := map[slot][]entry{}
49
+ // Track first-seen group order so multiple pairs report deterministically;
50
+ // a Go map's iteration order is randomized.
51
+ groups := map[slot]*[]entry{}
52
+ var groupOrder []slot
39
53
  for i, member := range members {
40
54
  if member == nil {
41
55
  continue
@@ -48,9 +62,14 @@ func (groupedAccessorPairs) Check(ctx *Context, node *shimast.Node) {
48
62
  continue
49
63
  }
50
64
  key := slot{name: name, static: hasModifier(member, shimast.KindStaticKeyword)}
51
- groups[key] = append(groups[key], entry{index: i, kind: member.Kind, node: member})
65
+ if groups[key] == nil {
66
+ groups[key] = &[]entry{}
67
+ groupOrder = append(groupOrder, key)
68
+ }
69
+ *groups[key] = append(*groups[key], entry{index: i, kind: member.Kind, node: member})
52
70
  }
53
- for _, entries := range groups {
71
+ for _, key := range groupOrder {
72
+ entries := *groups[key]
54
73
  if len(entries) < 2 {
55
74
  continue
56
75
  }
@@ -67,21 +86,61 @@ func (groupedAccessorPairs) Check(ctx *Context, node *shimast.Node) {
67
86
  if get == nil || set == nil {
68
87
  continue
69
88
  }
70
- // Adjacent indices in the members list satisfy the rule.
71
- diff := get.index - set.index
72
- if diff == 1 || diff == -1 {
89
+ former, later := get, set
90
+ if set.index < get.index {
91
+ former, later = set, get
92
+ }
93
+ // Non-adjacent indices in the members list break the rule.
94
+ if later.index-former.index > 1 {
95
+ // Report on whichever half appears later — the earlier one
96
+ // reads fine in isolation; the later one is the surprise.
97
+ ctx.Report(later.node, "Accessor pair should be grouped.")
73
98
  continue
74
99
  }
75
- // Report on whichever half appears later the earlier one
76
- // reads fine in isolation; the later one is the surprise.
77
- later := get
78
- if set.index > get.index {
79
- later = set
100
+ // The pair is adjacent; enforce the requested order, if any.
101
+ if (order == "getBeforeSet" && former.kind == shimast.KindSetAccessor) ||
102
+ (order == "setBeforeGet" && former.kind == shimast.KindGetAccessor) {
103
+ ctx.Report(later.node, "Expected "+accessorRole(later.kind)+" to be before "+accessorRole(former.kind)+".")
80
104
  }
81
- ctx.Report(later.node, "Accessor pair should be grouped.")
82
105
  }
83
106
  }
84
107
 
108
+ // groupedAccessorMembers returns the ordered member list the rule scans for
109
+ // a class body (its members) or an object literal (its properties). Any
110
+ // other node kind yields nil so the multi-visit rule can bail cheaply.
111
+ func groupedAccessorMembers(node *shimast.Node) []*shimast.Node {
112
+ if node != nil && node.Kind == shimast.KindObjectLiteralExpression {
113
+ if obj := node.AsObjectLiteralExpression(); obj != nil && obj.Properties != nil {
114
+ return obj.Properties.Nodes
115
+ }
116
+ return nil
117
+ }
118
+ return classMembers(node)
119
+ }
120
+
121
+ // groupedAccessorOrder resolves the positional order option. A single
122
+ // positional slot is transported as a bare JSON string (see
123
+ // RuleOptionsMap); any unknown or absent value falls back to `anyOrder`,
124
+ // which disables the ordering check.
125
+ func groupedAccessorOrder(ctx *Context) string {
126
+ var order string
127
+ _ = ctx.DecodeOptions(&order)
128
+ switch order {
129
+ case "getBeforeSet", "setBeforeGet":
130
+ return order
131
+ default:
132
+ return "anyOrder"
133
+ }
134
+ }
135
+
136
+ // accessorRole names an accessor half for the `invalidOrder` message.
137
+ func accessorRole(kind shimast.Kind) string {
138
+ if kind == shimast.KindSetAccessor {
139
+ return "setter"
140
+ }
141
+ return "getter"
142
+ }
143
+
85
144
  func init() {
86
145
  Register(groupedAccessorPairs{})
87
146
  }
@@ -1,18 +1,24 @@
1
1
  // guardForIn: `for (key in obj)` walks the prototype chain and yields
2
2
  // every enumerable name, inherited or own. Most authors only ever care
3
3
  // about own keys, so an unguarded body silently leaks work onto
4
- // prototype-chain entries someone else attached. The canonical guard is
5
- // `if (Object.hasOwn(obj, key))` or the older
6
- // `Object.prototype.hasOwnProperty.call(obj, key)`; an inverted-guard
7
- // `continue` (`if (!Object.hasOwn(...)) continue;`) is the equivalent
8
- // early-skip shape.
4
+ // prototype-chain entries someone else attached.
9
5
  //
10
- // Conservative baseline: the rule only inspects the FIRST executable
11
- // statement of the loop body. The body either opens with a guard
12
- // covering everything that follows, or it does not anything more
13
- // nuanced is the developer's responsibility. The guard expression must
14
- // reference the loop's own key binding; a hard-coded call on an
15
- // unrelated identifier is not a real guard.
6
+ // Ported from ESLint's `guard-for-in`, the check is purely STRUCTURAL:
7
+ // it never inspects what the guard `if` tests, only the SHAPE of the
8
+ // loop body. The body is accepted when it is one of five shapes,
9
+ // mirroring upstream's ordered early-returns exactly:
10
+ //
11
+ // 1. an empty statement (`for (k in o);`);
12
+ // 2. a bare `if` statement (`for (k in o) if (...) ...;`);
13
+ // 3. an empty block (`{}`);
14
+ // 4. a block whose sole statement is an `if`;
15
+ // 5. a block whose leading statement is an `if` whose consequent is a
16
+ // `continue` (bare, or a block containing only `continue`).
17
+ //
18
+ // Anything else is reported. Because the condition is never examined,
19
+ // `if (obj.hasOwnProperty(key))`, `if (Object.hasOwn(obj, key))`,
20
+ // `if (cond)`, and `if (key.startsWith("_")) continue;` are all
21
+ // accepted, matching ESLint.
16
22
  // https://eslint.org/docs/latest/rules/guard-for-in
17
23
  package linthost
18
24
 
@@ -29,168 +35,79 @@ func (guardForIn) Check(ctx *Context, node *shimast.Node) {
29
35
  if stmt == nil || stmt.Statement == nil {
30
36
  return
31
37
  }
32
- first := firstBodyStatement(stmt.Statement)
33
- if first == nil {
34
- return
35
- }
36
- key := forInKeyName(stmt.Initializer)
37
- if isHasOwnGuard(first, key) {
38
+ if isGuardedForInBody(stmt.Statement) {
38
39
  return
39
40
  }
40
- ctx.Report(node, "The body of a `for...in` should be wrapped in an `if (Object.hasOwn(...))` statement to filter unwanted properties from the prototype.")
41
+ ctx.Report(node, "The body of a `for...in` should be wrapped in an `if` statement to filter unwanted properties from the prototype.")
41
42
  }
42
43
 
43
- // firstBodyStatement returns the first executable statement inside a
44
- // loop body. A block body unwraps to its first statement; a bare
45
- // non-block body is itself the only statement. Returns nil for an
46
- // empty block — an empty body cannot leak inherited keys.
47
- func firstBodyStatement(body *shimast.Node) *shimast.Node {
48
- if body == nil {
49
- return nil
50
- }
51
- if body.Kind != shimast.KindBlock {
52
- return body
44
+ // isGuardedForInBody reports whether a `for...in` loop body has one of
45
+ // the five structural shapes ESLint's guard-for-in accepts. It mirrors
46
+ // upstream's ordered early-returns and deliberately never inspects the
47
+ // guard condition.
48
+ func isGuardedForInBody(body *shimast.Node) bool {
49
+ switch body.Kind {
50
+ case shimast.KindEmptyStatement:
51
+ // `for (k in o);` — an empty body cannot leak inherited keys.
52
+ return true
53
+ case shimast.KindIfStatement:
54
+ // `for (k in o) if (...) ...;` — a bare `if` body is itself the
55
+ // guard, whatever it tests.
56
+ return true
57
+ case shimast.KindBlock:
58
+ // Fall through to the block-shape analysis below.
59
+ default:
60
+ return false
53
61
  }
62
+
54
63
  block := body.AsBlock()
55
64
  if block == nil || block.Statements == nil {
56
- return nil
57
- }
58
- for _, s := range block.Statements.Nodes {
59
- if s != nil {
60
- return s
61
- }
62
- }
63
- return nil
64
- }
65
-
66
- // forInKeyName extracts the identifier text of the loop key from a
67
- // `for (… in obj)` initializer. The initializer is either a
68
- // VariableDeclarationList with one declaration (`for (const key in …)`)
69
- // or a bare Identifier (`for (key in …)`). Destructuring patterns and
70
- // other shapes return "" so the rule skips its key-identity check.
71
- func forInKeyName(init *shimast.Node) string {
72
- if init == nil {
73
- return ""
74
- }
75
- if name := identifierText(init); name != "" {
76
- return name
77
- }
78
- if init.Kind != shimast.KindVariableDeclarationList {
79
- return ""
80
- }
81
- list := init.AsVariableDeclarationList()
82
- if list == nil || list.Declarations == nil || len(list.Declarations.Nodes) != 1 {
83
- return ""
84
- }
85
- decl := list.Declarations.Nodes[0]
86
- if decl == nil {
87
- return ""
88
- }
89
- v := decl.AsVariableDeclaration()
90
- if v == nil {
91
- return ""
92
- }
93
- return identifierText(v.Name())
94
- }
95
-
96
- // isHasOwnGuard reports whether `stmt` is the canonical own-key guard
97
- // that should open a `for...in` body. Two shapes count:
98
- //
99
- // 1. `if (<has-own>(obj, key)) { … }` — positive guard wrapping the
100
- // real body. The else branch (if any) is allowed because the
101
- // developer has explicitly handled the inherited-key case.
102
- // 2. `if (!<has-own>(obj, key)) continue;` — inverted-guard early
103
- // skip. The then branch must be a single `continue` (with or
104
- // without a block wrapper) so the rest of the loop body only runs
105
- // for own keys.
106
- //
107
- // `<has-own>` is either `Object.hasOwn(obj, key)` or
108
- // `Object.prototype.hasOwnProperty.call(obj, key)`. The second
109
- // argument must match `key` when one was extracted from the
110
- // initializer; otherwise any second argument is accepted (the rule
111
- // has no reliable handle on the key name).
112
- func isHasOwnGuard(stmt *shimast.Node, key string) bool {
113
- if stmt == nil || stmt.Kind != shimast.KindIfStatement {
114
- return false
115
- }
116
- ifStmt := stmt.AsIfStatement()
117
- if ifStmt == nil || ifStmt.Expression == nil {
118
- return false
119
- }
120
- cond := stripParens(ifStmt.Expression)
121
- if cond == nil {
122
- return false
123
- }
124
- if isHasOwnCall(cond, key) {
65
+ // A block with no statement list behaves like an empty block.
125
66
  return true
126
67
  }
127
- if cond.Kind != shimast.KindPrefixUnaryExpression {
128
- return false
129
- }
130
- pre := cond.AsPrefixUnaryExpression()
131
- if pre == nil || pre.Operator != shimast.KindExclamationToken {
132
- return false
133
- }
134
- if !isHasOwnCall(stripParens(pre.Operand), key) {
135
- return false
136
- }
137
- // Negated guard: require the then branch to be a single `continue`
138
- // so the rest of the loop runs only for own keys.
139
- return isLoneContinue(ifStmt.ThenStatement)
140
- }
141
-
142
- // isHasOwnCall reports whether `node` is `Object.hasOwn(obj, key)` or
143
- // `Object.prototype.hasOwnProperty.call(obj, key)`. The second
144
- // argument must match `key` when one is supplied.
145
- func isHasOwnCall(node *shimast.Node, key string) bool {
146
- if node == nil || node.Kind != shimast.KindCallExpression {
147
- return false
68
+ stmts := block.Statements.Nodes
69
+ // Empty block.
70
+ if len(stmts) == 0 {
71
+ return true
148
72
  }
149
- call := node.AsCallExpression()
150
- if call == nil || call.Expression == nil || call.Arguments == nil {
73
+ // Every remaining accepted shape opens with an `if`.
74
+ lead := stmts[0]
75
+ if lead == nil || lead.Kind != shimast.KindIfStatement {
151
76
  return false
152
77
  }
153
- args := call.Arguments.Nodes
154
- if len(args) < 2 {
155
- return false
78
+ // Block whose sole statement is an `if`.
79
+ if len(stmts) == 1 {
80
+ return true
156
81
  }
157
- callee := stripParens(call.Expression)
158
- switch {
159
- case isMatchingPropertyAccess(callee, "Object", "hasOwn"):
160
- // `Object.hasOwn(obj, key)` both arguments belong to the
161
- // caller; the key check is the second argument.
162
- case isMatchingPropertyAccess(callee, "Object", "prototype", "hasOwnProperty", "call"):
163
- // `Object.prototype.hasOwnProperty.call(obj, key)` — same
164
- // argument layout because `.call(obj, key)` passes the
165
- // receiver explicitly.
166
- default:
82
+ // Block that opens with an `if` whose consequent is a `continue`, so
83
+ // the statements after it run only for retained keys.
84
+ ifStmt := lead.AsIfStatement()
85
+ if ifStmt == nil {
167
86
  return false
168
87
  }
169
- if key == "" {
170
- return true
171
- }
172
- return identifierText(stripParens(args[1])) == key
88
+ return isContinueConsequent(ifStmt.ThenStatement)
173
89
  }
174
90
 
175
- // isLoneContinue reports whether `stmt` is a bare `continue;` (or a
176
- // block containing exactly one `continue;`). Used to validate the
177
- // then-branch of the negated-guard shape.
178
- func isLoneContinue(stmt *shimast.Node) bool {
179
- if stmt == nil {
91
+ // isContinueConsequent reports whether an `if` consequent is a
92
+ // `continue` statement, either bare (`continue;`) or a block whose sole
93
+ // statement is `continue;`. Mirrors ESLint's check on the leading
94
+ // guard's consequent.
95
+ func isContinueConsequent(consequent *shimast.Node) bool {
96
+ if consequent == nil {
180
97
  return false
181
98
  }
182
- if stmt.Kind == shimast.KindContinueStatement {
99
+ if consequent.Kind == shimast.KindContinueStatement {
183
100
  return true
184
101
  }
185
- if stmt.Kind != shimast.KindBlock {
102
+ if consequent.Kind != shimast.KindBlock {
186
103
  return false
187
104
  }
188
- block := stmt.AsBlock()
105
+ block := consequent.AsBlock()
189
106
  if block == nil || block.Statements == nil || len(block.Statements.Nodes) != 1 {
190
107
  return false
191
108
  }
192
- return block.Statements.Nodes[0] != nil &&
193
- block.Statements.Nodes[0].Kind == shimast.KindContinueStatement
109
+ only := block.Statements.Nodes[0]
110
+ return only != nil && only.Kind == shimast.KindContinueStatement
194
111
  }
195
112
 
196
113
  func init() {