@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,312 @@
1
+ // typescript/ban-ts-comment: a faithful port of typescript-eslint's
2
+ // ban-ts-comment rule, including its recommended defaults, per-directive
3
+ // options, description thresholds, and description-format matching.
4
+ //
5
+ // The rule scans every parser-classified comment token (via
6
+ // forEachCommentToken)
7
+ // instead of reading `SourceFile.CommentDirectives`: the compiler-side
8
+ // list only carries the error-suppression directives (`@ts-expect-error`,
9
+ // `@ts-ignore`) and would miss `@ts-nocheck` / `@ts-check` entirely.
10
+ //
11
+ // Directive recognition mirrors the upstream rule (which in turn mirrors
12
+ // the TypeScript compiler's own regexes):
13
+ //
14
+ // - `@ts-check` / `@ts-nocheck` are pragmas: line comments with two or
15
+ // three leading slashes only. Block comments never activate them.
16
+ // - `@ts-expect-error` / `@ts-ignore` match line comments (any number
17
+ // of leading slashes) and the LAST line of a block comment — the only
18
+ // line the compiler honors.
19
+ // - `@ts-nocheck` is skipped when it appears at or after the first
20
+ // statement's line, because the compiler ignores it there.
21
+ //
22
+ // Reference: https://typescript-eslint.io/rules/ban-ts-comment
23
+ package linthost
24
+
25
+ import (
26
+ "bytes"
27
+ "encoding/json"
28
+ "fmt"
29
+ "regexp"
30
+ "strings"
31
+
32
+ shimast "github.com/microsoft/typescript-go/shim/ast"
33
+ shimscanner "github.com/microsoft/typescript-go/shim/scanner"
34
+ )
35
+
36
+ // tsCommentJSWhitespace is the ECMAScript `\s` character class. The
37
+ // upstream regexes run under JavaScript semantics where `\s` covers the
38
+ // Unicode space separators and the BOM; Go's `\s` is ASCII-only, so the
39
+ // class is spelled out to keep the port byte-faithful.
40
+ const tsCommentJSWhitespace = `[\t\n\v\f\r \x{00A0}\x{1680}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}\x{FEFF}]`
41
+
42
+ var (
43
+ // tsPragmaLineRegex matches the `@ts-check` / `@ts-nocheck` pragma form
44
+ // against the FULL line-comment text (`//...`): two or three leading
45
+ // slashes, optional whitespace, then the directive. Mirrors upstream's
46
+ // singleLinePragmaRegEx applied to `'//' + comment.value`.
47
+ tsPragmaLineRegex = regexp.MustCompile(`^///?` + tsCommentJSWhitespace + `*@ts-(?P<directive>check|nocheck)(?P<description>.*)$`)
48
+
49
+ // tsDirectiveLineRegex matches `@ts-expect-error` / `@ts-ignore` against
50
+ // a line comment's value (text after the opening `//`): any number of
51
+ // extra slashes, optional whitespace, then the directive.
52
+ tsDirectiveLineRegex = regexp.MustCompile(`^/*` + tsCommentJSWhitespace + `*@ts-(?P<directive>expect-error|ignore)(?P<description>.*)`)
53
+
54
+ // tsDirectiveBlockRegex matches `@ts-expect-error` / `@ts-ignore`
55
+ // against the last line of a block comment's value.
56
+ tsDirectiveBlockRegex = regexp.MustCompile(`^` + tsCommentJSWhitespace + `*(?:/|\*)*` + tsCommentJSWhitespace + `*@ts-(?P<directive>expect-error|ignore)(?P<description>.*)`)
57
+
58
+ // tsCommentLineBreakRegex splits block-comment values on the ECMAScript
59
+ // line terminators (upstream LINEBREAK_MATCHER).
60
+ tsCommentLineBreakRegex = regexp.MustCompile(`\r\n|[\r\n\x{2028}\x{2029}]`)
61
+ )
62
+
63
+ // tsCommentDirective is one recognized directive with the raw description
64
+ // text that follows it (untrimmed, exactly as the upstream regex captures
65
+ // it — descriptionFormat patterns match against this raw text).
66
+ type tsCommentDirective struct {
67
+ directive string // "check" | "nocheck" | "expect-error" | "ignore"
68
+ description string
69
+ }
70
+
71
+ // findTsDirectiveInComment classifies one comment token. `raw` is the full
72
+ // comment text including delimiters. Returns nil when the comment carries
73
+ // no supported directive.
74
+ func findTsDirectiveInComment(kind shimast.Kind, raw string) *tsCommentDirective {
75
+ if kind == shimast.KindSingleLineCommentTrivia {
76
+ if match := tsPragmaLineRegex.FindStringSubmatch(raw); match != nil {
77
+ return &tsCommentDirective{directive: match[1], description: match[2]}
78
+ }
79
+ if match := tsDirectiveLineRegex.FindStringSubmatch(raw[2:]); match != nil {
80
+ return &tsCommentDirective{directive: match[1], description: match[2]}
81
+ }
82
+ return nil
83
+ }
84
+ // Block comment: only the last line can carry an effective directive,
85
+ // matching the compiler's scanner (and the upstream rule).
86
+ value := strings.TrimSuffix(raw[2:], "*/")
87
+ lines := tsCommentLineBreakRegex.Split(value, -1)
88
+ if match := tsDirectiveBlockRegex.FindStringSubmatch(lines[len(lines)-1]); match != nil {
89
+ return &tsCommentDirective{directive: match[1], description: match[2]}
90
+ }
91
+ return nil
92
+ }
93
+
94
+ // tsDirectivePolicy is the normalized evaluation mode for one directive.
95
+ // At most one of `ban` and `requireDescription` is set; `format` is only
96
+ // non-nil when `requireDescription` is set.
97
+ type tsDirectivePolicy struct {
98
+ ban bool
99
+ requireDescription bool
100
+ format *regexp.Regexp
101
+ }
102
+
103
+ // banTsCommentOptions is the raw wire shape of the rule's options object.
104
+ // Directive values stay as raw JSON because each accepts a boolean,
105
+ // the string "allow-with-description", or `{ descriptionFormat }` — the
106
+ // upstream DirectiveConfig union.
107
+ type banTsCommentOptions struct {
108
+ MinimumDescriptionLength *int `json:"minimumDescriptionLength"`
109
+ TsCheck json.RawMessage `json:"ts-check"`
110
+ TsExpectError json.RawMessage `json:"ts-expect-error"`
111
+ TsIgnore json.RawMessage `json:"ts-ignore"`
112
+ TsNocheck json.RawMessage `json:"ts-nocheck"`
113
+ }
114
+
115
+ // resolvedBanTsCommentOptions is the per-run policy table after merging
116
+ // user options over the upstream recommended defaults.
117
+ type resolvedBanTsCommentOptions struct {
118
+ minimumDescriptionLength int
119
+ check tsDirectivePolicy
120
+ expectError tsDirectivePolicy
121
+ ignore tsDirectivePolicy
122
+ nocheck tsDirectivePolicy
123
+ }
124
+
125
+ func (o resolvedBanTsCommentOptions) policy(directive string) tsDirectivePolicy {
126
+ switch directive {
127
+ case "check":
128
+ return o.check
129
+ case "expect-error":
130
+ return o.expectError
131
+ case "ignore":
132
+ return o.ignore
133
+ case "nocheck":
134
+ return o.nocheck
135
+ }
136
+ return tsDirectivePolicy{}
137
+ }
138
+
139
+ // resolveBanTsCommentOptions merges the decoded options over the upstream
140
+ // recommended defaults:
141
+ //
142
+ // {
143
+ // minimumDescriptionLength: 3,
144
+ // "ts-check": false,
145
+ // "ts-expect-error": "allow-with-description",
146
+ // "ts-ignore": true,
147
+ // "ts-nocheck": true,
148
+ // }
149
+ //
150
+ // Absent keys keep their default; present keys replace it wholesale, the
151
+ // same per-key merge ESLint applies to defaultOptions.
152
+ func resolveBanTsCommentOptions(raw banTsCommentOptions) resolvedBanTsCommentOptions {
153
+ resolved := resolvedBanTsCommentOptions{
154
+ minimumDescriptionLength: 3,
155
+ check: tsDirectivePolicy{},
156
+ expectError: tsDirectivePolicy{requireDescription: true},
157
+ ignore: tsDirectivePolicy{ban: true},
158
+ nocheck: tsDirectivePolicy{ban: true},
159
+ }
160
+ if raw.MinimumDescriptionLength != nil {
161
+ resolved.minimumDescriptionLength = *raw.MinimumDescriptionLength
162
+ }
163
+ resolved.check = normalizeTsDirectiveConfig(raw.TsCheck, resolved.check)
164
+ resolved.expectError = normalizeTsDirectiveConfig(raw.TsExpectError, resolved.expectError)
165
+ resolved.ignore = normalizeTsDirectiveConfig(raw.TsIgnore, resolved.ignore)
166
+ resolved.nocheck = normalizeTsDirectiveConfig(raw.TsNocheck, resolved.nocheck)
167
+ return resolved
168
+ }
169
+
170
+ // normalizeTsDirectiveConfig turns one raw DirectiveConfig value into a
171
+ // policy. Upstream validates the shape through its JSON schema and rejects
172
+ // the whole config on mismatch; this host has no schema layer, so a value
173
+ // outside the documented union quietly keeps the directive's documented
174
+ // default rather than inventing a stricter policy.
175
+ func normalizeTsDirectiveConfig(raw json.RawMessage, def tsDirectivePolicy) tsDirectivePolicy {
176
+ if len(raw) == 0 || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
177
+ return def
178
+ }
179
+ var flag bool
180
+ if json.Unmarshal(raw, &flag) == nil {
181
+ return tsDirectivePolicy{ban: flag}
182
+ }
183
+ var literal string
184
+ if json.Unmarshal(raw, &literal) == nil {
185
+ if literal == "allow-with-description" {
186
+ return tsDirectivePolicy{requireDescription: true}
187
+ }
188
+ return def
189
+ }
190
+ var object struct {
191
+ DescriptionFormat *string `json:"descriptionFormat"`
192
+ }
193
+ if json.Unmarshal(raw, &object) == nil {
194
+ // Upstream treats an object without a truthy descriptionFormat as
195
+ // plain allowance: it neither bans nor demands a description.
196
+ if object.DescriptionFormat == nil || *object.DescriptionFormat == "" {
197
+ return tsDirectivePolicy{}
198
+ }
199
+ format, err := regexp.Compile(*object.DescriptionFormat)
200
+ if err != nil {
201
+ // The format cannot be enforced (upstream would have thrown while
202
+ // constructing the RegExp). Keep the description-length gate the
203
+ // object form implies instead of silently allowing everything.
204
+ return tsDirectivePolicy{requireDescription: true}
205
+ }
206
+ return tsDirectivePolicy{requireDescription: true, format: format}
207
+ }
208
+ return def
209
+ }
210
+
211
+ // banTsComment implements typescript/ban-ts-comment.
212
+ type banTsComment struct{ optionsRule }
213
+
214
+ func (banTsComment) Name() string { return "typescript/ban-ts-comment" }
215
+ func (banTsComment) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
216
+
217
+ func (banTsComment) Check(ctx *Context, node *shimast.Node) {
218
+ if ctx.File == nil {
219
+ return
220
+ }
221
+ text := ctx.File.Text()
222
+ // Cheap short-circuit: every supported directive contains this
223
+ // substring, so directive-free files skip the comment re-scan entirely
224
+ // (same strategy as parseLintInlineDirectives).
225
+ if !strings.Contains(text, "@ts-") {
226
+ return
227
+ }
228
+
229
+ var raw banTsCommentOptions
230
+ if err := ctx.DecodeOptions(&raw); err != nil {
231
+ return
232
+ }
233
+ options := resolveBanTsCommentOptions(raw)
234
+
235
+ // The compiler only honors `@ts-nocheck` before the first statement;
236
+ // upstream compares source lines, so a directive on the same line as
237
+ // the first statement is inert too and stays unreported.
238
+ firstStatementLine := -1
239
+ if statements := ctx.File.Statements; statements != nil && len(statements.Nodes) > 0 && statements.Nodes[0] != nil {
240
+ pos := shimscanner.SkipTrivia(text, statements.Nodes[0].Pos())
241
+ firstStatementLine = shimscanner.GetECMALineOfPosition(ctx.File, pos)
242
+ }
243
+
244
+ forEachCommentToken(ctx.File, func(kind shimast.Kind, pos, end int) {
245
+ matched := findTsDirectiveInComment(kind, text[pos:end])
246
+ if matched == nil {
247
+ return
248
+ }
249
+ if matched.directive == "nocheck" && firstStatementLine >= 0 &&
250
+ firstStatementLine <= shimscanner.GetECMALineOfPosition(ctx.File, pos) {
251
+ return
252
+ }
253
+
254
+ policy := options.policy(matched.directive)
255
+ if policy.ban {
256
+ if matched.directive == "ignore" {
257
+ reportTsIgnoreInsteadOfExpectError(ctx, text, pos, end)
258
+ } else {
259
+ ctx.ReportRange(pos, end, fmt.Sprintf(
260
+ "Do not use `@ts-%s` because it alters compilation errors.",
261
+ matched.directive,
262
+ ))
263
+ }
264
+ }
265
+ if policy.requireDescription {
266
+ if stringLength(strings.TrimSpace(matched.description)) < options.minimumDescriptionLength {
267
+ ctx.ReportRange(pos, end, fmt.Sprintf(
268
+ "Include a description after the `@ts-%[1]s` directive to explain why the @ts-%[1]s is necessary. The description must be %[2]d characters or longer.",
269
+ matched.directive, options.minimumDescriptionLength,
270
+ ))
271
+ } else if policy.format != nil && !policy.format.MatchString(matched.description) {
272
+ ctx.ReportRange(pos, end, fmt.Sprintf(
273
+ "The description for the `@ts-%s` directive must match the %s format.",
274
+ matched.directive, policy.format.String(),
275
+ ))
276
+ }
277
+ }
278
+ })
279
+ }
280
+
281
+ // reportTsIgnoreInsteadOfExpectError reports a banned `@ts-ignore` with the
282
+ // upstream opt-in suggestion: rewrite the first `@ts-ignore` occurrence in
283
+ // the comment body to `@ts-expect-error`. This must not be an autofix because
284
+ // an error-free following line turns the rewrite into compiler error TS2578.
285
+ func reportTsIgnoreInsteadOfExpectError(ctx *Context, text string, pos, end int) {
286
+ const message = "Use `@ts-expect-error` instead of `@ts-ignore`, as `@ts-ignore` will do nothing if the following line is error-free."
287
+ // Search the comment body (both `//` and `/*` delimiters are two bytes),
288
+ // mirroring upstream's comment.value.replace(/@ts-ignore/, ...) which
289
+ // rewrites the first occurrence in the delimiter-free value.
290
+ body := text[pos:end]
291
+ index := strings.Index(body[2:], "@ts-ignore")
292
+ if index < 0 {
293
+ ctx.ReportRange(pos, end, message)
294
+ return
295
+ }
296
+ editPos := pos + 2 + index
297
+ ctx.ReportRangeSuggestion(
298
+ pos,
299
+ end,
300
+ message,
301
+ "Replace \"@ts-ignore\" with \"@ts-expect-error\".",
302
+ TextEdit{
303
+ Pos: editPos,
304
+ End: editPos + len("@ts-ignore"),
305
+ Text: "@ts-expect-error",
306
+ },
307
+ )
308
+ }
309
+
310
+ func init() {
311
+ Register(banTsComment{})
312
+ }
@@ -12,22 +12,11 @@ import (
12
12
  shimast "github.com/microsoft/typescript-go/shim/ast"
13
13
  )
14
14
 
15
- type boundariesElementTypes struct{}
16
- type boundariesExternal struct{}
17
- type boundariesEntryPoint struct{}
18
- type boundariesNoPrivate struct{}
19
- type boundariesNoUnknown struct{}
20
-
21
- // boundariesDependencies is a v1 stub for the upstream unified
22
- // `boundaries/dependencies` rule, which replaces the legacy
23
- // `element-types` / `entry-point` / `external` / `no-private` /
24
- // `no-unknown` rules with a single direction-aware policy block.
25
- //
26
- // The native engine accepts the same `elements` + `rules` config shape so
27
- // projects can claim the rule id today, but the full direction
28
- // validation is deferred. Until then this rule registers, decodes
29
- // options without crashing, and emits no diagnostics.
30
- type boundariesDependencies struct{}
15
+ type boundariesElementTypes struct{ optionsRule }
16
+ type boundariesExternal struct{ optionsRule }
17
+ type boundariesEntryPoint struct{ optionsRule }
18
+ type boundariesNoPrivate struct{ optionsRule }
19
+ type boundariesNoUnknown struct{ optionsRule }
31
20
 
32
21
  func (boundariesElementTypes) Name() string { return "boundaries/element-types" }
33
22
  func (boundariesExternal) Name() string { return "boundaries/external" }
@@ -43,15 +32,6 @@ func (boundariesNoPrivate) Visits() []shimast.Kind { return []shimast.Kind{sh
43
32
  func (boundariesNoUnknown) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
44
33
  func (boundariesDependencies) Visits() []shimast.Kind { return []shimast.Kind{shimast.KindSourceFile} }
45
34
 
46
- // Check is intentionally a no-op for the v1 stub. The rule accepts the
47
- // upstream config shape via the shared `boundariesOptions` decoder so
48
- // downstream configs type-check and load, then exits without reporting
49
- // findings. Replace this body when the unified direction validation
50
- // lands.
51
- func (boundariesDependencies) Check(ctx *Context, node *shimast.Node) {
52
- _ = decodeBoundariesOptions(ctx)
53
- }
54
-
55
35
  func (boundariesElementTypes) Check(ctx *Context, node *shimast.Node) {
56
36
  opts := decodeBoundariesOptions(ctx)
57
37
  if len(opts.Elements) == 0 {
@@ -236,9 +216,12 @@ type boundaryFile struct {
236
216
  }
237
217
 
238
218
  type boundaryDependency struct {
239
- node *shimast.Node
240
- specifier string
241
- relative bool
219
+ node *shimast.Node
220
+ specifier string
221
+ relative bool
222
+ kind string
223
+ nodeKind string
224
+ specifiers []string
242
225
  }
243
226
 
244
227
  func decodeBoundariesOptions(ctx *Context) boundariesOptions {
@@ -250,7 +233,28 @@ func decodeBoundariesOptions(ctx *Context) boundariesOptions {
250
233
  }
251
234
 
252
235
  func collectBoundaryDependencies(node *shimast.Node) []boundaryDependency {
236
+ return collectBoundaryDependenciesWithSyntax(node, false)
237
+ }
238
+
239
+ func collectBoundaryPolicyDependencies(node *shimast.Node) []boundaryDependency {
240
+ return collectBoundaryDependenciesWithSyntax(node, true)
241
+ }
242
+
243
+ func collectBoundaryDependenciesWithSyntax(node *shimast.Node, extended bool) []boundaryDependency {
253
244
  out := []boundaryDependency{}
245
+ appendDependency := func(sourceNode *shimast.Node, specifier, kind, nodeKind string, specifiers []string) {
246
+ if sourceNode == nil || specifier == "" {
247
+ return
248
+ }
249
+ out = append(out, boundaryDependency{
250
+ node: sourceNode,
251
+ specifier: specifier,
252
+ relative: isRelativeBoundarySpecifier(specifier),
253
+ kind: kind,
254
+ nodeKind: nodeKind,
255
+ specifiers: specifiers,
256
+ })
257
+ }
254
258
  var walk func(*shimast.Node)
255
259
  walk = func(n *shimast.Node) {
256
260
  if n == nil {
@@ -260,25 +264,85 @@ func collectBoundaryDependencies(node *shimast.Node) []boundaryDependency {
260
264
  case shimast.KindImportDeclaration:
261
265
  if imp := n.AsImportDeclaration(); imp != nil && imp.ModuleSpecifier != nil {
262
266
  specifier := stringLiteralText(imp.ModuleSpecifier)
263
- if specifier != "" {
264
- out = append(out, boundaryDependency{
265
- node: imp.ModuleSpecifier,
266
- specifier: specifier,
267
- relative: isRelativeBoundarySpecifier(specifier),
268
- })
269
- }
267
+ names, typeOnly := noRestrictedImportsImportNames(imp)
268
+ appendDependency(
269
+ imp.ModuleSpecifier,
270
+ specifier,
271
+ boundaryDependencyKind(typeOnly, false),
272
+ "ImportDeclaration",
273
+ boundaryImportedNames(names),
274
+ )
270
275
  }
271
276
  case shimast.KindExportDeclaration:
272
277
  if exp := n.AsExportDeclaration(); exp != nil && exp.ModuleSpecifier != nil {
273
278
  specifier := stringLiteralText(exp.ModuleSpecifier)
274
- if specifier != "" {
275
- out = append(out, boundaryDependency{
276
- node: exp.ModuleSpecifier,
277
- specifier: specifier,
278
- relative: isRelativeBoundarySpecifier(specifier),
279
- })
279
+ names, typeOnly := noRestrictedImportsExportNames(nil, n, exp)
280
+ appendDependency(
281
+ exp.ModuleSpecifier,
282
+ specifier,
283
+ boundaryDependencyKind(typeOnly, false),
284
+ "ExportDeclaration",
285
+ boundaryImportedNames(names),
286
+ )
287
+ }
288
+ case shimast.KindImportEqualsDeclaration:
289
+ if !extended {
290
+ break
291
+ }
292
+ declaration := n.AsImportEqualsDeclaration()
293
+ if declaration != nil && declaration.ModuleReference != nil &&
294
+ declaration.ModuleReference.Kind == shimast.KindExternalModuleReference {
295
+ module := declaration.ModuleReference.AsExternalModuleReference()
296
+ if module != nil && module.Expression != nil {
297
+ appendDependency(
298
+ module.Expression,
299
+ stringLiteralText(module.Expression),
300
+ boundaryDependencyKind(declaration.IsTypeOnly, false),
301
+ "ImportEqualsDeclaration",
302
+ nil,
303
+ )
280
304
  }
281
305
  }
306
+ case shimast.KindCallExpression:
307
+ if !extended {
308
+ break
309
+ }
310
+ call := n.AsCallExpression()
311
+ if call == nil || call.Expression == nil || call.Arguments == nil || len(call.Arguments.Nodes) != 1 {
312
+ break
313
+ }
314
+ argument := call.Arguments.Nodes[0]
315
+ if argument == nil {
316
+ break
317
+ }
318
+ switch {
319
+ case call.Expression.Kind == shimast.KindImportKeyword:
320
+ appendDependency(argument, stringLiteralText(argument), "value", "ImportCall", nil)
321
+ case identifierText(call.Expression) == "require":
322
+ appendDependency(argument, stringLiteralText(argument), "value", "RequireCall", nil)
323
+ }
324
+ case shimast.KindImportType:
325
+ if !extended {
326
+ break
327
+ }
328
+ imported := n.AsImportTypeNode()
329
+ if imported == nil || imported.Argument == nil || imported.Argument.Kind != shimast.KindLiteralType {
330
+ break
331
+ }
332
+ literalType := imported.Argument.AsLiteralTypeNode()
333
+ if literalType == nil || literalType.Literal == nil {
334
+ break
335
+ }
336
+ appendDependency(
337
+ literalType.Literal,
338
+ stringLiteralText(literalType.Literal),
339
+ // An `import("m")` node in type position resolves types only, so it
340
+ // is a `type` dependency; the `typeof import("m")` flavor queries the
341
+ // module's value shape and keeps the dedicated `typeof` kind.
342
+ boundaryDependencyKind(true, imported.IsTypeOf),
343
+ "ImportType",
344
+ nil,
345
+ )
282
346
  }
283
347
  n.ForEachChild(func(child *shimast.Node) bool {
284
348
  walk(child)
@@ -289,6 +353,26 @@ func collectBoundaryDependencies(node *shimast.Node) []boundaryDependency {
289
353
  return out
290
354
  }
291
355
 
356
+ func boundaryDependencyKind(typeOnly, typeOf bool) string {
357
+ if typeOf {
358
+ return "typeof"
359
+ }
360
+ if typeOnly {
361
+ return "type"
362
+ }
363
+ return "value"
364
+ }
365
+
366
+ func boundaryImportedNames(names []noRestrictedImportsImportedName) []string {
367
+ out := make([]string, 0, len(names))
368
+ for _, name := range names {
369
+ if name.name != "" {
370
+ out = append(out, name.name)
371
+ }
372
+ }
373
+ return uniqueBoundaryStrings(out)
374
+ }
375
+
292
376
  func classifyBoundaryFile(fileName string, elements []boundaryElement) *boundaryFile {
293
377
  normalized := normalizeBoundaryPath(fileName)
294
378
  matches := []boundaryFile{}
@@ -357,10 +441,10 @@ func resolveBoundaryImport(sourceFileName, specifier string) (string, bool) {
357
441
  target := filepath.Clean(filepath.Join(base, filepath.FromSlash(specifier)))
358
442
  candidates := []string{target}
359
443
  if filepath.Ext(target) == "" {
360
- for _, ext := range []string{".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
444
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts", ".d.ts", ".d.mts", ".d.cts", ".js", ".jsx", ".mjs", ".cjs"} {
361
445
  candidates = append(candidates, target+ext)
362
446
  }
363
- for _, ext := range []string{".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"} {
447
+ for _, ext := range []string{".ts", ".tsx", ".mts", ".cts", ".d.ts", ".d.mts", ".d.cts", ".js", ".jsx", ".mjs", ".cjs"} {
364
448
  candidates = append(candidates, filepath.Join(target, "index"+ext))
365
449
  }
366
450
  }
@@ -499,6 +583,7 @@ func boundaryDisplayPath(path string) string {
499
583
  }
500
584
 
501
585
  func normalizeBoundaryPath(path string) string {
586
+ path = strings.ReplaceAll(path, `\`, "/")
502
587
  return filepath.ToSlash(filepath.Clean(path))
503
588
  }
504
589