@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
@@ -8,45 +8,168 @@
8
8
  package linthost
9
9
 
10
10
  import (
11
- "strconv"
12
11
  "strings"
13
12
 
14
13
  shimast "github.com/microsoft/typescript-go/shim/ast"
14
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
15
15
  )
16
16
 
17
- // noDupeElseIf: `if (a) {} else if (a) {}` the second branch is
18
- // unreachable.
17
+ // noDupeElseIf rejects an else-if branch when earlier conditions in the
18
+ // same chain already cover every path that can make its condition true.
19
19
  type noDupeElseIf struct{}
20
20
 
21
21
  func (noDupeElseIf) Name() string { return "no-dupe-else-if" }
22
22
  func (noDupeElseIf) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindIfStatement} }
23
23
  func (noDupeElseIf) Check(ctx *Context, node *shimast.Node) {
24
- // Only fire on the *outermost* if; the recursion below scans the
25
- // chain once.
26
- if parent := node.Parent; parent != nil {
27
- if parent.Kind == shimast.KindIfStatement {
28
- outer := parent.AsIfStatement()
29
- if outer != nil && outer.ElseStatement == node {
30
- return
31
- }
24
+ statement := node.AsIfStatement()
25
+ if statement == nil || statement.Expression == nil {
26
+ return
27
+ }
28
+
29
+ test := stripParens(statement.Expression)
30
+ conditionsToCheck := []*shimast.Node{test}
31
+ if noDupeElseIfLogicalOperator(test) == shimast.KindAmpersandAmpersandToken {
32
+ conditionsToCheck = append(conditionsToCheck, noDupeElseIfSplit(test, shimast.KindAmpersandAmpersandToken)...)
33
+ }
34
+
35
+ candidates := make([][][]*shimast.Node, 0, len(conditionsToCheck))
36
+ for _, condition := range conditionsToCheck {
37
+ orOperands := noDupeElseIfSplit(condition, shimast.KindBarBarToken)
38
+ conjunctions := make([][]*shimast.Node, 0, len(orOperands))
39
+ for _, operand := range orOperands {
40
+ conjunctions = append(conjunctions, noDupeElseIfSplit(operand, shimast.KindAmpersandAmpersandToken))
32
41
  }
42
+ candidates = append(candidates, conjunctions)
33
43
  }
34
- seen := map[string]bool{}
35
- cur := node
36
- for cur != nil && cur.Kind == shimast.KindIfStatement {
37
- stmt := cur.AsIfStatement()
38
- if stmt == nil || stmt.Expression == nil {
44
+
45
+ current := node
46
+ for current.Parent != nil && current.Parent.Kind == shimast.KindIfStatement {
47
+ parent := current.Parent
48
+ parentStatement := parent.AsIfStatement()
49
+ if parentStatement == nil || parentStatement.ElseStatement != current || parentStatement.Expression == nil {
39
50
  break
40
51
  }
41
- key := nodeText(ctx.File, stmt.Expression)
42
- if key != "" {
43
- if seen[key] {
44
- ctx.Report(stmt.Expression, "This branch can never execute. Its condition is a duplicate of an earlier branch.")
45
- } else {
46
- seen[key] = true
52
+
53
+ earlierOrOperands := noDupeElseIfSplit(stripParens(parentStatement.Expression), shimast.KindBarBarToken)
54
+ earlierConjunctions := make([][]*shimast.Node, 0, len(earlierOrOperands))
55
+ for _, operand := range earlierOrOperands {
56
+ earlierConjunctions = append(earlierConjunctions, noDupeElseIfSplit(operand, shimast.KindAmpersandAmpersandToken))
57
+ }
58
+
59
+ for i, disjunction := range candidates {
60
+ remaining := disjunction[:0]
61
+ for _, conjunction := range disjunction {
62
+ covered := false
63
+ for _, earlier := range earlierConjunctions {
64
+ if noDupeElseIfSubset(ctx.File, earlier, conjunction) {
65
+ covered = true
66
+ break
67
+ }
68
+ }
69
+ if !covered {
70
+ remaining = append(remaining, conjunction)
71
+ }
72
+ }
73
+ candidates[i] = remaining
74
+ if len(remaining) == 0 {
75
+ ctx.Report(statement.Expression, "This branch can never execute. Its condition is a duplicate or covered by previous conditions in the if-else-if chain.")
76
+ return
47
77
  }
48
78
  }
49
- cur = stmt.ElseStatement
79
+
80
+ current = parent
81
+ }
82
+ }
83
+
84
+ func noDupeElseIfLogicalOperator(node *shimast.Node) shimast.Kind {
85
+ node = stripParens(node)
86
+ if node == nil || node.Kind != shimast.KindBinaryExpression {
87
+ return shimast.KindUnknown
88
+ }
89
+ expression := node.AsBinaryExpression()
90
+ if expression == nil || expression.OperatorToken == nil {
91
+ return shimast.KindUnknown
92
+ }
93
+ switch expression.OperatorToken.Kind {
94
+ case shimast.KindAmpersandAmpersandToken, shimast.KindBarBarToken:
95
+ return expression.OperatorToken.Kind
96
+ default:
97
+ return shimast.KindUnknown
98
+ }
99
+ }
100
+
101
+ func noDupeElseIfSplit(node *shimast.Node, operator shimast.Kind) []*shimast.Node {
102
+ node = stripParens(node)
103
+ if noDupeElseIfLogicalOperator(node) != operator {
104
+ return []*shimast.Node{node}
105
+ }
106
+ expression := node.AsBinaryExpression()
107
+ operands := noDupeElseIfSplit(expression.Left, operator)
108
+ return append(operands, noDupeElseIfSplit(expression.Right, operator)...)
109
+ }
110
+
111
+ func noDupeElseIfSubset(file *shimast.SourceFile, subset, set []*shimast.Node) bool {
112
+ for _, candidate := range subset {
113
+ matched := false
114
+ for _, element := range set {
115
+ if noDupeElseIfEqual(file, candidate, element) {
116
+ matched = true
117
+ break
118
+ }
119
+ }
120
+ if !matched {
121
+ return false
122
+ }
123
+ }
124
+ return true
125
+ }
126
+
127
+ // noDupeElseIfEqual compares boolean expressions structurally. Logical AND
128
+ // and OR are commutative in a condition's truth table; every other expression
129
+ // must retain the same token kinds and values.
130
+ func noDupeElseIfEqual(file *shimast.SourceFile, left, right *shimast.Node) bool {
131
+ left = stripParens(left)
132
+ right = stripParens(right)
133
+ if left == nil || right == nil || left.Kind != right.Kind {
134
+ return false
135
+ }
136
+
137
+ operator := noDupeElseIfLogicalOperator(left)
138
+ if operator != shimast.KindUnknown && operator == noDupeElseIfLogicalOperator(right) {
139
+ leftExpression := left.AsBinaryExpression()
140
+ rightExpression := right.AsBinaryExpression()
141
+ return noDupeElseIfEqual(file, leftExpression.Left, rightExpression.Left) &&
142
+ noDupeElseIfEqual(file, leftExpression.Right, rightExpression.Right) ||
143
+ noDupeElseIfEqual(file, leftExpression.Left, rightExpression.Right) &&
144
+ noDupeElseIfEqual(file, leftExpression.Right, rightExpression.Left)
145
+ }
146
+
147
+ return noDupeElseIfEqualTokens(file, left, right)
148
+ }
149
+
150
+ func noDupeElseIfEqualTokens(file *shimast.SourceFile, left, right *shimast.Node) bool {
151
+ leftText := nodeText(file, left)
152
+ rightText := nodeText(file, right)
153
+ if leftText == "" || rightText == "" {
154
+ return false
155
+ }
156
+
157
+ leftScanner := shimscanner.NewScanner()
158
+ leftScanner.SetText(leftText)
159
+ leftScanner.SetSkipTrivia(true)
160
+ rightScanner := shimscanner.NewScanner()
161
+ rightScanner.SetText(rightText)
162
+ rightScanner.SetSkipTrivia(true)
163
+
164
+ for {
165
+ leftKind := leftScanner.Scan()
166
+ rightKind := rightScanner.Scan()
167
+ if leftKind != rightKind || leftScanner.TokenText() != rightScanner.TokenText() {
168
+ return false
169
+ }
170
+ if leftKind == shimast.KindEndOfFile {
171
+ return true
172
+ }
50
173
  }
51
174
  }
52
175
 
@@ -75,9 +198,9 @@ func (noExAssign) Check(ctx *Context, node *shimast.Node) {
75
198
  }
76
199
 
77
200
  // walkAssignments invokes `report` on every `<name> = ...` shape inside
78
- // `root`. Used by noExAssign to scan a single catch block; the
79
- // file-wide noFuncAssign / noClassAssign scans go through
80
- // reportAssignmentsToDeclarations instead.
201
+ // `root`. It is retained for noExAssign's catch-block-local scan; class and
202
+ // function assignment rules require checker binding identity and live in
203
+ // their dedicated files.
81
204
  func walkAssignments(root *shimast.Node, name string, report func(*shimast.Node)) {
82
205
  if root == nil {
83
206
  return
@@ -107,33 +230,12 @@ func (noEmptyCharacterClass) Visits() []shimast.Kind {
107
230
  return []shimast.Kind{shimast.KindRegularExpressionLiteral}
108
231
  }
109
232
  func (noEmptyCharacterClass) Check(ctx *Context, node *shimast.Node) {
110
- src := nodeText(ctx.File, node)
111
- if hasEmptyCharClass(src) {
233
+ parts, ok := parseRegexpLiteralParts(ctx, node)
234
+ if ok && regexpHasEmptyCharacterClass(parts) {
112
235
  ctx.Report(node, "Empty class.")
113
236
  }
114
237
  }
115
238
 
116
- // hasEmptyCharClass reports whether the regex source text src contains an
117
- // empty character class (`[]` or `[^]`). Respects backslash escapes.
118
- func hasEmptyCharClass(src string) bool {
119
- // Walk the regex literal source manually, respecting escapes.
120
- for i := 0; i < len(src); i++ {
121
- switch src[i] {
122
- case '\\':
123
- i++ // skip escape
124
- case '[':
125
- j := i + 1
126
- if j < len(src) && src[j] == '^' {
127
- j++
128
- }
129
- if j < len(src) && src[j] == ']' {
130
- return true
131
- }
132
- }
133
- }
134
- return false
135
- }
136
-
137
239
  // noMisleadingCharacterClass: `/[👍]/` — surrogate pairs in regex.
138
240
  type noMisleadingCharacterClass struct{}
139
241
 
@@ -179,10 +281,9 @@ func regexHasSurrogatePair(src string) bool {
179
281
  return false
180
282
  }
181
283
 
182
- // noLossOfPrecision: `9007199254740993` decimal numeric literal whose
183
- // source text changes when parsed as a JavaScript Number. We read the source
184
- // form instead of the parser's normalized .Text, which has already lost
185
- // precision.
284
+ // noLossOfPrecision: a Number literal whose requested significant digits
285
+ // change during IEEE-754 conversion. We read the source form instead of the
286
+ // parser's normalized .Text, which has already lost spelling and precision.
186
287
  type noLossOfPrecision struct{}
187
288
 
188
289
  func (noLossOfPrecision) Name() string { return "no-loss-of-precision" }
@@ -197,128 +298,6 @@ func (noLossOfPrecision) Check(ctx *Context, node *shimast.Node) {
197
298
  }
198
299
  }
199
300
 
200
- // numericLiteralLosesPrecision reports whether the decimal integer literal text
201
- // changes when parsed as a JavaScript Number. Non-decimal literals (hex, octal,
202
- // binary) and literals with exponents or decimal points are exempt because
203
- // their precision loss is caller-visible and intentional.
204
- func numericLiteralLosesPrecision(text string) bool {
205
- // Strip underscore separators, exponents, decimal/hex/oct/binary
206
- // markers — for the simple-base-10 integer case the round-trip
207
- // parse → format check is sufficient.
208
- clean := strings.ReplaceAll(text, "_", "")
209
- if strings.ContainsAny(clean, "eE.xXoObB") {
210
- return false
211
- }
212
- if len(clean) < 16 {
213
- return false
214
- }
215
- // Trim leading zeros for comparison.
216
- trimmed := strings.TrimLeft(clean, "0")
217
- if trimmed == "" {
218
- return false
219
- }
220
- // 2^53 is unsafe for arithmetic comparisons but still exactly representable.
221
- // 2^53+1 is the first plain decimal integer that changes when parsed.
222
- const firstPossibleLoss = "9007199254740993"
223
- if len(trimmed) < len(firstPossibleLoss) {
224
- return false
225
- }
226
- if len(trimmed) == len(firstPossibleLoss) && trimmed < firstPossibleLoss {
227
- return false
228
- }
229
- // Number.MAX_VALUE is around 1.79e308, so any integer with more than 309
230
- // decimal digits cannot round-trip through a finite JavaScript Number.
231
- const maxFiniteDecimalDigits = 309
232
- if len(trimmed) > maxFiniteDecimalDigits {
233
- return true
234
- }
235
- parsed, err := strconv.ParseFloat(trimmed, 64)
236
- if err != nil && parsed == 0 {
237
- return false
238
- }
239
- return strconv.FormatFloat(parsed, 'f', 0, 64) != trimmed
240
- }
241
-
242
- // noClassAssign: assigning to a class declaration's name.
243
- type noClassAssign struct{}
244
-
245
- func (noClassAssign) Name() string { return "no-class-assign" }
246
- func (noClassAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
247
- func (noClassAssign) Check(ctx *Context, node *shimast.Node) {
248
- reportAssignmentsToDeclarations(ctx, node, shimast.KindClassDeclaration, "is a class.")
249
- }
250
-
251
- // noFuncAssign: same idea, but for function declarations.
252
- type noFuncAssign struct{}
253
-
254
- func (noFuncAssign) Name() string { return "no-func-assign" }
255
- func (noFuncAssign) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
256
- func (noFuncAssign) Check(ctx *Context, node *shimast.Node) {
257
- reportAssignmentsToDeclarations(ctx, node, shimast.KindFunctionDeclaration, "is a function.")
258
- }
259
-
260
- // reportAssignmentsToDeclarations flags every `<name> = …` assignment whose
261
- // target identifier names a `declKind` declaration found anywhere in the
262
- // file. It walks the file exactly once — gathering declared names and
263
- // assignment targets in the same pass — so the cost is linear in file size.
264
- //
265
- // The earlier shape registered for `declKind` directly and, on every
266
- // declaration, re-scanned the whole file for assignments: O(declarations ×
267
- // file size), which blows up quadratically on a file with many top-level
268
- // functions. Visiting `KindSourceFile` once and cross-referencing afterward
269
- // keeps the same findings without the repeated scans.
270
- func reportAssignmentsToDeclarations(
271
- ctx *Context,
272
- file *shimast.Node,
273
- declKind shimast.Kind,
274
- noun string,
275
- ) {
276
- if ctx == nil || file == nil {
277
- return
278
- }
279
- declared := map[string]struct{}{}
280
- var targets []*shimast.Node
281
- walkDescendants(file, func(n *shimast.Node) {
282
- switch n.Kind {
283
- case declKind:
284
- if name := declarationName(n); name != "" {
285
- declared[name] = struct{}{}
286
- }
287
- case shimast.KindBinaryExpression:
288
- if expr := n.AsBinaryExpression(); expr != nil &&
289
- expr.OperatorToken != nil && isAssignmentOperator(expr.OperatorToken.Kind) &&
290
- expr.Left != nil && expr.Left.Kind == shimast.KindIdentifier {
291
- targets = append(targets, expr.Left)
292
- }
293
- }
294
- })
295
- if len(declared) == 0 || len(targets) == 0 {
296
- return
297
- }
298
- for _, target := range targets {
299
- name := identifierText(target)
300
- if _, ok := declared[name]; ok {
301
- ctx.Report(target, "'"+name+"' "+noun)
302
- }
303
- }
304
- }
305
-
306
- // declarationName returns the bound name of a class or function declaration
307
- // node, or "" when the node is neither (or is anonymous).
308
- func declarationName(n *shimast.Node) string {
309
- switch n.Kind {
310
- case shimast.KindFunctionDeclaration:
311
- if d := n.AsFunctionDeclaration(); d != nil {
312
- return identifierText(d.Name())
313
- }
314
- case shimast.KindClassDeclaration:
315
- if d := n.AsClassDeclaration(); d != nil {
316
- return identifierText(d.Name())
317
- }
318
- }
319
- return ""
320
- }
321
-
322
301
  // noPrototypeBuiltins: `obj.hasOwnProperty(x)` — should be
323
302
  // `Object.prototype.hasOwnProperty.call(obj, x)` or `Object.hasOwn`.
324
303
  type noPrototypeBuiltins struct{}
@@ -371,36 +350,6 @@ func (noAsyncPromiseExecutor) Check(ctx *Context, node *shimast.Node) {
371
350
  }
372
351
  }
373
352
 
374
- // noPromiseExecutorReturn: `new Promise(() => 1)` — the value is
375
- // thrown away.
376
- type noPromiseExecutorReturn struct{}
377
-
378
- func (noPromiseExecutorReturn) Name() string { return "no-promise-executor-return" }
379
- func (noPromiseExecutorReturn) Visits() []shimast.Kind {
380
- return []shimast.Kind{shimast.KindNewExpression}
381
- }
382
- func (noPromiseExecutorReturn) Check(ctx *Context, node *shimast.Node) {
383
- ne := node.AsNewExpression()
384
- if ne == nil || identifierText(ne.Expression) != "Promise" {
385
- return
386
- }
387
- if ne.Arguments == nil || len(ne.Arguments.Nodes) == 0 {
388
- return
389
- }
390
- executor := ne.Arguments.Nodes[0]
391
- if executor == nil || executor.Kind != shimast.KindArrowFunction {
392
- return
393
- }
394
- arrow := executor.AsArrowFunction()
395
- if arrow == nil || arrow.Body == nil {
396
- return
397
- }
398
- // Concise arrow body returns the value implicitly.
399
- if arrow.Body.Kind != shimast.KindBlock {
400
- ctx.Report(arrow.Body, "Return values from promise executor functions cannot be read.")
401
- }
402
- }
403
-
404
353
  // noControlRegex: `/\x00/` — control characters in regex are usually
405
354
  // the result of accidentally typing the escape rather than the printable
406
355
  // counterpart.
@@ -502,107 +451,6 @@ func isIrregularWhitespace(r rune) bool {
502
451
  return false
503
452
  }
504
453
 
505
- // noFallthrough: `switch` cases that fall through to the next label
506
- // without an explicit `break` / `return` / `throw` / `continue`.
507
- type noFallthrough struct{}
508
-
509
- func (noFallthrough) Name() string { return "no-fallthrough" }
510
- func (noFallthrough) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSwitchStatement} }
511
- func (noFallthrough) Check(ctx *Context, node *shimast.Node) {
512
- sw := node.AsSwitchStatement()
513
- if sw == nil || sw.CaseBlock == nil {
514
- return
515
- }
516
- block := sw.CaseBlock.AsCaseBlock()
517
- if block == nil || block.Clauses == nil {
518
- return
519
- }
520
- clauses := block.Clauses.Nodes
521
- for i := 0; i+1 < len(clauses); i++ {
522
- clause := clauses[i].AsCaseOrDefaultClause()
523
- if clause == nil || clause.Statements == nil {
524
- continue
525
- }
526
- stmts := clause.Statements.Nodes
527
- if len(stmts) == 0 {
528
- continue // empty case is intentional, never a fallthrough.
529
- }
530
- if !isTerminating(stmts[len(stmts)-1]) {
531
- ctx.Report(clauses[i+1], "Expected a 'break' statement before this case.")
532
- }
533
- }
534
- }
535
-
536
- // isTerminating reports whether stmt is a statement that unconditionally
537
- // transfers control out of the current block: break, continue, return,
538
- // throw, or a block whose last statement is terminating.
539
- func isTerminating(stmt *shimast.Node) bool {
540
- if stmt == nil {
541
- return false
542
- }
543
- switch stmt.Kind {
544
- case shimast.KindBreakStatement,
545
- shimast.KindContinueStatement,
546
- shimast.KindReturnStatement,
547
- shimast.KindThrowStatement:
548
- return true
549
- case shimast.KindBlock:
550
- block := stmt.AsBlock()
551
- if block == nil || block.Statements == nil {
552
- return false
553
- }
554
- nodes := block.Statements.Nodes
555
- if len(nodes) == 0 {
556
- return false
557
- }
558
- return isTerminating(nodes[len(nodes)-1])
559
- }
560
- return false
561
- }
562
-
563
- // noInnerDeclarations: `function foo() { if (x) { function bar() {} } }`
564
- // — inner function declarations are hoisted differently in strict mode
565
- // vs sloppy and are confusing.
566
- type noInnerDeclarations struct{}
567
-
568
- func (noInnerDeclarations) Name() string { return "no-inner-declarations" }
569
- func (noInnerDeclarations) Visits() []shimast.Kind {
570
- return []shimast.Kind{shimast.KindFunctionDeclaration, shimast.KindVariableStatement}
571
- }
572
- func (noInnerDeclarations) Check(ctx *Context, node *shimast.Node) {
573
- if node.Kind == shimast.KindVariableStatement {
574
- stmt := node.AsVariableStatement()
575
- if stmt == nil || stmt.DeclarationList == nil {
576
- return
577
- }
578
- // Only `var` is hoisted oddly.
579
- if !shimast.IsVar(stmt.DeclarationList) {
580
- return
581
- }
582
- }
583
- parent := node.Parent
584
- if parent == nil {
585
- return
586
- }
587
- switch parent.Kind {
588
- case shimast.KindSourceFile, shimast.KindModuleBlock:
589
- return
590
- case shimast.KindBlock:
591
- grand := parent.Parent
592
- if grand == nil {
593
- return
594
- }
595
- if isFunctionLikeKind(grand) {
596
- return
597
- }
598
- }
599
- what := "function"
600
- if node.Kind == shimast.KindVariableStatement {
601
- what = "variable"
602
- }
603
- ctx.Report(node, "Move "+what+" declaration to the function scope.")
604
- }
605
-
606
454
  // noObjCalls: `Math()`, `JSON()` — these globals are objects, not
607
455
  // callables. ESLint catches a small list.
608
456
  type noObjCalls struct{}
@@ -656,14 +504,9 @@ func init() {
656
504
  Register(noEmptyCharacterClass{})
657
505
  Register(noMisleadingCharacterClass{})
658
506
  Register(noLossOfPrecision{})
659
- Register(noClassAssign{})
660
- Register(noFuncAssign{})
661
507
  Register(noPrototypeBuiltins{})
662
508
  Register(noAsyncPromiseExecutor{})
663
- Register(noPromiseExecutorReturn{})
664
509
  Register(noControlRegex{})
665
510
  Register(noIrregularWhitespace{})
666
- Register(noFallthrough{})
667
- Register(noInnerDeclarations{})
668
511
  Register(noObjCalls{})
669
512
  }