@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
@@ -1,23 +1,34 @@
1
- // unicorn/number-literal-case: hex / binary / octal numeric literals
2
- // have a conventional canonical formthe radix prefix is lowercase
3
- // (`0x`, `0b`, `0o`) while hex digits are uppercase (`0xFF`). Mixed-case
4
- // alternatives (`0Xff`, `0xff`, `0xFf`, `0XFF`) read inconsistently and
5
- // the rule normalizes the codebase by reporting every literal that
6
- // isn't already canonical.
1
+ // unicorn/number-literal-case: numeric and BigInt literals have a
2
+ // conventional canonical spellingevery letter that carries no value is
3
+ // lowercase (the radix prefix `0x` / `0b` / `0o`, the exponent `e`, the
4
+ // BigInt suffix `n`) except the hex digits, which are uppercase (`0xFF`).
5
+ // Mixed-case alternatives (`0Xff`, `0xfF`, `1E10`, `2E-5`) read
6
+ // inconsistently, so the rule reports every literal whose raw source text
7
+ // differs from that spelling and autofixes it in place.
7
8
  //
8
- // AST-only: visit `KindNumericLiteral` and `KindBigIntLiteral`, read
9
- // the raw source text via `nodeText` so the prefix and digit casing
10
- // survive the parser's normalization, and report when the prefix letter
11
- // is uppercase OR any hex digit is lowercase. Decimal literals lack a
12
- // prefix and never fire.
9
+ // AST-only: visit `KindNumericLiteral` and `KindBigIntLiteral`, read the
10
+ // raw source text (the parser's normalized `.Text` has already dropped the
11
+ // casing), and compare it against the spelling upstream's fixer produces —
12
+ // lowercase the whole literal, then uppercase whatever follows a `0x`
13
+ // prefix. The BigInt `n` is held out of that pass, which is what stops
14
+ // `0xFFn` from becoming `0xFFN`. Uppercasing the hex digits *after* the
15
+ // lowercase pass is also what keeps `E` correct in both worlds: inside a
16
+ // hex literal `E` is a digit and stays uppercase (`0xFFE10`), and a hex
17
+ // literal has no exponent, while a decimal literal has no letter other
18
+ // than its exponent `e`.
19
+ //
20
+ // Only letter case changes — digits, `.`, `_`, and the exponent sign are
21
+ // copied through — so the fix never alters the literal's value.
13
22
  //
14
23
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/number-literal-case.md
15
24
  package linthost
16
25
 
17
26
  import (
27
+ "fmt"
18
28
  "strings"
19
29
 
20
30
  shimast "github.com/microsoft/typescript-go/shim/ast"
31
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
21
32
  )
22
33
 
23
34
  type unicornNumberLiteralCase struct{}
@@ -27,41 +38,49 @@ func (unicornNumberLiteralCase) Visits() []shimast.Kind {
27
38
  return []shimast.Kind{shimast.KindNumericLiteral, shimast.KindBigIntLiteral}
28
39
  }
29
40
  func (unicornNumberLiteralCase) Check(ctx *Context, node *shimast.Node) {
30
- source := strings.TrimSpace(nodeText(ctx.File, node))
31
- if len(source) < 2 || source[0] != '0' {
41
+ if ctx == nil || ctx.File == nil || node == nil {
42
+ return
43
+ }
44
+ source := ctx.File.Text()
45
+ start := shimscanner.SkipTrivia(source, node.Pos())
46
+ end := node.End()
47
+ if start < 0 || end > len(source) || start >= end {
32
48
  return
33
49
  }
34
- prefix := source[1]
35
- switch prefix {
36
- case 'x', 'X':
37
- if unicornNumberLiteralCaseHexNeedsFix(source, prefix) {
38
- ctx.Report(node, "Number literals should use a lowercase prefix and uppercase digits — e.g. `0xFF` instead of `0xff`.")
39
- }
40
- case 'b', 'B', 'o', 'O':
41
- // Binary / octal digits are 0-1 / 0-7; only the prefix letter
42
- // can be in the wrong case.
43
- if prefix == 'B' || prefix == 'O' {
44
- ctx.Report(node, "Number literals should use a lowercase prefix and uppercase digits — e.g. `0xFF` instead of `0xff`.")
45
- }
50
+ original := source[start:end]
51
+ canonical := unicornNumberLiteralCaseCanonical(original)
52
+ if canonical == original {
53
+ return
46
54
  }
55
+ ctx.ReportFix(
56
+ node,
57
+ fmt.Sprintf("Number literal `%s` should be written as `%s`.", original, canonical),
58
+ TextEdit{Pos: start, End: end, Text: canonical},
59
+ )
47
60
  }
48
61
 
49
- // unicornNumberLiteralCaseHexNeedsFix reports whether the hex literal's
50
- // raw source text deviates from `0x` + uppercase digits. The trailing
51
- // `n` of a hex bigint literal (`0xFFn`) is allowed.
52
- func unicornNumberLiteralCaseHexNeedsFix(source string, prefix byte) bool {
53
- if prefix != 'x' {
54
- return true
62
+ // unicornNumberLiteralCaseCanonical returns the canonical spelling of one
63
+ // literal's raw source text. A BigInt literal is canonicalized by its
64
+ // number part alone so the mandatory lowercase `n` suffix survives the hex
65
+ // digits' uppercase pass.
66
+ func unicornNumberLiteralCaseCanonical(source string) string {
67
+ if strings.HasSuffix(source, "n") {
68
+ return unicornNumberLiteralCaseCanonicalNumber(strings.TrimSuffix(source, "n")) + "n"
55
69
  }
56
- digits := source[2:]
57
- digits = strings.TrimSuffix(digits, "n")
58
- for i := 0; i < len(digits); i++ {
59
- ch := digits[i]
60
- if ch >= 'a' && ch <= 'f' {
61
- return true
62
- }
70
+ return unicornNumberLiteralCaseCanonicalNumber(source)
71
+ }
72
+
73
+ // unicornNumberLiteralCaseCanonicalNumber canonicalizes a literal's number
74
+ // part: everything lowercases (radix prefix, exponent), then a hex
75
+ // literal's digits are uppercased. Decimal, binary, octal, and legacy octal
76
+ // literals hold no case-bearing digit, so the lowercase pass is all they
77
+ // need.
78
+ func unicornNumberLiteralCaseCanonicalNumber(number string) string {
79
+ lowered := strings.ToLower(number)
80
+ if strings.HasPrefix(lowered, "0x") {
81
+ return "0x" + strings.ToUpper(lowered[2:])
63
82
  }
64
- return false
83
+ return lowered
65
84
  }
66
85
 
67
86
  func init() {
@@ -7,9 +7,10 @@
7
7
  // AST-only: visit each `ElementAccessExpression`. The index expression
8
8
  // must be a `BinaryExpression` whose operator is `-`, whose left side is
9
9
  // `PropertyAccess(_, length)`, and whose right side is a positive
10
- // numeric literal. The receiver of the `.length` access does not need
11
- // to match the element-access receiver textually; mismatches are
12
- // uncommon enough that the AST-only check stays useful in practice.
10
+ // numeric literal. The `.length` receiver must be structurally equivalent to
11
+ // the indexed receiver; otherwise recommending `.at(-N)` would change which
12
+ // index is selected. The shared reference comparator ignores runtime-neutral
13
+ // TypeScript wrappers while rejecting effectful repeated expressions.
13
14
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-at.md
14
15
  package linthost
15
16
 
@@ -43,6 +44,9 @@ func (unicornPreferAt) Check(ctx *Context, node *shimast.Node) {
43
44
  if prop == nil || identifierText(prop.Name()) != "length" {
44
45
  return
45
46
  }
47
+ if !sameReferenceExpression(access.Expression, prop.Expression) {
48
+ return
49
+ }
46
50
  right := stripParens(bin.Right)
47
51
  if right == nil || right.Kind != shimast.KindNumericLiteral {
48
52
  return
@@ -5,82 +5,415 @@
5
5
  // `false`, while the global `isNaN("abc")` is `true`) and are more
6
6
  // discoverable. The rule pushes authors to the namespaced spellings.
7
7
  //
8
- // AST-only: visit each `Identifier`. The text must be one of the six
9
- // shadowable names. The identifier must be in a value-expression
10
- // position not the name part of a property access (`obj.isNaN`),
11
- // not the key of a property assignment, and not a binding name. Reports
12
- // on the identifier.
8
+ // Faithful port of eslint-plugin-unicorn `rules/prefer-number-properties.js`.
9
+ // The upstream rule tracks *references to the global binding* through scope
10
+ // analysis; this port reproduces that with the TypeScript-Go checker so a
11
+ // locally shadowed `parseInt` / `isNaN` (a different value) is never flagged,
12
+ // while genuine value-position reads — including an object initializer
13
+ // (`{normalize: parseFloat}`) and a shorthand (`{parseInt}`) — are. Options
14
+ // `checkInfinity` and `checkNaN` both default to `false`, so `Infinity` and
15
+ // `NaN` are only checked when the caller opts in. `parseInt(x)` and
16
+ // `parseInt(x, 10)` already yield what `Number.parseInt` would and are left
17
+ // alone; assignment / update / destructuring / `delete` targets are not reads
18
+ // and are excluded. Diagnostics interpolate the real spellings
19
+ // (`Number.<property>` over `<description>`) and carry an autofix, or a
20
+ // suggestion where the rewrite could change runtime behavior.
13
21
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-number-properties.md
14
22
  package linthost
15
23
 
16
- import shimast "github.com/microsoft/typescript-go/shim/ast"
24
+ import (
25
+ "bytes"
26
+ "encoding/json"
27
+ "fmt"
28
+ "strings"
17
29
 
18
- var unicornPreferNumberPropertiesNames = map[string]struct{}{
19
- "isNaN": {},
20
- "isFinite": {},
21
- "parseFloat": {},
22
- "parseInt": {},
23
- "NaN": {},
24
- "Infinity": {},
30
+ shimast "github.com/microsoft/typescript-go/shim/ast"
31
+ shimchecker "github.com/microsoft/typescript-go/shim/checker"
32
+ )
33
+
34
+ // unicornPreferNumberPropertiesGlobals maps each shadowable global name to
35
+ // whether replacing it with `Number.<name>` is unconditionally safe (upstream's
36
+ // `globalObjects`). `parseInt`, `parseFloat`, `NaN`, and `Infinity` are pure
37
+ // aliases, so the fix applies automatically; `isNaN` and `isFinite` differ from
38
+ // `Number.isNaN` / `Number.isFinite` on non-number arguments, so they autofix
39
+ // only when the sole argument is provably a number and are otherwise offered as
40
+ // an opt-in suggestion.
41
+ var unicornPreferNumberPropertiesGlobals = map[string]bool{
42
+ "parseInt": true,
43
+ "parseFloat": true,
44
+ "NaN": true,
45
+ "Infinity": true,
46
+ "isNaN": false,
47
+ "isFinite": false,
25
48
  }
26
49
 
27
- type unicornPreferNumberProperties struct{}
50
+ type unicornPreferNumberProperties struct{ optionsRule }
51
+
52
+ type unicornPreferNumberPropertiesOptions struct {
53
+ checkInfinity bool
54
+ checkNaN bool
55
+ }
28
56
 
29
57
  func (unicornPreferNumberProperties) Name() string {
30
58
  return "unicorn/prefer-number-properties"
31
59
  }
60
+ func (unicornPreferNumberProperties) NeedsTypeChecker() bool { return true }
32
61
  func (unicornPreferNumberProperties) Visits() []shimast.Kind {
33
62
  return []shimast.Kind{shimast.KindIdentifier}
34
63
  }
64
+
65
+ func (unicornPreferNumberProperties) ValidateOptions(raw json.RawMessage) error {
66
+ _, err := decodeUnicornPreferNumberPropertiesOptions(raw)
67
+ return err
68
+ }
69
+
35
70
  func (unicornPreferNumberProperties) Check(ctx *Context, node *shimast.Node) {
36
71
  name := identifierText(node)
37
- if _, ok := unicornPreferNumberPropertiesNames[name]; !ok {
72
+ isSafeGlobal, tracked := unicornPreferNumberPropertiesGlobals[name]
73
+ if !tracked {
38
74
  return
39
75
  }
40
- parent := node.Parent
41
- if parent == nil {
76
+ options, err := decodeUnicornPreferNumberPropertiesOptions(ctx.Options)
77
+ if err != nil {
78
+ // Malformed options are already surfaced as a configuration error at
79
+ // engine construction; stay side-effect-free here.
42
80
  return
43
81
  }
44
- switch parent.Kind {
45
- case shimast.KindPropertyAccessExpression:
46
- // `obj.isNaN` the identifier is the property name, not a
47
- // reference to the global. `Number.isNaN(...)` itself also
48
- // reaches here and must not be flagged.
49
- access := parent.AsPropertyAccessExpression()
50
- if access != nil && access.Name() == node {
82
+ // `Infinity` and `NaN` leave the tracked set unless the caller opts in;
83
+ // both options default to false.
84
+ if name == "Infinity" && !options.checkInfinity {
85
+ return
86
+ }
87
+ if name == "NaN" && !options.checkNaN {
88
+ return
89
+ }
90
+ // Only value-expression reads are candidates. Declaration names, property
91
+ // keys, member-access right sides, type positions, and import/export
92
+ // specifiers are name slots, not references to the global; value positions
93
+ // such as `{key: parseFloat}` and the shorthand `{parseInt}` are kept.
94
+ if !isUnicornValuePositionIdentifier(node) {
95
+ return
96
+ }
97
+ // Assignment / update / destructuring / delete targets are writes, not reads.
98
+ if unicornPreferNumberPropertiesIsLeftHandSide(node) {
99
+ return
100
+ }
101
+ if name == "Infinity" && unicornPreferNumberPropertiesIsDeletedNegativeInfinity(node) {
102
+ return
103
+ }
104
+ // `parseInt(x)` (no radix) and `parseInt(x, 10)` (explicit base 10) already
105
+ // produce what `Number.parseInt` would, so upstream skips them.
106
+ if name == "parseInt" && unicornPreferNumberPropertiesIsBase10OrNoRadixCall(ctx, node) {
107
+ return
108
+ }
109
+ // Bind the identifier: a locally shadowed name is a different value.
110
+ if !unicornPreferNumberPropertiesResolvesGlobal(ctx, node, name) {
111
+ return
112
+ }
113
+ unicornPreferNumberPropertiesReport(ctx, node, name, isSafeGlobal)
114
+ }
115
+
116
+ // unicornPreferNumberPropertiesReport emits the diagnostic with the substituted
117
+ // message plus an autofix or a suggestion, mirroring upstream's
118
+ // getPropertyProblem. `Infinity` maps to `POSITIVE_INFINITY`, or, when negated,
119
+ // to a `NEGATIVE_INFINITY` fix anchored on the whole `-Infinity` unary.
120
+ func unicornPreferNumberPropertiesReport(ctx *Context, node *shimast.Node, name string, isSafeGlobal bool) {
121
+ property := name
122
+ description := name
123
+
124
+ if name == "Infinity" {
125
+ if unicornPreferNumberPropertiesIsNegative(node) {
126
+ unary := node.Parent
127
+ message := unicornPreferNumberPropertiesMessage("NEGATIVE_INFINITY", "-Infinity")
128
+ if edits, ok := unicornPreferNumberPropertiesNegativeInfinityFix(ctx, unary); ok {
129
+ ctx.ReportFix(unary, message, edits...)
130
+ return
131
+ }
132
+ ctx.Report(unary, message)
51
133
  return
52
134
  }
53
- case shimast.KindQualifiedName:
54
- // Type-position `A.B` — same rationale as PropertyAccess.
135
+ property = "POSITIVE_INFINITY"
136
+ }
137
+
138
+ message := unicornPreferNumberPropertiesMessage(property, description)
139
+ edits, ok := unicornPreferNumberPropertiesReplaceFix(ctx, node, property)
140
+ if !ok {
141
+ ctx.Report(node, message)
55
142
  return
56
- case shimast.KindPropertyAssignment,
57
- shimast.KindShorthandPropertyAssignment,
58
- shimast.KindMethodDeclaration,
59
- shimast.KindPropertyDeclaration,
60
- shimast.KindEnumMember,
61
- shimast.KindPropertySignature,
62
- shimast.KindMethodSignature,
63
- shimast.KindGetAccessor,
64
- shimast.KindSetAccessor:
65
- // Member name slots on object/class shapes.
143
+ }
144
+ if isSafeGlobal || unicornPreferNumberPropertiesIsCallWithNumberArgument(ctx, node) {
145
+ ctx.ReportFix(node, message, edits...)
66
146
  return
67
- case shimast.KindParameter,
68
- shimast.KindVariableDeclaration,
147
+ }
148
+ title := fmt.Sprintf("Replace `%s` with `Number.%s`.", description, property)
149
+ ctx.ReportSuggestion(node, message, title, edits...)
150
+ }
151
+
152
+ func unicornPreferNumberPropertiesMessage(property, description string) string {
153
+ return fmt.Sprintf("Prefer `Number.%s` over `%s`.", property, description)
154
+ }
155
+
156
+ // unicornPreferNumberPropertiesReplaceFix builds the edit that rewrites the
157
+ // identifier to `Number.<property>`. A shorthand property value must expand to
158
+ // `name: Number.<property>` because `{Number.parseInt}` is not valid syntax
159
+ // (upstream's replaceReferenceIdentifier).
160
+ func unicornPreferNumberPropertiesReplaceFix(ctx *Context, node *shimast.Node, property string) ([]TextEdit, bool) {
161
+ pos, end := tokenRange(ctx.File, node)
162
+ if pos < 0 {
163
+ return nil, false
164
+ }
165
+ replacement := "Number." + property
166
+ if parent := node.Parent; parent != nil && parent.Kind == shimast.KindShorthandPropertyAssignment {
167
+ if shorthand := parent.AsShorthandPropertyAssignment(); shorthand != nil && shorthand.Name() == node {
168
+ replacement = identifierText(node) + ": " + replacement
169
+ }
170
+ }
171
+ return []TextEdit{{Pos: pos, End: end, Text: replacement}}, true
172
+ }
173
+
174
+ // unicornPreferNumberPropertiesNegativeInfinityFix rewrites a `-Infinity` unary
175
+ // to `Number.NEGATIVE_INFINITY`, inserting a leading space when the preceding
176
+ // byte is an identifier part so a keyword operand (`return-Infinity`) does not
177
+ // merge with the replacement (upstream's fixSpaceAroundKeyword).
178
+ func unicornPreferNumberPropertiesNegativeInfinityFix(ctx *Context, unary *shimast.Node) ([]TextEdit, bool) {
179
+ pos, end := tokenRange(ctx.File, unary)
180
+ if pos < 0 {
181
+ return nil, false
182
+ }
183
+ replacement := "Number.NEGATIVE_INFINITY"
184
+ if src := ctx.File.Text(); pos > 0 && pos <= len(src) && isIdentifierPart(src[pos-1]) {
185
+ replacement = " " + replacement
186
+ }
187
+ return []TextEdit{{Pos: pos, End: end, Text: replacement}}, true
188
+ }
189
+
190
+ // unicornPreferNumberPropertiesResolvesGlobal reports whether the identifier is
191
+ // a reference to the program's global binding of `name`, not a local shadow.
192
+ // The same-file value-declaration guard covers script files whose top-level
193
+ // binding merges into the checker global table; module bindings already resolve
194
+ // to a distinct symbol.
195
+ func unicornPreferNumberPropertiesResolvesGlobal(ctx *Context, node *shimast.Node, name string) bool {
196
+ if ctx == nil || ctx.Checker == nil || ctx.File == nil {
197
+ return false
198
+ }
199
+ resolved := valueSymbolAtIdentifier(ctx, node)
200
+ global := ctx.Checker.GetGlobalSymbol(name, shimast.SymbolFlagsValue, nil)
201
+ if resolved == nil || global == nil {
202
+ return false
203
+ }
204
+ resolved = ctx.Checker.GetMergedSymbol(resolved)
205
+ global = ctx.Checker.GetMergedSymbol(global)
206
+ if resolved != global {
207
+ return false
208
+ }
209
+ for _, declaration := range resolved.Declarations {
210
+ if declaration != nil &&
211
+ shimast.GetSourceFileOfNode(declaration) == ctx.File &&
212
+ unicornPreferNumberPropertiesDeclarationIntroducesValue(declaration) {
213
+ return false
214
+ }
215
+ }
216
+ return true
217
+ }
218
+
219
+ // unicornPreferNumberPropertiesDeclarationIntroducesValue distinguishes a
220
+ // same-file value binding that shadows the built-in from a type-only
221
+ // declaration that legitimately merges with the global's interface.
222
+ func unicornPreferNumberPropertiesDeclarationIntroducesValue(declaration *shimast.Node) bool {
223
+ switch declaration.Kind {
224
+ case shimast.KindVariableDeclaration,
69
225
  shimast.KindBindingElement,
226
+ shimast.KindParameter,
70
227
  shimast.KindFunctionDeclaration,
71
- shimast.KindFunctionExpression,
72
228
  shimast.KindClassDeclaration,
73
- shimast.KindClassExpression,
74
- shimast.KindImportSpecifier,
229
+ shimast.KindEnumDeclaration,
230
+ shimast.KindModuleDeclaration,
75
231
  shimast.KindImportClause,
76
- shimast.KindNamespaceImport,
77
- shimast.KindExportSpecifier,
78
- shimast.KindTypeParameter,
79
- shimast.KindLabeledStatement:
80
- // Binding / declaration names, not references to the global.
81
- return
232
+ shimast.KindImportSpecifier,
233
+ shimast.KindImportEqualsDeclaration,
234
+ shimast.KindNamespaceImport:
235
+ return true
236
+ }
237
+ return false
238
+ }
239
+
240
+ // unicornPreferNumberPropertiesIsLeftHandSide reports whether the identifier is
241
+ // written rather than read: an assignment/compound-assignment target, an update
242
+ // operand, a `delete` argument, or a destructuring-assignment target (upstream's
243
+ // isLeftHandSide).
244
+ func unicornPreferNumberPropertiesIsLeftHandSide(node *shimast.Node) bool {
245
+ parent := node.Parent
246
+ if parent == nil {
247
+ return false
248
+ }
249
+ switch parent.Kind {
250
+ case shimast.KindBinaryExpression:
251
+ binary := parent.AsBinaryExpression()
252
+ if binary != nil && binary.Left == node && binary.OperatorToken != nil &&
253
+ isAssignmentOperator(binary.OperatorToken.Kind) {
254
+ return true
255
+ }
256
+ case shimast.KindPrefixUnaryExpression:
257
+ unary := parent.AsPrefixUnaryExpression()
258
+ if unary != nil && unary.Operand == node &&
259
+ (unary.Operator == shimast.KindPlusPlusToken || unary.Operator == shimast.KindMinusMinusToken) {
260
+ return true
261
+ }
262
+ case shimast.KindPostfixUnaryExpression:
263
+ unary := parent.AsPostfixUnaryExpression()
264
+ if unary != nil && unary.Operand == node &&
265
+ (unary.Operator == shimast.KindPlusPlusToken || unary.Operator == shimast.KindMinusMinusToken) {
266
+ return true
267
+ }
268
+ case shimast.KindDeleteExpression:
269
+ del := parent.AsDeleteExpression()
270
+ if del != nil && del.Expression == node {
271
+ return true
272
+ }
273
+ }
274
+ return isDestructuringAssignmentTarget(node)
275
+ }
276
+
277
+ // unicornPreferNumberPropertiesIsNegative reports whether the identifier is the
278
+ // operand of a unary minus (`-Infinity`).
279
+ func unicornPreferNumberPropertiesIsNegative(node *shimast.Node) bool {
280
+ parent := node.Parent
281
+ if parent == nil || parent.Kind != shimast.KindPrefixUnaryExpression {
282
+ return false
283
+ }
284
+ unary := parent.AsPrefixUnaryExpression()
285
+ return unary != nil && unary.Operator == shimast.KindMinusToken && unary.Operand == node
286
+ }
287
+
288
+ // unicornPreferNumberPropertiesIsDeletedNegativeInfinity reports whether the
289
+ // identifier is the `Infinity` of a `delete -Infinity` no-op, which upstream
290
+ // leaves untouched.
291
+ func unicornPreferNumberPropertiesIsDeletedNegativeInfinity(node *shimast.Node) bool {
292
+ if !unicornPreferNumberPropertiesIsNegative(node) {
293
+ return false
294
+ }
295
+ unary := node.Parent
296
+ if unary == nil || unary.Parent == nil || unary.Parent.Kind != shimast.KindDeleteExpression {
297
+ return false
298
+ }
299
+ del := unary.Parent.AsDeleteExpression()
300
+ return del != nil && del.Expression == unary
301
+ }
302
+
303
+ // unicornPreferNumberPropertiesIsBase10OrNoRadixCall reports whether the
304
+ // identifier is a `parseInt` callee with no radix or an explicit base-10 radix.
305
+ // The radix is read statically through the checker so a `const R = 10;
306
+ // parseInt(x, R)` folds the same way upstream's getStaticValue does.
307
+ func unicornPreferNumberPropertiesIsBase10OrNoRadixCall(ctx *Context, node *shimast.Node) bool {
308
+ parent := node.Parent
309
+ if parent == nil || parent.Kind != shimast.KindCallExpression {
310
+ return false
311
+ }
312
+ call := parent.AsCallExpression()
313
+ if call == nil || call.Expression != node || call.Arguments == nil {
314
+ return false
315
+ }
316
+ args := call.Arguments.Nodes
317
+ if len(args) < 2 {
318
+ return true
319
+ }
320
+ radix := args[1]
321
+ if radix == nil || radix.Kind == shimast.KindSpreadElement {
322
+ return false
323
+ }
324
+ if ctx.Checker == nil {
325
+ return false
326
+ }
327
+ t := ctx.Checker.GetTypeAtLocation(radix)
328
+ if t == nil || t.Flags()&shimchecker.TypeFlagsNumberLiteral == 0 {
329
+ return false
330
+ }
331
+ return strings.TrimSpace(ctx.Checker.TypeToString(t)) == "10"
332
+ }
333
+
334
+ // unicornPreferNumberPropertiesIsCallWithNumberArgument reports whether the
335
+ // identifier is a callee invoked with exactly one argument whose type is
336
+ // provably a number, the condition under which `Number.isNaN` / `Number.isFinite`
337
+ // preserve `isNaN` / `isFinite` behavior and the rewrite is safe to autofix.
338
+ func unicornPreferNumberPropertiesIsCallWithNumberArgument(ctx *Context, node *shimast.Node) bool {
339
+ parent := node.Parent
340
+ if parent == nil || parent.Kind != shimast.KindCallExpression || ctx.Checker == nil {
341
+ return false
342
+ }
343
+ call := parent.AsCallExpression()
344
+ if call == nil || call.Expression != node || call.Arguments == nil || len(call.Arguments.Nodes) != 1 {
345
+ return false
346
+ }
347
+ argument := call.Arguments.Nodes[0]
348
+ if argument == nil || argument.Kind == shimast.KindSpreadElement {
349
+ return false
350
+ }
351
+ return unicornPreferNumberPropertiesTypeIsNumber(ctx.Checker.GetTypeAtLocation(argument))
352
+ }
353
+
354
+ // unicornPreferNumberPropertiesTypeIsNumber reports whether every constituent of
355
+ // `t` is number-like. `any` / `unknown` and any non-number member answer false,
356
+ // keeping the autofix conservative — such calls fall back to a suggestion.
357
+ func unicornPreferNumberPropertiesTypeIsNumber(t *shimchecker.Type) bool {
358
+ if t == nil {
359
+ return false
360
+ }
361
+ flags := t.Flags()
362
+ if flags&shimchecker.TypeFlagsNumberLike != 0 {
363
+ return true
364
+ }
365
+ if flags&shimchecker.TypeFlagsUnion != 0 {
366
+ parts := t.Types()
367
+ if len(parts) == 0 {
368
+ return false
369
+ }
370
+ for _, part := range parts {
371
+ if !unicornPreferNumberPropertiesTypeIsNumber(part) {
372
+ return false
373
+ }
374
+ }
375
+ return true
376
+ }
377
+ return false
378
+ }
379
+
380
+ // decodeUnicornPreferNumberPropertiesOptions enforces the upstream schema: an
381
+ // optional object whose only keys are the boolean `checkInfinity` and `checkNaN`
382
+ // (both default false).
383
+ func decodeUnicornPreferNumberPropertiesOptions(raw json.RawMessage) (unicornPreferNumberPropertiesOptions, error) {
384
+ options := unicornPreferNumberPropertiesOptions{}
385
+ trimmed := bytes.TrimSpace(raw)
386
+ if len(trimmed) == 0 {
387
+ return options, nil
388
+ }
389
+ if trimmed[0] != '{' {
390
+ return options, fmt.Errorf("options must be an object")
391
+ }
392
+ var fields map[string]json.RawMessage
393
+ if err := json.Unmarshal(trimmed, &fields); err != nil || fields == nil {
394
+ if err == nil {
395
+ err = fmt.Errorf("null object")
396
+ }
397
+ return options, fmt.Errorf("options must be an object: %w", err)
398
+ }
399
+ for name := range fields {
400
+ if name != "checkInfinity" && name != "checkNaN" {
401
+ return options, fmt.Errorf("unknown option %q", name)
402
+ }
403
+ }
404
+ if value, exists := fields["checkInfinity"]; exists {
405
+ if bytes.Equal(bytes.TrimSpace(value), []byte("null")) ||
406
+ json.Unmarshal(value, &options.checkInfinity) != nil {
407
+ return options, fmt.Errorf("option %q must be a boolean", "checkInfinity")
408
+ }
409
+ }
410
+ if value, exists := fields["checkNaN"]; exists {
411
+ if bytes.Equal(bytes.TrimSpace(value), []byte("null")) ||
412
+ json.Unmarshal(value, &options.checkNaN) != nil {
413
+ return options, fmt.Errorf("option %q must be a boolean", "checkNaN")
414
+ }
82
415
  }
83
- ctx.Report(node, "Prefer `Number.<X>` over the global `<X>` (e.g. `Number.isNaN`, `Number.parseInt`).")
416
+ return options, nil
84
417
  }
85
418
 
86
419
  func init() {