@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
package/linthost/host.go CHANGED
@@ -13,8 +13,10 @@ import (
13
13
  "context"
14
14
  "errors"
15
15
  "fmt"
16
+ "os"
16
17
  "path/filepath"
17
18
  "strings"
19
+ "sync/atomic"
18
20
 
19
21
  shimast "github.com/microsoft/typescript-go/shim/ast"
20
22
  "github.com/microsoft/typescript-go/shim/bundled"
@@ -25,24 +27,29 @@ import (
25
27
  shimtspath "github.com/microsoft/typescript-go/shim/tspath"
26
28
  "github.com/microsoft/typescript-go/shim/vfs/cachedvfs"
27
29
  "github.com/microsoft/typescript-go/shim/vfs/osvfs"
30
+
31
+ publicrule "github.com/samchon/ttsc/packages/lint/rule"
28
32
  )
29
33
 
30
- // program bundles the tsgo Program with the parsed config and a checker
31
- // release callback so the orchestration code can clean up after itself.
34
+ var programLifecycleSequence atomic.Uint64
35
+
36
+ // program bundles the tsgo Program with the parsed config and the standalone
37
+ // checker used only by type-aware lint rules.
32
38
  type program struct {
33
- cwd string
34
- tsProgram *shimcompiler.Program
35
- parsed *tsoptions.ParsedCommandLine
36
- checker *shimchecker.Checker
37
- releaseChecker func()
39
+ cwd string
40
+ tsProgram *shimcompiler.Program
41
+ parsed *tsoptions.ParsedCommandLine
42
+ checker *shimchecker.Checker
43
+ identity publicrule.ProjectIdentity
44
+ projectCycle *projectCycle
38
45
  }
39
46
 
40
47
  type loadProgramOptions struct {
41
48
  forceEmit bool
42
49
  forceNoEmit bool
43
50
  outDir string
44
- // needsRuleChecker asks loadProgram to pin the checker pool and acquire the
45
- // checker that type-aware lint rules receive through Context.Checker.
51
+ // needsRuleChecker asks loadProgram to create the standalone checker that
52
+ // type-aware lint rules receive through Context.Checker.
46
53
  needsRuleChecker bool
47
54
  // singleThreaded mirrors `tsgo --singleThreaded`: one checker, serial
48
55
  // parse/check/emit.
@@ -54,11 +61,12 @@ type loadProgramOptions struct {
54
61
  // `--target es2020`, …). They are parsed through TypeScript-Go's own
55
62
  // command-line parser into a CompilerOptions overlay that wins over the
56
63
  // tsconfig, exactly as tsgo's CLI merges them.
57
- tsgoArgs []string
64
+ tsgoArgs []string
65
+ projectIdentity publicrule.ProjectIdentity
58
66
  }
59
67
 
60
68
  // loadProgram parses the given tsconfig and builds a Program. When
61
- // needsRuleChecker is set, it also acquires a type checker for lint rules.
69
+ // needsRuleChecker is set, it also creates a standalone checker for lint rules.
62
70
  // Mirrors the canonical bootstrap pattern from
63
71
  // `03-tsgo.md` — the only ttsc-specific bit is that `forceEmit`/
64
72
  // `forceNoEmit`/`outDir` overrides are merged into the parsed config
@@ -111,20 +119,15 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
111
119
  overrideOutDir(cwd, parsed, options.outDir)
112
120
  }
113
121
  applyThreading(parsed, options.singleThreaded, options.checkers)
114
- if options.needsRuleChecker {
115
- forceSingleChecker(parsed)
116
- }
117
122
 
118
- // SingleThreaded is left unset so the program keeps TypeScript-Go's parallel
119
- // source parsing and parallel emit. For type-aware lint rules, the checker
120
- // pool is pinned to a single checker (see forceSingleChecker): the lint
121
- // engine walks files serially against the one checker GetTypeChecker hands
122
- // back, and rules ask that checker to resolve types in nodes drawn from every
123
- // source file. TypeScript-Go's multi-checker pool affinitizes each file to a
124
- // different checker and forbids mixing types across them, so a type whose
125
- // declarations span files on different checkers (e.g. a circular
126
- // indexed-access alias) resolves to `any` on the borrowed checker. AST-only
127
- // lint rules do not receive a checker, so they keep the user's checker pool.
123
+ // Keep the user's checker pool intact for parallel semantic diagnostics.
124
+ // Type-aware lint rules cannot borrow one member of that pool: they walk
125
+ // every source file through a single Context.Checker, while TypeScript-Go
126
+ // affinitizes files to different pool members and forbids mixing their type
127
+ // graphs. Instead, lint owns a standalone checker over the same Program.
128
+ // Every lint type is then produced by one checker, while the Program's pool
129
+ // remains free to check its file groups in parallel. The engine serializes
130
+ // type-aware walks, so this dedicated checker is never accessed concurrently.
128
131
  tsProgram := shimcompiler.NewProgram(shimcompiler.ProgramOptions{
129
132
  Config: parsed,
130
133
  Host: host,
@@ -134,29 +137,145 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
134
137
  return nil, nil, errors.New("compiler.NewProgram returned nil")
135
138
  }
136
139
  var checker *shimchecker.Checker
137
- var release func()
138
140
  if options.needsRuleChecker {
139
- checker, release = tsProgram.GetTypeChecker(context.Background())
141
+ checker, _ = shimchecker.NewChecker(tsProgram, nil)
140
142
  }
141
143
  return &program{
142
- cwd: cwd,
143
- tsProgram: tsProgram,
144
- parsed: parsed,
145
- checker: checker,
146
- releaseChecker: release,
144
+ cwd: cwd,
145
+ tsProgram: tsProgram,
146
+ parsed: parsed,
147
+ checker: checker,
148
+ identity: normalizeProjectIdentity(options.projectIdentity, cwd, resolved),
147
149
  }, nil, nil
148
150
  }
149
151
 
150
- // close releases the type checker acquired by loadProgram. Safe to call on
151
- // a nil receiver and idempotent after the first call.
152
+ func normalizeProjectIdentity(
153
+ identity publicrule.ProjectIdentity,
154
+ cwd string,
155
+ configPath string,
156
+ ) publicrule.ProjectIdentity {
157
+ if identity.InvocationCwd == "" {
158
+ identity.InvocationCwd = cwd
159
+ }
160
+ if identity.LogicalConfigPath == "" {
161
+ identity.LogicalConfigPath = absoluteProjectPath(identity.InvocationCwd, configPath)
162
+ }
163
+ if identity.LogicalProjectRoot == "" {
164
+ identity.LogicalProjectRoot = filepath.Dir(identity.LogicalConfigPath)
165
+ }
166
+ if identity.PhysicalConfigPath == "" {
167
+ identity.PhysicalConfigPath = realProjectPath(absoluteProjectPath(cwd, configPath))
168
+ }
169
+ if identity.PhysicalProjectRoot == "" {
170
+ identity.PhysicalProjectRoot = realProjectPath(cwd)
171
+ }
172
+ if identity.PluginConfigOrigin == "" {
173
+ if origin := os.Getenv("TTSC_PLUGIN_CONFIG_DIR"); origin != "" {
174
+ identity.PluginConfigOrigin = absoluteProjectPath(identity.InvocationCwd, origin)
175
+ }
176
+ }
177
+ identity.LifecycleID = fmt.Sprintf(
178
+ "%d:%d",
179
+ os.Getpid(),
180
+ programLifecycleSequence.Add(1),
181
+ )
182
+ return identity
183
+ }
184
+
185
+ func absoluteProjectPath(cwd string, target string) string {
186
+ if filepath.IsAbs(target) {
187
+ return filepath.Clean(target)
188
+ }
189
+ return filepath.Clean(filepath.Join(cwd, target))
190
+ }
191
+
192
+ func realProjectPath(target string) string {
193
+ original := filepath.Clean(target)
194
+ resolved := original
195
+ seen := make(map[string]struct{})
196
+ for range 255 {
197
+ key := filepath.Clean(resolved)
198
+ if _, exists := seen[key]; exists {
199
+ return original
200
+ }
201
+ seen[key] = struct{}{}
202
+
203
+ next, ok := resolveProjectPathAncestor(resolved)
204
+ if !ok {
205
+ return filepath.Clean(resolved)
206
+ }
207
+ resolved = next
208
+ }
209
+ return original
210
+ }
211
+
212
+ // resolveProjectPathAncestor resolves the nearest existing or symlink-like
213
+ // ancestor and reattaches the remaining path. Besides ordinary links, this
214
+ // lets Windows expand an 8.3 short-name ancestor even when the target itself
215
+ // has not been created yet. Some directory junctions are readable through
216
+ // os.Readlink while EvalSymlinks either leaves them unchanged or fails on their
217
+ // children, so both resolution paths are retained.
218
+ func resolveProjectPathAncestor(target string) (string, bool) {
219
+ original := filepath.Clean(target)
220
+ probe := original
221
+ suffix := make([]string, 0)
222
+ evaluateAncestor := true
223
+ for {
224
+ if evaluateAncestor {
225
+ if evaluated, err := filepath.EvalSymlinks(probe); err == nil {
226
+ // EvalSymlinks resolves the probe's complete ancestry. If it leaves
227
+ // the spelling unchanged, retrying it on every parent cannot reveal
228
+ // anything new; retain only the os.Readlink walk for junctions that
229
+ // EvalSymlinks does not expose.
230
+ evaluateAncestor = false
231
+ candidate := filepath.Clean(evaluated)
232
+ for i := len(suffix) - 1; i >= 0; i-- {
233
+ candidate = filepath.Join(candidate, suffix[i])
234
+ }
235
+ candidate = filepath.Clean(candidate)
236
+ if candidate != original {
237
+ return candidate, true
238
+ }
239
+ }
240
+ }
241
+ if _, err := os.Readlink(probe); err == nil {
242
+ destination := resolveDirLink(probe)
243
+ for i := len(suffix) - 1; i >= 0; i-- {
244
+ destination = filepath.Join(destination, suffix[i])
245
+ }
246
+ destination = filepath.Clean(destination)
247
+ if destination != original {
248
+ return destination, true
249
+ }
250
+ }
251
+ parent := filepath.Dir(probe)
252
+ if parent == probe {
253
+ return "", false
254
+ }
255
+ suffix = append(suffix, filepath.Base(probe))
256
+ probe = parent
257
+ }
258
+ }
259
+
260
+ func (p *program) runLintCycle(engine *Engine) []*Finding {
261
+ if p == nil || engine == nil {
262
+ return nil
263
+ }
264
+ files := p.userSourceFiles()
265
+ if p.projectCycle == nil {
266
+ p.projectCycle = engine.evaluateProject(p.identity, files, p.checker)
267
+ }
268
+ fileFindings := engine.runFiles(files, p.checker, p.projectCycle.results, p.cwd)
269
+ return append(p.projectCycle.finalize(), fileFindings...)
270
+ }
271
+
272
+ // close drops the standalone lint checker. Safe to call on a nil receiver and
273
+ // idempotent after the first call.
152
274
  func (p *program) close() {
153
275
  if p == nil {
154
276
  return
155
277
  }
156
- if p.releaseChecker != nil {
157
- p.releaseChecker()
158
- p.releaseChecker = nil
159
- }
278
+ p.checker = nil
160
279
  }
161
280
 
162
281
  // userSourceFiles returns the tsconfig-selected source files the lint engine
@@ -290,10 +409,9 @@ func parseTsgoArgs(args []string, host shimcompiler.CompilerHost) (*shimcore.Com
290
409
  // CompilerOptions, and both Program.SingleThreaded() and the checker pool read
291
410
  // them from there. SingleThreaded wins over Checkers, matching the pool.
292
411
  //
293
- // When a type-aware lint rule is active, loadProgram calls forceSingleChecker
294
- // afterwards, so a `--checkers N` greater than 1 is recorded here and then
295
- // clamped back to a single checker. AST-only lint runs keep the recorded
296
- // checker count. `--singleThreaded` still takes full effect.
412
+ // Type-aware lint rules use their own standalone checker, so this pool size is
413
+ // preserved for the Program's semantic diagnostics. `--singleThreaded` still
414
+ // takes full effect across the Program and the serial type-aware lint walk.
297
415
  func applyThreading(parsed *tsoptions.ParsedCommandLine, singleThreaded bool, checkers int) {
298
416
  if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
299
417
  return
@@ -308,26 +426,6 @@ func applyThreading(parsed *tsoptions.ParsedCommandLine, singleThreaded bool, ch
308
426
  }
309
427
  }
310
428
 
311
- // forceSingleChecker pins the TypeScript-Go checker pool to a single checker.
312
- //
313
- // The lint engine walks the program serially and obtains types through the
314
- // single checker GetTypeChecker hands back. Rules query types on nodes from
315
- // arbitrary source files, so the checker must be the same one that checked
316
- // every file. A pool of size > 1 affinitizes files to distinct checkers;
317
- // resolving a type whose declarations cross that boundary yields `any`.
318
- // Parallel parsing and emit are unaffected — they do not consult the count.
319
- func forceSingleChecker(parsed *tsoptions.ParsedCommandLine) {
320
- if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
321
- return
322
- }
323
- options := parsed.ParsedConfig.CompilerOptions
324
- if options.SingleThreaded == shimcore.TSTrue {
325
- return
326
- }
327
- one := 1
328
- options.Checkers = &one
329
- }
330
-
331
429
  // overrideOutDir replaces the parsed config's OutDir with `outDir`.
332
430
  // Relative outDir values are resolved against `cwd`; absolute paths are
333
431
  // used as-is. Paths are converted to forward slashes for tsgo
@@ -0,0 +1,117 @@
1
+ // Raw-source scanning for the numeric escape sequences (`\xHH`, `\uHHHH`,
2
+ // `\u{HEX...}`) that `unicorn/no-hex-escape` and `unicorn/escape-case`
3
+ // police. Both rules read the raw literal source — the parser already
4
+ // decodes escapes into the `.Text` value, so a normal accessor would see
5
+ // `©` instead of `\xA9` — and both therefore need the same two guards a
6
+ // regex port cannot express in RE2:
7
+ //
8
+ // - Backslash parity: only a backslash preceded by an even-length
9
+ // backslash run opens an escape. Upstream spells this as the
10
+ // lookbehind `(?<=(?:^|[^\\])(?:\\\\)*\\)` and, in its byte-scanning
11
+ // rules, as `isActiveBackslash`. Without it, the second backslash of
12
+ // an escaped `\\` donates itself to a bogus match and `"\\x64"` (a
13
+ // backslash followed by the literal text `x64`) reports.
14
+ // - Fixed-width digit runs: `\x` takes exactly two hex digits and `\u`
15
+ // exactly four, so an escape never absorbs the literal hex-looking
16
+ // characters that follow it (`"\x41bcd"` is a canonical `\x41` plus
17
+ // the letters `bcd`). Only the braced `\u{HEX...}` form is variable
18
+ // width, and its `}` terminates it.
19
+ //
20
+ // Byte scanning is UTF-8 safe: every byte the scanner tests is ASCII, and
21
+ // a multi-byte sequence contains no ASCII bytes.
22
+ package linthost
23
+
24
+ // literalEscape is one numeric escape sequence found in raw literal source.
25
+ // `Prefix` is the character introducing it (`x` for `\xHH`, `u` for both
26
+ // `\uHHHH` and `\u{HEX...}`) and `Digits` is its hex payload with the `\u{`
27
+ // / `}` brackets stripped.
28
+ type literalEscape struct {
29
+ Prefix byte
30
+ Digits string
31
+ }
32
+
33
+ // hasActiveLiteralEscape reports whether `text` contains an escape sequence
34
+ // accepted by `match` whose backslash is active — preceded by an even-length
35
+ // run of backslashes.
36
+ //
37
+ // `text` is the raw source of one string literal or template element token,
38
+ // delimiters included. Including them changes nothing: a quote, backtick,
39
+ // `}`, or `${` is never a backslash, so the parity of every run inside the
40
+ // payload is the same as it would be for the delimiter-free raw value
41
+ // upstream reads off `TemplateElement.value.raw`.
42
+ func hasActiveLiteralEscape(text string, match func(escape literalEscape) bool) bool {
43
+ for index := 0; index < len(text); {
44
+ if text[index] != '\\' {
45
+ index++
46
+ continue
47
+ }
48
+ run := 0
49
+ for index < len(text) && text[index] == '\\' {
50
+ run++
51
+ index++
52
+ }
53
+ // An even-length run is a sequence of escaped backslashes: each one
54
+ // consumes the next, none is left over to open an escape, and the text
55
+ // behind the run is literal. Only the last backslash of an odd-length
56
+ // run is active, and it opens the escape at `index`.
57
+ if run%2 == 0 {
58
+ continue
59
+ }
60
+ if escape, ok := literalEscapeAt(text[index:]); ok && match(escape) {
61
+ return true
62
+ }
63
+ // `text[index]` is the escaped character, never a backslash (the run
64
+ // above consumed every one), so the outer loop may step over it.
65
+ }
66
+ return false
67
+ }
68
+
69
+ // literalEscapeAt parses the escape sequence opened at `text[0]`, the byte
70
+ // right after an active backslash. It recognizes exactly the three numeric
71
+ // forms of upstream's `x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4}|u{[\dA-Fa-f]+}`
72
+ // alternation; any other escape (`\n`, `\\`, `\$`, a line continuation, an
73
+ // identifier escape) is not a numeric escape and fails the parse.
74
+ func literalEscapeAt(text string) (literalEscape, bool) {
75
+ if text == "" {
76
+ return literalEscape{}, false
77
+ }
78
+ switch text[0] {
79
+ case 'x':
80
+ if digits, ok := literalEscapeDigits(text[1:], 2); ok {
81
+ return literalEscape{Prefix: 'x', Digits: digits}, true
82
+ }
83
+ case 'u':
84
+ // The braced form is the only variable-width escape. `{` is not a hex
85
+ // digit, so the fixed-width `\uHHHH` form can never match here and the
86
+ // brace branch owns the decision.
87
+ if len(text) > 1 && text[1] == '{' {
88
+ end := 2
89
+ for end < len(text) && hexDigit(text[end]) >= 0 {
90
+ end++
91
+ }
92
+ if end == 2 || end >= len(text) || text[end] != '}' {
93
+ return literalEscape{}, false
94
+ }
95
+ return literalEscape{Prefix: 'u', Digits: text[2:end]}, true
96
+ }
97
+ if digits, ok := literalEscapeDigits(text[1:], 4); ok {
98
+ return literalEscape{Prefix: 'u', Digits: digits}, true
99
+ }
100
+ }
101
+ return literalEscape{}, false
102
+ }
103
+
104
+ // literalEscapeDigits returns the first `count` bytes of `text` when every
105
+ // one of them is a hex digit — the fixed-width digit run of a `\xHH` or
106
+ // `\uHHHH` escape.
107
+ func literalEscapeDigits(text string, count int) (string, bool) {
108
+ if len(text) < count {
109
+ return "", false
110
+ }
111
+ for index := 0; index < count; index++ {
112
+ if hexDigit(text[index]) < 0 {
113
+ return "", false
114
+ }
115
+ }
116
+ return text[:count], true
117
+ }