@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
@@ -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.
@@ -68,6 +79,7 @@ func registerContributors() {
68
79
  }
69
80
  Register(adapter)
70
81
  }
82
+ registerProjectContributors()
71
83
  }
72
84
 
73
85
  // contributorMetadata is the immutable host-side view of a public contributor
@@ -76,10 +88,12 @@ func registerContributors() {
76
88
  // panic later during registry sorting or engine construction.
77
89
  type contributorMetadata struct {
78
90
  inner rule.Rule
91
+ acceptsOptions bool
79
92
  isFormat bool
80
93
  name string
81
94
  visits []shimast.Kind
82
95
  visitsDeclarationFiles bool
96
+ needsTypeChecker bool
83
97
  }
84
98
 
85
99
  // inspectContributor evaluates the public rule metadata behind a recover
@@ -95,9 +109,11 @@ func inspectContributor(contributor rule.Rule) (metadata contributorMetadata, er
95
109
  }()
96
110
  metadata = contributorMetadata{
97
111
  inner: contributor,
112
+ acceptsOptions: true,
98
113
  name: contributor.Name(),
99
114
  visits: append([]shimast.Kind(nil), contributor.Visits()...),
100
115
  visitsDeclarationFiles: true,
116
+ needsTypeChecker: true,
101
117
  }
102
118
  if formatRule, ok := contributor.(rule.FormatRule); ok {
103
119
  metadata.isFormat = formatRule.IsFormat()
@@ -105,6 +121,12 @@ func inspectContributor(contributor rule.Rule) (metadata contributorMetadata, er
105
121
  if declarationRule, ok := contributor.(rule.DeclarationFileRule); ok {
106
122
  metadata.visitsDeclarationFiles = declarationRule.VisitsDeclarationFiles()
107
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
+ }
108
130
  return metadata, nil
109
131
  }
110
132
 
@@ -115,9 +137,11 @@ func inspectContributor(contributor rule.Rule) (metadata contributorMetadata, er
115
137
  func newContributorAdapter(metadata contributorMetadata) contributorAdapter {
116
138
  return contributorAdapter{
117
139
  inner: metadata.inner,
140
+ acceptsOptions: metadata.acceptsOptions,
118
141
  name: metadata.name,
119
142
  visits: metadata.visits,
120
143
  visitsDeclarationFiles: metadata.visitsDeclarationFiles,
144
+ needsTypeChecker: metadata.needsTypeChecker,
121
145
  }
122
146
  }
123
147
 
@@ -130,16 +154,30 @@ func newContributorAdapter(metadata contributorMetadata) contributorAdapter {
130
154
  // same shim AST types, so no wrapping / unwrapping of nodes is needed.
131
155
  type contributorAdapter struct {
132
156
  inner rule.Rule
157
+ acceptsOptions bool
133
158
  name string
134
159
  visits []shimast.Kind
135
160
  visitsDeclarationFiles bool
161
+ needsTypeChecker bool
162
+ }
163
+
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
136
170
  }
137
171
 
138
- // NeedsTypeChecker keeps contributor rules on the historical checker path.
139
- // The public rule.Context exposes Checker and has no mandatory marker, so the
140
- // host cannot safely infer that a third-party rule is AST-only.
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.
141
179
  func (a contributorAdapter) NeedsTypeChecker() bool {
142
- return true
180
+ return a.needsTypeChecker
143
181
  }
144
182
 
145
183
  // VisitsDeclarationFiles keeps contributor rules running on declaration
@@ -169,12 +207,13 @@ func (a contributorAdapter) Check(ctx *Context, node *shimast.Node) {
169
207
  if ctx == nil {
170
208
  return
171
209
  }
172
- pubCtx := rule.NewContext(
210
+ pubCtx := rule.NewContextWithProjectResults(
173
211
  ctx.File,
174
212
  ctx.Checker,
175
213
  rule.Severity(ctx.Severity),
176
214
  ctx.Options,
177
215
  contextReporter{ctx: ctx},
216
+ ctx.projectResults,
178
217
  )
179
218
  a.inner.Check(pubCtx, node)
180
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:])