@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.
- 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 +129 -25
- 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,15 +1,24 @@
|
|
|
1
|
-
// unicorn/text-encoding-identifier-case: the IANA text-encoding labels
|
|
2
|
-
// `UTF-8`, `
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// Picking one canonical form per encoding makes string-equality checks
|
|
6
|
-
// against those echoes stable. The rule asks every encoding-shaped
|
|
7
|
-
// literal to use the canonical lowercase, hyphenated form.
|
|
1
|
+
// unicorn/text-encoding-identifier-case: the IANA/WHATWG text-encoding labels
|
|
2
|
+
// `utf8`, `UTF-8`, `ascii`, etc. all resolve to the same encoding in browsers
|
|
3
|
+
// and Node, but the ecosystem varies on which spelling it echoes back. Picking
|
|
4
|
+
// one canonical form keeps string-equality checks against those echoes stable.
|
|
8
5
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
6
|
+
// Upstream parity (verified against `rules/text-encoding-identifier-case.js`):
|
|
7
|
+
// only `utf-8`/`utf8` and `ascii` are handled — every other label passes
|
|
8
|
+
// through untouched. The canonical form defaults to the dash-less `utf8`; the
|
|
9
|
+
// dashed WHATWG spelling `utf-8` is enforced only when the `withDash` option is
|
|
10
|
+
// set or the literal sits in a context that demands it: `new TextDecoder(...)`,
|
|
11
|
+
// a JSX `<meta charset>`, or a JSX `<form accept-charset>` attribute. `ascii`
|
|
12
|
+
// is always canonical lowercase.
|
|
13
|
+
//
|
|
14
|
+
// Only `fs.readFile()` / `fs.readFileSync()`'s encoding argument is auto-fixed;
|
|
15
|
+
// every other position offers an editor suggestion instead, matching upstream's
|
|
16
|
+
// fix-vs-suggestion split.
|
|
17
|
+
//
|
|
18
|
+
// AST-only: visit `StringLiteral` and `NoSubstitutionTemplateLiteral`. Matching
|
|
19
|
+
// and rewriting both run on the raw text between the delimiters (upstream's
|
|
20
|
+
// `node.raw.slice(1, -1)` / `replaceStringRaw`), so an escaped spelling such as
|
|
21
|
+
// `"utf-8"` is left alone and a fix never disturbs the quotes.
|
|
13
22
|
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/text-encoding-identifier-case.md
|
|
14
23
|
package linthost
|
|
15
24
|
|
|
@@ -17,54 +26,205 @@ import (
|
|
|
17
26
|
"strings"
|
|
18
27
|
|
|
19
28
|
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
29
|
+
shimscanner "github.com/microsoft/typescript-go/shim/scanner"
|
|
20
30
|
)
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
const unicornTextEncodingIdentifierCaseRuleName = "unicorn/text-encoding-identifier-case"
|
|
33
|
+
|
|
34
|
+
type unicornTextEncodingIdentifierCase struct{ optionsRule }
|
|
23
35
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"utf-16be": "utf-16be",
|
|
31
|
-
"ascii": "ascii",
|
|
32
|
-
"latin1": "latin1",
|
|
33
|
-
"iso-8859-1": "iso-8859-1",
|
|
34
|
-
"windows-1252": "windows-1252",
|
|
36
|
+
// unicornTextEncodingIdentifierCaseOptions decodes the single `{withDash}`
|
|
37
|
+
// option slot. `withDash: true` prefers the dashed WHATWG spelling (`utf-8`)
|
|
38
|
+
// everywhere; the default (`false`) prefers `utf8` outside the dash-only
|
|
39
|
+
// contexts.
|
|
40
|
+
type unicornTextEncodingIdentifierCaseOptions struct {
|
|
41
|
+
WithDash bool `json:"withDash"`
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
func (unicornTextEncodingIdentifierCase) Name() string {
|
|
38
|
-
return
|
|
45
|
+
return unicornTextEncodingIdentifierCaseRuleName
|
|
39
46
|
}
|
|
40
47
|
func (unicornTextEncodingIdentifierCase) Visits() []shimast.Kind {
|
|
41
48
|
return []shimast.Kind{shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral}
|
|
42
49
|
}
|
|
43
50
|
func (unicornTextEncodingIdentifierCase) Check(ctx *Context, node *shimast.Node) {
|
|
44
|
-
|
|
45
|
-
if text == "" {
|
|
51
|
+
if ctx == nil || ctx.File == nil || node == nil {
|
|
46
52
|
return
|
|
47
53
|
}
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
// Upstream's getStringLiteralValue rejects a template literal whose parent is
|
|
55
|
+
// a TaggedTemplateExpression (`String.raw`utf8``): the tag owns the raw text.
|
|
56
|
+
if node.Kind == shimast.KindNoSubstitutionTemplateLiteral &&
|
|
57
|
+
node.Parent != nil && node.Parent.Kind == shimast.KindTaggedTemplateExpression {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
source := ctx.File.Text()
|
|
61
|
+
innerStart, innerEnd, ok := unicornTextEncodingInnerRange(source, node)
|
|
50
62
|
if !ok {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
value := source[innerStart:innerEnd]
|
|
66
|
+
if value == "" {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
// Bail before decoding options or walking the parent chain unless the label
|
|
70
|
+
// is one the rule handles: `withDash` never turns an unknown label into a
|
|
71
|
+
// match, so an early reject here is behavior-preserving.
|
|
72
|
+
if unicornTextEncodingReplacement(value, false) == "" {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var options unicornTextEncodingIdentifierCaseOptions
|
|
77
|
+
_ = ctx.DecodeOptions(&options)
|
|
78
|
+
withDash := options.WithDash || unicornTextEncodingShouldEnforceDash(node)
|
|
79
|
+
|
|
80
|
+
replacement := unicornTextEncodingReplacement(value, withDash)
|
|
81
|
+
if replacement == "" || replacement == value {
|
|
82
|
+
return
|
|
63
83
|
}
|
|
64
|
-
|
|
84
|
+
|
|
85
|
+
edit := TextEdit{Pos: innerStart, End: innerEnd, Text: replacement}
|
|
86
|
+
message := "Prefer `" + replacement + "` over `" + value + "`."
|
|
87
|
+
if unicornTextEncodingIsFsReadFileEncoding(node) {
|
|
88
|
+
// `fs.{readFile,readFileSync}` is the one auto-fixed position upstream.
|
|
89
|
+
ctx.ReportFix(node, message, edit)
|
|
65
90
|
return
|
|
66
91
|
}
|
|
67
|
-
|
|
92
|
+
title := "Replace `" + value + "` with `" + replacement + "`."
|
|
93
|
+
ctx.ReportSuggestion(node, message, title, edit)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// unicornTextEncodingReplacement ports upstream's `getReplacement`: only
|
|
97
|
+
// `utf-8`/`utf8` (→ `utf-8` when dashed, else `utf8`) and `ascii` are known;
|
|
98
|
+
// any other label yields "" (no replacement). The comparison is
|
|
99
|
+
// case-insensitive so `UTF-8`, `Utf8`, and `ASCII` all normalize.
|
|
100
|
+
func unicornTextEncodingReplacement(encoding string, withDash bool) string {
|
|
101
|
+
switch strings.ToLower(encoding) {
|
|
102
|
+
case "utf-8", "utf8":
|
|
103
|
+
if withDash {
|
|
104
|
+
return "utf-8"
|
|
105
|
+
}
|
|
106
|
+
return "utf8"
|
|
107
|
+
case "ascii":
|
|
108
|
+
return "ascii"
|
|
109
|
+
}
|
|
110
|
+
return ""
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// unicornTextEncodingShouldEnforceDash reports the contexts where the dashed
|
|
114
|
+
// WHATWG spelling is required regardless of the `withDash` option: the first
|
|
115
|
+
// argument of `new TextDecoder(...)`, a JSX `<meta charset>` attribute value,
|
|
116
|
+
// or a JSX `<form accept-charset>` attribute value.
|
|
117
|
+
func unicornTextEncodingShouldEnforceDash(node *shimast.Node) bool {
|
|
118
|
+
if node == nil || node.Parent == nil {
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
if node.Parent.Kind == shimast.KindNewExpression {
|
|
122
|
+
if newExpr := node.Parent.AsNewExpression(); newExpr != nil &&
|
|
123
|
+
identifierText(newExpr.Expression) == "TextDecoder" &&
|
|
124
|
+
newExpr.Arguments != nil && len(newExpr.Arguments.Nodes) > 0 &&
|
|
125
|
+
newExpr.Arguments.Nodes[0] == node {
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return unicornTextEncodingIsJsxCharsetAttribute(node)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// unicornTextEncodingIsJsxCharsetAttribute matches `<meta charset="...">` and
|
|
133
|
+
// `<form accept-charset="...">` (React's camelCase `acceptCharset` included),
|
|
134
|
+
// where `node` is the attribute's string-literal initializer.
|
|
135
|
+
func unicornTextEncodingIsJsxCharsetAttribute(node *shimast.Node) bool {
|
|
136
|
+
attr := node.Parent
|
|
137
|
+
if attr == nil || attr.Kind != shimast.KindJsxAttribute {
|
|
138
|
+
return false
|
|
139
|
+
}
|
|
140
|
+
jsxAttr := attr.AsJsxAttribute()
|
|
141
|
+
if jsxAttr == nil || jsxAttr.Initializer != node {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
attrs := attr.Parent
|
|
145
|
+
if attrs == nil || attrs.Kind != shimast.KindJsxAttributes {
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
var tagName *shimast.Node
|
|
149
|
+
switch element := attrs.Parent; {
|
|
150
|
+
case element == nil:
|
|
151
|
+
return false
|
|
152
|
+
case element.Kind == shimast.KindJsxOpeningElement:
|
|
153
|
+
if opening := element.AsJsxOpeningElement(); opening != nil {
|
|
154
|
+
tagName = opening.TagName
|
|
155
|
+
}
|
|
156
|
+
case element.Kind == shimast.KindJsxSelfClosingElement:
|
|
157
|
+
if selfClosing := element.AsJsxSelfClosingElement(); selfClosing != nil {
|
|
158
|
+
tagName = selfClosing.TagName
|
|
159
|
+
}
|
|
160
|
+
default:
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
attrName := strings.ToLower(jsxAttrName(jsxAttr.Name()))
|
|
164
|
+
switch strings.ToLower(jsxTagName(tagName)) {
|
|
165
|
+
case "meta":
|
|
166
|
+
return attrName == "charset"
|
|
167
|
+
case "form":
|
|
168
|
+
return attrName == "acceptcharset" || attrName == "accept-charset"
|
|
169
|
+
}
|
|
170
|
+
return false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// unicornTextEncodingIsFsReadFileEncoding matches the encoding argument of a
|
|
174
|
+
// non-optional `obj.readFile(...)` / `obj.readFileSync(...)` call — `node` is
|
|
175
|
+
// the second argument and the first is not a spread. Mirrors upstream's
|
|
176
|
+
// `isMethodCall(..., {optionalCall:false, optionalMember:false})` plus the
|
|
177
|
+
// `arguments[1] === node && arguments[0].type !== 'SpreadElement'` guards.
|
|
178
|
+
func unicornTextEncodingIsFsReadFileEncoding(node *shimast.Node) bool {
|
|
179
|
+
if node == nil || node.Parent == nil || node.Parent.Kind != shimast.KindCallExpression {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
call := node.Parent.AsCallExpression()
|
|
183
|
+
if call == nil || call.QuestionDotToken != nil || call.Expression == nil ||
|
|
184
|
+
call.Expression.Kind != shimast.KindPropertyAccessExpression {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
access := call.Expression.AsPropertyAccessExpression()
|
|
188
|
+
if access == nil || access.QuestionDotToken != nil {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
if method := identifierText(access.Name()); method != "readFile" && method != "readFileSync" {
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
if call.Arguments == nil || len(call.Arguments.Nodes) < 2 ||
|
|
195
|
+
call.Arguments.Nodes[1] != node ||
|
|
196
|
+
call.Arguments.Nodes[0].Kind == shimast.KindSpreadElement {
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
return true
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// unicornTextEncodingInnerRange bounds the raw text between the quote or
|
|
203
|
+
// backtick delimiters of a string / no-substitution-template literal, mirroring
|
|
204
|
+
// upstream's `[range[0]+1, range[1]-1]` arithmetic.
|
|
205
|
+
func unicornTextEncodingInnerRange(source string, node *shimast.Node) (int, int, bool) {
|
|
206
|
+
start := shimscanner.SkipTrivia(source, node.Pos())
|
|
207
|
+
end := node.End()
|
|
208
|
+
if start < 0 || end > len(source) || start+2 > end {
|
|
209
|
+
return 0, 0, false
|
|
210
|
+
}
|
|
211
|
+
open := source[start]
|
|
212
|
+
switch node.Kind {
|
|
213
|
+
case shimast.KindStringLiteral:
|
|
214
|
+
if open != '\'' && open != '"' {
|
|
215
|
+
return 0, 0, false
|
|
216
|
+
}
|
|
217
|
+
if source[end-1] != open {
|
|
218
|
+
return 0, 0, false
|
|
219
|
+
}
|
|
220
|
+
case shimast.KindNoSubstitutionTemplateLiteral:
|
|
221
|
+
if open != '`' || source[end-1] != '`' {
|
|
222
|
+
return 0, 0, false
|
|
223
|
+
}
|
|
224
|
+
default:
|
|
225
|
+
return 0, 0, false
|
|
226
|
+
}
|
|
227
|
+
return start + 1, end - 1, true
|
|
68
228
|
}
|
|
69
229
|
|
|
70
230
|
func init() {
|
|
@@ -1,31 +1,42 @@
|
|
|
1
1
|
// unicorn/throw-new-error: `throw Foo(...)` and `throw new Foo(...)` both
|
|
2
|
-
// produce an Error instance
|
|
2
|
+
// produce an Error instance for the BUILT-IN Error constructors, which stay
|
|
3
3
|
// callable without `new`, but the call-form is a footgun — readers expect
|
|
4
|
-
// `throw` to be paired with `new`, and
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// `throw` to be paired with `new`, and a user-defined `class FooError extends
|
|
5
|
+
// Error` cannot be called without `new` at all: the call throws a TypeError
|
|
6
|
+
// instead of the error the author meant to throw. The rule requires
|
|
7
|
+
// `throw new` whenever the operand is a direct call to an error-like
|
|
8
|
+
// constructor.
|
|
9
|
+
//
|
|
10
|
+
// The callee predicate is upstream's: an `Identifier` callee, or a
|
|
11
|
+
// non-computed member callee whose property is an `Identifier`, whose name
|
|
12
|
+
// matches `^(?:[A-Z][\da-z]*)*Error$`. That covers the built-ins
|
|
13
|
+
// (`TypeError`) AND user-defined classes (`ValidationError`,
|
|
14
|
+
// `ns.HttpError`), which are the rule's most common real-world target.
|
|
8
15
|
//
|
|
9
16
|
// AST-only: visit each `ThrowStatement`, peel parentheses off its operand,
|
|
10
17
|
// and fire when the operand is a `CallExpression` (NOT a `NewExpression`)
|
|
11
|
-
// whose callee
|
|
12
|
-
//
|
|
13
|
-
//
|
|
18
|
+
// whose callee matches. Computed access (`lib["Error"]()`) is skipped, and
|
|
19
|
+
// so is any optional-chained callee (`Error?.()`, `lib?.Error()`) because
|
|
20
|
+
// `new` cannot be applied to an optional chain. Shadowed bindings and
|
|
21
|
+
// type-aware widening are intentionally out of scope — the rule is a
|
|
22
|
+
// syntactic nudge, not a full Error-tracking pass. Unlike upstream, which
|
|
23
|
+
// flags every error-constructing call site, this port stays scoped to
|
|
24
|
+
// `throw` operands.
|
|
14
25
|
// https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/throw-new-error.md
|
|
15
26
|
package linthost
|
|
16
27
|
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
import (
|
|
29
|
+
"regexp"
|
|
30
|
+
|
|
31
|
+
shimast "github.com/microsoft/typescript-go/shim/ast"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// unicornThrowNewErrorNamePattern is upstream's `customError` regex: any
|
|
35
|
+
// number of capitalized words (each a capital followed by digits/lowercase)
|
|
36
|
+
// closed by a literal `Error`. It accepts `Error`, `TypeError`, `HTTPError`,
|
|
37
|
+
// and `Abc3Error`, and rejects `fooError` (no leading capital), `ERROR` (the
|
|
38
|
+
// suffix is case-sensitive), `getError`, and `Errors`.
|
|
39
|
+
var unicornThrowNewErrorNamePattern = regexp.MustCompile(`^(?:[A-Z][\da-z]*)*Error$`)
|
|
29
40
|
|
|
30
41
|
type unicornThrowNewError struct{}
|
|
31
42
|
|
|
@@ -35,7 +46,7 @@ func (unicornThrowNewError) Visits() []shimast.Kind {
|
|
|
35
46
|
}
|
|
36
47
|
func (unicornThrowNewError) Check(ctx *Context, node *shimast.Node) {
|
|
37
48
|
throw := node.AsThrowStatement()
|
|
38
|
-
if throw == nil {
|
|
49
|
+
if throw == nil || throw.Expression == nil {
|
|
39
50
|
return
|
|
40
51
|
}
|
|
41
52
|
expr := stripParens(throw.Expression)
|
|
@@ -43,17 +54,198 @@ func (unicornThrowNewError) Check(ctx *Context, node *shimast.Node) {
|
|
|
43
54
|
return
|
|
44
55
|
}
|
|
45
56
|
call := expr.AsCallExpression()
|
|
46
|
-
if call == nil {
|
|
57
|
+
if call == nil || call.Expression == nil {
|
|
47
58
|
return
|
|
48
59
|
}
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
// `new` cannot be applied to an optional chain (`new lib?.Error()` is a
|
|
61
|
+
// syntax error), so upstream reports neither an optional call nor an
|
|
62
|
+
// optional-chained callee.
|
|
63
|
+
if call.QuestionDotToken != nil || unicornThrowNewErrorHasOptionalChain(call.Expression) {
|
|
51
64
|
return
|
|
52
65
|
}
|
|
53
|
-
|
|
66
|
+
callee := stripParens(call.Expression)
|
|
67
|
+
if !unicornThrowNewErrorIsErrorCallee(callee) {
|
|
54
68
|
return
|
|
55
69
|
}
|
|
56
|
-
ctx.
|
|
70
|
+
ctx.ReportFix(
|
|
71
|
+
expr,
|
|
72
|
+
"Use `new` when throwing an error.",
|
|
73
|
+
unicornThrowNewErrorFix(ctx.File, throw.Expression, expr, call.Expression)...,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// unicornThrowNewErrorIsErrorCallee reports whether a callee names an error
|
|
78
|
+
// constructor. A bare identifier matches on its own name; a property access
|
|
79
|
+
// matches on its property name, so `ns.FooError()` counts while the computed
|
|
80
|
+
// `ns["FooError"]()` (an element access) never does — computed keys are opaque
|
|
81
|
+
// to upstream's selector too.
|
|
82
|
+
//
|
|
83
|
+
// `Data.TaggedError(...)` is upstream's Effect-library carve-out: the call
|
|
84
|
+
// builds an error CLASS rather than an error instance, so `new` would be wrong.
|
|
85
|
+
// https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2654
|
|
86
|
+
func unicornThrowNewErrorIsErrorCallee(callee *shimast.Node) bool {
|
|
87
|
+
if callee == nil {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
switch callee.Kind {
|
|
91
|
+
case shimast.KindIdentifier:
|
|
92
|
+
return unicornThrowNewErrorNamePattern.MatchString(identifierText(callee))
|
|
93
|
+
case shimast.KindPropertyAccessExpression:
|
|
94
|
+
access := callee.AsPropertyAccessExpression()
|
|
95
|
+
if access == nil {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
name := identifierText(access.Name())
|
|
99
|
+
if !unicornThrowNewErrorNamePattern.MatchString(name) {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
return name != "TaggedError" ||
|
|
103
|
+
identifierText(stripParens(access.Expression)) != "Data"
|
|
104
|
+
default:
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// unicornThrowNewErrorHasOptionalChain reports whether any element on a
|
|
110
|
+
// callee's left-hand chain uses `?.`, mirroring upstream's
|
|
111
|
+
// `hasOptionalChainElement`. It differs from the shared `containsOptionalChain`
|
|
112
|
+
// by seeing through parentheses and TypeScript's type-only wrappers, so
|
|
113
|
+
// `(lib?.foo)!.Error()` is still recognized as an optional chain; `new` on any
|
|
114
|
+
// such callee is a syntax error, and the rule must stay silent rather than
|
|
115
|
+
// offer a fix that breaks the file.
|
|
116
|
+
func unicornThrowNewErrorHasOptionalChain(node *shimast.Node) bool {
|
|
117
|
+
node = unwrapReferenceExpression(node)
|
|
118
|
+
if node == nil {
|
|
119
|
+
return false
|
|
120
|
+
}
|
|
121
|
+
switch node.Kind {
|
|
122
|
+
case shimast.KindPropertyAccessExpression:
|
|
123
|
+
access := node.AsPropertyAccessExpression()
|
|
124
|
+
if access == nil {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
return access.QuestionDotToken != nil ||
|
|
128
|
+
unicornThrowNewErrorHasOptionalChain(access.Expression)
|
|
129
|
+
case shimast.KindElementAccessExpression:
|
|
130
|
+
access := node.AsElementAccessExpression()
|
|
131
|
+
if access == nil {
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
return access.QuestionDotToken != nil ||
|
|
135
|
+
unicornThrowNewErrorHasOptionalChain(access.Expression)
|
|
136
|
+
case shimast.KindCallExpression:
|
|
137
|
+
call := node.AsCallExpression()
|
|
138
|
+
if call == nil {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
return call.QuestionDotToken != nil ||
|
|
142
|
+
unicornThrowNewErrorHasOptionalChain(call.Expression)
|
|
143
|
+
default:
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// unicornThrowNewErrorFix builds upstream's autofix: insert `new ` in front of
|
|
149
|
+
// the reported call. Two adjustments keep the rewritten source parseable, both
|
|
150
|
+
// owed to upstream's shared `switchCallExpressionToNewExpression` fixer.
|
|
151
|
+
//
|
|
152
|
+
// A `throw` keyword that abuts its operand (`throw(Error())`,
|
|
153
|
+
// `throw[lib][0].Error()`) needs a separating space, or the inserted keyword
|
|
154
|
+
// welds onto it (`thrownew ...`). The space belongs at the operand's own start
|
|
155
|
+
// — outside any parentheses wrapping it — and merges into the `new ` insert
|
|
156
|
+
// when the two offsets coincide, because two zero-width inserts at one offset
|
|
157
|
+
// cannot both survive edit selection.
|
|
158
|
+
//
|
|
159
|
+
// A callee that still exposes a call needs parentheses, because a `new`
|
|
160
|
+
// expression's callee grammar ends at the first argument list; see
|
|
161
|
+
// unicornThrowNewErrorCalleeNeedsParens.
|
|
162
|
+
func unicornThrowNewErrorFix(
|
|
163
|
+
file *shimast.SourceFile,
|
|
164
|
+
operand *shimast.Node,
|
|
165
|
+
call *shimast.Node,
|
|
166
|
+
rawCallee *shimast.Node,
|
|
167
|
+
) []TextEdit {
|
|
168
|
+
if file == nil {
|
|
169
|
+
return nil
|
|
170
|
+
}
|
|
171
|
+
callee := stripParens(rawCallee)
|
|
172
|
+
operandStart, _ := tokenRange(file, operand)
|
|
173
|
+
// A callee is its call's first token, so the call and its callee share one
|
|
174
|
+
// start offset; the opening parenthesis therefore rides along with the `new `
|
|
175
|
+
// insert instead of being a second zero-width edit at that same offset.
|
|
176
|
+
callStart, _ := tokenRange(file, call)
|
|
177
|
+
_, calleeEnd := tokenRange(file, callee)
|
|
178
|
+
if operandStart < 0 || callStart < 0 || calleeEnd < 0 {
|
|
179
|
+
return nil
|
|
180
|
+
}
|
|
181
|
+
src := file.Text()
|
|
182
|
+
parenthesize := unicornThrowNewErrorCalleeNeedsParens(rawCallee, callee)
|
|
183
|
+
insert := "new "
|
|
184
|
+
if parenthesize {
|
|
185
|
+
insert += "("
|
|
186
|
+
}
|
|
187
|
+
edits := make([]TextEdit, 0, 3)
|
|
188
|
+
if operandStart > 0 && isIdentifierPart(src[operandStart-1]) {
|
|
189
|
+
if operandStart == callStart {
|
|
190
|
+
insert = " " + insert
|
|
191
|
+
} else {
|
|
192
|
+
edits = append(edits, TextEdit{Pos: operandStart, End: operandStart, Text: " "})
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
edits = append(edits, TextEdit{Pos: callStart, End: callStart, Text: insert})
|
|
196
|
+
if parenthesize {
|
|
197
|
+
edits = append(edits, TextEdit{Pos: calleeEnd, End: calleeEnd, Text: ")"})
|
|
198
|
+
}
|
|
199
|
+
return edits
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// unicornThrowNewErrorCalleeNeedsParens reports whether the fix has to wrap the
|
|
203
|
+
// callee in parentheses: only when the source does not already parenthesize the
|
|
204
|
+
// callee itself and an unshielded call sits on its object chain, whose argument
|
|
205
|
+
// list `new` would otherwise consume as its own.
|
|
206
|
+
//
|
|
207
|
+
// The walk stops at a parenthesized object because parentheses already shield
|
|
208
|
+
// everything inside them — `throw (getGlobalThis()).Error()` fixes to
|
|
209
|
+
// `throw new (getGlobalThis()).Error()`, whose callee parse ends at `.Error`.
|
|
210
|
+
// A non-null assertion shields nothing, so it stays transparent: without that
|
|
211
|
+
// step `throw lib.getError()!.FooError()` would fix to
|
|
212
|
+
// `throw new lib.getError()!.FooError()`, which constructs `lib.getError()` and
|
|
213
|
+
// then reads `.FooError()` off the instance.
|
|
214
|
+
func unicornThrowNewErrorCalleeNeedsParens(rawCallee, callee *shimast.Node) bool {
|
|
215
|
+
if rawCallee == nil || rawCallee.Kind == shimast.KindParenthesizedExpression {
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
if callee == nil || callee.Kind != shimast.KindPropertyAccessExpression {
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
access := callee.AsPropertyAccessExpression()
|
|
222
|
+
if access == nil {
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
current := access.Expression
|
|
226
|
+
for current != nil {
|
|
227
|
+
switch current.Kind {
|
|
228
|
+
case shimast.KindCallExpression:
|
|
229
|
+
return true
|
|
230
|
+
case shimast.KindPropertyAccessExpression:
|
|
231
|
+
inner := current.AsPropertyAccessExpression()
|
|
232
|
+
if inner == nil {
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
current = inner.Expression
|
|
236
|
+
case shimast.KindElementAccessExpression:
|
|
237
|
+
inner := current.AsElementAccessExpression()
|
|
238
|
+
if inner == nil {
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
current = inner.Expression
|
|
242
|
+
case shimast.KindNonNullExpression:
|
|
243
|
+
current = current.Expression()
|
|
244
|
+
default:
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return false
|
|
57
249
|
}
|
|
58
250
|
|
|
59
251
|
func init() {
|