@ttsc/lint 0.18.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +95 -43
  2. package/go.mod +5 -4
  3. package/internal/rulecode/rulecode.go +88 -0
  4. package/lib/index.d.ts +10 -1
  5. package/lib/index.js +45 -17
  6. package/lib/index.js.map +1 -1
  7. package/lib/structures/ITtscLintConfig.d.ts +3 -0
  8. package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
  9. package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
  10. package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
  11. package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
  12. package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
  13. package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
  14. package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
  15. package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
  16. package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
  17. package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
  18. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
  19. package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
  20. package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
  21. package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
  22. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
  23. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
  24. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
  25. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
  26. package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
  27. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
  28. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
  29. package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
  30. package/lib/structures/rules/index.d.ts +3 -0
  31. package/lib/structures/rules/index.js +3 -0
  32. package/lib/structures/rules/index.js.map +1 -1
  33. package/linthost/ast_helpers.go +521 -23
  34. package/linthost/ast_selector_matcher.go +1628 -0
  35. package/linthost/ast_selector_parser.go +628 -0
  36. package/linthost/comment_scan.go +154 -0
  37. package/linthost/compile.go +129 -79
  38. package/linthost/config.go +352 -88
  39. package/linthost/config_format.go +15 -1
  40. package/linthost/contrib_adapter.go +129 -25
  41. package/linthost/declaration_rules.go +12 -11
  42. package/linthost/directives.go +9 -67
  43. package/linthost/dispatch.go +37 -3
  44. package/linthost/engine.go +368 -53
  45. package/linthost/escape_string.go +77 -0
  46. package/linthost/fix.go +133 -23
  47. package/linthost/flags_gen.go +18 -17
  48. package/linthost/format.go +73 -167
  49. package/linthost/format_editor_settings.go +134 -26
  50. package/linthost/grapheme_tables_gen.go +1920 -0
  51. package/linthost/host.go +160 -61
  52. package/linthost/literal_escape_scan.go +117 -0
  53. package/linthost/lsp.go +545 -98
  54. package/linthost/numeric_literal_precision.go +228 -0
  55. package/linthost/polyfill_browserslist.go +1439 -0
  56. package/linthost/polyfill_browserslist_config.go +603 -0
  57. package/linthost/polyfill_corejs.go +750 -0
  58. package/linthost/polyfill_data.go +113 -0
  59. package/linthost/polyfill_data_gen.json +39868 -0
  60. package/linthost/print_nodes_array.go +7 -1
  61. package/linthost/print_nodes_object.go +7 -1
  62. package/linthost/project_engine.go +151 -0
  63. package/linthost/project_rules.go +78 -0
  64. package/linthost/regex_clean.go +191 -0
  65. package/linthost/regex_tree.go +1544 -0
  66. package/linthost/regex_tree_optimizer.go +1685 -0
  67. package/linthost/regexp_cache.go +44 -0
  68. package/linthost/rule_codes.go +105 -0
  69. package/linthost/rule_codes.json +745 -0
  70. package/linthost/rules_ban_ts_comment.go +312 -0
  71. package/linthost/rules_boundaries.go +128 -43
  72. package/linthost/rules_boundaries_dependencies.go +1013 -0
  73. package/linthost/rules_core_extra.go +62 -92
  74. package/linthost/rules_cypress.go +1 -1
  75. package/linthost/rules_default_case.go +87 -11
  76. package/linthost/rules_dupes.go +38 -13
  77. package/linthost/rules_empty.go +188 -20
  78. package/linthost/rules_escape.go +62 -48
  79. package/linthost/rules_format_arrow_parens.go +19 -5
  80. package/linthost/rules_format_bracket_spacing.go +1 -1
  81. package/linthost/rules_format_clause_join.go +1 -1
  82. package/linthost/rules_format_declaration_header.go +31 -19
  83. package/linthost/rules_format_indent.go +1 -1
  84. package/linthost/rules_format_jsdoc.go +1 -1
  85. package/linthost/rules_format_orphan_semi.go +1 -1
  86. package/linthost/rules_format_parameter_properties.go +17 -7
  87. package/linthost/rules_format_print_width.go +1 -1
  88. package/linthost/rules_format_quote_props.go +1 -1
  89. package/linthost/rules_format_quotes.go +1 -1
  90. package/linthost/rules_format_semi.go +106 -79
  91. package/linthost/rules_format_sort_imports.go +79 -45
  92. package/linthost/rules_format_statement_split.go +1 -1
  93. package/linthost/rules_format_ternary_nullish_parens.go +1 -1
  94. package/linthost/rules_format_trailing_comma.go +64 -1
  95. package/linthost/rules_format_whitespace.go +1 -1
  96. package/linthost/rules_functional.go +21 -22
  97. package/linthost/rules_gap.go +80 -3
  98. package/linthost/rules_grouped_accessor_pairs.go +84 -25
  99. package/linthost/rules_guard_for_in.go +64 -147
  100. package/linthost/rules_jsx_a11y.go +232 -22
  101. package/linthost/rules_logic.go +84 -38
  102. package/linthost/rules_nextjs.go +105 -16
  103. package/linthost/rules_no_class_assign.go +70 -0
  104. package/linthost/rules_no_duplicate_imports.go +266 -0
  105. package/linthost/rules_no_else_return.go +156 -38
  106. package/linthost/rules_no_extend_native.go +113 -17
  107. package/linthost/rules_no_fallthrough.go +1013 -0
  108. package/linthost/rules_no_func_assign.go +70 -0
  109. package/linthost/rules_no_import_assign.go +278 -86
  110. package/linthost/rules_no_inner_declarations.go +186 -0
  111. package/linthost/rules_no_loop_func.go +438 -53
  112. package/linthost/rules_no_mixed_operators.go +208 -52
  113. package/linthost/rules_no_param_reassign.go +376 -61
  114. package/linthost/rules_no_promise_executor_return.go +156 -0
  115. package/linthost/rules_no_restricted_imports.go +1013 -57
  116. package/linthost/rules_no_restricted_syntax.go +190 -26
  117. package/linthost/rules_no_useless_constructor.go +6 -36
  118. package/linthost/rules_problems.go +154 -311
  119. package/linthost/rules_promise.go +2578 -109
  120. package/linthost/rules_react_extras.go +71 -45
  121. package/linthost/rules_react_hooks.go +18 -2
  122. package/linthost/rules_react_perf.go +1 -0
  123. package/linthost/rules_react_refresh.go +1 -1
  124. package/linthost/rules_regexp.go +230 -14
  125. package/linthost/rules_security.go +45 -19
  126. package/linthost/rules_self.go +21 -1
  127. package/linthost/rules_solid.go +13 -7
  128. package/linthost/rules_storybook.go +1 -1
  129. package/linthost/rules_suggestions.go +490 -84
  130. package/linthost/rules_testing_library.go +12 -1
  131. package/linthost/rules_ts.go +227 -54
  132. package/linthost/rules_ts_async.go +0 -131
  133. package/linthost/rules_ts_no_base_to_string.go +3 -4
  134. package/linthost/rules_ts_no_misused_promises.go +817 -0
  135. package/linthost/rules_ts_no_restricted_types.go +320 -34
  136. package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
  137. package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
  138. package/linthost/rules_ts_require_array_sort_compare.go +5 -5
  139. package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
  140. package/linthost/rules_unicorn_better_regex.go +224 -16
  141. package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
  142. package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
  143. package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
  144. package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
  145. package/linthost/rules_unicorn_escape_case.go +30 -19
  146. package/linthost/rules_unicorn_filename_case.go +763 -16
  147. package/linthost/rules_unicorn_import_style.go +783 -17
  148. package/linthost/rules_unicorn_isolated_functions.go +934 -14
  149. package/linthost/rules_unicorn_no_hex_escape.go +26 -11
  150. package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
  151. package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
  152. package/linthost/rules_unicorn_no_unused_properties.go +756 -19
  153. package/linthost/rules_unicorn_number_literal_case.go +58 -39
  154. package/linthost/rules_unicorn_prefer_at.go +7 -3
  155. package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
  156. package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
  157. package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
  158. package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
  159. package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
  160. package/linthost/rules_unicorn_string_content.go +611 -15
  161. package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
  162. package/linthost/rules_unicorn_template_indent.go +923 -18
  163. package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
  164. package/linthost/rules_unicorn_throw_new_error.go +218 -26
  165. package/linthost/rules_var.go +829 -142
  166. package/linthost/string_length.go +222 -0
  167. package/package.json +4 -3
  168. package/rule/project.go +145 -0
  169. package/rule/rule.go +58 -1
  170. package/src/index.ts +58 -17
  171. package/src/structures/ITtscLintConfig.ts +3 -0
  172. package/src/structures/TtscLintRuleSetting.ts +3 -2
  173. package/src/structures/format/ITtscLintFormat.ts +3 -2
  174. package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
  175. package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
  176. package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
  177. package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
  178. package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
  179. package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
  180. package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
  181. package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
  182. package/src/structures/rules/ITtscLintRules.ts +7 -2
  183. package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
  184. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
  185. package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
  186. package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
  187. package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
  188. package/src/structures/rules/index.ts +3 -0
@@ -4,39 +4,54 @@
4
4
  // escape, while a hex escape silently widens to a Unicode escape at
5
5
  // runtime anyway. The rule nudges authors toward the Unicode form.
6
6
  //
7
- // AST-only: visit `KindStringLiteral` and `KindNoSubstitutionTemplateLiteral`,
8
- // read the raw source text via `nodeText` (the parser already decodes
9
- // escapes into the `.Text` value, so a normal accessor would see `©`
10
- // instead of `\xA9`), and fire when the source contains a `\xHH`
11
- // occurrence where HH is two hex digits.
7
+ // AST-only: visit `KindStringLiteral`, `KindNoSubstitutionTemplateLiteral`,
8
+ // and the `KindTemplateHead`/`Middle`/`Tail` elements an interpolated
9
+ // template (or a template literal type) is built from, read each token's raw
10
+ // source text via `nodeText` (the parser already decodes escapes into the
11
+ // `.Text` value, so a normal accessor would see `©` instead of `\xA9`), and
12
+ // fire when the text carries an active `\xHH` escape. Tagged templates are
13
+ // skipped: their tag observes the raw text, where `\xA9` and `©` differ.
14
+ // See `literal_escape_scan.go` for the parity and digit-width rules the scan
15
+ // enforces.
12
16
  //
13
17
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-hex-escape.md
14
18
  package linthost
15
19
 
16
20
  import (
17
- "regexp"
18
-
19
21
  shimast "github.com/microsoft/typescript-go/shim/ast"
20
22
  )
21
23
 
22
- var unicornNoHexEscapePattern = regexp.MustCompile(`\\x[0-9A-Fa-f]{2}`)
23
-
24
24
  type unicornNoHexEscape struct{}
25
25
 
26
26
  func (unicornNoHexEscape) Name() string { return "unicorn/no-hex-escape" }
27
27
  func (unicornNoHexEscape) Visits() []shimast.Kind {
28
- return []shimast.Kind{shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral}
28
+ return []shimast.Kind{
29
+ shimast.KindStringLiteral,
30
+ shimast.KindNoSubstitutionTemplateLiteral,
31
+ shimast.KindTemplateHead,
32
+ shimast.KindTemplateMiddle,
33
+ shimast.KindTemplateTail,
34
+ }
29
35
  }
30
36
  func (unicornNoHexEscape) Check(ctx *Context, node *shimast.Node) {
37
+ if isTaggedTemplateElement(node) {
38
+ return
39
+ }
31
40
  source := nodeText(ctx.File, node)
32
41
  if source == "" {
33
42
  return
34
43
  }
35
- if unicornNoHexEscapePattern.MatchString(source) {
44
+ if hasActiveLiteralEscape(source, unicornNoHexEscapeIsHex) {
36
45
  ctx.Report(node, "Prefer Unicode escapes (`\\uXXXX`) over hex escapes (`\\xHH`).")
37
46
  }
38
47
  }
39
48
 
49
+ // unicornNoHexEscapeIsHex selects the `\xHH` form. The `\uHHHH` and
50
+ // `\u{HEX...}` forms are the shapes upstream rewrites *to*, so they pass.
51
+ func unicornNoHexEscapeIsHex(escape literalEscape) bool {
52
+ return escape.Prefix == 'x'
53
+ }
54
+
40
55
  func init() {
41
56
  Register(unicornNoHexEscape{})
42
57
  }
@@ -1,29 +1,78 @@
1
- // unicorn/no-typeof-undefined: comparing `typeof X` against the string
2
- // literal `"undefined"` is a leftover guard from pre-strict-mode code,
3
- // when referencing an undeclared identifier threw a ReferenceError.
4
- // Modern code can compare the value to `undefined` directly, which is
5
- // shorter and reads as the intent it actually expresses.
1
+ // unicorn/no-typeof-undefined: comparing `typeof X` against the string literal
2
+ // `"undefined"` is a leftover guard from pre-strict-mode code, when referencing
3
+ // an undeclared identifier threw a ReferenceError. Modern code can compare the
4
+ // value to `undefined` directly, which is shorter and reads as the intent it
5
+ // actually expresses.
6
6
  //
7
- // AST-only: each visited BinaryExpression is rejected when its operator
8
- // is one of `===`, `==`, `!==`, `!=` and its two operands include
9
- // exactly one TypeOfExpression and exactly one string-shaped literal
10
- // (StringLiteral or NoSubstitutionTemplateLiteral) whose text value is
11
- // `undefined`. Either side may carry the typeof; either side may carry
12
- // the literal.
7
+ // The match mirrors upstream exactly:
8
+ //
9
+ // - the `typeof` must be the LEFT operand of `===` / `==` / `!==` / `!=`
10
+ // (a reversed `"undefined" === typeof x` is left alone),
11
+ // - the right operand must be a string Literal whose value is `undefined`
12
+ // (a template literal “ `undefined` “ is a TemplateLiteral, never a
13
+ // Literal, so it is excluded), and
14
+ // - a global operand is skipped unless `checkGlobalVariables` is enabled,
15
+ // because rewriting `typeof window === "undefined"` to `window ===
16
+ // undefined` throws a ReferenceError when the global is undeclared. A
17
+ // global is an identifier that resolves to no local binding, which the
18
+ // checker answers; every other operand (member access, call, `this`,
19
+ // literal) is never a global identifier and stays checked.
20
+ //
21
+ // The autofix removes the `typeof` keyword, upgrades `==` / `!=` to their
22
+ // strict form, and replaces the `"undefined"` literal with the `undefined`
23
+ // identifier. Removing a leading keyword can create an ASI hazard, so the fix
24
+ // is declined (the diagnostic still reports) when `typeof` and its operand span
25
+ // different lines or the operand begins with a continuation character; upstream
26
+ // inserts a guarding `;` or parentheses there, which this port conservatively
27
+ // avoids emitting rather than risk a wrong edit. When `checkGlobalVariables`
28
+ // surfaces a global, the same edits are offered as an opt-in suggestion instead
29
+ // of an automatic fix, matching upstream.
13
30
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-typeof-undefined.md
14
31
  package linthost
15
32
 
16
- import shimast "github.com/microsoft/typescript-go/shim/ast"
33
+ import (
34
+ "bytes"
35
+ "encoding/json"
36
+ "errors"
37
+ "fmt"
17
38
 
18
- type unicornNoTypeofUndefined struct{}
39
+ shimast "github.com/microsoft/typescript-go/shim/ast"
40
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
41
+ )
42
+
43
+ type unicornNoTypeofUndefined struct{ optionsRule }
44
+
45
+ // unicornNoTypeofUndefinedOptions is the decoded option payload. Upstream's
46
+ // only option is `checkGlobalVariables`, defaulting to false.
47
+ type unicornNoTypeofUndefinedOptions struct {
48
+ checkGlobalVariables bool
49
+ }
19
50
 
20
51
  func (unicornNoTypeofUndefined) Name() string { return "unicorn/no-typeof-undefined" }
21
52
  func (unicornNoTypeofUndefined) Visits() []shimast.Kind {
22
53
  return []shimast.Kind{shimast.KindBinaryExpression}
23
54
  }
55
+
56
+ // The global guard is scope analysis, not syntax: resolving the operand to its
57
+ // binding is what separates a shadowing `let window` (checked) from the
58
+ // ambient `window` global (skipped by default).
59
+ func (unicornNoTypeofUndefined) NeedsTypeChecker() bool { return true }
60
+
61
+ func (unicornNoTypeofUndefined) ValidateOptions(raw json.RawMessage) error {
62
+ _, err := decodeUnicornNoTypeofUndefinedOptions(raw)
63
+ return err
64
+ }
65
+
24
66
  func (unicornNoTypeofUndefined) Check(ctx *Context, node *shimast.Node) {
67
+ if ctx == nil || ctx.File == nil || ctx.Checker == nil || node == nil {
68
+ return
69
+ }
70
+ options, err := decodeUnicornNoTypeofUndefinedOptions(ctx.Options)
71
+ if err != nil {
72
+ return
73
+ }
25
74
  expr := node.AsBinaryExpression()
26
- if expr == nil || expr.OperatorToken == nil {
75
+ if expr == nil || expr.OperatorToken == nil || expr.Left == nil || expr.Right == nil {
27
76
  return
28
77
  }
29
78
  switch expr.OperatorToken.Kind {
@@ -34,23 +83,183 @@ func (unicornNoTypeofUndefined) Check(ctx *Context, node *shimast.Node) {
34
83
  default:
35
84
  return
36
85
  }
37
- left := stripParens(expr.Left)
38
- right := stripParens(expr.Right)
39
- if left == nil || right == nil {
86
+ // Upstream requires `binaryExpression.left` to be the `typeof` unary; the
87
+ // reversed operand order is one of its explicit valid cases.
88
+ if expr.Left.Kind != shimast.KindTypeOfExpression {
40
89
  return
41
90
  }
42
- var literal *shimast.Node
43
- if left.Kind == shimast.KindTypeOfExpression {
44
- literal = right
45
- } else if right.Kind == shimast.KindTypeOfExpression {
46
- literal = left
47
- } else {
91
+ // `isLiteral(right, 'undefined')` matches an ESTree Literal only. A
92
+ // NoSubstitutionTemplateLiteral is a TemplateLiteral, so it never matches.
93
+ if expr.Right.Kind != shimast.KindStringLiteral {
94
+ return
95
+ }
96
+ if lit := expr.Right.AsStringLiteral(); lit == nil || lit.Text != "undefined" {
97
+ return
98
+ }
99
+
100
+ typeofExpr := expr.Left.AsTypeOfExpression()
101
+ if typeofExpr == nil || typeofExpr.Expression == nil {
102
+ return
103
+ }
104
+ // Parentheses are elided in ESTree, so `typeof (foo)` reads through to `foo`
105
+ // for the global-identifier test.
106
+ operand := stripParens(typeofExpr.Expression)
107
+ isGlobal := unicornNoTypeofUndefinedIsGlobal(ctx, operand)
108
+ if isGlobal && !options.checkGlobalVariables {
109
+ return
110
+ }
111
+
112
+ text := ctx.File.Text()
113
+ typeofStart := shimscanner.SkipTrivia(text, expr.Left.Pos())
114
+ const typeofKeyword = "typeof"
115
+ typeofEnd := typeofStart + len(typeofKeyword)
116
+ if typeofStart < 0 || typeofEnd > len(text) || text[typeofStart:typeofEnd] != typeofKeyword {
117
+ return
118
+ }
119
+
120
+ const message = "Compare with `undefined` directly instead of using `typeof`."
121
+ edits, safe := unicornNoTypeofUndefinedFix(text, expr, typeofExpr, typeofStart, typeofEnd)
122
+ if !safe {
123
+ ctx.ReportRange(typeofStart, typeofEnd, message)
48
124
  return
49
125
  }
50
- if stringLiteralText(literal) != "undefined" {
126
+ if isGlobal {
127
+ // Reachable only under checkGlobalVariables. Rewriting a global can throw,
128
+ // so upstream advertises an opt-in suggestion rather than an autofix.
129
+ ctx.ReportRangeSuggestion(
130
+ typeofStart,
131
+ typeofEnd,
132
+ message,
133
+ unicornNoTypeofUndefinedSuggestionTitle(expr.OperatorToken.Kind),
134
+ edits...,
135
+ )
51
136
  return
52
137
  }
53
- ctx.Report(node, "Compare with `undefined` directly instead of using `typeof`.")
138
+ ctx.ReportRangeFix(typeofStart, typeofEnd, message, edits...)
139
+ }
140
+
141
+ // decodeUnicornNoTypeofUndefinedOptions parses the `checkGlobalVariables`
142
+ // option. An absent payload keeps the upstream default (false); an unknown key,
143
+ // a non-object payload, or a non-boolean value is rejected so a malformed
144
+ // config fails loudly instead of silently defaulting.
145
+ func decodeUnicornNoTypeofUndefinedOptions(raw json.RawMessage) (unicornNoTypeofUndefinedOptions, error) {
146
+ options := unicornNoTypeofUndefinedOptions{}
147
+ trimmed := bytes.TrimSpace(raw)
148
+ if len(trimmed) == 0 {
149
+ return options, nil
150
+ }
151
+ if trimmed[0] != '{' {
152
+ return options, errors.New("options must be an object")
153
+ }
154
+ var rawFields map[string]json.RawMessage
155
+ if err := json.Unmarshal(trimmed, &rawFields); err != nil {
156
+ return options, fmt.Errorf("options must be an object: %w", err)
157
+ }
158
+ for name := range rawFields {
159
+ if name != "checkGlobalVariables" {
160
+ return options, fmt.Errorf("unknown option %q", name)
161
+ }
162
+ }
163
+ if value, ok := rawFields["checkGlobalVariables"]; ok {
164
+ if bytes.Equal(bytes.TrimSpace(value), []byte("null")) {
165
+ return options, errors.New(`option "checkGlobalVariables" must be a boolean`)
166
+ }
167
+ if err := json.Unmarshal(value, &options.checkGlobalVariables); err != nil {
168
+ return options, fmt.Errorf(`option "checkGlobalVariables" must be a boolean: %w`, err)
169
+ }
170
+ }
171
+ return options, nil
172
+ }
173
+
174
+ // unicornNoTypeofUndefinedIsGlobal ports upstream's `isGlobalIdentifier`: an
175
+ // identifier that resolves to no local binding — either unresolved (an implicit
176
+ // global) or declared only outside this file (a lib/ambient global). Any other
177
+ // operand shape (member access, call, `this`, a literal) is never a global
178
+ // identifier and returns false so it stays checked.
179
+ func unicornNoTypeofUndefinedIsGlobal(ctx *Context, operand *shimast.Node) bool {
180
+ if operand == nil || operand.Kind != shimast.KindIdentifier {
181
+ return false
182
+ }
183
+ symbol := ctx.Checker.GetSymbolAtLocation(operand)
184
+ if symbol == nil {
185
+ return true
186
+ }
187
+ for _, declaration := range symbol.Declarations {
188
+ if declaration == nil {
189
+ continue
190
+ }
191
+ if shimast.GetSourceFileOfNode(declaration) == ctx.File {
192
+ return false
193
+ }
194
+ }
195
+ return true
196
+ }
197
+
198
+ // unicornNoTypeofUndefinedFix builds the upstream autofix edits and reports
199
+ // whether they are safe to apply. The three edits — remove `typeof` plus the
200
+ // whitespace after it, upgrade `==`/`!=`, and replace the literal — reproduce
201
+ // upstream's non-hazard rewrite. Removing the leading `typeof` is declined
202
+ // (safe=false) when the operand sits on a different line than `typeof` or
203
+ // begins with an ASI-continuation character, the two shapes where upstream
204
+ // instead emits a guarding `;` or wrapping parentheses.
205
+ func unicornNoTypeofUndefinedFix(
206
+ text string,
207
+ expr *shimast.BinaryExpression,
208
+ typeofExpr *shimast.TypeOfExpression,
209
+ typeofStart, typeofEnd int,
210
+ ) ([]TextEdit, bool) {
211
+ operandStart := shimscanner.SkipTrivia(text, typeofExpr.Expression.Pos())
212
+ if operandStart < typeofEnd || operandStart >= len(text) {
213
+ return nil, false
214
+ }
215
+ // (A) A line break between `typeof` and its operand (a newline or a
216
+ // multi-line comment) risks ASI once `typeof` is gone, notably inside a
217
+ // `return`/`throw` argument.
218
+ if bytes.ContainsAny([]byte(text[typeofEnd:operandStart]), "\n\r") {
219
+ return nil, false
220
+ }
221
+ // (B) An operand that begins with a continuation character could merge with a
222
+ // preceding token: `foo⏎typeof [] === "undefined"` would become `foo⏎[] ===
223
+ // undefined`, i.e. `foo[]`.
224
+ switch text[operandStart] {
225
+ case '[', '(', '`', '+', '-', '*', '/', ',', '.':
226
+ return nil, false
227
+ }
228
+
229
+ edits := make([]TextEdit, 0, 3)
230
+ // Drop `typeof` and the whitespace run that follows it, mirroring
231
+ // `fixer.remove(typeofToken)` + `removeSpacesAfter(typeofToken)`. The run
232
+ // stops at the operand or an intervening comment, so a same-line comment is
233
+ // preserved rather than deleted.
234
+ removeEnd := typeofEnd
235
+ for removeEnd < len(text) && (text[removeEnd] == ' ' || text[removeEnd] == '\t') {
236
+ removeEnd++
237
+ }
238
+ edits = append(edits, TextEdit{Pos: typeofStart, End: removeEnd, Text: ""})
239
+
240
+ if expr.OperatorToken.Kind == shimast.KindEqualsEqualsToken ||
241
+ expr.OperatorToken.Kind == shimast.KindExclamationEqualsToken {
242
+ edits = append(edits, TextEdit{
243
+ Pos: expr.OperatorToken.End(),
244
+ End: expr.OperatorToken.End(),
245
+ Text: "=",
246
+ })
247
+ }
248
+
249
+ literalStart := shimscanner.SkipTrivia(text, expr.Right.Pos())
250
+ edits = append(edits, TextEdit{Pos: literalStart, End: expr.Right.End(), Text: "undefined"})
251
+ return edits, true
252
+ }
253
+
254
+ // unicornNoTypeofUndefinedSuggestionTitle renders upstream's suggestion label,
255
+ // whose operator is `!==` for a negated comparison and `===` otherwise.
256
+ func unicornNoTypeofUndefinedSuggestionTitle(operator shimast.Kind) string {
257
+ replacement := "==="
258
+ if operator == shimast.KindExclamationEqualsEqualsToken ||
259
+ operator == shimast.KindExclamationEqualsToken {
260
+ replacement = "!=="
261
+ }
262
+ return "Switch to `… " + replacement + " undefined`."
54
263
  }
55
264
 
56
265
  func init() {