@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
@@ -0,0 +1,266 @@
1
+ // noDuplicateImports reports an import (and, with `includeExports`, a
2
+ // re-export) declaration whose module specifier already appeared above,
3
+ // but only when the two declarations could be consolidated into one
4
+ // legal declaration. Same-module declarations TypeScript cannot merge —
5
+ // named next to namespace bindings, or (since ESLint 9.30.1) a
6
+ // type-only default next to type-only named bindings — are not
7
+ // duplicates, and `allowSeparateTypeImports` additionally keeps one
8
+ // clause-level `import type` declaration apart from value-bearing
9
+ // declarations.
10
+ // https://eslint.org/docs/latest/rules/no-duplicate-imports
11
+ package linthost
12
+
13
+ import (
14
+ "strings"
15
+
16
+ shimast "github.com/microsoft/typescript-go/shim/ast"
17
+ )
18
+
19
+ // noDuplicateImportsOptions mirrors the official rule's options object.
20
+ // Both keys default to false, matching ESLint's `defaultOptions`.
21
+ type noDuplicateImportsOptions struct {
22
+ // AllowSeparateTypeImports keeps clause-level `import type` / `export
23
+ // type` declarations out of the duplicate comparison with
24
+ // value-bearing declarations of the same module. Inline type
25
+ // specifiers (`import { type Foo }`) stay on the value side because
26
+ // their import clause is not type-only.
27
+ AllowSeparateTypeImports bool `json:"allowSeparateTypeImports"`
28
+
29
+ // IncludeExports folds `export … from` declarations into the same
30
+ // duplicate/mergeability analysis, adding the official
31
+ // duplicated-as-export / duplicated-as-import pairings.
32
+ IncludeExports bool `json:"includeExports"`
33
+ }
34
+
35
+ // importExportCategory mirrors the specifier categories the official
36
+ // implementation derives from ESTree specifier types
37
+ // (`getImportExportType`).
38
+ type importExportCategory int
39
+
40
+ const (
41
+ // `import def from "m"` — ImportDefaultSpecifier.
42
+ importExportDefault importExportCategory = iota
43
+ // `import { a } from "m"` / `export { a } from "m"` —
44
+ // ImportSpecifier / ExportSpecifier.
45
+ importExportNamed
46
+ // `import * as ns from "m"` / `export * as ns from "m"` —
47
+ // ImportNamespaceSpecifier / ExportNamespaceSpecifier.
48
+ importExportNamespace
49
+ // `export * from "m"` — ExportAllDeclaration without an alias.
50
+ importExportAll
51
+ // `import "m"` and specifier-less clauses such as `import {} from
52
+ // "m"` / `export {} from "m"` — SideEffectImport.
53
+ importExportSideEffect
54
+ )
55
+
56
+ // importExportEntry retains the shape of one earlier import/re-export
57
+ // declaration so later declarations of the same module can answer the
58
+ // mergeability question against it.
59
+ type importExportEntry struct {
60
+ node *shimast.Node
61
+ category importExportCategory
62
+ typeOnly bool
63
+ isExport bool
64
+ }
65
+
66
+ type noDuplicateImports struct{ optionsRule }
67
+
68
+ func (noDuplicateImports) Name() string { return "no-duplicate-imports" }
69
+ func (noDuplicateImports) Visits() []shimast.Kind {
70
+ return []shimast.Kind{shimast.KindSourceFile}
71
+ }
72
+ func (noDuplicateImports) Check(ctx *Context, node *shimast.Node) {
73
+ var opts noDuplicateImportsOptions
74
+ _ = ctx.DecodeOptions(&opts)
75
+ modules := map[string][]importExportEntry{}
76
+ node.ForEachChild(func(child *shimast.Node) bool {
77
+ if child == nil {
78
+ return false
79
+ }
80
+ switch child.Kind {
81
+ case shimast.KindImportDeclaration:
82
+ decl := child.AsImportDeclaration()
83
+ if decl == nil {
84
+ return false
85
+ }
86
+ module := duplicateImportsModule(decl.ModuleSpecifier)
87
+ if module == "" {
88
+ return false
89
+ }
90
+ entry := duplicateImportsImportEntry(child, decl)
91
+ reportDuplicateImports(ctx, opts, modules[module], entry, module)
92
+ modules[module] = append(modules[module], entry)
93
+ case shimast.KindExportDeclaration:
94
+ // Without `includeExports` the official rule does not even
95
+ // record re-exports, so they can neither be reported nor make
96
+ // a later import count as duplicated-as-export.
97
+ if !opts.IncludeExports {
98
+ return false
99
+ }
100
+ decl := child.AsExportDeclaration()
101
+ if decl == nil {
102
+ return false
103
+ }
104
+ module := duplicateImportsModule(decl.ModuleSpecifier)
105
+ if module == "" {
106
+ return false
107
+ }
108
+ entry := duplicateImportsExportEntry(child, decl)
109
+ reportDuplicateImports(ctx, opts, modules[module], entry, module)
110
+ modules[module] = append(modules[module], entry)
111
+ }
112
+ return false
113
+ })
114
+ }
115
+
116
+ // duplicateImportsModule returns the trimmed module specifier text, or
117
+ // "" when the declaration has no usable string specifier (`export { x
118
+ // }` without `from`, an empty string module). Trimming mirrors the
119
+ // official implementation, which compares `source.value.trim()`.
120
+ func duplicateImportsModule(spec *shimast.Node) string {
121
+ return strings.TrimSpace(stringLiteralText(spec))
122
+ }
123
+
124
+ // duplicateImportsImportEntry categorizes one `import` declaration the
125
+ // way the official rule reads ESTree specifiers: the first named or
126
+ // namespace specifier decides the category, a lone default binding is
127
+ // the default category, and a clause-less or specifier-less import is a
128
+ // side-effect import.
129
+ func duplicateImportsImportEntry(node *shimast.Node, decl *shimast.ImportDeclaration) importExportEntry {
130
+ entry := importExportEntry{node: node, category: importExportSideEffect}
131
+ clause := decl.ImportClause
132
+ if clause == nil {
133
+ return entry
134
+ }
135
+ entry.typeOnly = clause.IsTypeOnly()
136
+ clauseData := clause.AsImportClause()
137
+ if clauseData == nil {
138
+ return entry
139
+ }
140
+ if bindings := clauseData.NamedBindings; bindings != nil {
141
+ switch bindings.Kind {
142
+ case shimast.KindNamespaceImport:
143
+ entry.category = importExportNamespace
144
+ return entry
145
+ case shimast.KindNamedImports:
146
+ // `import def, {} from "m"` has no named specifier in ESTree;
147
+ // the empty block falls through to the default binding below.
148
+ if named := bindings.AsNamedImports(); named != nil && named.Elements != nil && len(named.Elements.Nodes) > 0 {
149
+ entry.category = importExportNamed
150
+ return entry
151
+ }
152
+ }
153
+ }
154
+ if clauseData.Name() != nil {
155
+ entry.category = importExportDefault
156
+ }
157
+ return entry
158
+ }
159
+
160
+ // duplicateImportsExportEntry categorizes one `export … from`
161
+ // declaration. `export * from` is the export-all category, `export * as
162
+ // ns from` is a namespace specifier, non-empty `export { … } from` is
163
+ // named, and the degenerate `export {} from "m"` matches the official
164
+ // specifier-less SideEffectImport category.
165
+ func duplicateImportsExportEntry(node *shimast.Node, decl *shimast.ExportDeclaration) importExportEntry {
166
+ entry := importExportEntry{
167
+ node: node,
168
+ category: importExportSideEffect,
169
+ typeOnly: decl.IsTypeOnly,
170
+ isExport: true,
171
+ }
172
+ clause := decl.ExportClause
173
+ if clause == nil {
174
+ entry.category = importExportAll
175
+ return entry
176
+ }
177
+ switch clause.Kind {
178
+ case shimast.KindNamespaceExport:
179
+ entry.category = importExportNamespace
180
+ case shimast.KindNamedExports:
181
+ if named := clause.AsNamedExports(); named != nil && named.Elements != nil && len(named.Elements.Nodes) > 0 {
182
+ entry.category = importExportNamed
183
+ }
184
+ }
185
+ return entry
186
+ }
187
+
188
+ // reportDuplicateImports mirrors the official checkAndReport: an import
189
+ // is reported once against earlier imports and once more against
190
+ // earlier re-exports, a re-export against earlier re-exports and
191
+ // earlier imports, each pairing with its own message. Earlier
192
+ // re-exports only exist when `includeExports` recorded them.
193
+ func reportDuplicateImports(ctx *Context, opts noDuplicateImportsOptions, previous []importExportEntry, entry importExportEntry, module string) {
194
+ if len(previous) == 0 {
195
+ return
196
+ }
197
+ if entry.isExport {
198
+ if duplicateImportsShouldReport(entry, previous, true, opts.AllowSeparateTypeImports) {
199
+ ctx.Report(entry.node, "`"+module+"` export is duplicated.")
200
+ }
201
+ if duplicateImportsShouldReport(entry, previous, false, opts.AllowSeparateTypeImports) {
202
+ ctx.Report(entry.node, "`"+module+"` export is duplicated as import.")
203
+ }
204
+ return
205
+ }
206
+ if duplicateImportsShouldReport(entry, previous, false, opts.AllowSeparateTypeImports) {
207
+ ctx.Report(entry.node, "`"+module+"` import is duplicated.")
208
+ }
209
+ if duplicateImportsShouldReport(entry, previous, true, opts.AllowSeparateTypeImports) {
210
+ ctx.Report(entry.node, "`"+module+"` import is duplicated as export.")
211
+ }
212
+ }
213
+
214
+ // duplicateImportsShouldReport mirrors the official
215
+ // shouldReportImportExport: the new declaration is a duplicate when at
216
+ // least one earlier declaration of the requested kind could legally
217
+ // absorb it. With `allowSeparateTypeImports`, earlier declarations
218
+ // whose clause-level type-ness differs from the new declaration's are
219
+ // exempt from the comparison.
220
+ func duplicateImportsShouldReport(entry importExportEntry, previous []importExportEntry, wantExport bool, allowSeparateTypeImports bool) bool {
221
+ for _, prev := range previous {
222
+ if prev.isExport != wantExport {
223
+ continue
224
+ }
225
+ if allowSeparateTypeImports && entry.typeOnly != prev.typeOnly {
226
+ continue
227
+ }
228
+ if duplicateImportsCanMerge(entry, prev) {
229
+ return true
230
+ }
231
+ }
232
+ return false
233
+ }
234
+
235
+ // duplicateImportsCanMerge mirrors the official
236
+ // isImportExportCanBeMerged: two same-module declarations are duplicates
237
+ // only when one legal declaration could carry both clauses.
238
+ func duplicateImportsCanMerge(a, b importExportEntry) bool {
239
+ // ESLint 9.30.1 correction: a type-only default and type-only named
240
+ // bindings cannot be merged, because `import type Def, { Named }` is
241
+ // not legal TypeScript.
242
+ if a.typeOnly && b.typeOnly {
243
+ if (a.category == importExportDefault && b.category == importExportNamed) ||
244
+ (b.category == importExportDefault && a.category == importExportNamed) {
245
+ return false
246
+ }
247
+ }
248
+ // `export * from` merges only with another export-all or with a bare
249
+ // side-effect import; default, named, and namespace forms all need a
250
+ // different declaration shape.
251
+ if (a.category == importExportAll && b.category != importExportAll && b.category != importExportSideEffect) ||
252
+ (b.category == importExportAll && a.category != importExportAll && a.category != importExportSideEffect) {
253
+ return false
254
+ }
255
+ // A namespace binding and named bindings cannot share one
256
+ // declaration.
257
+ if (a.category == importExportNamespace && b.category == importExportNamed) ||
258
+ (b.category == importExportNamespace && a.category == importExportNamed) {
259
+ return false
260
+ }
261
+ return true
262
+ }
263
+
264
+ func init() {
265
+ Register(noDuplicateImports{})
266
+ }
@@ -1,71 +1,189 @@
1
- // noElseReturn: when an `if` block ends in a `return` / `throw` /
2
- // `break` / `continue` statement, the matching `else` block is
3
- // redundant control already left the function (or loop), so flatten
4
- // the `else` body into the surrounding scope to keep one less level of
5
- // nesting and one less branch to read.
1
+ // noElseReturn: an `else` block whose preceding `if` branch ends in a
2
+ // `return` is redundant control already left the function on that
3
+ // branch, so the `else` body can be flattened into the surrounding
4
+ // scope for one less level of nesting and one less branch to read.
6
5
  // https://eslint.org/docs/latest/rules/no-else-return
7
6
  //
8
- // AST-only: each visited `IfStatement` whose `ThenStatement` provably
9
- // terminates control flow on every branch reports on the `else`
10
- // keyword. A `then` branch terminates when:
7
+ // This is an AST-only port of the ESLint rule, faithful to its
8
+ // analysis:
11
9
  //
12
- // - it is itself a terminating statement (`return`, `throw`, etc.), or
13
- // - it is a `Block` whose final statement terminates AND every
14
- // `if/else if/else` chain inside it terminates on every leaf, or
15
- // - it is an `IfStatement` whose `then` AND `else` both terminate
16
- // (chained early returns).
17
- //
18
- // The analysis is conservative; ambiguous shapes (e.g. an `if` with no
19
- // `else`, or a loop with `break` only) are left alone.
10
+ // - Only `return` terminates a branch (`checkForReturn`). `throw`,
11
+ // `break`, and `continue` do NOT count matching upstream, which
12
+ // leaves those shapes alone.
13
+ // - `allowElseIf` (default `true`) walks the `if / else if` chain and
14
+ // bails when it ends without a plain `else`, so the very common
15
+ // `return` + `else if` early-return chain stays valid. Set it to
16
+ // `false` to also flag an `else if`.
17
+ // - Analysis starts only at an `if` that sits directly in a statement
18
+ // list (source file, block, class static block, or switch clause),
19
+ // mirroring upstream's `STATEMENT_LIST_PARENTS` gate. A nested
20
+ // `else if` is reached through its chain head's walk, so the rule
21
+ // reports exactly ONCE — on the chain's terminal `else`.
20
22
  package linthost
21
23
 
22
24
  import (
23
25
  shimast "github.com/microsoft/typescript-go/shim/ast"
24
26
  )
25
27
 
26
- type noElseReturn struct{}
28
+ const noElseReturnMessage = "Remove the `else` — the preceding `if` branch already returns."
29
+
30
+ type noElseReturn struct{ optionsRule }
31
+
32
+ // noElseReturnOptions mirrors ESLint's single object option. `AllowElseIf`
33
+ // is a pointer so a missing option decodes to the upstream default (`true`)
34
+ // rather than Go's zero value (`false`).
35
+ type noElseReturnOptions struct {
36
+ AllowElseIf *bool `json:"allowElseIf"`
37
+ }
27
38
 
28
39
  func (noElseReturn) Name() string { return "no-else-return" }
29
40
  func (noElseReturn) Visits() []shimast.Kind {
30
41
  return []shimast.Kind{shimast.KindIfStatement}
31
42
  }
43
+
32
44
  func (noElseReturn) Check(ctx *Context, node *shimast.Node) {
45
+ if node == nil || node.AsIfStatement() == nil {
46
+ return
47
+ }
48
+ // Only analyze a chain head: an `if` whose parent is a statement list.
49
+ // A nested `else if` (parent is the outer `if`) is folded into its head's
50
+ // walk, and a bare loop/label body `if` is left alone — both match
51
+ // upstream's STATEMENT_LIST_PARENTS gate.
52
+ if !noElseReturnStatementListParent(node.Parent) {
53
+ return
54
+ }
55
+
56
+ allowElseIf := true
57
+ var opts noElseReturnOptions
58
+ _ = ctx.DecodeOptions(&opts)
59
+ if opts.AllowElseIf != nil {
60
+ allowElseIf = *opts.AllowElseIf
61
+ }
62
+
63
+ if allowElseIf {
64
+ noElseReturnCheckWithoutElse(ctx, node)
65
+ } else {
66
+ noElseReturnCheckWithElse(ctx, node)
67
+ }
68
+ }
69
+
70
+ // noElseReturnCheckWithoutElse ports upstream `checkIfWithoutElse`
71
+ // (`allowElseIf: true`). Starting at the chain head `node`, it walks the
72
+ // `if / else if` chain; if the chain ends without a plain `else` it bails,
73
+ // otherwise it reports once on the terminal `else` when every consequent
74
+ // always returns.
75
+ func noElseReturnCheckWithoutElse(ctx *Context, node *shimast.Node) {
76
+ consequents := make([]*shimast.Node, 0)
77
+ var alternate *shimast.Node
78
+ for current := node; current != nil && current.Kind == shimast.KindIfStatement; {
79
+ ifStmt := current.AsIfStatement()
80
+ if ifStmt == nil || ifStmt.ElseStatement == nil {
81
+ return
82
+ }
83
+ consequents = append(consequents, ifStmt.ThenStatement)
84
+ alternate = ifStmt.ElseStatement
85
+ current = ifStmt.ElseStatement
86
+ }
87
+ for _, consequent := range consequents {
88
+ if !noElseReturnAlwaysReturns(consequent) {
89
+ return
90
+ }
91
+ }
92
+ ctx.Report(alternate, noElseReturnMessage)
93
+ }
94
+
95
+ // noElseReturnCheckWithElse ports upstream `checkIfWithElse`
96
+ // (`allowElseIf: false`): a single `if` with an `else` (which may itself be
97
+ // an `else if`) reports when its consequent always returns.
98
+ func noElseReturnCheckWithElse(ctx *Context, node *shimast.Node) {
33
99
  ifStmt := node.AsIfStatement()
34
100
  if ifStmt == nil || ifStmt.ElseStatement == nil {
35
101
  return
36
102
  }
37
- if !noElseReturnTerminates(ifStmt.ThenStatement) {
38
- return
103
+ if noElseReturnAlwaysReturns(ifStmt.ThenStatement) {
104
+ ctx.Report(ifStmt.ElseStatement, noElseReturnMessage)
39
105
  }
40
- ctx.Report(ifStmt.ElseStatement, "Remove the `else` — the preceding branch already terminates.")
41
106
  }
42
107
 
43
- func noElseReturnTerminates(stmt *shimast.Node) bool {
44
- if stmt == nil {
108
+ // noElseReturnAlwaysReturns ports upstream `alwaysReturns`: a block returns
109
+ // when ANY of its statements returns (or is an `if/else` that returns on both
110
+ // paths); a non-block returns when it is itself such a statement.
111
+ func noElseReturnAlwaysReturns(node *shimast.Node) bool {
112
+ if node == nil {
45
113
  return false
46
114
  }
47
- switch stmt.Kind {
48
- case shimast.KindReturnStatement,
49
- shimast.KindThrowStatement,
50
- shimast.KindBreakStatement,
51
- shimast.KindContinueStatement:
52
- return true
53
- case shimast.KindBlock:
54
- block := stmt.AsBlock()
115
+ if node.Kind == shimast.KindBlock {
116
+ block := node.AsBlock()
55
117
  if block == nil || block.Statements == nil {
56
118
  return false
57
119
  }
58
- stmts := block.Statements.Nodes
59
- if len(stmts) == 0 {
60
- return false
120
+ for _, stmt := range block.Statements.Nodes {
121
+ if noElseReturnCheckForReturnOrIf(stmt) {
122
+ return true
123
+ }
61
124
  }
62
- return noElseReturnTerminates(stmts[len(stmts)-1])
63
- case shimast.KindIfStatement:
64
- inner := stmt.AsIfStatement()
65
- if inner == nil || inner.ElseStatement == nil {
125
+ return false
126
+ }
127
+ return noElseReturnCheckForReturnOrIf(node)
128
+ }
129
+
130
+ // noElseReturnCheckForReturnOrIf ports upstream `checkForReturnOrIf`.
131
+ func noElseReturnCheckForReturnOrIf(node *shimast.Node) bool {
132
+ return noElseReturnCheckForReturn(node) || noElseReturnCheckForIf(node)
133
+ }
134
+
135
+ // noElseReturnCheckForReturn ports upstream `checkForReturn`: the terminator
136
+ // is a `return`, and only a `return`.
137
+ func noElseReturnCheckForReturn(node *shimast.Node) bool {
138
+ return node != nil && node.Kind == shimast.KindReturnStatement
139
+ }
140
+
141
+ // noElseReturnCheckForIf ports upstream `checkForIf`: an `if` with an `else`
142
+ // whose then AND else branches each naively return (chained early returns).
143
+ func noElseReturnCheckForIf(node *shimast.Node) bool {
144
+ if node == nil || node.Kind != shimast.KindIfStatement {
145
+ return false
146
+ }
147
+ ifStmt := node.AsIfStatement()
148
+ if ifStmt == nil || ifStmt.ThenStatement == nil || ifStmt.ElseStatement == nil {
149
+ return false
150
+ }
151
+ return noElseReturnNaiveHasReturn(ifStmt.ElseStatement) &&
152
+ noElseReturnNaiveHasReturn(ifStmt.ThenStatement)
153
+ }
154
+
155
+ // noElseReturnNaiveHasReturn ports upstream `naiveHasReturn`: a block returns
156
+ // when its LAST statement is a `return`; a non-block returns when it is itself
157
+ // a `return`.
158
+ func noElseReturnNaiveHasReturn(node *shimast.Node) bool {
159
+ if node == nil {
160
+ return false
161
+ }
162
+ if node.Kind == shimast.KindBlock {
163
+ block := node.AsBlock()
164
+ if block == nil || block.Statements == nil || len(block.Statements.Nodes) == 0 {
66
165
  return false
67
166
  }
68
- return noElseReturnTerminates(inner.ThenStatement) && noElseReturnTerminates(inner.ElseStatement)
167
+ stmts := block.Statements.Nodes
168
+ return noElseReturnCheckForReturn(stmts[len(stmts)-1])
169
+ }
170
+ return noElseReturnCheckForReturn(node)
171
+ }
172
+
173
+ // noElseReturnStatementListParent reports whether `parent` is a node whose
174
+ // body is a statement list, the TypeScript-AST equivalents of upstream's
175
+ // STATEMENT_LIST_PARENTS (Program, BlockStatement, StaticBlock, SwitchCase).
176
+ func noElseReturnStatementListParent(parent *shimast.Node) bool {
177
+ if parent == nil {
178
+ return false
179
+ }
180
+ switch parent.Kind {
181
+ case shimast.KindSourceFile,
182
+ shimast.KindBlock,
183
+ shimast.KindClassStaticBlockDeclaration,
184
+ shimast.KindCaseClause,
185
+ shimast.KindDefaultClause:
186
+ return true
69
187
  }
70
188
  return false
71
189
  }
@@ -2,38 +2,134 @@ package linthost
2
2
 
3
3
  import shimast "github.com/microsoft/typescript-go/shim/ast"
4
4
 
5
- // noExtendNative: forbid assignments to `<Builtin>.prototype.<key>`,
6
- // which mutate a shared global and leak across the entire realm.
5
+ // noExtendNative forbids extending a native builtin's prototype, which mutates a
6
+ // shared global and leaks across the entire realm. Mirroring ESLint's
7
+ // no-extend-native, it flags every shape that adds to `<Builtin>.prototype`:
8
+ // direct member assignment (`X.prototype.y = …` and `X.prototype["y"] = …`) and
9
+ // `Object.defineProperty` / `Object.defineProperties` calls whose target is a
10
+ // native prototype. The `exceptions` option removes builtins from the set.
7
11
  // https://eslint.org/docs/latest/rules/no-extend-native
8
- type noExtendNative struct{}
12
+ type noExtendNative struct{ optionsRule }
9
13
 
10
- func (noExtendNative) Name() string { return "no-extend-native" }
11
- func (noExtendNative) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBinaryExpression} }
14
+ type noExtendNativeOptions struct {
15
+ Exceptions []string `json:"exceptions"`
16
+ }
17
+
18
+ func (noExtendNative) Name() string { return "no-extend-native" }
19
+ func (noExtendNative) Visits() []shimast.Kind {
20
+ return []shimast.Kind{shimast.KindBinaryExpression, shimast.KindCallExpression}
21
+ }
12
22
  func (noExtendNative) Check(ctx *Context, node *shimast.Node) {
23
+ switch node.Kind {
24
+ case shimast.KindBinaryExpression:
25
+ checkExtendNativeAssignment(ctx, node)
26
+ case shimast.KindCallExpression:
27
+ checkExtendNativeDefineProperty(ctx, node)
28
+ }
29
+ }
30
+
31
+ // checkExtendNativeAssignment reports `<Builtin>.prototype.<key> = …` and
32
+ // `<Builtin>.prototype["<key>"] = …`. Mirroring upstream's isAssigningToPropertyOf,
33
+ // the assigned member key itself is not inspected: any write to a member of the
34
+ // prototype extends it.
35
+ func checkExtendNativeAssignment(ctx *Context, node *shimast.Node) {
13
36
  expr := node.AsBinaryExpression()
14
- if expr == nil || expr.OperatorToken == nil {
37
+ if expr == nil || expr.OperatorToken == nil || expr.OperatorToken.Kind != shimast.KindEqualsToken {
15
38
  return
16
39
  }
17
- if expr.OperatorToken.Kind != shimast.KindEqualsToken {
40
+ target := stripParens(expr.Left)
41
+ if target == nil {
18
42
  return
19
43
  }
20
- outer := stripParens(expr.Left)
21
- if outer == nil || outer.Kind != shimast.KindPropertyAccessExpression {
44
+ var object *shimast.Node
45
+ switch target.Kind {
46
+ case shimast.KindPropertyAccessExpression:
47
+ if access := target.AsPropertyAccessExpression(); access != nil {
48
+ object = access.Expression
49
+ }
50
+ case shimast.KindElementAccessExpression:
51
+ if access := target.AsElementAccessExpression(); access != nil {
52
+ object = access.Expression
53
+ }
54
+ default:
22
55
  return
23
56
  }
24
- receiver := stripParens(outer.AsPropertyAccessExpression().Expression)
25
- if receiver == nil || receiver.Kind != shimast.KindPropertyAccessExpression {
26
- return
57
+ if builtin := extendNativePrototypeBuiltin(ctx, object); builtin != "" {
58
+ ctx.Report(node, builtin+" prototype is read only, properties should not be added.")
27
59
  }
28
- inner := receiver.AsPropertyAccessExpression()
29
- if identifierText(inner.Name()) != "prototype" {
60
+ }
61
+
62
+ // checkExtendNativeDefineProperty reports
63
+ // `Object.defineProperty(<Builtin>.prototype, …)` and the `defineProperties`
64
+ // variant, mirroring upstream's isInDefinePropertyCall: the extended prototype
65
+ // is the call's first argument.
66
+ func checkExtendNativeDefineProperty(ctx *Context, node *shimast.Node) {
67
+ call := node.AsCallExpression()
68
+ if call == nil || call.Arguments == nil || len(call.Arguments.Nodes) == 0 {
30
69
  return
31
70
  }
32
- builtin := identifierText(stripParens(inner.Expression))
33
- if !isExtendNativeBuiltin(builtin) {
71
+ callee := stripParens(call.Expression)
72
+ if !isMatchingPropertyAccess(callee, "Object", "defineProperty") &&
73
+ !isMatchingPropertyAccess(callee, "Object", "defineProperties") {
34
74
  return
35
75
  }
36
- ctx.Report(node, builtin+" prototype is read only, properties should not be added.")
76
+ if builtin := extendNativePrototypeBuiltin(ctx, call.Arguments.Nodes[0]); builtin != "" {
77
+ ctx.Report(node, builtin+" prototype is read only, properties should not be added.")
78
+ }
79
+ }
80
+
81
+ // extendNativePrototypeBuiltin returns the native builtin name when `node` reads
82
+ // `<Builtin>.prototype` — through `.prototype` property access or a static
83
+ // `["prototype"]` element access — and the builtin is protected (a known native
84
+ // not listed in the rule's `exceptions` option). Otherwise it returns "".
85
+ // Mirrors upstream's isPrototypePropertyAccessed, which resolves the accessed
86
+ // key with getStaticPropertyName, combined with the exceptions filter.
87
+ func extendNativePrototypeBuiltin(ctx *Context, node *shimast.Node) string {
88
+ node = stripParens(node)
89
+ if node == nil {
90
+ return ""
91
+ }
92
+ var object *shimast.Node
93
+ var key string
94
+ switch node.Kind {
95
+ case shimast.KindPropertyAccessExpression:
96
+ access := node.AsPropertyAccessExpression()
97
+ if access == nil {
98
+ return ""
99
+ }
100
+ object = access.Expression
101
+ key = identifierText(access.Name())
102
+ case shimast.KindElementAccessExpression:
103
+ access := node.AsElementAccessExpression()
104
+ if access == nil {
105
+ return ""
106
+ }
107
+ object = access.Expression
108
+ key = stringLiteralText(access.ArgumentExpression)
109
+ default:
110
+ return ""
111
+ }
112
+ if key != "prototype" {
113
+ return ""
114
+ }
115
+ builtin := identifierText(stripParens(object))
116
+ if !isExtendNativeBuiltin(builtin) || extendNativeExcepted(ctx, builtin) {
117
+ return ""
118
+ }
119
+ return builtin
120
+ }
121
+
122
+ // extendNativeExcepted reports whether the rule's `exceptions` option opts the
123
+ // given builtin out of the protected set.
124
+ func extendNativeExcepted(ctx *Context, builtin string) bool {
125
+ var options noExtendNativeOptions
126
+ _ = ctx.DecodeOptions(&options)
127
+ for _, name := range options.Exceptions {
128
+ if name == builtin {
129
+ return true
130
+ }
131
+ }
132
+ return false
37
133
  }
38
134
 
39
135
  func isExtendNativeBuiltin(name string) bool {