@ttsc/lint 0.18.3 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/README.md +95 -43
  2. package/go.mod +5 -4
  3. package/internal/rulecode/rulecode.go +88 -0
  4. package/lib/index.d.ts +10 -1
  5. package/lib/index.js +45 -17
  6. package/lib/index.js.map +1 -1
  7. package/lib/structures/ITtscLintConfig.d.ts +3 -0
  8. package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
  9. package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
  10. package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
  11. package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
  12. package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
  13. package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
  14. package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
  15. package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
  16. package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
  17. package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
  18. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
  19. package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
  20. package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
  21. package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
  22. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
  23. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
  24. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
  25. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
  26. package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
  27. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
  28. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
  29. package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
  30. package/lib/structures/rules/index.d.ts +3 -0
  31. package/lib/structures/rules/index.js +3 -0
  32. package/lib/structures/rules/index.js.map +1 -1
  33. package/linthost/ast_helpers.go +521 -23
  34. package/linthost/ast_selector_matcher.go +1628 -0
  35. package/linthost/ast_selector_parser.go +628 -0
  36. package/linthost/comment_scan.go +154 -0
  37. package/linthost/compile.go +129 -79
  38. package/linthost/config.go +352 -88
  39. package/linthost/config_format.go +15 -1
  40. package/linthost/contrib_adapter.go +129 -25
  41. package/linthost/declaration_rules.go +12 -11
  42. package/linthost/directives.go +9 -67
  43. package/linthost/dispatch.go +37 -3
  44. package/linthost/engine.go +368 -53
  45. package/linthost/escape_string.go +77 -0
  46. package/linthost/fix.go +133 -23
  47. package/linthost/flags_gen.go +18 -17
  48. package/linthost/format.go +73 -167
  49. package/linthost/format_editor_settings.go +134 -26
  50. package/linthost/grapheme_tables_gen.go +1920 -0
  51. package/linthost/host.go +160 -61
  52. package/linthost/literal_escape_scan.go +117 -0
  53. package/linthost/lsp.go +545 -98
  54. package/linthost/numeric_literal_precision.go +228 -0
  55. package/linthost/polyfill_browserslist.go +1439 -0
  56. package/linthost/polyfill_browserslist_config.go +603 -0
  57. package/linthost/polyfill_corejs.go +750 -0
  58. package/linthost/polyfill_data.go +113 -0
  59. package/linthost/polyfill_data_gen.json +39868 -0
  60. package/linthost/print_nodes_array.go +7 -1
  61. package/linthost/print_nodes_object.go +7 -1
  62. package/linthost/project_engine.go +151 -0
  63. package/linthost/project_rules.go +78 -0
  64. package/linthost/regex_clean.go +191 -0
  65. package/linthost/regex_tree.go +1544 -0
  66. package/linthost/regex_tree_optimizer.go +1685 -0
  67. package/linthost/regexp_cache.go +44 -0
  68. package/linthost/rule_codes.go +105 -0
  69. package/linthost/rule_codes.json +745 -0
  70. package/linthost/rules_ban_ts_comment.go +312 -0
  71. package/linthost/rules_boundaries.go +128 -43
  72. package/linthost/rules_boundaries_dependencies.go +1013 -0
  73. package/linthost/rules_core_extra.go +62 -92
  74. package/linthost/rules_cypress.go +1 -1
  75. package/linthost/rules_default_case.go +87 -11
  76. package/linthost/rules_dupes.go +38 -13
  77. package/linthost/rules_empty.go +188 -20
  78. package/linthost/rules_escape.go +62 -48
  79. package/linthost/rules_format_arrow_parens.go +19 -5
  80. package/linthost/rules_format_bracket_spacing.go +1 -1
  81. package/linthost/rules_format_clause_join.go +1 -1
  82. package/linthost/rules_format_declaration_header.go +31 -19
  83. package/linthost/rules_format_indent.go +1 -1
  84. package/linthost/rules_format_jsdoc.go +1 -1
  85. package/linthost/rules_format_orphan_semi.go +1 -1
  86. package/linthost/rules_format_parameter_properties.go +17 -7
  87. package/linthost/rules_format_print_width.go +1 -1
  88. package/linthost/rules_format_quote_props.go +1 -1
  89. package/linthost/rules_format_quotes.go +1 -1
  90. package/linthost/rules_format_semi.go +106 -79
  91. package/linthost/rules_format_sort_imports.go +79 -45
  92. package/linthost/rules_format_statement_split.go +1 -1
  93. package/linthost/rules_format_ternary_nullish_parens.go +1 -1
  94. package/linthost/rules_format_trailing_comma.go +64 -1
  95. package/linthost/rules_format_whitespace.go +1 -1
  96. package/linthost/rules_functional.go +21 -22
  97. package/linthost/rules_gap.go +80 -3
  98. package/linthost/rules_grouped_accessor_pairs.go +84 -25
  99. package/linthost/rules_guard_for_in.go +64 -147
  100. package/linthost/rules_jsx_a11y.go +232 -22
  101. package/linthost/rules_logic.go +84 -38
  102. package/linthost/rules_nextjs.go +105 -16
  103. package/linthost/rules_no_class_assign.go +70 -0
  104. package/linthost/rules_no_duplicate_imports.go +266 -0
  105. package/linthost/rules_no_else_return.go +156 -38
  106. package/linthost/rules_no_extend_native.go +113 -17
  107. package/linthost/rules_no_fallthrough.go +1013 -0
  108. package/linthost/rules_no_func_assign.go +70 -0
  109. package/linthost/rules_no_import_assign.go +278 -86
  110. package/linthost/rules_no_inner_declarations.go +186 -0
  111. package/linthost/rules_no_loop_func.go +438 -53
  112. package/linthost/rules_no_mixed_operators.go +208 -52
  113. package/linthost/rules_no_param_reassign.go +376 -61
  114. package/linthost/rules_no_promise_executor_return.go +156 -0
  115. package/linthost/rules_no_restricted_imports.go +1013 -57
  116. package/linthost/rules_no_restricted_syntax.go +190 -26
  117. package/linthost/rules_no_useless_constructor.go +6 -36
  118. package/linthost/rules_problems.go +154 -311
  119. package/linthost/rules_promise.go +2578 -109
  120. package/linthost/rules_react_extras.go +71 -45
  121. package/linthost/rules_react_hooks.go +18 -2
  122. package/linthost/rules_react_perf.go +1 -0
  123. package/linthost/rules_react_refresh.go +1 -1
  124. package/linthost/rules_regexp.go +230 -14
  125. package/linthost/rules_security.go +45 -19
  126. package/linthost/rules_self.go +21 -1
  127. package/linthost/rules_solid.go +13 -7
  128. package/linthost/rules_storybook.go +1 -1
  129. package/linthost/rules_suggestions.go +490 -84
  130. package/linthost/rules_testing_library.go +12 -1
  131. package/linthost/rules_ts.go +227 -54
  132. package/linthost/rules_ts_async.go +0 -131
  133. package/linthost/rules_ts_no_base_to_string.go +3 -4
  134. package/linthost/rules_ts_no_misused_promises.go +817 -0
  135. package/linthost/rules_ts_no_restricted_types.go +320 -34
  136. package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
  137. package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
  138. package/linthost/rules_ts_require_array_sort_compare.go +5 -5
  139. package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
  140. package/linthost/rules_unicorn_better_regex.go +224 -16
  141. package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
  142. package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
  143. package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
  144. package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
  145. package/linthost/rules_unicorn_escape_case.go +30 -19
  146. package/linthost/rules_unicorn_filename_case.go +763 -16
  147. package/linthost/rules_unicorn_import_style.go +783 -17
  148. package/linthost/rules_unicorn_isolated_functions.go +934 -14
  149. package/linthost/rules_unicorn_no_hex_escape.go +26 -11
  150. package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
  151. package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
  152. package/linthost/rules_unicorn_no_unused_properties.go +756 -19
  153. package/linthost/rules_unicorn_number_literal_case.go +58 -39
  154. package/linthost/rules_unicorn_prefer_at.go +7 -3
  155. package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
  156. package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
  157. package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
  158. package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
  159. package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
  160. package/linthost/rules_unicorn_string_content.go +611 -15
  161. package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
  162. package/linthost/rules_unicorn_template_indent.go +923 -18
  163. package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
  164. package/linthost/rules_unicorn_throw_new_error.go +218 -26
  165. package/linthost/rules_var.go +829 -142
  166. package/linthost/string_length.go +222 -0
  167. package/package.json +4 -3
  168. package/rule/project.go +145 -0
  169. package/rule/rule.go +58 -1
  170. package/src/index.ts +58 -17
  171. package/src/structures/ITtscLintConfig.ts +3 -0
  172. package/src/structures/TtscLintRuleSetting.ts +3 -2
  173. package/src/structures/format/ITtscLintFormat.ts +3 -2
  174. package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
  175. package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
  176. package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
  177. package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
  178. package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
  179. package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
  180. package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
  181. package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
  182. package/src/structures/rules/ITtscLintRules.ts +7 -2
  183. package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
  184. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
  185. package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
  186. package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
  187. package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
  188. package/src/structures/rules/index.ts +3 -0
@@ -10,21 +10,32 @@
10
10
  // `package main` init functions and the relative order across files in
11
11
  // the same package is alphabetical — so we cannot blindly run the
12
12
  // contributor wiring from a file-level `init()` and expect built-in
13
- // registration to have already completed. Instead, `registerContributors`
14
- // is invoked explicitly from `main.run` after all built-in `init` calls
15
- // have settled, which makes the collision check meaningful.
13
+ // registration to have already completed. Instead, `registerContributorsOnce`
14
+ // is invoked explicitly from `run` after all built-in `init` calls have
15
+ // settled, which makes the collision check meaningful.
16
16
  package linthost
17
17
 
18
18
  import (
19
19
  "fmt"
20
20
  "os"
21
21
  "sort"
22
+ "sync"
22
23
 
23
24
  shimast "github.com/microsoft/typescript-go/shim/ast"
24
25
 
25
26
  "github.com/samchon/ttsc/packages/lint/rule"
26
27
  )
27
28
 
29
+ var contributorBootstrap sync.Once
30
+
31
+ // registerContributorsOnce installs the immutable init-time contributor
32
+ // registries exactly once. Main is a reusable library entry and may be called
33
+ // concurrently, so every caller must wait for the same completed bootstrap
34
+ // before constructing an Engine that reads the global rule maps.
35
+ func registerContributorsOnce() {
36
+ contributorBootstrap.Do(registerContributors)
37
+ }
38
+
28
39
  // registerContributors wraps every contributor rule registered through
29
40
  // the public `rule` package into the engine's internal `Rule` interface
30
41
  // and pushes it onto the same dispatch table the built-in rules use.
@@ -34,12 +45,23 @@ import (
34
45
  // with a stderr warning. The host prefers a deterministic, debuggable
35
46
  // outcome over panicking inside startup.
36
47
  func registerContributors() {
37
- contributors := rule.Registered()
48
+ registered := rule.Registered()
49
+ contributors := make([]contributorMetadata, 0, len(registered))
50
+ for _, contributor := range registered {
51
+ metadata, err := inspectContributor(contributor)
52
+ if err != nil {
53
+ fmt.Fprintf(os.Stderr,
54
+ "@ttsc/lint: %v; dropping contributor entry\n",
55
+ err)
56
+ continue
57
+ }
58
+ contributors = append(contributors, metadata)
59
+ }
38
60
  sort.SliceStable(contributors, func(i, j int) bool {
39
- return contributors[i].Name() < contributors[j].Name()
61
+ return contributors[i].name < contributors[j].name
40
62
  })
41
63
  for _, contributor := range contributors {
42
- name := contributor.Name()
64
+ name := contributor.name
43
65
  if name == "" {
44
66
  fmt.Fprintln(os.Stderr, "@ttsc/lint: contributor rule with empty name ignored")
45
67
  continue
@@ -50,41 +72,122 @@ func registerContributors() {
50
72
  name)
51
73
  continue
52
74
  }
53
- if fr, ok := contributor.(rule.FormatRule); ok && fr.IsFormat() {
54
- Register(formatContributorAdapter{contributorAdapter{inner: contributor}})
75
+ adapter := newContributorAdapter(contributor)
76
+ if contributor.isFormat {
77
+ Register(formatContributorAdapter{adapter})
55
78
  continue
56
79
  }
57
- Register(contributorAdapter{inner: contributor})
80
+ Register(adapter)
81
+ }
82
+ registerProjectContributors()
83
+ }
84
+
85
+ // contributorMetadata is the immutable host-side view of a public contributor
86
+ // rule. Every contributor-defined metadata method is evaluated exactly once
87
+ // behind inspectContributor's recover barrier so a broken declaration cannot
88
+ // panic later during registry sorting or engine construction.
89
+ type contributorMetadata struct {
90
+ inner rule.Rule
91
+ acceptsOptions bool
92
+ isFormat bool
93
+ name string
94
+ visits []shimast.Kind
95
+ visitsDeclarationFiles bool
96
+ needsTypeChecker bool
97
+ }
98
+
99
+ // inspectContributor evaluates the public rule metadata behind a recover
100
+ // barrier. Check itself is protected separately by runRuleCheck; this function
101
+ // covers the startup methods the engine must call before it can dispatch a
102
+ // node. A panicking contributor is rejected as one entry while the rest of the
103
+ // lint registry remains usable.
104
+ func inspectContributor(contributor rule.Rule) (metadata contributorMetadata, err error) {
105
+ defer func() {
106
+ if recovered := recover(); recovered != nil {
107
+ err = fmt.Errorf("contributor %T metadata panicked: %v", contributor, recovered)
108
+ }
109
+ }()
110
+ metadata = contributorMetadata{
111
+ inner: contributor,
112
+ acceptsOptions: true,
113
+ name: contributor.Name(),
114
+ visits: append([]shimast.Kind(nil), contributor.Visits()...),
115
+ visitsDeclarationFiles: true,
116
+ needsTypeChecker: true,
117
+ }
118
+ if formatRule, ok := contributor.(rule.FormatRule); ok {
119
+ metadata.isFormat = formatRule.IsFormat()
120
+ }
121
+ if declarationRule, ok := contributor.(rule.DeclarationFileRule); ok {
122
+ metadata.visitsDeclarationFiles = declarationRule.VisitsDeclarationFiles()
123
+ }
124
+ if typeAware, ok := contributor.(rule.TypeAwareRule); ok {
125
+ metadata.needsTypeChecker = typeAware.NeedsTypeChecker()
126
+ }
127
+ if optionsRule, ok := contributor.(rule.OptionsRule); ok {
128
+ metadata.acceptsOptions = optionsRule.AcceptsTtscLintOptions()
129
+ }
130
+ return metadata, nil
131
+ }
132
+
133
+ // newContributorAdapter builds the engine-facing adapter from metadata already
134
+ // evaluated by inspectContributor, so no adapter method re-enters contributor
135
+ // code after startup. This is the only construction path; a zero-value
136
+ // contributorAdapter carries no usable metadata.
137
+ func newContributorAdapter(metadata contributorMetadata) contributorAdapter {
138
+ return contributorAdapter{
139
+ inner: metadata.inner,
140
+ acceptsOptions: metadata.acceptsOptions,
141
+ name: metadata.name,
142
+ visits: metadata.visits,
143
+ visitsDeclarationFiles: metadata.visitsDeclarationFiles,
144
+ needsTypeChecker: metadata.needsTypeChecker,
58
145
  }
59
146
  }
60
147
 
61
148
  // contributorAdapter wraps a public `rule.Rule` so the engine's
62
- // `Register` accepts it. Forward `Name` and `Visits` directly; bridge
63
- // `Check` by constructing a `rule.Context` whose `Reporter` calls back
64
- // into the engine's existing `Context.Report` / `ReportRange`. The
149
+ // `Register` accepts it. Name, Visits, and declaration-file policy are
150
+ // cached by inspectContributor; Check bridges through a `rule.Context`
151
+ // whose `Reporter` calls back into the engine's existing Context.Report /
152
+ // ReportRange. The
65
153
  // public `rule.Context` and the engine's internal `Context` share the
66
154
  // same shim AST types, so no wrapping / unwrapping of nodes is needed.
67
155
  type contributorAdapter struct {
68
- inner rule.Rule
156
+ inner rule.Rule
157
+ acceptsOptions bool
158
+ name string
159
+ visits []shimast.Kind
160
+ visitsDeclarationFiles bool
161
+ needsTypeChecker bool
69
162
  }
70
163
 
71
- // NeedsTypeChecker keeps contributor rules on the historical checker path.
72
- // The public rule.Context exposes Checker and has no mandatory marker, so the
73
- // host cannot safely infer that a third-party rule is AST-only.
164
+ // AcceptsTtscLintOptions preserves the public contributor contract:
165
+ // contributors have historically been allowed to decode an arbitrary options
166
+ // blob, so the conservative default is true. A contributor can implement
167
+ // rule.OptionsRule and return false to declare a genuinely optionless rule.
168
+ func (a contributorAdapter) AcceptsTtscLintOptions() bool {
169
+ return a.acceptsOptions
170
+ }
171
+
172
+ // NeedsTypeChecker keeps contributor rules on the conservative checker path
173
+ // unless the contributor opts out through the public `rule.TypeAwareRule`
174
+ // marker. The public rule.Context exposes Checker, so the host cannot infer a
175
+ // third-party rule is AST-only — the default is type-aware, and only a rule
176
+ // that explicitly returns false skips the standalone checker and keeps the
177
+ // parallel file walk. The default-true / marker-override policy is applied
178
+ // once in inspectContributor.
74
179
  func (a contributorAdapter) NeedsTypeChecker() bool {
75
- return true
180
+ return a.needsTypeChecker
76
181
  }
77
182
 
78
183
  // VisitsDeclarationFiles keeps contributor rules running on declaration
79
184
  // files unless the contributor opts out through the public
80
185
  // `rule.DeclarationFileRule` marker. Same conservative-default reasoning
81
186
  // as NeedsTypeChecker: the host cannot infer a third-party rule's grammar
82
- // shape, and a wrong skip silently loses findings.
187
+ // shape, and a wrong skip silently loses findings. The default-true /
188
+ // marker-override policy is applied once in inspectContributor.
83
189
  func (a contributorAdapter) VisitsDeclarationFiles() bool {
84
- if dr, ok := a.inner.(rule.DeclarationFileRule); ok {
85
- return dr.VisitsDeclarationFiles()
86
- }
87
- return true
190
+ return a.visitsDeclarationFiles
88
191
  }
89
192
 
90
193
  // formatContributorAdapter is the FormatRule-tagged variant of
@@ -98,18 +201,19 @@ type formatContributorAdapter struct {
98
201
 
99
202
  func (formatContributorAdapter) IsFormat() bool { return true }
100
203
 
101
- func (a contributorAdapter) Name() string { return a.inner.Name() }
102
- func (a contributorAdapter) Visits() []shimast.Kind { return a.inner.Visits() }
204
+ func (a contributorAdapter) Name() string { return a.name }
205
+ func (a contributorAdapter) Visits() []shimast.Kind { return a.visits }
103
206
  func (a contributorAdapter) Check(ctx *Context, node *shimast.Node) {
104
207
  if ctx == nil {
105
208
  return
106
209
  }
107
- pubCtx := rule.NewContext(
210
+ pubCtx := rule.NewContextWithProjectResults(
108
211
  ctx.File,
109
212
  ctx.Checker,
110
213
  rule.Severity(ctx.Severity),
111
214
  ctx.Options,
112
215
  contextReporter{ctx: ctx},
216
+ ctx.projectResults,
113
217
  )
114
218
  a.inner.Check(pubCtx, node)
115
219
  }
@@ -159,15 +159,16 @@ var declarationFileRuleNames = map[string]bool{
159
159
  "functional/type-declaration-immutability": true,
160
160
 
161
161
  // unicorn/* rules over file names, comments, identifier naming, and
162
- // the numeric literals that appear in enum initializers and literal
163
- // types.
164
- "unicorn/empty-brace-spaces": true,
165
- "unicorn/expiring-todo-comments": true,
166
- "unicorn/filename-case": true,
167
- "unicorn/no-abusive-eslint-disable": true,
168
- "unicorn/no-empty-file": true,
169
- "unicorn/no-keyword-prefix": true,
170
- "unicorn/number-literal-case": true,
171
- "unicorn/numeric-separators-style": true,
172
- "unicorn/prevent-abbreviations": true,
162
+ // the literals that appear in enum initializers and literal types
163
+ // (numeric literals, template literal types).
164
+ "unicorn/consistent-template-literal-escape": true,
165
+ "unicorn/empty-brace-spaces": true,
166
+ "unicorn/expiring-todo-comments": true,
167
+ "unicorn/filename-case": true,
168
+ "unicorn/no-abusive-eslint-disable": true,
169
+ "unicorn/no-empty-file": true,
170
+ "unicorn/no-keyword-prefix": true,
171
+ "unicorn/number-literal-case": true,
172
+ "unicorn/numeric-separators-style": true,
173
+ "unicorn/prevent-abbreviations": true,
173
174
  }
@@ -98,28 +98,17 @@ func filterInlineDisabledFindingsWithDirectives(file *shimast.SourceFile, findin
98
98
  }
99
99
  filtered := findings[:0]
100
100
  for _, finding := range findings {
101
- if finding == nil || !directives.suppresses(file, finding) {
101
+ if finding == nil || finding.engineFailure || !directives.suppresses(file, finding) {
102
102
  filtered = append(filtered, finding)
103
103
  }
104
104
  }
105
105
  return filtered
106
106
  }
107
107
 
108
- // parseLintInlineDirectives scans all comment tokens in `file` for
109
- // recognized directive markers and returns a structured summary of the
110
- // per-line and range-style suppressions found.
111
- //
112
- // The raw scanner does not split `KindTemplateExpression` on its own:
113
- // after returning `KindTemplateHead`/`KindTemplateMiddle`, it resumes
114
- // lexing the substitution as ordinary code, and a later `}` is reported
115
- // as `KindCloseBraceToken` instead of re-entering the template body.
116
- // Without intervention the next backtick would open a fresh template
117
- // scan that swallows the rest of the file (including every disable
118
- // directive comment) as one runaway unterminated literal. The parser
119
- // avoids this by calling `ReScanTemplateToken` on the matching `}`;
120
- // this loop mirrors that behavior with a brace-depth stack so comment
121
- // positions stay aligned with the source bytes past any template
122
- // substitution.
108
+ // parseLintInlineDirectives scans all parser-classified comment tokens in
109
+ // `file` (via the shared `forEachCommentToken` loop) for recognized directive
110
+ // markers and returns a structured summary of the per-line and range-style
111
+ // suppressions found.
123
112
  func parseLintInlineDirectives(file *shimast.SourceFile) *lintInlineDirectives {
124
113
  directives := &lintInlineDirectives{
125
114
  lines: make(map[int][]lintDirectiveRules),
@@ -135,58 +124,11 @@ func parseLintInlineDirectives(file *shimast.SourceFile) *lintInlineDirectives {
135
124
  if !strings.Contains(text, "-disable") && !strings.Contains(text, "-enable") {
136
125
  return directives
137
126
  }
138
- scanner := shimscanner.NewScanner()
139
- scanner.SetText(text)
140
- scanner.SetSkipTrivia(false)
141
-
142
- // templateBraceDepth tracks `{` nesting inside each open template
143
- // substitution. A zero on top means the next `}` matches the original
144
- // `${` and must be re-scanned as a template middle/tail token.
145
- var templateBraceDepth []int
146
-
147
- scan:
148
- for {
149
- kind := scanner.Scan()
150
- switch kind {
151
- case shimast.KindEndOfFile:
152
- break scan
153
- case shimast.KindTemplateHead, shimast.KindTemplateMiddle:
154
- // Entering a `${...}` substitution; account for its closing `}`.
155
- templateBraceDepth = append(templateBraceDepth, 0)
156
- continue
157
- case shimast.KindOpenBraceToken:
158
- if n := len(templateBraceDepth); n > 0 {
159
- templateBraceDepth[n-1]++
160
- }
161
- continue
162
- case shimast.KindCloseBraceToken:
163
- n := len(templateBraceDepth)
164
- if n == 0 {
165
- continue
166
- }
167
- if templateBraceDepth[n-1] > 0 {
168
- templateBraceDepth[n-1]--
169
- continue
170
- }
171
- // Matching `}` for the original `${`. Pop the substitution and
172
- // rescan as template; a `KindTemplateMiddle` reopens a new
173
- // substitution, a `KindTemplateTail` closes the template literal.
174
- templateBraceDepth = templateBraceDepth[:n-1]
175
- rescanned := scanner.ReScanTemplateToken(false /*isTaggedTemplate*/)
176
- if rescanned == shimast.KindTemplateMiddle {
177
- templateBraceDepth = append(templateBraceDepth, 0)
178
- }
179
- continue
180
- case shimast.KindSingleLineCommentTrivia, shimast.KindMultiLineCommentTrivia:
181
- default:
182
- continue
183
- }
184
- directive, ok := parseLintDirectiveComment(scanner.TokenText())
127
+ forEachCommentToken(file, func(_ shimast.Kind, start, end int) {
128
+ directive, ok := parseLintDirectiveComment(text[start:end])
185
129
  if !ok {
186
- continue
130
+ return
187
131
  }
188
- start := scanner.TokenStart()
189
- end := scanner.TokenEnd()
190
132
  startLine := shimscanner.GetECMALineOfPosition(file, start)
191
133
  endLine := startLine
192
134
  if end > start {
@@ -217,7 +159,7 @@ scan:
217
159
  directives.records = append(directives.records, lintDirectiveRecord{
218
160
  ruleList: append([]string(nil), directive.ruleList...),
219
161
  })
220
- }
162
+ })
221
163
  return directives
222
164
  }
223
165
 
@@ -8,6 +8,7 @@ package linthost
8
8
 
9
9
  import (
10
10
  "fmt"
11
+ "io"
11
12
  "os"
12
13
  )
13
14
 
@@ -29,6 +30,39 @@ func Main(args []string) int {
29
30
  return run(args)
30
31
  }
31
32
 
33
+ // MainWithIO dispatches the browser-owned project commands without consulting
34
+ // process-global stdout or stderr. Native-only mutation and LSP commands retain
35
+ // Main as their CLI entrypoint.
36
+ func MainWithIO(args []string, stdout, stderr io.Writer) int {
37
+ if stdout == nil {
38
+ stdout = io.Discard
39
+ }
40
+ if stderr == nil {
41
+ stderr = io.Discard
42
+ }
43
+ if len(args) == 0 {
44
+ fmt.Fprintln(stderr, "@ttsc/lint: command required (expected check|build|transform|version)")
45
+ return 2
46
+ }
47
+ switch args[0] {
48
+ case "-v", "--version", "version":
49
+ fmt.Fprintf(stdout, "@ttsc/lint %s\n", Version)
50
+ return 0
51
+ case "check":
52
+ registerContributorsOnce()
53
+ return RunCheckWithIO(args[1:], stdout, stderr)
54
+ case "build":
55
+ registerContributorsOnce()
56
+ return RunBuildWithIO(args[1:], stdout, stderr)
57
+ case "transform":
58
+ registerContributorsOnce()
59
+ return RunTransformWithIO(args[1:], stdout, stderr)
60
+ default:
61
+ fmt.Fprintf(stderr, "@ttsc/lint: command %q is unavailable in the browser host\n", args[0])
62
+ return 2
63
+ }
64
+ }
65
+
32
66
  // run is the package-local dispatcher invoked by Main and by the in-tree
33
67
  // test/command corpus, which exercises end-to-end subcommand routing through
34
68
  // the same entry point the CLI uses.
@@ -47,9 +81,9 @@ func run(args []string) int {
47
81
  fmt.Fprintf(os.Stderr, "@ttsc/lint: unknown command %q\n", args[0])
48
82
  return 2
49
83
  }
50
- // Wire contributor rules into the engine's dispatch table after every
51
- // package init has settled. See contrib_adapter.go for the rationale.
52
- registerContributors()
84
+ // Wire init-time contributor rules into the engine's dispatch table once,
85
+ // after every package init has settled. See contrib_adapter.go.
86
+ registerContributorsOnce()
53
87
  switch args[0] {
54
88
  case "check":
55
89
  return RunCheck(args[1:])