@ttsc/lint 0.18.4 → 0.19.1

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 +125 -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 +159 -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 +231 -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 +206 -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,33 +1,52 @@
1
- // switchExhaustivenessCheck reports `switch (e)` statements where the
2
- // discriminant is a union of literal types or an enum and at least one
3
- // constituent has no matching `case` clause AND the switch carries no
4
- // `default` clause. Either covering every member explicitly or adding a
5
- // `default` arm makes the intent visible and forces the compiler to
6
- // flag any future addition to the union/enum at every switch site.
7
- // typescript-eslint strict-type-checked:
1
+ // typescript/switch-exhaustiveness-check: require switches over finite
2
+ // discriminants to cover every enumerable member. The implementation mirrors
3
+ // typescript-eslint's scalar defaults and option interactions while using the
4
+ // TypeScript-Go Checker as the type oracle.
5
+ //
6
+ // Reference:
8
7
  // https://typescript-eslint.io/rules/switch-exhaustiveness-check/
9
8
  package linthost
10
9
 
11
10
  import (
11
+ "regexp"
12
+ "strings"
13
+
12
14
  shimast "github.com/microsoft/typescript-go/shim/ast"
13
15
  shimchecker "github.com/microsoft/typescript-go/shim/checker"
16
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
14
17
  )
15
18
 
16
- // switchExhaustivenessCheck is type-aware: the discriminant is resolved
17
- // via the Checker, and uncovered members are computed by removing every
18
- // case-clause expression's type from the union's constituent set. The
19
- // rule only fires when the discriminant resolves to a union — plain
20
- // `string` or `number` discriminants are open-ended and outside the
21
- // rule's scope, matching typescript-eslint's behavior.
22
- //
23
- // Identity is established by `*Type` pointer comparison rather than
24
- // literal value comparison: TypeScript canonicalizes string and number
25
- // literal types (`stringLiteralTypes` / `numberLiteralTypes` maps inside
26
- // the checker), so two references to `"a"` resolve to the same `*Type`
27
- // instance. Enum literal types are likewise canonicalized per enum
28
- // member, so `case E.A:` and the `E.A` constituent of the discriminant's
29
- // union share their `*Type` pointer.
30
- type switchExhaustivenessCheck struct{}
19
+ type switchExhaustivenessCheck struct{ optionsRule }
20
+
21
+ type switchExhaustivenessCheckOptions struct {
22
+ AllowDefaultCaseForExhaustiveSwitch *bool `json:"allowDefaultCaseForExhaustiveSwitch"`
23
+ ConsiderDefaultExhaustiveForUnions *bool `json:"considerDefaultExhaustiveForUnions"`
24
+ DefaultCaseCommentPattern *string `json:"defaultCaseCommentPattern"`
25
+ RequireDefaultForNonUnion *bool `json:"requireDefaultForNonUnion"`
26
+ }
27
+
28
+ type resolvedSwitchExhaustivenessCheckOptions struct {
29
+ allowDefaultCaseForExhaustiveSwitch bool
30
+ considerDefaultExhaustiveForUnions bool
31
+ defaultCaseCommentPattern *regexp.Regexp
32
+ requireDefaultForNonUnion bool
33
+ }
34
+
35
+ type switchExhaustivenessCheckDefaultCase struct {
36
+ node *shimast.Node
37
+ pos int
38
+ end int
39
+ }
40
+
41
+ type switchExhaustivenessCheckMetadata struct {
42
+ containsNonLiteralType bool
43
+ defaultCase *switchExhaustivenessCheckDefaultCase
44
+ caseBlock *shimast.Node
45
+ lastClause *shimast.Node
46
+ missingMembers []*shimchecker.Type
47
+ }
48
+
49
+ var switchExhaustivenessCheckDefaultCommentPattern = regexp.MustCompile(`(?i)^no default$`)
31
50
 
32
51
  func (switchExhaustivenessCheck) Name() string { return "typescript/switch-exhaustiveness-check" }
33
52
  func (switchExhaustivenessCheck) NeedsTypeChecker() bool {
@@ -40,106 +59,557 @@ func (switchExhaustivenessCheck) Check(ctx *Context, node *shimast.Node) {
40
59
  if ctx.Checker == nil {
41
60
  return
42
61
  }
43
- sw := node.AsSwitchStatement()
44
- if sw == nil || sw.Expression == nil || sw.CaseBlock == nil {
62
+
63
+ var rawOptions switchExhaustivenessCheckOptions
64
+ if err := ctx.DecodeOptions(&rawOptions); err != nil {
45
65
  return
46
66
  }
47
- block := sw.CaseBlock.AsCaseBlock()
48
- if block == nil || block.Clauses == nil {
67
+ options := resolveSwitchExhaustivenessCheckOptions(rawOptions)
68
+ metadata := switchExhaustivenessCheckCollectMetadata(
69
+ ctx,
70
+ node,
71
+ options.defaultCaseCommentPattern,
72
+ )
73
+ if metadata == nil {
49
74
  return
50
75
  }
51
- // A `default` clause covers anything not enumerated above, so the
52
- // rule has nothing to add. typescript-eslint also exits early in
53
- // this case.
54
- for _, clause := range block.Clauses.Nodes {
55
- if clause != nil && clause.Kind == shimast.KindDefaultClause {
56
- return
76
+
77
+ sw := node.AsSwitchStatement()
78
+ if sw == nil || sw.Expression == nil {
79
+ return
80
+ }
81
+
82
+ if !(options.considerDefaultExhaustiveForUnions && metadata.defaultCase != nil) &&
83
+ len(metadata.missingMembers) > 0 {
84
+ missing := make([]string, 0, len(metadata.missingMembers))
85
+ for _, member := range metadata.missingMembers {
86
+ missing = append(missing, switchExhaustivenessCheckTypeName(ctx.Checker, sw.Expression, member))
87
+ }
88
+ message := "Switch is not exhaustive. Cases not matched: " + strings.Join(missing, " | ")
89
+ if edit, ok := switchExhaustivenessCheckSuggestion(
90
+ ctx,
91
+ node,
92
+ metadata,
93
+ metadata.missingMembers,
94
+ false,
95
+ ); ok {
96
+ ctx.ReportSuggestion(sw.Expression, message, "Add branches for missing cases.", edit)
97
+ } else {
98
+ ctx.Report(sw.Expression, message)
57
99
  }
58
100
  }
59
- discriminantType := ctx.Checker.GetTypeAtLocation(sw.Expression)
60
- if discriminantType == nil {
61
- return
101
+
102
+ if !options.allowDefaultCaseForExhaustiveSwitch &&
103
+ len(metadata.missingMembers) == 0 &&
104
+ metadata.defaultCase != nil &&
105
+ !metadata.containsNonLiteralType {
106
+ switchExhaustivenessCheckReportDefaultCase(
107
+ ctx,
108
+ metadata.defaultCase,
109
+ "The switch statement is exhaustive, so the default case is unnecessary.",
110
+ )
62
111
  }
63
- // Only consider discriminants whose static type is a finite union of
64
- // literal-like constituents. Plain `string` / `number` / `unknown`
65
- // are open-ended and never exhaustively coverable.
66
- members := switchExhaustivenessCheckCollectMembers(discriminantType)
67
- if len(members) == 0 {
68
- return
112
+
113
+ if options.requireDefaultForNonUnion &&
114
+ metadata.containsNonLiteralType &&
115
+ metadata.defaultCase == nil {
116
+ const message = "Switch is not exhaustive. Cases not matched: default"
117
+ if edit, ok := switchExhaustivenessCheckSuggestion(
118
+ ctx,
119
+ node,
120
+ metadata,
121
+ nil,
122
+ true,
123
+ ); ok {
124
+ ctx.ReportSuggestion(sw.Expression, message, "Add a default branch.", edit)
125
+ } else {
126
+ ctx.Report(sw.Expression, message)
127
+ }
128
+ }
129
+ }
130
+
131
+ func resolveSwitchExhaustivenessCheckOptions(
132
+ raw switchExhaustivenessCheckOptions,
133
+ ) resolvedSwitchExhaustivenessCheckOptions {
134
+ resolved := resolvedSwitchExhaustivenessCheckOptions{
135
+ allowDefaultCaseForExhaustiveSwitch: true,
136
+ defaultCaseCommentPattern: switchExhaustivenessCheckDefaultCommentPattern,
137
+ }
138
+ if raw.AllowDefaultCaseForExhaustiveSwitch != nil {
139
+ resolved.allowDefaultCaseForExhaustiveSwitch = *raw.AllowDefaultCaseForExhaustiveSwitch
140
+ }
141
+ if raw.ConsiderDefaultExhaustiveForUnions != nil {
142
+ resolved.considerDefaultExhaustiveForUnions = *raw.ConsiderDefaultExhaustiveForUnions
143
+ }
144
+ if raw.RequireDefaultForNonUnion != nil {
145
+ resolved.requireDefaultForNonUnion = *raw.RequireDefaultForNonUnion
146
+ }
147
+ if raw.DefaultCaseCommentPattern != nil {
148
+ pattern, err := regexp.Compile(*raw.DefaultCaseCommentPattern)
149
+ if err != nil {
150
+ resolved.defaultCaseCommentPattern = nil
151
+ } else {
152
+ resolved.defaultCaseCommentPattern = pattern
153
+ }
69
154
  }
70
- covered := make(map[*shimchecker.Type]bool, len(members))
155
+ return resolved
156
+ }
157
+
158
+ // switchExhaustivenessCheckCollectMetadata resolves the discriminant and case
159
+ // expressions through their base constraints, then computes missing literal
160
+ // branches before applying any policy option. Open primitive constituents are
161
+ // recorded independently, so `string | undefined` keeps `undefined` enumerable
162
+ // without pretending the `string` portion can be exhausted.
163
+ func switchExhaustivenessCheckCollectMetadata(
164
+ ctx *Context,
165
+ node *shimast.Node,
166
+ commentPattern *regexp.Regexp,
167
+ ) *switchExhaustivenessCheckMetadata {
168
+ sw := node.AsSwitchStatement()
169
+ if sw == nil || sw.Expression == nil || sw.CaseBlock == nil {
170
+ return nil
171
+ }
172
+ block := sw.CaseBlock.AsCaseBlock()
173
+ if block == nil || block.Clauses == nil {
174
+ return nil
175
+ }
176
+
177
+ discriminantType := switchExhaustivenessCheckConstrainedType(
178
+ ctx.Checker,
179
+ ctx.Checker.GetTypeAtLocation(sw.Expression),
180
+ )
181
+ if discriminantType == nil {
182
+ return nil
183
+ }
184
+
185
+ caseTypes := make([]*shimchecker.Type, 0, len(block.Clauses.Nodes))
186
+ var defaultCase *switchExhaustivenessCheckDefaultCase
187
+ var lastClause *shimast.Node
71
188
  for _, clause := range block.Clauses.Nodes {
72
- if clause == nil || clause.Kind != shimast.KindCaseClause {
189
+ if clause == nil {
190
+ continue
191
+ }
192
+ lastClause = clause
193
+ if clause.Kind == shimast.KindDefaultClause {
194
+ if defaultCase == nil {
195
+ pos, _ := tokenRange(ctx.File, clause)
196
+ defaultCase = &switchExhaustivenessCheckDefaultCase{node: clause, pos: pos}
197
+ }
198
+ continue
199
+ }
200
+ if clause.Kind != shimast.KindCaseClause {
73
201
  continue
74
202
  }
75
203
  caseClause := clause.AsCaseOrDefaultClause()
76
204
  if caseClause == nil || caseClause.Expression == nil {
77
205
  continue
78
206
  }
79
- caseType := ctx.Checker.GetTypeAtLocation(caseClause.Expression)
80
- if caseType == nil {
207
+ caseType := switchExhaustivenessCheckConstrainedType(
208
+ ctx.Checker,
209
+ ctx.Checker.GetTypeAtLocation(caseClause.Expression),
210
+ )
211
+ if caseType != nil {
212
+ caseTypes = append(caseTypes, caseType)
213
+ }
214
+ }
215
+ if defaultCase == nil {
216
+ defaultCase = switchExhaustivenessCheckCommentDefaultCase(
217
+ ctx,
218
+ sw.CaseBlock,
219
+ lastClause,
220
+ commentPattern,
221
+ )
222
+ }
223
+
224
+ members, containsNonLiteralType := switchExhaustivenessCheckTypeParts(ctx.Checker, discriminantType)
225
+ missing := make([]*shimchecker.Type, 0, len(members))
226
+ for _, member := range members {
227
+ if !switchExhaustivenessCheckCaseTypesCover(caseTypes, member) {
228
+ missing = append(missing, member)
229
+ }
230
+ }
231
+ return &switchExhaustivenessCheckMetadata{
232
+ containsNonLiteralType: containsNonLiteralType,
233
+ defaultCase: defaultCase,
234
+ caseBlock: sw.CaseBlock,
235
+ lastClause: lastClause,
236
+ missingMembers: missing,
237
+ }
238
+ }
239
+
240
+ func switchExhaustivenessCheckConstrainedType(
241
+ checker *shimchecker.Checker,
242
+ t *shimchecker.Type,
243
+ ) *shimchecker.Type {
244
+ if checker == nil || t == nil {
245
+ return t
246
+ }
247
+ if constraint := checker.GetBaseConstraintOfType(t); constraint != nil {
248
+ t = constraint
249
+ }
250
+ return shimchecker.Checker_getRegularTypeOfLiteralType(checker, t)
251
+ }
252
+
253
+ // switchExhaustivenessCheckTypeParts mirrors typescript-eslint's nested
254
+ // unionConstituents/intersectionConstituents walk. Literal-like intersection
255
+ // pieces remain enumerable even when another piece is open-ended. A union part
256
+ // is considered non-literal only when every one of its intersection pieces is
257
+ // non-literal.
258
+ func switchExhaustivenessCheckTypeParts(
259
+ checker *shimchecker.Checker,
260
+ t *shimchecker.Type,
261
+ ) ([]*shimchecker.Type, bool) {
262
+ if t == nil {
263
+ return nil, false
264
+ }
265
+ unionParts := []*shimchecker.Type{t}
266
+ if t.Flags()&shimchecker.TypeFlagsUnion != 0 {
267
+ unionParts = t.Types()
268
+ }
269
+
270
+ members := make([]*shimchecker.Type, 0, len(unionParts))
271
+ seen := make(map[*shimchecker.Type]struct{}, len(unionParts))
272
+ containsNonLiteralType := false
273
+ for _, unionPart := range unionParts {
274
+ if unionPart == nil {
81
275
  continue
82
276
  }
83
- covered[caseType] = true
277
+ intersectionParts := []*shimchecker.Type{unionPart}
278
+ if unionPart.Flags()&shimchecker.TypeFlagsIntersection != 0 {
279
+ intersectionParts = unionPart.Types()
280
+ }
281
+ partContainsLiteral := false
282
+ for _, intersectionPart := range intersectionParts {
283
+ intersectionPart = shimchecker.Checker_getRegularTypeOfLiteralType(checker, intersectionPart)
284
+ if !switchExhaustivenessCheckIsLiteralLike(intersectionPart) {
285
+ continue
286
+ }
287
+ partContainsLiteral = true
288
+ if switchExhaustivenessCheckContainsEquivalentMember(members, intersectionPart) {
289
+ continue
290
+ }
291
+ if _, ok := seen[intersectionPart]; ok {
292
+ continue
293
+ }
294
+ seen[intersectionPart] = struct{}{}
295
+ members = append(members, intersectionPart)
296
+ }
297
+ if !partContainsLiteral {
298
+ containsNonLiteralType = true
299
+ }
300
+ }
301
+ return members, containsNonLiteralType
302
+ }
303
+
304
+ func switchExhaustivenessCheckContainsEquivalentMember(
305
+ members []*shimchecker.Type,
306
+ candidate *shimchecker.Type,
307
+ ) bool {
308
+ if candidate == nil || candidate.Flags()&shimchecker.TypeFlagsUndefined == 0 {
309
+ return false
84
310
  }
85
- missing := 0
86
311
  for _, member := range members {
87
- if !covered[member] {
88
- missing++
312
+ if member != nil && member.Flags()&shimchecker.TypeFlagsUndefined != 0 {
313
+ return true
89
314
  }
90
315
  }
91
- if missing == 0 {
92
- return
316
+ return false
317
+ }
318
+
319
+ func switchExhaustivenessCheckCaseTypesCover(
320
+ caseTypes []*shimchecker.Type,
321
+ member *shimchecker.Type,
322
+ ) bool {
323
+ if member == nil {
324
+ return false
93
325
  }
94
- ctx.Report(node, "Switch is not exhaustive. Cases not matched: add a `case` for every union/enum member, or include a `default` clause.")
326
+ for _, caseType := range caseTypes {
327
+ if caseType == member {
328
+ return true
329
+ }
330
+ // TypeScript represents `undefined`, optional, and no-unchecked-indexed-
331
+ // access missing types with distinct pointers. They all carry the
332
+ // Undefined flag and denote the same runtime switch value.
333
+ if caseType != nil &&
334
+ caseType.Flags()&shimchecker.TypeFlagsUndefined != 0 &&
335
+ member.Flags()&shimchecker.TypeFlagsUndefined != 0 {
336
+ return true
337
+ }
338
+ }
339
+ return false
95
340
  }
96
341
 
97
- // switchExhaustivenessCheckCollectMembers returns the finite literal /
98
- // enum-literal constituents of t when t is a discriminable union, and
99
- // nil when t is open-ended (plain `string`, `number`, `any`, `unknown`,
100
- // etc.). A union whose constituents are all literals or enum literals
101
- // is treated as exhaustively coverable; any non-literal constituent
102
- // (e.g. `string` mixed into `"a" | "b" | string`) widens the union and
103
- // disqualifies the whole discriminant.
104
- func switchExhaustivenessCheckCollectMembers(t *shimchecker.Type) []*shimchecker.Type {
342
+ func switchExhaustivenessCheckIsLiteralLike(t *shimchecker.Type) bool {
105
343
  if t == nil {
344
+ return false
345
+ }
346
+ return t.Flags()&(shimchecker.TypeFlagsLiteral|
347
+ shimchecker.TypeFlagsUndefined|
348
+ shimchecker.TypeFlagsNull|
349
+ shimchecker.TypeFlagsUniqueESSymbol) != 0
350
+ }
351
+
352
+ func switchExhaustivenessCheckTypeName(
353
+ checker *shimchecker.Checker,
354
+ location *shimast.Node,
355
+ t *shimchecker.Type,
356
+ ) string {
357
+ if t == nil {
358
+ return "unknown"
359
+ }
360
+ if t.Flags()&shimchecker.TypeFlagsESSymbolLike != 0 {
361
+ if symbol := t.Symbol(); symbol != nil {
362
+ if name := shimchecker.Checker_symbolToValueString(checker, symbol, location); name != "" {
363
+ return "typeof " + name
364
+ }
365
+ if symbol.Name != "" {
366
+ return "typeof " + symbol.Name
367
+ }
368
+ }
369
+ }
370
+ if name := shimchecker.Checker_typeToStringFullyQualified(checker, t, location); name != "" {
371
+ return name
372
+ }
373
+ return checker.TypeToString(t)
374
+ }
375
+
376
+ func switchExhaustivenessCheckSuggestion(
377
+ ctx *Context,
378
+ switchNode *shimast.Node,
379
+ metadata *switchExhaustivenessCheckMetadata,
380
+ members []*shimchecker.Type,
381
+ includeDefault bool,
382
+ ) (TextEdit, bool) {
383
+ if ctx == nil || ctx.File == nil || ctx.Checker == nil ||
384
+ switchNode == nil || metadata == nil {
385
+ return TextEdit{}, false
386
+ }
387
+ sw := switchNode.AsSwitchStatement()
388
+ if sw == nil || sw.Expression == nil {
389
+ return TextEdit{}, false
390
+ }
391
+ branches := make([]string, 0, len(members)+1)
392
+ for _, member := range members {
393
+ expression, ok := switchExhaustivenessCheckCaseExpression(
394
+ ctx.Checker,
395
+ sw.Expression,
396
+ member,
397
+ )
398
+ if !ok {
399
+ return TextEdit{}, false
400
+ }
401
+ branches = append(
402
+ branches,
403
+ `case `+expression+`: { throw new Error("Not implemented yet"); }`,
404
+ )
405
+ }
406
+ if includeDefault {
407
+ branches = append(branches, `default: { throw new Error("Not implemented yet"); }`)
408
+ }
409
+ if len(branches) == 0 {
410
+ return TextEdit{}, false
411
+ }
412
+ return switchExhaustivenessCheckInsertionEdit(ctx.File, switchNode, metadata, branches)
413
+ }
414
+
415
+ func switchExhaustivenessCheckCaseExpression(
416
+ checker *shimchecker.Checker,
417
+ location *shimast.Node,
418
+ member *shimchecker.Type,
419
+ ) (string, bool) {
420
+ if checker == nil || location == nil || member == nil {
421
+ return "", false
422
+ }
423
+ if symbol := member.Symbol(); symbol != nil {
424
+ if !shimchecker.Checker_isSymbolAccessibleAsValue(checker, symbol, location) {
425
+ return "", false
426
+ }
427
+ expression := strings.TrimSpace(
428
+ shimchecker.Checker_symbolToValueString(checker, symbol, location),
429
+ )
430
+ return expression, expression != ""
431
+ }
432
+ expression := strings.TrimSpace(
433
+ shimchecker.Checker_typeToStringFullyQualified(checker, member, location),
434
+ )
435
+ return expression, expression != ""
436
+ }
437
+
438
+ func switchExhaustivenessCheckInsertionEdit(
439
+ file *shimast.SourceFile,
440
+ switchNode *shimast.Node,
441
+ metadata *switchExhaustivenessCheckMetadata,
442
+ branches []string,
443
+ ) (TextEdit, bool) {
444
+ if file == nil || switchNode == nil || metadata == nil || len(branches) == 0 {
445
+ return TextEdit{}, false
446
+ }
447
+ source := file.Text()
448
+ newline := "\n"
449
+ if strings.Contains(source, "\r\n") {
450
+ newline = "\r\n"
451
+ }
452
+
453
+ if metadata.defaultCase != nil &&
454
+ metadata.defaultCase.pos >= 0 && metadata.defaultCase.pos <= len(source) {
455
+ pos := metadata.defaultCase.pos
456
+ indent := switchExhaustivenessCheckLineIndent(source, pos)
457
+ text := strings.Join(branches, newline+indent) + newline + indent
458
+ if !switchExhaustivenessCheckLineHasOnlyIndent(source, pos) {
459
+ indent = switchExhaustivenessCheckCaseIndent(source, file, switchNode, metadata)
460
+ text = newline + indent + strings.Join(branches, newline+indent) + newline + indent
461
+ }
462
+ return TextEdit{Pos: pos, End: pos, Text: text}, true
463
+ }
464
+
465
+ if metadata.lastClause != nil {
466
+ pos := metadata.lastClause.End()
467
+ if pos < 0 || pos > len(source) {
468
+ return TextEdit{}, false
469
+ }
470
+ clausePos, _ := tokenRange(file, metadata.lastClause)
471
+ indent := switchExhaustivenessCheckLineIndent(source, clausePos)
472
+ text := newline + indent + strings.Join(branches, newline+indent)
473
+ return TextEdit{Pos: pos, End: pos, Text: text}, true
474
+ }
475
+
476
+ if metadata.caseBlock == nil ||
477
+ shimscanner.ScanTokenAtPosition(file, metadata.caseBlock.Pos()) != shimast.KindOpenBraceToken {
478
+ return TextEdit{}, false
479
+ }
480
+ openingBrace := shimscanner.GetRangeOfTokenAtPosition(file, metadata.caseBlock.Pos())
481
+ if shimscanner.ScanTokenAtPosition(file, openingBrace.End()) != shimast.KindCloseBraceToken {
482
+ return TextEdit{}, false
483
+ }
484
+ closingBrace := shimscanner.GetRangeOfTokenAtPosition(file, openingBrace.End())
485
+ if openingBrace.End() < 0 || closingBrace.Pos() < openingBrace.End() ||
486
+ closingBrace.Pos() > len(source) {
487
+ return TextEdit{}, false
488
+ }
489
+ switchPos, _ := tokenRange(file, switchNode)
490
+ switchIndent := switchExhaustivenessCheckLineIndent(source, switchPos)
491
+ caseIndent := switchIndent + " "
492
+ branchText := strings.Join(branches, newline+caseIndent)
493
+ interior := source[openingBrace.End():closingBrace.Pos()]
494
+ if strings.TrimSpace(interior) == "" {
495
+ return TextEdit{
496
+ Pos: openingBrace.End(),
497
+ End: closingBrace.Pos(),
498
+ Text: newline + caseIndent + branchText + newline + switchIndent,
499
+ }, true
500
+ }
501
+ return TextEdit{
502
+ Pos: openingBrace.End(),
503
+ End: openingBrace.End(),
504
+ Text: newline + caseIndent + branchText + newline + caseIndent,
505
+ }, true
506
+ }
507
+
508
+ func switchExhaustivenessCheckLineIndent(source string, pos int) string {
509
+ if pos < 0 || pos > len(source) {
510
+ return ""
511
+ }
512
+ lineStart := strings.LastIndex(source[:pos], "\n") + 1
513
+ indent := source[lineStart:pos]
514
+ for _, ch := range indent {
515
+ if ch != ' ' && ch != '\t' && ch != '\r' {
516
+ return ""
517
+ }
518
+ }
519
+ return indent
520
+ }
521
+
522
+ func switchExhaustivenessCheckLineHasOnlyIndent(source string, pos int) bool {
523
+ if pos < 0 || pos > len(source) {
524
+ return false
525
+ }
526
+ lineStart := strings.LastIndex(source[:pos], "\n") + 1
527
+ return strings.TrimSpace(source[lineStart:pos]) == ""
528
+ }
529
+
530
+ func switchExhaustivenessCheckCaseIndent(
531
+ source string,
532
+ file *shimast.SourceFile,
533
+ switchNode *shimast.Node,
534
+ metadata *switchExhaustivenessCheckMetadata,
535
+ ) string {
536
+ if metadata != nil && metadata.lastClause != nil {
537
+ clausePos, _ := tokenRange(file, metadata.lastClause)
538
+ if switchExhaustivenessCheckLineHasOnlyIndent(source, clausePos) {
539
+ return switchExhaustivenessCheckLineIndent(source, clausePos)
540
+ }
541
+ }
542
+ switchPos, _ := tokenRange(file, switchNode)
543
+ return switchExhaustivenessCheckLineIndent(source, switchPos) + " "
544
+ }
545
+
546
+ // switchExhaustivenessCheckCommentDefaultCase recognizes only the last comment
547
+ // after the last real clause and before the case block's closing brace. This is
548
+ // the same scope as SourceCode#getCommentsAfter(lastCase); a matching comment
549
+ // elsewhere in the switch cannot suppress a diagnostic.
550
+ func switchExhaustivenessCheckCommentDefaultCase(
551
+ ctx *Context,
552
+ caseBlock *shimast.Node,
553
+ lastClause *shimast.Node,
554
+ pattern *regexp.Regexp,
555
+ ) *switchExhaustivenessCheckDefaultCase {
556
+ if ctx.File == nil || caseBlock == nil || lastClause == nil || pattern == nil {
106
557
  return nil
107
558
  }
108
- flags := t.Flags()
109
- if flags&shimchecker.TypeFlagsUnion == 0 {
559
+ text := ctx.File.Text()
560
+ from := lastClause.End()
561
+ if shimscanner.ScanTokenAtPosition(ctx.File, lastClause.End()) != shimast.KindCloseBraceToken {
110
562
  return nil
111
563
  }
112
- parts := t.Types()
113
- if len(parts) == 0 {
564
+ closeBrace := shimscanner.GetRangeOfTokenAtPosition(ctx.File, lastClause.End())
565
+ to := closeBrace.Pos()
566
+ if from < 0 || to < from || to > len(text) {
114
567
  return nil
115
568
  }
116
- members := make([]*shimchecker.Type, 0, len(parts))
117
- for _, part := range parts {
118
- if part == nil {
119
- return nil
120
- }
121
- if !switchExhaustivenessCheckIsLiteralLike(part) {
122
- return nil
123
- }
124
- members = append(members, part)
569
+ candidateKind := shimast.KindUnknown
570
+ candidatePos := -1
571
+ candidateEnd := -1
572
+ // `from:to` is itself a parser-classified trivia gap: it begins after the
573
+ // last clause and ends before the case block's closing-brace token. Scan the
574
+ // bounded gap directly so a file with many switches does not re-enumerate
575
+ // the whole AST for every switch node.
576
+ scanCommentGap(shimscanner.NewScanner(), text, from, to, func(kind shimast.Kind, pos, end int) {
577
+ candidateKind = kind
578
+ candidatePos = pos
579
+ candidateEnd = end
580
+ })
581
+ if candidatePos < 0 || candidateEnd <= candidatePos {
582
+ return nil
583
+ }
584
+ value := text[candidatePos:candidateEnd]
585
+ switch candidateKind {
586
+ case shimast.KindSingleLineCommentTrivia:
587
+ value = strings.TrimPrefix(value, "//")
588
+ case shimast.KindMultiLineCommentTrivia:
589
+ value = strings.TrimSuffix(strings.TrimPrefix(value, "/*"), "*/")
590
+ }
591
+ if !pattern.MatchString(strings.TrimSpace(value)) {
592
+ return nil
593
+ }
594
+ return &switchExhaustivenessCheckDefaultCase{
595
+ pos: candidatePos,
596
+ end: candidateEnd,
125
597
  }
126
- return members
127
598
  }
128
599
 
129
- // switchExhaustivenessCheckIsLiteralLike reports whether t is a
130
- // constituent that participates in finite enumeration: a unit type
131
- // (string/number/bigint/boolean literal), an enum literal, or one of
132
- // the nullish singletons. The matching set mirrors typescript-eslint's
133
- // `lib.isTypeFlagSet(constituent, TypeFlags.Literal | ...)` filter.
134
- func switchExhaustivenessCheckIsLiteralLike(t *shimchecker.Type) bool {
135
- flags := t.Flags()
136
- if flags&shimchecker.TypeFlagsLiteral != 0 {
137
- return true
600
+ func switchExhaustivenessCheckReportDefaultCase(
601
+ ctx *Context,
602
+ defaultCase *switchExhaustivenessCheckDefaultCase,
603
+ message string,
604
+ ) {
605
+ if defaultCase == nil {
606
+ return
138
607
  }
139
- if flags&(shimchecker.TypeFlagsUndefined|shimchecker.TypeFlagsNull|shimchecker.TypeFlagsVoid) != 0 {
140
- return true
608
+ if defaultCase.node != nil {
609
+ ctx.Report(defaultCase.node, message)
610
+ return
141
611
  }
142
- return false
612
+ ctx.ReportRange(defaultCase.pos, defaultCase.end, message)
143
613
  }
144
614
 
145
615
  func init() {