@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,15 +1,24 @@
1
- // unicorn/text-encoding-identifier-case: the IANA text-encoding labels
2
- // `UTF-8`, `utf8`, `ASCII`, etc. all resolve to the same encoding in
3
- // browsers and Node, but the surrounding ecosystem (HTTP headers,
4
- // `TextDecoder`, file APIs) varies on which spelling it echoes back.
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
- // AST-only: visit `StringLiteral` and `NoSubstitutionTemplateLiteral`
10
- // nodes. The literal's lowercased text is matched against the well-known
11
- // encoding identifiers; a mismatch with the canonical form (the same
12
- // identifier already in canonical case) fires the rule.
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
- type unicornTextEncodingIdentifierCase struct{}
32
+ const unicornTextEncodingIdentifierCaseRuleName = "unicorn/text-encoding-identifier-case"
33
+
34
+ type unicornTextEncodingIdentifierCase struct{ optionsRule }
23
35
 
24
- // unicornCanonicalTextEncodings maps the lowercased label to its canonical
25
- // spelling. Authors who already wrote the canonical form pass; anything
26
- // else with the same lowercased label fires.
27
- var unicornCanonicalTextEncodings = map[string]string{
28
- "utf-8": "utf-8",
29
- "utf-16le": "utf-16le",
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 "unicorn/text-encoding-identifier-case"
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
- text := stringLiteralText(node)
45
- if text == "" {
51
+ if ctx == nil || ctx.File == nil || node == nil {
46
52
  return
47
53
  }
48
- lower := strings.ToLower(text)
49
- canonical, ok := unicornCanonicalTextEncodings[lower]
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
- // `utf8`/`UTF8` share a canonical form with `utf-8` but lowercase
52
- // differently — handle the hyphen-stripped aliases explicitly.
53
- switch lower {
54
- case "utf8":
55
- canonical = "utf-8"
56
- case "utf16le":
57
- canonical = "utf-16le"
58
- case "utf16be":
59
- canonical = "utf-16be"
60
- default:
61
- return
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
- if text == canonical {
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
- ctx.Report(node, "Use the canonical lowercase form for text-encoding identifiers (e.g. `utf-8`).")
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 because the built-in Error constructors are
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 some tooling (notably custom Error
5
- // subclasses) stops working when the call-form is mixed in. The rule
6
- // requires `throw new` whenever the operand is a direct call to one of the
7
- // eight built-in Error constructor names.
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 is an `Identifier` matching the built-in allowlist. Shadowed
12
- // bindings and type-aware widening are intentionally out of scope — the
13
- // rule is a syntactic nudge, not a full Error-tracking pass.
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 shimast "github.com/microsoft/typescript-go/shim/ast"
18
-
19
- var unicornThrowNewErrorBuiltinNames = map[string]struct{}{
20
- "Error": {},
21
- "TypeError": {},
22
- "RangeError": {},
23
- "SyntaxError": {},
24
- "ReferenceError": {},
25
- "EvalError": {},
26
- "URIError": {},
27
- "AggregateError": {},
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
- name := identifierText(call.Expression)
50
- if name == "" {
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
- if _, ok := unicornThrowNewErrorBuiltinNames[name]; !ok {
66
+ callee := stripParens(call.Expression)
67
+ if !unicornThrowNewErrorIsErrorCallee(callee) {
54
68
  return
55
69
  }
56
- ctx.Report(expr, "Use `new` when throwing an error.")
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() {