@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.
- package/README.md +95 -43
- package/go.mod +5 -4
- package/internal/rulecode/rulecode.go +88 -0
- package/lib/index.d.ts +10 -1
- package/lib/index.js +45 -17
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +3 -0
- package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
- package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
- package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
- package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
- package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
- package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
- package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
- package/lib/structures/rules/index.d.ts +3 -0
- package/lib/structures/rules/index.js +3 -0
- package/lib/structures/rules/index.js.map +1 -1
- package/linthost/ast_helpers.go +521 -23
- package/linthost/ast_selector_matcher.go +1628 -0
- package/linthost/ast_selector_parser.go +628 -0
- package/linthost/comment_scan.go +154 -0
- package/linthost/compile.go +129 -79
- package/linthost/config.go +352 -88
- package/linthost/config_format.go +15 -1
- package/linthost/contrib_adapter.go +47 -8
- package/linthost/declaration_rules.go +12 -11
- package/linthost/directives.go +9 -67
- package/linthost/dispatch.go +37 -3
- package/linthost/engine.go +368 -53
- package/linthost/escape_string.go +77 -0
- package/linthost/fix.go +133 -23
- package/linthost/flags_gen.go +18 -17
- package/linthost/format.go +73 -167
- package/linthost/format_editor_settings.go +134 -26
- package/linthost/grapheme_tables_gen.go +1920 -0
- package/linthost/host.go +160 -61
- package/linthost/literal_escape_scan.go +117 -0
- package/linthost/lsp.go +545 -98
- package/linthost/numeric_literal_precision.go +228 -0
- package/linthost/polyfill_browserslist.go +1439 -0
- package/linthost/polyfill_browserslist_config.go +603 -0
- package/linthost/polyfill_corejs.go +750 -0
- package/linthost/polyfill_data.go +113 -0
- package/linthost/polyfill_data_gen.json +39868 -0
- package/linthost/print_nodes_array.go +7 -1
- package/linthost/print_nodes_object.go +7 -1
- package/linthost/project_engine.go +151 -0
- package/linthost/project_rules.go +78 -0
- package/linthost/regex_clean.go +191 -0
- package/linthost/regex_tree.go +1544 -0
- package/linthost/regex_tree_optimizer.go +1685 -0
- package/linthost/regexp_cache.go +44 -0
- package/linthost/rule_codes.go +105 -0
- package/linthost/rule_codes.json +745 -0
- package/linthost/rules_ban_ts_comment.go +312 -0
- package/linthost/rules_boundaries.go +128 -43
- package/linthost/rules_boundaries_dependencies.go +1013 -0
- package/linthost/rules_core_extra.go +62 -92
- package/linthost/rules_cypress.go +1 -1
- package/linthost/rules_default_case.go +87 -11
- package/linthost/rules_dupes.go +38 -13
- package/linthost/rules_empty.go +188 -20
- package/linthost/rules_escape.go +62 -48
- package/linthost/rules_format_arrow_parens.go +19 -5
- package/linthost/rules_format_bracket_spacing.go +1 -1
- package/linthost/rules_format_clause_join.go +1 -1
- package/linthost/rules_format_declaration_header.go +31 -19
- package/linthost/rules_format_indent.go +1 -1
- package/linthost/rules_format_jsdoc.go +1 -1
- package/linthost/rules_format_orphan_semi.go +1 -1
- package/linthost/rules_format_parameter_properties.go +17 -7
- package/linthost/rules_format_print_width.go +1 -1
- package/linthost/rules_format_quote_props.go +1 -1
- package/linthost/rules_format_quotes.go +1 -1
- package/linthost/rules_format_semi.go +106 -79
- package/linthost/rules_format_sort_imports.go +79 -45
- package/linthost/rules_format_statement_split.go +1 -1
- package/linthost/rules_format_ternary_nullish_parens.go +1 -1
- package/linthost/rules_format_trailing_comma.go +64 -1
- package/linthost/rules_format_whitespace.go +1 -1
- package/linthost/rules_functional.go +21 -22
- package/linthost/rules_gap.go +80 -3
- package/linthost/rules_grouped_accessor_pairs.go +84 -25
- package/linthost/rules_guard_for_in.go +64 -147
- package/linthost/rules_jsx_a11y.go +232 -22
- package/linthost/rules_logic.go +84 -38
- package/linthost/rules_nextjs.go +105 -16
- package/linthost/rules_no_class_assign.go +70 -0
- package/linthost/rules_no_duplicate_imports.go +266 -0
- package/linthost/rules_no_else_return.go +156 -38
- package/linthost/rules_no_extend_native.go +113 -17
- package/linthost/rules_no_fallthrough.go +1013 -0
- package/linthost/rules_no_func_assign.go +70 -0
- package/linthost/rules_no_import_assign.go +278 -86
- package/linthost/rules_no_inner_declarations.go +186 -0
- package/linthost/rules_no_loop_func.go +438 -53
- package/linthost/rules_no_mixed_operators.go +208 -52
- package/linthost/rules_no_param_reassign.go +376 -61
- package/linthost/rules_no_promise_executor_return.go +156 -0
- package/linthost/rules_no_restricted_imports.go +1013 -57
- package/linthost/rules_no_restricted_syntax.go +190 -26
- package/linthost/rules_no_useless_constructor.go +6 -36
- package/linthost/rules_problems.go +154 -311
- package/linthost/rules_promise.go +2578 -109
- package/linthost/rules_react_extras.go +71 -45
- package/linthost/rules_react_hooks.go +18 -2
- package/linthost/rules_react_perf.go +1 -0
- package/linthost/rules_react_refresh.go +1 -1
- package/linthost/rules_regexp.go +230 -14
- package/linthost/rules_security.go +45 -19
- package/linthost/rules_self.go +21 -1
- package/linthost/rules_solid.go +13 -7
- package/linthost/rules_storybook.go +1 -1
- package/linthost/rules_suggestions.go +490 -84
- package/linthost/rules_testing_library.go +12 -1
- package/linthost/rules_ts.go +227 -54
- package/linthost/rules_ts_async.go +0 -131
- package/linthost/rules_ts_no_base_to_string.go +3 -4
- package/linthost/rules_ts_no_misused_promises.go +817 -0
- package/linthost/rules_ts_no_restricted_types.go +320 -34
- package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
- package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
- package/linthost/rules_ts_require_array_sort_compare.go +5 -5
- package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
- package/linthost/rules_unicorn_better_regex.go +224 -16
- package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
- package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
- package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
- package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
- package/linthost/rules_unicorn_escape_case.go +30 -19
- package/linthost/rules_unicorn_filename_case.go +763 -16
- package/linthost/rules_unicorn_import_style.go +783 -17
- package/linthost/rules_unicorn_isolated_functions.go +934 -14
- package/linthost/rules_unicorn_no_hex_escape.go +26 -11
- package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
- package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
- package/linthost/rules_unicorn_no_unused_properties.go +756 -19
- package/linthost/rules_unicorn_number_literal_case.go +58 -39
- package/linthost/rules_unicorn_prefer_at.go +7 -3
- package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
- package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
- package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
- package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
- package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
- package/linthost/rules_unicorn_string_content.go +611 -15
- package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
- package/linthost/rules_unicorn_template_indent.go +923 -18
- package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
- package/linthost/rules_unicorn_throw_new_error.go +218 -26
- package/linthost/rules_var.go +829 -142
- package/linthost/string_length.go +222 -0
- package/package.json +4 -3
- package/rule/project.go +145 -0
- package/rule/rule.go +58 -1
- package/src/index.ts +58 -17
- package/src/structures/ITtscLintConfig.ts +3 -0
- package/src/structures/TtscLintRuleSetting.ts +3 -2
- package/src/structures/format/ITtscLintFormat.ts +3 -2
- package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
- package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
- package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
- package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
- package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
- package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
- package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
- package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
- package/src/structures/rules/ITtscLintRules.ts +7 -2
- package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
- package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
- package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
- package/src/structures/rules/index.ts +3 -0
|
@@ -1,23 +1,34 @@
|
|
|
1
|
-
// unicorn/number-literal-case:
|
|
2
|
-
//
|
|
3
|
-
// (`0x
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// unicorn/number-literal-case: numeric and BigInt literals have a
|
|
2
|
+
// conventional canonical spelling — every 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
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// prefix
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
//
|
|
50
|
-
// raw source text
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
|
24
|
+
import (
|
|
25
|
+
"bytes"
|
|
26
|
+
"encoding/json"
|
|
27
|
+
"fmt"
|
|
28
|
+
"strings"
|
|
17
29
|
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
72
|
+
isSafeGlobal, tracked := unicornPreferNumberPropertiesGlobals[name]
|
|
73
|
+
if !tracked {
|
|
38
74
|
return
|
|
39
75
|
}
|
|
40
|
-
|
|
41
|
-
if
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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.
|
|
74
|
-
shimast.
|
|
229
|
+
shimast.KindEnumDeclaration,
|
|
230
|
+
shimast.KindModuleDeclaration,
|
|
75
231
|
shimast.KindImportClause,
|
|
76
|
-
shimast.
|
|
77
|
-
shimast.
|
|
78
|
-
shimast.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
416
|
+
return options, nil
|
|
84
417
|
}
|
|
85
418
|
|
|
86
419
|
func init() {
|