@ttsc/lint 0.18.4 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +95 -43
  2. package/go.mod +5 -4
  3. package/internal/rulecode/rulecode.go +88 -0
  4. package/lib/index.d.ts +10 -1
  5. package/lib/index.js +45 -17
  6. package/lib/index.js.map +1 -1
  7. package/lib/structures/ITtscLintConfig.d.ts +3 -0
  8. package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
  9. package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
  10. package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
  11. package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
  12. package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
  13. package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
  14. package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
  15. package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
  16. package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
  17. package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
  18. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
  19. package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
  20. package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
  21. package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
  22. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
  23. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
  24. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
  25. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
  26. package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
  27. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
  28. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
  29. package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
  30. package/lib/structures/rules/index.d.ts +3 -0
  31. package/lib/structures/rules/index.js +3 -0
  32. package/lib/structures/rules/index.js.map +1 -1
  33. package/linthost/ast_helpers.go +521 -23
  34. package/linthost/ast_selector_matcher.go +1628 -0
  35. package/linthost/ast_selector_parser.go +628 -0
  36. package/linthost/comment_scan.go +154 -0
  37. package/linthost/compile.go +129 -79
  38. package/linthost/config.go +352 -88
  39. package/linthost/config_format.go +15 -1
  40. package/linthost/contrib_adapter.go +47 -8
  41. package/linthost/declaration_rules.go +12 -11
  42. package/linthost/directives.go +9 -67
  43. package/linthost/dispatch.go +37 -3
  44. package/linthost/engine.go +368 -53
  45. package/linthost/escape_string.go +77 -0
  46. package/linthost/fix.go +133 -23
  47. package/linthost/flags_gen.go +18 -17
  48. package/linthost/format.go +73 -167
  49. package/linthost/format_editor_settings.go +134 -26
  50. package/linthost/grapheme_tables_gen.go +1920 -0
  51. package/linthost/host.go +160 -61
  52. package/linthost/literal_escape_scan.go +117 -0
  53. package/linthost/lsp.go +545 -98
  54. package/linthost/numeric_literal_precision.go +228 -0
  55. package/linthost/polyfill_browserslist.go +1439 -0
  56. package/linthost/polyfill_browserslist_config.go +603 -0
  57. package/linthost/polyfill_corejs.go +750 -0
  58. package/linthost/polyfill_data.go +113 -0
  59. package/linthost/polyfill_data_gen.json +39868 -0
  60. package/linthost/print_nodes_array.go +7 -1
  61. package/linthost/print_nodes_object.go +7 -1
  62. package/linthost/project_engine.go +151 -0
  63. package/linthost/project_rules.go +78 -0
  64. package/linthost/regex_clean.go +191 -0
  65. package/linthost/regex_tree.go +1544 -0
  66. package/linthost/regex_tree_optimizer.go +1685 -0
  67. package/linthost/regexp_cache.go +44 -0
  68. package/linthost/rule_codes.go +105 -0
  69. package/linthost/rule_codes.json +745 -0
  70. package/linthost/rules_ban_ts_comment.go +312 -0
  71. package/linthost/rules_boundaries.go +128 -43
  72. package/linthost/rules_boundaries_dependencies.go +1013 -0
  73. package/linthost/rules_core_extra.go +62 -92
  74. package/linthost/rules_cypress.go +1 -1
  75. package/linthost/rules_default_case.go +87 -11
  76. package/linthost/rules_dupes.go +38 -13
  77. package/linthost/rules_empty.go +188 -20
  78. package/linthost/rules_escape.go +62 -48
  79. package/linthost/rules_format_arrow_parens.go +19 -5
  80. package/linthost/rules_format_bracket_spacing.go +1 -1
  81. package/linthost/rules_format_clause_join.go +1 -1
  82. package/linthost/rules_format_declaration_header.go +31 -19
  83. package/linthost/rules_format_indent.go +1 -1
  84. package/linthost/rules_format_jsdoc.go +1 -1
  85. package/linthost/rules_format_orphan_semi.go +1 -1
  86. package/linthost/rules_format_parameter_properties.go +17 -7
  87. package/linthost/rules_format_print_width.go +1 -1
  88. package/linthost/rules_format_quote_props.go +1 -1
  89. package/linthost/rules_format_quotes.go +1 -1
  90. package/linthost/rules_format_semi.go +106 -79
  91. package/linthost/rules_format_sort_imports.go +79 -45
  92. package/linthost/rules_format_statement_split.go +1 -1
  93. package/linthost/rules_format_ternary_nullish_parens.go +1 -1
  94. package/linthost/rules_format_trailing_comma.go +64 -1
  95. package/linthost/rules_format_whitespace.go +1 -1
  96. package/linthost/rules_functional.go +21 -22
  97. package/linthost/rules_gap.go +80 -3
  98. package/linthost/rules_grouped_accessor_pairs.go +84 -25
  99. package/linthost/rules_guard_for_in.go +64 -147
  100. package/linthost/rules_jsx_a11y.go +232 -22
  101. package/linthost/rules_logic.go +84 -38
  102. package/linthost/rules_nextjs.go +105 -16
  103. package/linthost/rules_no_class_assign.go +70 -0
  104. package/linthost/rules_no_duplicate_imports.go +266 -0
  105. package/linthost/rules_no_else_return.go +156 -38
  106. package/linthost/rules_no_extend_native.go +113 -17
  107. package/linthost/rules_no_fallthrough.go +1013 -0
  108. package/linthost/rules_no_func_assign.go +70 -0
  109. package/linthost/rules_no_import_assign.go +278 -86
  110. package/linthost/rules_no_inner_declarations.go +186 -0
  111. package/linthost/rules_no_loop_func.go +438 -53
  112. package/linthost/rules_no_mixed_operators.go +208 -52
  113. package/linthost/rules_no_param_reassign.go +376 -61
  114. package/linthost/rules_no_promise_executor_return.go +156 -0
  115. package/linthost/rules_no_restricted_imports.go +1013 -57
  116. package/linthost/rules_no_restricted_syntax.go +190 -26
  117. package/linthost/rules_no_useless_constructor.go +6 -36
  118. package/linthost/rules_problems.go +154 -311
  119. package/linthost/rules_promise.go +2578 -109
  120. package/linthost/rules_react_extras.go +71 -45
  121. package/linthost/rules_react_hooks.go +18 -2
  122. package/linthost/rules_react_perf.go +1 -0
  123. package/linthost/rules_react_refresh.go +1 -1
  124. package/linthost/rules_regexp.go +230 -14
  125. package/linthost/rules_security.go +45 -19
  126. package/linthost/rules_self.go +21 -1
  127. package/linthost/rules_solid.go +13 -7
  128. package/linthost/rules_storybook.go +1 -1
  129. package/linthost/rules_suggestions.go +490 -84
  130. package/linthost/rules_testing_library.go +12 -1
  131. package/linthost/rules_ts.go +227 -54
  132. package/linthost/rules_ts_async.go +0 -131
  133. package/linthost/rules_ts_no_base_to_string.go +3 -4
  134. package/linthost/rules_ts_no_misused_promises.go +817 -0
  135. package/linthost/rules_ts_no_restricted_types.go +320 -34
  136. package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
  137. package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
  138. package/linthost/rules_ts_require_array_sort_compare.go +5 -5
  139. package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
  140. package/linthost/rules_unicorn_better_regex.go +224 -16
  141. package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
  142. package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
  143. package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
  144. package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
  145. package/linthost/rules_unicorn_escape_case.go +30 -19
  146. package/linthost/rules_unicorn_filename_case.go +763 -16
  147. package/linthost/rules_unicorn_import_style.go +783 -17
  148. package/linthost/rules_unicorn_isolated_functions.go +934 -14
  149. package/linthost/rules_unicorn_no_hex_escape.go +26 -11
  150. package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
  151. package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
  152. package/linthost/rules_unicorn_no_unused_properties.go +756 -19
  153. package/linthost/rules_unicorn_number_literal_case.go +58 -39
  154. package/linthost/rules_unicorn_prefer_at.go +7 -3
  155. package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
  156. package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
  157. package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
  158. package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
  159. package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
  160. package/linthost/rules_unicorn_string_content.go +611 -15
  161. package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
  162. package/linthost/rules_unicorn_template_indent.go +923 -18
  163. package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
  164. package/linthost/rules_unicorn_throw_new_error.go +218 -26
  165. package/linthost/rules_var.go +829 -142
  166. package/linthost/string_length.go +222 -0
  167. package/package.json +4 -3
  168. package/rule/project.go +145 -0
  169. package/rule/rule.go +58 -1
  170. package/src/index.ts +58 -17
  171. package/src/structures/ITtscLintConfig.ts +3 -0
  172. package/src/structures/TtscLintRuleSetting.ts +3 -2
  173. package/src/structures/format/ITtscLintFormat.ts +3 -2
  174. package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
  175. package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
  176. package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
  177. package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
  178. package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
  179. package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
  180. package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
  181. package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
  182. package/src/structures/rules/ITtscLintRules.ts +7 -2
  183. package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
  184. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
  185. package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
  186. package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
  187. package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
  188. package/src/structures/rules/index.ts +3 -0
@@ -3,7 +3,7 @@
3
3
  // hadn't migrated yet.
4
4
  //
5
5
  // Implemented here:
6
- // - no-await-in-loop: await inside a loop body iterates sequentially
6
+ // - no-await-in-loop: explicit or implicit await in a repeated loop position
7
7
  // - no-dupe-class-members: duplicate class member declarations
8
8
  // - no-this-before-super: `this` (or `super.x`) before `super()` in
9
9
  // derived constructors
@@ -14,75 +14,85 @@ import (
14
14
  shimast "github.com/microsoft/typescript-go/shim/ast"
15
15
  )
16
16
 
17
- // noAwaitInLoop reports an `await` expression evaluated inside the body
18
- // of a sequential loop (for / while / do-while / for-in / for-of). Each
19
- // iteration of the loop blocks on the previous one's microtask hop, so
20
- // the loop body runs strictly serially even when the underlying
21
- // operations are independent and could overlap with Promise.all.
17
+ // noAwaitInLoop reports explicit and implicit awaits evaluated in a repeated
18
+ // loop position. Initializers and iterable expressions run only once per loop,
19
+ // while tests, updates, bodies, nested for-await statements, and await-using
20
+ // declarations in repeated positions serialize iterations.
22
21
  // https://eslint.org/docs/latest/rules/no-await-in-loop
23
- //
24
- // `for await … of` loops are exempt — the for-await iterator protocol
25
- // is the whole reason the loop exists, and rejecting its `await` would
26
- // just suggest the developer abandon the iterator.
27
22
  type noAwaitInLoop struct{}
28
23
 
29
24
  func (noAwaitInLoop) Name() string { return "no-await-in-loop" }
30
25
  func (noAwaitInLoop) Visits() []shimast.Kind {
31
- return []shimast.Kind{shimast.KindAwaitExpression}
26
+ return []shimast.Kind{
27
+ shimast.KindAwaitExpression,
28
+ shimast.KindForOfStatement,
29
+ shimast.KindVariableDeclarationList,
30
+ }
32
31
  }
33
32
  func (noAwaitInLoop) Check(ctx *Context, node *shimast.Node) {
34
- for cur := node.Parent; cur != nil; cur = cur.Parent {
35
- if isFunctionLikeKind(cur) {
36
- return
37
- }
38
- switch cur.Kind {
39
- case shimast.KindForStatement,
40
- shimast.KindWhileStatement,
41
- shimast.KindDoStatement,
42
- shimast.KindForInStatement:
43
- ctx.Report(node, "Unexpected `await` inside a loop — iterations run sequentially; prefer `Promise.all` when independent.")
44
- return
45
- case shimast.KindForOfStatement:
46
- // `for await … of` loops are exempt by design — the await
47
- // is the iteration itself, not a sequential block.
48
- if isForAwaitOfStatement(ctx.File, cur) {
49
- return
50
- }
33
+ if node == nil || !isAwaitInLoopCandidate(node) {
34
+ return
35
+ }
36
+ child := node
37
+ for parent := node.Parent; parent != nil && !isAwaitInLoopBoundary(parent); parent = parent.Parent {
38
+ if isRepeatedLoopPosition(child, parent) {
51
39
  ctx.Report(node, "Unexpected `await` inside a loop — iterations run sequentially; prefer `Promise.all` when independent.")
52
40
  return
53
41
  }
42
+ child = parent
54
43
  }
55
44
  }
56
45
 
57
- // isForAwaitOfStatement reports whether the for-of statement at `node`
58
- // is a `for await … of` (rather than a plain `for … of`). The shim AST
59
- // does not expose `AwaitModifier` as a typed field, so the check is
60
- // textual: locate the `for` keyword and look for the `await` keyword
61
- // in the small window before the opening parenthesis. The width 24
62
- // covers any reasonable spacing/comment-free header.
63
- func isForAwaitOfStatement(file *shimast.SourceFile, node *shimast.Node) bool {
64
- if file == nil || node == nil {
65
- return false
66
- }
67
- forPos := keywordStart(file, node, "for")
68
- if forPos < 0 {
46
+ func isAwaitInLoopCandidate(node *shimast.Node) bool {
47
+ switch node.Kind {
48
+ case shimast.KindAwaitExpression:
49
+ return true
50
+ case shimast.KindForOfStatement:
51
+ statement := node.AsForInOrOfStatement()
52
+ return statement != nil && statement.AwaitModifier != nil
53
+ case shimast.KindVariableDeclarationList:
54
+ return isAwaitUsingDeclarationList(node)
55
+ default:
69
56
  return false
70
57
  }
71
- src := file.Text()
72
- limit := forPos + 24
73
- if limit > len(src) {
74
- limit = len(src)
58
+ }
59
+
60
+ func isAwaitInLoopBoundary(node *shimast.Node) bool {
61
+ if isFunctionLikeKind(node) {
62
+ return true
75
63
  }
76
- if node.End() < limit {
77
- limit = node.End()
64
+ if node == nil || node.Kind != shimast.KindForOfStatement {
65
+ return false
78
66
  }
79
- for i := forPos + 3; i < limit; i++ {
80
- if src[i] == '(' {
81
- limit = i
82
- break
83
- }
67
+ statement := node.AsForInOrOfStatement()
68
+ return statement != nil && statement.AwaitModifier != nil
69
+ }
70
+
71
+ func isRepeatedLoopPosition(child, parent *shimast.Node) bool {
72
+ switch parent.Kind {
73
+ case shimast.KindForStatement:
74
+ statement := parent.AsForStatement()
75
+ return statement != nil &&
76
+ (child == statement.Condition || child == statement.Incrementor || child == statement.Statement)
77
+ case shimast.KindForInStatement, shimast.KindForOfStatement:
78
+ statement := parent.AsForInOrOfStatement()
79
+ return statement != nil &&
80
+ (child == statement.Statement ||
81
+ (child == statement.Initializer && isAwaitUsingDeclarationList(child)))
82
+ case shimast.KindWhileStatement:
83
+ statement := parent.AsWhileStatement()
84
+ return statement != nil && (child == statement.Expression || child == statement.Statement)
85
+ case shimast.KindDoStatement:
86
+ statement := parent.AsDoStatement()
87
+ return statement != nil && (child == statement.Expression || child == statement.Statement)
88
+ default:
89
+ return false
84
90
  }
85
- return findKeyword(file, forPos+3, limit, "await") >= 0
91
+ }
92
+
93
+ func isAwaitUsingDeclarationList(node *shimast.Node) bool {
94
+ return node != nil && node.Kind == shimast.KindVariableDeclarationList &&
95
+ shimast.GetCombinedNodeFlags(node)&shimast.NodeFlagsBlockScoped == shimast.NodeFlagsAwaitUsing
86
96
  }
87
97
 
88
98
  // noDupeClassMembers reports two declarations of the same member on a
@@ -377,45 +387,6 @@ func objectLiteralIsEmpty(node *shimast.Node) bool {
377
387
  return len(lit.Properties.Nodes) == 0
378
388
  }
379
389
 
380
- // noDuplicateImports reports two import declarations that resolve to
381
- // the same module specifier. Consolidating them into one import keeps
382
- // the dependency graph at the head of the file readable and avoids
383
- // surprising load-order interactions when the import has side effects.
384
- // https://eslint.org/docs/latest/rules/no-duplicate-imports
385
- //
386
- // The check is textual on the module specifier string — two imports
387
- // with the same exact string literal collide. `import type { … } from`
388
- // is folded together with value imports because the runtime sees only
389
- // one module load.
390
- type noDuplicateImports struct{}
391
-
392
- func (noDuplicateImports) Name() string { return "no-duplicate-imports" }
393
- func (noDuplicateImports) Visits() []shimast.Kind {
394
- return []shimast.Kind{shimast.KindSourceFile}
395
- }
396
- func (noDuplicateImports) Check(ctx *Context, node *shimast.Node) {
397
- seen := map[string]bool{}
398
- node.ForEachChild(func(child *shimast.Node) bool {
399
- if child == nil || child.Kind != shimast.KindImportDeclaration {
400
- return false
401
- }
402
- decl := child.AsImportDeclaration()
403
- if decl == nil || decl.ModuleSpecifier == nil {
404
- return false
405
- }
406
- spec := stringLiteralText(decl.ModuleSpecifier)
407
- if spec == "" {
408
- return false
409
- }
410
- if seen[spec] {
411
- ctx.Report(child, "Module `"+spec+"` is already imported above; consolidate the imports.")
412
- return false
413
- }
414
- seen[spec] = true
415
- return false
416
- })
417
- }
418
-
419
390
  // getterReturn reports a `get` accessor whose body completes without
420
391
  // returning a value. The runtime returns `undefined` from such a
421
392
  // getter; in practice that is always a bug — the caller expects the
@@ -728,7 +699,6 @@ func init() {
728
699
  Register(noAwaitInLoop{})
729
700
  Register(noConstructorReturn{})
730
701
  Register(noDupeClassMembers{})
731
- Register(noDuplicateImports{})
732
702
  Register(noImplicitCoercion{})
733
703
  Register(noNewSymbol{})
734
704
  Register(noThisBeforeSuper{})
@@ -12,7 +12,7 @@ type cypressNoUnnecessaryWaiting struct{}
12
12
  type cypressNoForce struct{}
13
13
  type cypressNoPause struct{}
14
14
  type cypressNoDebug struct{}
15
- type cypressUnsafeToChainCommand struct{}
15
+ type cypressUnsafeToChainCommand struct{ optionsRule }
16
16
  type cypressAssertionBeforeScreenshot struct{}
17
17
  type cypressNoAsyncTests struct{}
18
18
  type cypressNoAsyncBefore struct{}
@@ -1,39 +1,115 @@
1
1
  // defaultCase: a `switch` statement that omits the `default` clause
2
2
  // silently drops every discriminant value not matched by an explicit
3
- // `case` label. The rule wants the catch-all path written out so an
4
- // unhandled discriminant becomes an intentional branch even if that
5
- // branch is a `// no default` comment marker.
3
+ // `case` label. The rule wants the catch-all path written out unless the
4
+ // omission is deliberately marked by a trailing `// no default` comment.
6
5
  //
7
- // AST-only: each visited `SwitchStatement` checks its `CaseBlock`
8
- // clauses for any node of kind `DefaultClause`. The rule reports on the
9
- // switch keyword itself rather than the case block so the diagnostic
10
- // lands at the head of the statement.
6
+ // Behavior mirrors ESLint's `default-case`:
7
+ //
8
+ // - An empty switch (`switch (x) {}`, zero clauses) is skipped: an empty
9
+ // case block has no clause to attach the marker comment to, so upstream
10
+ // bails with `if (!node.cases.length) return;`.
11
+ // - A `default` clause anywhere in the block satisfies the rule.
12
+ // - Otherwise the LAST comment trailing the final clause (between its end
13
+ // and the case block's `}`) is tested against `commentPattern`. When the
14
+ // comment's trimmed text matches, the omission is intentional and the
15
+ // switch is left alone; otherwise the rule reports on the `switch`
16
+ // keyword so the diagnostic lands at the head of the statement.
17
+ //
18
+ // `commentPattern` replaces the default marker pattern `/^no default$/i`
19
+ // (ESLint's DEFAULT_COMMENT_PATTERN). The trailing-comment scan reuses
20
+ // comment_scan.go's parser-aware gap scanner over the trivia-only region
21
+ // after the last clause.
11
22
  // https://eslint.org/docs/latest/rules/default-case
12
23
  package linthost
13
24
 
14
- import shimast "github.com/microsoft/typescript-go/shim/ast"
25
+ import (
26
+ "regexp"
27
+ "strings"
28
+
29
+ shimast "github.com/microsoft/typescript-go/shim/ast"
30
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
31
+ )
32
+
33
+ // defaultCaseDefaultCommentPattern is ESLint's DEFAULT_COMMENT_PATTERN
34
+ // (`/^no default$/iu`) in RE2: the trailing comment's trimmed text must be
35
+ // exactly `no default`, in any letter case.
36
+ var defaultCaseDefaultCommentPattern = regexp.MustCompile(`(?i)^no default$`)
37
+
38
+ // defaultCaseOptions mirrors the upstream rule's single-object schema.
39
+ type defaultCaseOptions struct {
40
+ // CommentPattern replaces the default `no default` marker pattern when
41
+ // non-empty. Upstream compiles it with the `u` flag and no `i`, so the
42
+ // custom pattern is case-sensitive unless it opts in. An uncompilable
43
+ // pattern falls back to the default marker (matching no-fallthrough) so a
44
+ // config typo cannot silently turn every marked switch into a finding.
45
+ CommentPattern string `json:"commentPattern"`
46
+ }
15
47
 
16
- type defaultCase struct{}
48
+ type defaultCase struct{ optionsRule }
17
49
 
18
50
  func (defaultCase) Name() string { return "default-case" }
19
51
  func (defaultCase) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSwitchStatement} }
20
52
  func (defaultCase) Check(ctx *Context, node *shimast.Node) {
21
53
  sw := node.AsSwitchStatement()
22
- if sw == nil || sw.CaseBlock == nil {
54
+ if sw == nil || sw.CaseBlock == nil || ctx.File == nil {
23
55
  return
24
56
  }
25
57
  block := sw.CaseBlock.AsCaseBlock()
26
58
  if block == nil || block.Clauses == nil {
27
59
  return
28
60
  }
29
- for _, clause := range block.Clauses.Nodes {
61
+ clauses := block.Clauses.Nodes
62
+ // Empty switch: upstream skips it because an empty case block offers no
63
+ // clause to read the marker comment from.
64
+ if len(clauses) == 0 {
65
+ return
66
+ }
67
+ for _, clause := range clauses {
30
68
  if clause != nil && clause.Kind == shimast.KindDefaultClause {
31
69
  return
32
70
  }
33
71
  }
72
+
73
+ var opts defaultCaseOptions
74
+ ctx.DecodeOptions(&opts)
75
+ pattern := defaultCaseDefaultCommentPattern
76
+ if opts.CommentPattern != "" {
77
+ if custom, err := compileUserPattern(opts.CommentPattern); err == nil {
78
+ pattern = custom
79
+ }
80
+ }
81
+
82
+ // ESLint's sourceCode.getCommentsAfter(lastCase).at(-1): the LAST comment
83
+ // between the final clause and the case block's closing brace marks the
84
+ // omission intentional when its trimmed text matches the pattern. A
85
+ // trailing non-marker comment after a `// no default` therefore invalidates
86
+ // the marker.
87
+ if lastClause := clauses[len(clauses)-1]; lastClause != nil {
88
+ text := ctx.File.Text()
89
+ closeBrace := sw.CaseBlock.End() - 1 // exclusive End covers the `}` token
90
+ if pos, end, ok := defaultCaseLastTrailingComment(text, lastClause.End(), closeBrace); ok &&
91
+ pattern.MatchString(strings.TrimSpace(commentInnerText(text[pos:end]))) {
92
+ return
93
+ }
94
+ }
95
+
34
96
  ctx.Report(node, "Expected a default case.")
35
97
  }
36
98
 
99
+ // defaultCaseLastTrailingComment returns the source range of the LAST comment
100
+ // in the trivia region [from, to), reusing comment_scan.go's parser-aware gap
101
+ // scanner. The region between a switch's last clause and its closing brace is
102
+ // trivia-only by construction, so scanning the isolated gap is exact.
103
+ func defaultCaseLastTrailingComment(text string, from, to int) (pos, end int, ok bool) {
104
+ if from < 0 || to > len(text) || from >= to {
105
+ return 0, 0, false
106
+ }
107
+ scanCommentGap(shimscanner.NewScanner(), text, from, to, func(_ shimast.Kind, p, e int) {
108
+ pos, end, ok = p, e, true
109
+ })
110
+ return pos, end, ok
111
+ }
112
+
37
113
  func init() {
38
114
  Register(defaultCase{})
39
115
  }
@@ -99,11 +99,11 @@ func (noDupeArgs) Check(ctx *Context, node *shimast.Node) {
99
99
  }
100
100
  }
101
101
 
102
- // propertyKey returns a stable key for a property in an object literal.
103
- // Computed names without a literal payload return "" so the dedupe pass
104
- // can skip them safely (a typed field is fine duplicate identical
105
- // expressions still won't cause false positives because the same expr
106
- // produces the same source text).
102
+ // propertyKey returns a stable dedupe key for a property in an object literal,
103
+ // mirroring ESLint's getStaticPropertyName. A property whose name is not
104
+ // statically known a computed name built from a non-constant expression —
105
+ // returns "" so the dedupe pass skips it and never treats two distinct dynamic
106
+ // keys as duplicates.
107
107
  func propertyKey(file *shimast.SourceFile, prop *shimast.Node) string {
108
108
  if prop == nil {
109
109
  return ""
@@ -152,11 +152,15 @@ func propertyKey(file *shimast.SourceFile, prop *shimast.Node) string {
152
152
  return ""
153
153
  }
154
154
 
155
- // staticPropertyKey extracts a comparable string key from a property name node.
156
- // Identifiers, string/numeric literals, and computed names with a literal
157
- // payload all produce a stable string. Other computed names (dynamic
158
- // expressions) return "" so the caller skips them without false positives.
159
- func staticPropertyKey(file *shimast.SourceFile, name *shimast.Node) string {
155
+ // staticPropertyKey extracts a comparable string key from a property name node,
156
+ // mirroring ESLint's getStaticPropertyName. A bare identifier yields its name,
157
+ // and string / numeric / bigint literals yield their text. A computed name
158
+ // yields a key only when its bracketed expression is itself a constant literal
159
+ // (resolved like getStaticStringValue): `["a"]` resolves to `a` so it collides
160
+ // with the identifier key `a`, while a non-constant computed name such as
161
+ // `[f()]` or `[x]` resolves to "" and the caller skips it. The file argument is
162
+ // unused here but kept so the whole property-key family shares one signature.
163
+ func staticPropertyKey(_ *shimast.SourceFile, name *shimast.Node) string {
160
164
  if name == nil {
161
165
  return ""
162
166
  }
@@ -168,9 +172,30 @@ func staticPropertyKey(file *shimast.SourceFile, name *shimast.Node) string {
168
172
  case shimast.KindNumericLiteral, shimast.KindBigIntLiteral:
169
173
  return numericLiteralText(name)
170
174
  case shimast.KindComputedPropertyName:
171
- // fall back to source text so a computed `[`foo`]` still compares
172
- // against the literal `foo` form when both appear together.
173
- return nodeText(file, name)
175
+ computed := name.AsComputedPropertyName()
176
+ if computed == nil {
177
+ return ""
178
+ }
179
+ return staticComputedKey(computed.Expression)
180
+ }
181
+ return ""
182
+ }
183
+
184
+ // staticComputedKey resolves the expression inside a `[...]` computed property
185
+ // name to a static key, mirroring ESLint's getStaticStringValue: only a
186
+ // constant literal (string, template without substitutions, numeric, or bigint)
187
+ // contributes a key. Parentheses are transparent, matching ESTree, which has no
188
+ // parenthesized-expression node. Any other expression contributes "".
189
+ func staticComputedKey(expr *shimast.Node) string {
190
+ expr = stripParens(expr)
191
+ if expr == nil {
192
+ return ""
193
+ }
194
+ switch expr.Kind {
195
+ case shimast.KindStringLiteral, shimast.KindNoSubstitutionTemplateLiteral:
196
+ return stringLiteralText(expr)
197
+ case shimast.KindNumericLiteral, shimast.KindBigIntLiteral:
198
+ return numericLiteralText(expr)
174
199
  }
175
200
  return ""
176
201
  }
@@ -1,35 +1,67 @@
1
1
  package linthost
2
2
 
3
- import shimast "github.com/microsoft/typescript-go/shim/ast"
3
+ import (
4
+ shimast "github.com/microsoft/typescript-go/shim/ast"
5
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
6
+ )
4
7
 
5
- // noEmpty: empty block statements (`{}`), but allow empty catch
6
- // clauses since they're idiomatic for "ignore the error".
8
+ // noEmpty rejects empty block and switch statements unless an interior
9
+ // comment documents the intentional no-op.
7
10
  // https://eslint.org/docs/latest/rules/no-empty
8
- type noEmpty struct{}
11
+ type noEmpty struct{ optionsRule }
9
12
 
10
- func (noEmpty) Name() string { return "no-empty" }
11
- func (noEmpty) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindBlock} }
13
+ type noEmptyOptions struct {
14
+ AllowEmptyCatch bool `json:"allowEmptyCatch"`
15
+ }
16
+
17
+ func (noEmpty) Name() string { return "no-empty" }
18
+ func (noEmpty) Visits() []shimast.Kind {
19
+ return []shimast.Kind{shimast.KindBlock, shimast.KindSwitchStatement}
20
+ }
12
21
  func (noEmpty) Check(ctx *Context, node *shimast.Node) {
13
- block := node.AsBlock()
14
- if block == nil || block.Statements == nil {
22
+ if node.Kind == shimast.KindSwitchStatement {
23
+ statement := node.AsSwitchStatement()
24
+ if statement == nil || statement.CaseBlock == nil {
25
+ return
26
+ }
27
+ caseBlock := statement.CaseBlock.AsCaseBlock()
28
+ if caseBlock == nil || caseBlock.Clauses != nil && len(caseBlock.Clauses.Nodes) != 0 {
29
+ return
30
+ }
31
+ if !emptyBracedBodyHasComment(ctx.File, statement.CaseBlock) {
32
+ ctx.Report(statement.CaseBlock, "Empty switch statement.")
33
+ }
15
34
  return
16
35
  }
17
- if len(block.Statements.Nodes) > 0 {
36
+
37
+ block := node.AsBlock()
38
+ if block == nil || block.Statements != nil && len(block.Statements.Nodes) != 0 {
18
39
  return
19
40
  }
20
41
  parent := node.Parent
21
- if parent != nil && parent.Kind == shimast.KindCatchClause {
22
- return // tolerated — see ESLint default options
23
- }
24
42
  if isFunctionLikeKind(parent) {
25
43
  return // empty function body is `no-empty-function`'s job
26
44
  }
27
- ctx.Report(node, "Empty block statement.")
45
+ if parent != nil && parent.Kind == shimast.KindClassStaticBlockDeclaration {
46
+ return // static blocks have their own rule and are not BlockStatement in ESTree
47
+ }
48
+ var options noEmptyOptions
49
+ _ = ctx.DecodeOptions(&options)
50
+ if options.AllowEmptyCatch && parent != nil && parent.Kind == shimast.KindCatchClause {
51
+ return
52
+ }
53
+ if !emptyBracedBodyHasComment(ctx.File, node) {
54
+ ctx.Report(node, "Empty block statement.")
55
+ }
28
56
  }
29
57
 
30
58
  // noEmptyFunction: empty function / method / arrow / accessor bodies.
31
59
  // https://eslint.org/docs/latest/rules/no-empty-function
32
- type noEmptyFunction struct{}
60
+ type noEmptyFunction struct{ optionsRule }
61
+
62
+ type noEmptyFunctionOptions struct {
63
+ Allow []string `json:"allow"`
64
+ }
33
65
 
34
66
  func (noEmptyFunction) Name() string { return "no-empty-function" }
35
67
  func (noEmptyFunction) Visits() []shimast.Kind {
@@ -45,19 +77,155 @@ func (noEmptyFunction) Visits() []shimast.Kind {
45
77
  }
46
78
  func (noEmptyFunction) Check(ctx *Context, node *shimast.Node) {
47
79
  body := node.Body()
48
- if body == nil {
80
+ if body == nil || body.Kind != shimast.KindBlock {
49
81
  return
50
82
  }
51
- if body.Kind != shimast.KindBlock {
83
+ block := body.AsBlock()
84
+ if block == nil || block.Statements != nil && len(block.Statements.Nodes) != 0 {
52
85
  return
53
86
  }
54
- block := body.AsBlock()
55
- if block == nil || block.Statements == nil {
87
+ var options noEmptyFunctionOptions
88
+ _ = ctx.DecodeOptions(&options)
89
+ kind := noEmptyFunctionKind(node)
90
+ if kind == "" || noEmptyFunctionAllowed(node, kind, options.Allow) || emptyBracedBodyHasComment(ctx.File, body) {
56
91
  return
57
92
  }
58
- if len(block.Statements.Nodes) == 0 {
59
- ctx.Report(node, "Unexpected empty function.")
93
+ ctx.Report(body, "Unexpected empty "+noEmptyFunctionLabel(kind)+".")
94
+ }
95
+
96
+ func noEmptyFunctionKind(node *shimast.Node) string {
97
+ if node == nil {
98
+ return ""
99
+ }
100
+ switch node.Kind {
101
+ case shimast.KindArrowFunction:
102
+ return "arrowFunctions"
103
+ case shimast.KindConstructor:
104
+ return "constructors"
105
+ case shimast.KindGetAccessor:
106
+ return "getters"
107
+ case shimast.KindSetAccessor:
108
+ return "setters"
109
+ case shimast.KindMethodDeclaration:
110
+ if noEmptyFunctionIsGenerator(node) {
111
+ return "generatorMethods"
112
+ }
113
+ if node.ModifierFlags()&shimast.ModifierFlagsAsync != 0 {
114
+ return "asyncMethods"
115
+ }
116
+ return "methods"
117
+ case shimast.KindFunctionDeclaration, shimast.KindFunctionExpression:
118
+ if noEmptyFunctionIsGenerator(node) {
119
+ return "generatorFunctions"
120
+ }
121
+ if node.ModifierFlags()&shimast.ModifierFlagsAsync != 0 {
122
+ return "asyncFunctions"
123
+ }
124
+ return "functions"
125
+ default:
126
+ return ""
127
+ }
128
+ }
129
+
130
+ func noEmptyFunctionIsGenerator(node *shimast.Node) bool {
131
+ switch node.Kind {
132
+ case shimast.KindFunctionDeclaration:
133
+ declaration := node.AsFunctionDeclaration()
134
+ return declaration != nil && declaration.AsteriskToken != nil
135
+ case shimast.KindFunctionExpression:
136
+ expression := node.AsFunctionExpression()
137
+ return expression != nil && expression.AsteriskToken != nil
138
+ case shimast.KindMethodDeclaration:
139
+ declaration := node.AsMethodDeclaration()
140
+ return declaration != nil && declaration.AsteriskToken != nil
141
+ default:
142
+ return false
143
+ }
144
+ }
145
+
146
+ func noEmptyFunctionAllowed(node *shimast.Node, kind string, allow []string) bool {
147
+ if noEmptyFunctionOptionIncludes(allow, kind) {
148
+ return true
149
+ }
150
+ if kind == "constructors" {
151
+ if hasParameterProperty(node) {
152
+ return true
153
+ }
154
+ modifiers := node.ModifierFlags()
155
+ if modifiers&shimast.ModifierFlagsPrivate != 0 && noEmptyFunctionOptionIncludes(allow, "privateConstructors") {
156
+ return true
157
+ }
158
+ if modifiers&shimast.ModifierFlagsProtected != 0 && noEmptyFunctionOptionIncludes(allow, "protectedConstructors") {
159
+ return true
160
+ }
161
+ }
162
+ switch kind {
163
+ case "methods", "generatorMethods", "asyncMethods", "getters", "setters":
164
+ if len(node.Decorators()) != 0 && noEmptyFunctionOptionIncludes(allow, "decoratedFunctions") {
165
+ return true
166
+ }
167
+ if node.ModifierFlags()&shimast.ModifierFlagsOverride != 0 && noEmptyFunctionOptionIncludes(allow, "overrideMethods") {
168
+ return true
169
+ }
170
+ }
171
+ return false
172
+ }
173
+
174
+ func noEmptyFunctionOptionIncludes(allow []string, candidate string) bool {
175
+ for _, option := range allow {
176
+ if option == candidate {
177
+ return true
178
+ }
179
+ }
180
+ return false
181
+ }
182
+
183
+ func noEmptyFunctionLabel(kind string) string {
184
+ switch kind {
185
+ case "arrowFunctions":
186
+ return "arrow function"
187
+ case "generatorFunctions":
188
+ return "generator function"
189
+ case "methods":
190
+ return "method"
191
+ case "generatorMethods":
192
+ return "generator method"
193
+ case "getters":
194
+ return "getter"
195
+ case "setters":
196
+ return "setter"
197
+ case "constructors":
198
+ return "constructor"
199
+ case "asyncFunctions":
200
+ return "async function"
201
+ case "asyncMethods":
202
+ return "async method"
203
+ default:
204
+ return "function"
205
+ }
206
+ }
207
+
208
+ // emptyBracedBodyHasComment scans only the trivia between the parser-owned
209
+ // opening and closing brace tokens of an already-proven empty body. Exterior
210
+ // comments cannot enter this range, and context-sensitive expression tokens
211
+ // cannot occur because the body has no statements or switch clauses.
212
+ func emptyBracedBodyHasComment(file *shimast.SourceFile, body *shimast.Node) bool {
213
+ if file == nil || body == nil {
214
+ return false
215
+ }
216
+ opening := shimscanner.GetScannerForSourceFile(file, body.Pos())
217
+ if opening.Token() != shimast.KindOpenBraceToken || opening.TokenEnd() > body.End() {
218
+ return false
219
+ }
220
+ closing := shimscanner.GetScannerForSourceFile(file, opening.TokenEnd())
221
+ if closing.Token() != shimast.KindCloseBraceToken || closing.TokenEnd() > body.End() {
222
+ return false
60
223
  }
224
+ found := false
225
+ scanCommentGap(shimscanner.NewScanner(), file.Text(), opening.TokenEnd(), closing.TokenStart(), func(_ shimast.Kind, _, _ int) {
226
+ found = true
227
+ })
228
+ return found
61
229
  }
62
230
 
63
231
  // noEmptyPattern: `({}) => x` or `function ({}) {}` — destructuring