@ttsc/lint 0.33.1 → 0.34.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 (78) hide show
  1. package/go.mod +2 -2
  2. package/linthost/ast_helpers.go +2 -2
  3. package/linthost/base_chain_walk_crosses_generic_boundary_test.go +3 -3
  4. package/linthost/check_observations.go +14 -14
  5. package/linthost/check_observations_retains_failed_program_and_raw_inputs_test.go +3 -2
  6. package/linthost/command_check_preserves_compiler_argument_cwd_test.go +2 -2
  7. package/linthost/comment_scan_handles_malformed_parser_recovery_test.go +2 -1
  8. package/linthost/comment_scan_uses_parser_lexical_ranges_across_source_modes_test.go +2 -1
  9. package/linthost/compile.go +5 -8
  10. package/linthost/config.go +4 -2
  11. package/linthost/config_loader_sources_escape_every_literal_percent_test.go +2 -1
  12. package/linthost/contributor_diagnostic_ranges_are_bounded_test.go +19 -19
  13. package/linthost/contributor_range_fix_bounds_diagnostic_independently_from_edit_test.go +2 -2
  14. package/linthost/contributor_report_related_reaches_finding_test.go +1 -1
  15. package/linthost/engine.go +1 -1
  16. package/linthost/engine_quarantines_panicking_rule_per_file_test.go +4 -4
  17. package/linthost/engine_run_parallel_matches_serial_finding_set_test.go +1 -1
  18. package/linthost/fix.go +1 -1
  19. package/linthost/helpers_test.go +5 -4
  20. package/linthost/host.go +130 -52
  21. package/linthost/imported_tsx_source_joins_the_read_scope_test.go +1 -1
  22. package/linthost/input_observation.go +24 -27
  23. package/linthost/invalid_contributor_range_cannot_panic_inline_directive_filtering_test.go +1 -1
  24. package/linthost/lint_corpus_filename_separators_share_materialized_source_identity_test.go +1 -1
  25. package/linthost/load_config_resolver_compact_globs_preserve_entry_policy_test.go +2 -2
  26. package/linthost/load_program_keeps_checker_pool_for_type_aware_rules_test.go +4 -4
  27. package/linthost/lsp.go +10 -5
  28. package/linthost/no_floating_promises_javascript_generic_defaults_test.go +1 -1
  29. package/linthost/no_floating_promises_rejects_inapplicable_generic_receiver_branches_test.go +1 -1
  30. package/linthost/project_engine.go +1 -1
  31. package/linthost/project_rule_sources_apply_only_global_ignores_test.go +1 -1
  32. package/linthost/project_rule_sources_include_imported_sibling_source_test.go +2 -2
  33. package/linthost/project_source_files_follow_tsconfig_selection_test.go +2 -2
  34. package/linthost/project_source_selection_preserves_explicit_bundled_roots_test.go +6 -5
  35. package/linthost/resident_whole_corpus_matches_cold_load_test.go +1 -1
  36. package/linthost/rules_boundaries.go +7 -7
  37. package/linthost/rules_boundaries_dependencies.go +9 -9
  38. package/linthost/rules_format_declaration_header.go +11 -4
  39. package/linthost/rules_gap.go +1 -1
  40. package/linthost/rules_jsdoc.go +1 -1
  41. package/linthost/rules_logic.go +2 -2
  42. package/linthost/rules_nextjs.go +1 -1
  43. package/linthost/rules_no_loop_func.go +4 -4
  44. package/linthost/rules_no_promise_executor_return.go +1 -1
  45. package/linthost/rules_promise.go +16 -16
  46. package/linthost/rules_react_perf.go +1 -1
  47. package/linthost/rules_react_refresh.go +2 -2
  48. package/linthost/rules_security.go +4 -4
  49. package/linthost/rules_storybook.go +1 -1
  50. package/linthost/rules_testing_library.go +1 -1
  51. package/linthost/rules_ts_no_base_to_string.go +1 -1
  52. package/linthost/rules_ts_no_deprecated.go +3 -3
  53. package/linthost/rules_ts_no_unnecessary_qualifier.go +4 -4
  54. package/linthost/rules_ts_no_unnecessary_type_arguments.go +2 -2
  55. package/linthost/rules_ts_no_unsafe_assignment.go +1 -1
  56. package/linthost/rules_ts_no_unsafe_enum_comparison.go +1 -1
  57. package/linthost/rules_ts_prefer_readonly.go +1 -1
  58. package/linthost/rules_ts_switch_exhaustiveness_check.go +2 -2
  59. package/linthost/rules_ts_unbound_method.go +1 -1
  60. package/linthost/rules_unicorn_consistent_destructuring.go +2 -2
  61. package/linthost/rules_unicorn_consistent_existence_index_check.go +2 -2
  62. package/linthost/rules_unicorn_consistent_function_scoping.go +7 -7
  63. package/linthost/rules_unicorn_filename_case.go +1 -1
  64. package/linthost/rules_unicorn_isolated_functions.go +3 -3
  65. package/linthost/rules_unicorn_no_typeof_undefined.go +1 -1
  66. package/linthost/rules_unicorn_no_unnecessary_polyfills.go +1 -1
  67. package/linthost/rules_unicorn_no_unused_properties.go +3 -3
  68. package/linthost/rules_unicorn_prefer_number_properties.go +1 -1
  69. package/linthost/rules_unicorn_prefer_simple_condition_first.go +1 -1
  70. package/linthost/rules_unicorn_prevent_abbreviations.go +7 -7
  71. package/linthost/rules_var.go +1 -1
  72. package/linthost/solid_helpers_test.go +1 -1
  73. package/linthost/type_alias_reference_node_uses_borrowed_builder_context_test.go +2 -2
  74. package/linthost/type_instantiation_mappers_reach_runtime_endpoints_test.go +3 -3
  75. package/linthost/user_source_files_exclude_imported_declarations_and_javascript_test.go +2 -2
  76. package/linthost/user_source_files_span_program_typescript_sources_test.go +1 -1
  77. package/linthost/windows_project_source_selection_preserves_physical_aliases_test.go +5 -4
  78. package/package.json +2 -2
package/linthost/host.go CHANGED
@@ -48,9 +48,12 @@ type program struct {
48
48
  configInputs []configDependencyFingerprint
49
49
  tsProgram *shimcompiler.Program
50
50
  parsed *tsoptions.ParsedCommandLine
51
- checker *shimchecker.Checker
52
- identity publicrule.ProjectIdentity
53
- projectCycle *projectCycle
51
+ // hosts builds every generation's compiler host and owns the content-mapper
52
+ // processes those hosts share on the TypeScript 7.1 target; close releases it.
53
+ hosts *shimcompiler.ProjectHosts
54
+ checker *shimchecker.Checker
55
+ identity publicrule.ProjectIdentity
56
+ projectCycle *projectCycle
54
57
  // projectRoots memoizes projectSourceFileNames, which resolves every
55
58
  // selected file's path and therefore costs filesystem work. The tsconfig
56
59
  // selection cannot change while one program is loaded — a config edit or a
@@ -107,6 +110,14 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
107
110
  if !filepath.IsAbs(resolved) {
108
111
  resolved = filepath.Join(cwd, resolved)
109
112
  }
113
+ configFile, ok := shimtspath.TryRootedFilePathFromAbsolute(resolved)
114
+ if !ok {
115
+ return nil, nil, fmt.Errorf("loadProgram: tsconfig path is not absolute: %s", resolved)
116
+ }
117
+ cwdDir, ok := rootedDirectory(cwd)
118
+ if !ok {
119
+ return nil, nil, fmt.Errorf("loadProgram: cwd is not absolute: %s", cwd)
120
+ }
110
121
 
111
122
  var fs vfs.FS = bundled.WrapFS(cachedvfs.From(osvfs.FS()))
112
123
  var observer *inputObservationFS
@@ -116,18 +127,19 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
116
127
  reader = newProjectInputReader(observer)
117
128
  fs = observer
118
129
  }
119
- host := shimcompiler.NewCompilerHost(cwd, fs, bundled.LibPath(), nil, nil)
120
- argumentHost := host
130
+ // Forwarded flags convert relative values against their own argv base, so
131
+ // only the parser's directory changes.
132
+ argumentDir := cwdDir
121
133
  if options.tsgoArgsCwd != "" {
122
134
  argumentCwd := options.tsgoArgsCwd
123
135
  if !filepath.IsAbs(argumentCwd) {
124
136
  argumentCwd = filepath.Join(cwd, argumentCwd)
125
137
  }
126
- if argumentCwd != cwd {
127
- argumentHost = shimcompiler.NewCompilerHost(argumentCwd, fs, bundled.LibPath(), nil, nil)
138
+ if argumentDir, ok = rootedDirectory(argumentCwd); !ok {
139
+ return nil, nil, fmt.Errorf("loadProgram: tsgo args cwd is not absolute: %s", argumentCwd)
128
140
  }
129
141
  }
130
- commandLine, cliDiags := parseTsgoArgs(options.tsgoArgs, argumentHost)
142
+ commandLine, cliDiags := parseTsgoArgs(options.tsgoArgs, fs, argumentDir)
131
143
  if len(cliDiags) > 0 {
132
144
  return nil, cliDiags, nil
133
145
  }
@@ -140,10 +152,10 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
140
152
  cliOptions = &shimcore.CompilerOptions{}
141
153
  }
142
154
  parsed, parseDiags := tsoptions.GetParsedCommandLineOfConfigFile(
143
- resolved,
155
+ configFile,
144
156
  cliOptions,
145
157
  tsoptions.CommandLineRawOptions(commandLine),
146
- argumentHost,
158
+ tsoptions.NewParseConfigHost(fs, argumentDir),
147
159
  nil,
148
160
  )
149
161
  if parsed == nil {
@@ -155,9 +167,13 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
155
167
  if len(parsed.Errors) > 0 {
156
168
  return nil, parsed.Errors, nil
157
169
  }
158
- if err := applySemanticConfigPath(parsed, options.semanticConfigPath); err != nil {
170
+ parsed, semanticDiags, err := parseSemanticConfig(parsed, options.semanticConfigPath, commandLine, fs)
171
+ if err != nil {
159
172
  return nil, nil, err
160
173
  }
174
+ if len(semanticDiags) > 0 {
175
+ return nil, semanticDiags, nil
176
+ }
161
177
  if options.forceNoEmit {
162
178
  forceNoEmit(parsed)
163
179
  }
@@ -165,9 +181,13 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
165
181
  forceEmit(parsed)
166
182
  }
167
183
  if options.outDir != "" {
168
- overrideOutDir(cwd, parsed, options.outDir)
184
+ overrideOutDir(cwdDir, parsed, options.outDir)
169
185
  }
170
186
  applyThreading(parsed, options.singleThreaded, options.checkers)
187
+ // The parsed project decides whether its content mappers run, so the
188
+ // Program's host is built only now, over the same filesystem.
189
+ hosts := shimcompiler.NewProjectHosts(parsed)
190
+ host := hosts.Host(cwdDir, fs, shimtspath.RootedDirectoryPath(bundled.LibPath()))
171
191
 
172
192
  // Keep the user's checker pool intact for parallel semantic diagnostics.
173
193
  // Type-aware lint rules cannot borrow one member of that pool: they walk
@@ -177,11 +197,7 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
177
197
  // Every lint type is then produced by one checker, while the Program's pool
178
198
  // remains free to check its file groups in parallel. The engine serializes
179
199
  // type-aware walks, so this dedicated checker is never accessed concurrently.
180
- tsProgram := shimcompiler.NewProgram(shimcompiler.ProgramOptions{
181
- Config: parsed,
182
- Host: host,
183
- UseSourceOfProjectReference: true,
184
- })
200
+ tsProgram := shimcompiler.NewProgram(shimcompiler.NewProgramOptions(parsed, host, true))
185
201
  if tsProgram == nil {
186
202
  return nil, nil, errors.New("compiler.NewProgram returned nil")
187
203
  }
@@ -195,21 +211,53 @@ func loadProgram(cwd, tsconfigPath string, options loadProgramOptions) (*program
195
211
  inputReader: reader,
196
212
  tsProgram: tsProgram,
197
213
  parsed: parsed,
214
+ hosts: hosts,
198
215
  checker: checker,
199
216
  identity: normalizeProjectIdentity(options.projectIdentity, cwd, resolved),
200
217
  }, nil, nil
201
218
  }
202
219
 
203
- func applySemanticConfigPath(parsed *tsoptions.ParsedCommandLine, semanticConfigPath string) error {
220
+ // parseSemanticConfig makes the user-authored config the parsed project when
221
+ // a generated wrapper extends it from a scratch directory. TypeScript-Go
222
+ // anchors resolution, automatic type discovery and the Program's current
223
+ // directory at the directory of the config file it parsed, so the wrapper must
224
+ // not become the project. The wrapper's merged options, which already carry the
225
+ // user config's own options, the overlay and the command line, take
226
+ // command-line precedence over a fresh parse of the user config; the command
227
+ // line's raw options keep its resets.
228
+ func parseSemanticConfig(wrapper *tsoptions.ParsedCommandLine, semanticConfigPath string, commandLine *tsoptions.ParsedCommandLine, fs vfs.FS) (*tsoptions.ParsedCommandLine, []*shimast.Diagnostic, error) {
204
229
  configured := strings.TrimSpace(semanticConfigPath)
205
230
  if configured == "" {
206
- return nil
231
+ return wrapper, nil, nil
207
232
  }
208
233
  if !filepath.IsAbs(configured) {
209
- return fmt.Errorf("linthost: semantic config path must be absolute: %s", configured)
234
+ return nil, nil, fmt.Errorf("linthost: semantic config path must be absolute: %s", configured)
210
235
  }
211
- parsed.ParsedConfig.CompilerOptions.ConfigFilePath = shimtspath.ResolvePath(configured)
212
- return nil
236
+ semanticConfig, ok := shimtspath.TryRootedFilePathFromAbsolute(configured)
237
+ if !ok {
238
+ return nil, nil, fmt.Errorf("linthost: semantic config path cannot be rooted: %s", configured)
239
+ }
240
+ wrapperConfig := wrapper.CompilerOptions().ConfigFilePath
241
+ if wrapperConfig != "" && shimtspath.CompareFilePaths(vfs.CaseSensitivityOf(fs), wrapperConfig, semanticConfig) == 0 {
242
+ return wrapper, nil, nil
243
+ }
244
+ parsed, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile(
245
+ semanticConfig,
246
+ wrapper.CompilerOptions().Clone(),
247
+ tsoptions.CommandLineRawOptions(commandLine),
248
+ tsoptions.NewParseConfigHost(fs, shimtspath.FileDirectory(semanticConfig)),
249
+ nil,
250
+ )
251
+ if parsed == nil {
252
+ return nil, nil, fmt.Errorf("tsoptions: parsed command line was nil for %s", string(semanticConfig))
253
+ }
254
+ if len(diagnostics) > 0 {
255
+ return nil, diagnostics, nil
256
+ }
257
+ if len(parsed.Errors) > 0 {
258
+ return nil, parsed.Errors, nil
259
+ }
260
+ return parsed, nil, nil
213
261
  }
214
262
 
215
263
  func normalizeProjectIdentity(
@@ -394,13 +442,17 @@ func (p *program) runCycleOver(engine *Engine, files []*shimast.SourceFile) []*F
394
442
  return append(p.projectCycle.finalize(), fileFindings...)
395
443
  }
396
444
 
397
- // close drops the standalone lint checker. Safe to call on a nil receiver and
398
- // idempotent after the first call.
445
+ // close drops the standalone lint checker and stops the project's
446
+ // content-mapper processes. Safe to call on a nil receiver and idempotent after
447
+ // the first call. A mapper that fails to stop has already been killed, and no
448
+ // lint result depends on its exit, so the close error is not reported.
399
449
  func (p *program) close() {
400
450
  if p == nil {
401
451
  return
402
452
  }
403
453
  p.checker = nil
454
+ _ = p.hosts.Close()
455
+ p.hosts = nil
404
456
  }
405
457
 
406
458
  // sourceFileByPath returns the resident Program's source file whose name matches
@@ -415,7 +467,7 @@ func (p *program) sourceFileByPath(absPath string) *shimast.SourceFile {
415
467
  if file == nil {
416
468
  continue
417
469
  }
418
- if filepath.ToSlash(file.FileName()) == normalized {
470
+ if filepath.ToSlash(string(file.FileName())) == normalized {
419
471
  return file
420
472
  }
421
473
  }
@@ -432,18 +484,23 @@ func (p *program) sourceFileByPath(absPath string) *shimast.SourceFile {
432
484
  // when the edit reshaped the import graph, and that rebuilt Program is still
433
485
  // correct, but callers use the reused flag to distinguish incremental updates
434
486
  // from full Program reconstruction in product telemetry.
487
+ // It returns false, leaving the Program unchanged, when absPath has no rooted
488
+ // file name.
435
489
  func (p *program) applyChange(absPath string) bool {
436
490
  if p == nil || p.tsProgram == nil {
437
491
  return false
438
492
  }
439
- name := absPath
440
- if file := p.sourceFileByPath(absPath); file != nil {
441
- name = file.FileName()
493
+ name, ok := p.changedFileName(absPath)
494
+ if !ok {
495
+ return false
442
496
  }
443
497
  fs := bundled.WrapFS(cachedvfs.From(osvfs.FS()))
444
- host := shimcompiler.NewCompilerHost(p.cwd, fs, bundled.LibPath(), nil, nil)
445
- changed := shimtspath.ToPath(name, p.cwd, fs.UseCaseSensitiveFileNames())
446
- newProg, reused := p.tsProgram.UpdateProgram(changed, host, nil)
498
+ // The new host keeps the Program's content mappers, so a mapped file is
499
+ // still transformed by the processes the first generation started.
500
+ host := p.hosts.Host(shimtspath.RootedDirectoryPath(p.cwd), fs, shimtspath.RootedDirectoryPath(bundled.LibPath()))
501
+ // The second result is the changed file as host parsed it, returned so a
502
+ // host with a parse cache can release it; this host parses without one.
503
+ newProg, _, reused := shimcompiler.UpdateProgram(p.tsProgram, shimcompiler.ProgramPathKey(p.tsProgram, name), host)
447
504
  if newProg != nil {
448
505
  // An updated generation cannot inherit a one-shot check's observed inputs.
449
506
  // Resident commands currently negotiate no check sidecar; keep an explicit
@@ -464,6 +521,31 @@ func (p *program) applyChange(absPath string) bool {
464
521
  return reused
465
522
  }
466
523
 
524
+ // changedFileName names the file absPath addresses as the compiler keys it. The
525
+ // resident Program's own spelling wins; otherwise absPath resolves against the
526
+ // Program's cwd. It reports false when no rooted name exists.
527
+ func (p *program) changedFileName(absPath string) (shimtspath.RootedFilePath, bool) {
528
+ if file := p.sourceFileByPath(absPath); file != nil {
529
+ return file.FileName(), true
530
+ }
531
+ cwd, ok := rootedDirectory(p.cwd)
532
+ if !ok || absPath == "" {
533
+ return "", false
534
+ }
535
+ return shimtspath.ToRootedFilePath(absPath, cwd), true
536
+ }
537
+
538
+ // rootedDirectory gives an absolute native directory the compiler's rooted
539
+ // identity. A relative directory has no such identity, so it reports false
540
+ // instead of resolving against an implicit working directory.
541
+ func rootedDirectory(absolute string) (shimtspath.RootedDirectoryPath, bool) {
542
+ path, ok := shimtspath.TryRootedPathFromAbsolute(absolute)
543
+ if !ok {
544
+ return "", false
545
+ }
546
+ return shimtspath.RootedDirectoryPathFromPath(path), true
547
+ }
548
+
467
549
  // userSourceFiles returns the source files the lint engine reads for one cycle:
468
550
  // the tsconfig-selected TS/JS roots plus every TypeScript source the Program
469
551
  // pulled in through an import.
@@ -495,7 +577,7 @@ func (p *program) userSourceFiles() []*shimast.SourceFile {
495
577
  if f == nil {
496
578
  continue
497
579
  }
498
- if p.selectedByProject(roots, f.FileName()) {
580
+ if p.selectedByProject(roots, string(f.FileName())) {
499
581
  out = append(out, f)
500
582
  continue
501
583
  }
@@ -516,7 +598,7 @@ func (p *program) projectSourceFiles() []*shimast.SourceFile {
516
598
  if f == nil {
517
599
  continue
518
600
  }
519
- if !p.selectedByProject(roots, f.FileName()) {
601
+ if !p.selectedByProject(roots, string(f.FileName())) {
520
602
  continue
521
603
  }
522
604
  out = append(out, f)
@@ -555,14 +637,14 @@ func (p *program) projectSourceFileNames() map[string]struct{} {
555
637
  out := make(map[string]struct{})
556
638
  if p.parsed != nil && p.parsed.ParsedConfig != nil {
557
639
  for _, fileName := range p.parsed.ParsedConfig.FileNames {
558
- if !isLintSourceFileName(fileName) {
640
+ if !isLintSourceFileName(string(fileName)) {
559
641
  continue
560
642
  }
561
- if bundled.IsBundled(fileName) {
562
- out[fileName] = struct{}{}
643
+ if bundled.IsBundled(string(fileName)) {
644
+ out[string(fileName)] = struct{}{}
563
645
  continue
564
646
  }
565
- absolute := absoluteProjectPath(p.cwd, fileName)
647
+ absolute := absoluteProjectPath(p.cwd, string(fileName))
566
648
  out[canonicalProjectPath(p.cwd, absolute)] = struct{}{}
567
649
  out[canonicalProjectPath(p.cwd, realProjectPath(absolute))] = struct{}{}
568
650
  }
@@ -617,7 +699,7 @@ func (p *program) projectWritableFindings(findings []*Finding) []*Finding {
617
699
  if finding == nil || finding.File == nil {
618
700
  continue
619
701
  }
620
- if !p.selectedByProject(roots, finding.File.FileName()) {
702
+ if !p.selectedByProject(roots, string(finding.File.FileName())) {
621
703
  continue
622
704
  }
623
705
  out = append(out, finding)
@@ -638,7 +720,7 @@ func isImportedLintSourceFile(file *shimast.SourceFile) bool {
638
720
  if file == nil || file.IsDeclarationFile {
639
721
  return false
640
722
  }
641
- return isTypeScriptSourceFileName(file.FileName())
723
+ return isTypeScriptSourceFileName(string(file.FileName()))
642
724
  }
643
725
 
644
726
  // isLintSourceFileName reports whether a tsconfig-selected file is a lint/format
@@ -695,7 +777,7 @@ func (p *program) programDiagnostics() []*shimast.Diagnostic {
695
777
  func (p *program) findSourceFile(target string) *shimast.SourceFile {
696
778
  want := shimtspath.NormalizePath(target)
697
779
  for _, file := range p.tsProgram.SourceFiles() {
698
- if shimtspath.NormalizePath(file.FileName()) == want {
780
+ if shimtspath.NormalizePath(string(file.FileName())) == want {
699
781
  return file
700
782
  }
701
783
  }
@@ -728,11 +810,11 @@ func forceNoEmit(parsed *tsoptions.ParsedCommandLine) {
728
810
  // loadProgram merges over the tsconfig — so a flag like `ttsc --strict` reaches
729
811
  // the in-process lint program even though @ttsc/lint never shells out to
730
812
  // `tsgo`. Returns nil when there are no forwarded flags.
731
- func parseTsgoArgs(args []string, host shimcompiler.CompilerHost) (*tsoptions.ParsedCommandLine, []*shimast.Diagnostic) {
813
+ func parseTsgoArgs(args []string, fs vfs.FS, cwd shimtspath.RootedDirectoryPath) (*tsoptions.ParsedCommandLine, []*shimast.Diagnostic) {
732
814
  if len(args) == 0 {
733
815
  return nil, nil
734
816
  }
735
- cli := tsoptions.ParseCommandLine(args, host)
817
+ cli := tsoptions.ParseCommandLineAt(args, fs, cwd)
736
818
  if cli == nil {
737
819
  return nil, nil
738
820
  }
@@ -764,19 +846,15 @@ func applyThreading(parsed *tsoptions.ParsedCommandLine, singleThreaded bool, ch
764
846
  }
765
847
  }
766
848
 
767
- // overrideOutDir replaces the parsed config's OutDir with `outDir`.
768
- // Relative outDir values are resolved against `cwd`; absolute paths are
769
- // used as-is. Paths are converted to forward slashes for tsgo
770
- // compatibility.
771
- func overrideOutDir(cwd string, parsed *tsoptions.ParsedCommandLine, outDir string) {
849
+ // overrideOutDir replaces the parsed config's OutDir with outDir, which the
850
+ // caller has checked is nonempty. Relative values resolve against cwd, and
851
+ // absolute values keep their location. The compiler stores the result as a
852
+ // rooted directory, which expresses the forward-slash form tsgo expects.
853
+ func overrideOutDir(cwd shimtspath.RootedDirectoryPath, parsed *tsoptions.ParsedCommandLine, outDir string) {
772
854
  if parsed == nil || parsed.ParsedConfig == nil || parsed.ParsedConfig.CompilerOptions == nil {
773
855
  return
774
856
  }
775
- if filepath.IsAbs(outDir) {
776
- parsed.ParsedConfig.CompilerOptions.OutDir = filepath.ToSlash(outDir)
777
- return
778
- }
779
- parsed.ParsedConfig.CompilerOptions.OutDir = filepath.ToSlash(filepath.Join(cwd, outDir))
857
+ parsed.ParsedConfig.CompilerOptions.OutDir = shimtspath.ToRootedDirectoryPath(outDir, cwd)
780
858
  }
781
859
 
782
860
  // projectInputReaders exposes only this loaded generation's reader. Manual or
@@ -52,7 +52,7 @@ func TestImportedTsxSourceJoinsTheReadScope(t *testing.T) {
52
52
 
53
53
  names := make([]string, 0)
54
54
  for _, file := range prog.userSourceFiles() {
55
- rel, err := filepath.Rel(root, file.FileName())
55
+ rel, err := filepath.Rel(root, string(file.FileName()))
56
56
  if err != nil {
57
57
  t.Fatal(err)
58
58
  }
@@ -113,7 +113,7 @@ type observedInput struct {
113
113
  // released; returned listing/text sizes still determine retained bytes.
114
114
  type inputObservationFS struct {
115
115
  vfs.FS
116
- caseSensitive bool
116
+ caseSensitivity shimtspath.CaseSensitivity
117
117
  mu sync.Mutex
118
118
  observations map[string]observedInput
119
119
  observationOrder []string
@@ -125,7 +125,7 @@ type inputObservationFS struct {
125
125
  func newInputObservationFS(inner vfs.FS) *inputObservationFS {
126
126
  return &inputObservationFS{
127
127
  FS: inner,
128
- caseSensitive: inner.UseCaseSensitiveFileNames(),
128
+ caseSensitivity: vfs.CaseSensitivityOf(inner),
129
129
  observations: map[string]observedInput{},
130
130
  observationSpellings: map[string]string{},
131
131
  }
@@ -133,34 +133,34 @@ func newInputObservationFS(inner vfs.FS) *inputObservationFS {
133
133
 
134
134
  // FileExists records the returned existence answer; a true answer additionally
135
135
  // records the same VFS's identity spelling without inventing file contents.
136
- func (fs *inputObservationFS) FileExists(path string) bool {
136
+ func (fs *inputObservationFS) FileExists(path shimtspath.RootedFilePath) bool {
137
137
  exists := fs.FS.FileExists(path)
138
138
  proof := transformInputObservation{FileExists: boolPointer(exists)}
139
139
  if exists {
140
140
  // Existence participates in resolution, but only ReadFile returns bytes
141
141
  // that can influence the resident Program. Do not duplicate every
142
142
  // resolver probe with an eager file read.
143
- proof.Realpath = fs.currentRealpath(path)
143
+ proof.Realpath = fs.currentRealpath(shimtspath.RootedPath(path))
144
144
  }
145
- fs.observe(path, observedInput{proof: proof})
145
+ fs.observe(string(path), observedInput{proof: proof})
146
146
  return exists
147
147
  }
148
148
 
149
149
  // ReadFile hashes the text actually returned by the compiler VFS. A failed read
150
150
  // records failure without hashing a second read or guessing the path's kind.
151
- func (fs *inputObservationFS) ReadFile(path string) (string, bool) {
151
+ func (fs *inputObservationFS) ReadFile(path shimtspath.RootedFilePath) (string, bool) {
152
152
  contents, ok := fs.FS.ReadFile(path)
153
153
  if ok {
154
154
  digest := sha256.Sum256([]byte(contents))
155
155
  hash := hex.EncodeToString(digest[:])
156
- fs.observe(path, observedInput{
156
+ fs.observe(string(path), observedInput{
157
157
  proof: transformInputObservation{
158
158
  ReadFile: &transformInputReadObservation{OK: true, Hash: hash},
159
- Realpath: fs.currentRealpath(path),
159
+ Realpath: fs.currentRealpath(shimtspath.RootedPath(path)),
160
160
  },
161
161
  })
162
162
  } else {
163
- fs.observe(path, observedInput{
163
+ fs.observe(string(path), observedInput{
164
164
  proof: transformInputObservation{
165
165
  ReadFile: &transformInputReadObservation{OK: false},
166
166
  },
@@ -171,21 +171,21 @@ func (fs *inputObservationFS) ReadFile(path string) (string, bool) {
171
171
 
172
172
  // DirectoryExists records existence independently of FileExists, since a
173
173
  // false file predicate and a true directory predicate are compatible.
174
- func (fs *inputObservationFS) DirectoryExists(path string) bool {
174
+ func (fs *inputObservationFS) DirectoryExists(path shimtspath.RootedDirectoryPath) bool {
175
175
  exists := fs.FS.DirectoryExists(path)
176
176
  proof := transformInputObservation{DirectoryExists: boolPointer(exists)}
177
177
  if exists {
178
- proof.Realpath = fs.currentRealpath(path)
178
+ proof.Realpath = fs.currentRealpath(shimtspath.RootedPath(path))
179
179
  }
180
- fs.observe(path, observedInput{proof: proof})
180
+ fs.observe(string(path), observedInput{proof: proof})
181
181
  return exists
182
182
  }
183
183
 
184
184
  // GetAccessibleEntries copies the returned lists before retaining them so the
185
185
  // caller cannot mutate the witness. It does not add an unconsumed traversal.
186
- func (fs *inputObservationFS) GetAccessibleEntries(path string) vfs.Entries {
186
+ func (fs *inputObservationFS) GetAccessibleEntries(path shimtspath.RootedDirectoryPath) vfs.Entries {
187
187
  entries := fs.FS.GetAccessibleEntries(path)
188
- fs.observe(path, observedInput{
188
+ fs.observe(string(path), observedInput{
189
189
  proof: transformInputObservation{
190
190
  AccessibleEntries: &transformInputEntriesObservation{
191
191
  Directories: append([]string{}, entries.Directories...),
@@ -198,16 +198,16 @@ func (fs *inputObservationFS) GetAccessibleEntries(path string) vfs.Entries {
198
198
 
199
199
  // Stat records the supplied VFS's nil/file/directory distinction and its
200
200
  // identity spelling for existing objects, preserving the returned FileInfo.
201
- func (fs *inputObservationFS) Stat(path string) vfs.FileInfo {
201
+ func (fs *inputObservationFS) Stat(path shimtspath.RootedPath) vfs.FileInfo {
202
202
  info := fs.FS.Stat(path)
203
203
  kind := "missing"
204
204
  if info == nil {
205
- fs.observe(path, observedInput{
205
+ fs.observe(string(path), observedInput{
206
206
  proof: transformInputObservation{Stat: &kind},
207
207
  })
208
208
  } else if info.IsDir() {
209
209
  kind = "directory"
210
- fs.observe(path, observedInput{
210
+ fs.observe(string(path), observedInput{
211
211
  proof: transformInputObservation{
212
212
  Stat: &kind,
213
213
  Realpath: fs.currentRealpath(path),
@@ -215,7 +215,7 @@ func (fs *inputObservationFS) Stat(path string) vfs.FileInfo {
215
215
  })
216
216
  } else {
217
217
  kind = "file"
218
- fs.observe(path, observedInput{
218
+ fs.observe(string(path), observedInput{
219
219
  proof: transformInputObservation{
220
220
  Stat: &kind,
221
221
  Realpath: fs.currentRealpath(path),
@@ -227,18 +227,18 @@ func (fs *inputObservationFS) Stat(path string) vfs.FileInfo {
227
227
 
228
228
  // Realpath records the adapter's answer and returns it unchanged; a lexical
229
229
  // fallback remains an adapter answer, not separately proven physical identity.
230
- func (fs *inputObservationFS) Realpath(path string) string {
230
+ func (fs *inputObservationFS) Realpath(path shimtspath.RootedPath) shimtspath.RootedPath {
231
231
  realpath := fs.FS.Realpath(path)
232
- fs.observe(path, observedInput{
233
- proof: transformInputObservation{Realpath: realpathObservation(realpath)},
232
+ fs.observe(string(path), observedInput{
233
+ proof: transformInputObservation{Realpath: realpathObservation(string(realpath))},
234
234
  })
235
235
  return realpath
236
236
  }
237
237
 
238
238
  // currentRealpath obtains the identity predicate beside an existing-object
239
239
  // query without routing that supporting call back through the observer.
240
- func (fs *inputObservationFS) currentRealpath(path string) *transformInputRealpathObservation {
241
- return realpathObservation(fs.FS.Realpath(path))
240
+ func (fs *inputObservationFS) currentRealpath(path shimtspath.RootedPath) *transformInputRealpathObservation {
241
+ return realpathObservation(string(fs.FS.Realpath(path)))
242
242
  }
243
243
 
244
244
  // realpathObservation distinguishes an empty adapter result from its cleaned
@@ -264,10 +264,7 @@ func (fs *inputObservationFS) observationKey(path string) string {
264
264
  if !filepath.IsAbs(path) {
265
265
  return ""
266
266
  }
267
- return shimtspath.GetCanonicalFileName(
268
- shimtspath.NormalizePath(path),
269
- fs.caseSensitive,
270
- )
267
+ return shimtspath.Canonicalize(fs.caseSensitivity, shimtspath.NormalizePath(path))
271
268
  }
272
269
 
273
270
  // observe merges under the ledger lock and preserves first lexical spelling.
@@ -21,7 +21,7 @@ func TestInvalidContributorRangeCannotPanicInlineDirectiveFiltering(t *testing.T
21
21
  const value = 1;
22
22
  `)
23
23
  contributor := &boundedDiagnosticRangeContributor{
24
- spans: map[string][2]int{file.FileName(): {999, 1200}},
24
+ spans: map[string][2]int{string(file.FileName()): {999, 1200}},
25
25
  }
26
26
  metadata, err := inspectContributor(contributor)
27
27
  if err != nil {
@@ -70,7 +70,7 @@ func TestLintCorpusFilenameSeparatorsShareMaterializedSourceIdentity(t *testing.
70
70
  finding := findings[0]
71
71
  start := strings.Index(source, "var legacy")
72
72
  end := start + len("var legacy = 1;")
73
- if finding.Pos != start || finding.End != end || finding.File.FileName() != filepath.ToSlash(location) {
73
+ if finding.Pos != start || finding.End != end || string(finding.File.FileName()) != filepath.ToSlash(location) {
74
74
  t.Fatalf("main source identity/range: got %s [%d,%d), want %s [%d,%d)", finding.File.FileName(), finding.Pos, finding.End, filepath.ToSlash(location), start, end)
75
75
  }
76
76
  })
@@ -79,7 +79,7 @@ func TestLoadConfigResolverCompactGlobsPreserveEntryPolicy(t *testing.T) {
79
79
  scoped := filepath.Join(root, "scoped.config.json")
80
80
  writeFile(t, scoped, `{"files":["{src,test}/**/{main,unit}.ts"],"ignores":["src/{local,other}/**"],"format":{"severity":"off","semi":false}}`)
81
81
  scopedResolver := load(scoped)
82
- selected := (formatCommandResolver{inner: scopedResolver}).ResolveRules(main.FileName())
82
+ selected := (formatCommandResolver{inner: scopedResolver}).ResolveRules(string(main.FileName()))
83
83
  if selected.OutOfScope || selected.Ignored || selected.Rules.Severity("format/semi") != SeverityWarn || string(selected.RuleOptions("format/semi")) != `{"prefer":"never"}` {
84
84
  t.Errorf("scoped formatter positive did not execute its selected policy: %+v options=%s", selected, selected.RuleOptions("format/semi"))
85
85
  }
@@ -91,7 +91,7 @@ func TestLoadConfigResolverCompactGlobsPreserveEntryPolicy(t *testing.T) {
91
91
  }
92
92
  writeFile(t, base, `{"rules":{"no-var":"error"}}`)
93
93
  reloaded := load(final)
94
- if got := reloaded.ResolveRules(ignored.FileName()); got.Ignored || got.Rules.Severity("no-var") != SeverityError {
94
+ if got := reloaded.ResolveRules(string(ignored.FileName())); got.Ignored || got.Rules.Severity("no-var") != SeverityError {
95
95
  t.Errorf("changed global ignore did not refresh: %+v", got)
96
96
  }
97
97
  }
@@ -122,7 +122,7 @@ func TestLoadProgramKeepsCheckerPoolForTypeAwareRules(t *testing.T) {
122
122
  t.Fatalf("unexpected generated-wrapper diagnostics: %#v", generatedDiags)
123
123
  }
124
124
  defer generated.close()
125
- if got := generated.tsProgram.Options().ConfigFilePath; got != shimtspath.ResolvePath(semanticConfig) {
125
+ if got := string(generated.tsProgram.Options().ConfigFilePath); got != shimtspath.ResolvePath(semanticConfig) {
126
126
  t.Fatalf("generated wrapper semantic config = %q, want %q", got, shimtspath.ResolvePath(semanticConfig))
127
127
  }
128
128
 
@@ -143,7 +143,7 @@ func TestLoadProgramKeepsCheckerPoolForTypeAwareRules(t *testing.T) {
143
143
  if len(coldLSPDiags) != 0 {
144
144
  t.Fatalf("unexpected cold LSP diagnostics: %#v", coldLSPDiags)
145
145
  }
146
- if got := coldLSP.tsProgram.Options().ConfigFilePath; got != shimtspath.ResolvePath(semanticConfig) {
146
+ if got := string(coldLSP.tsProgram.Options().ConfigFilePath); got != shimtspath.ResolvePath(semanticConfig) {
147
147
  t.Fatalf("cold LSP semantic config = %q, want %q", got, shimtspath.ResolvePath(semanticConfig))
148
148
  }
149
149
 
@@ -155,7 +155,7 @@ func TestLoadProgramKeepsCheckerPoolForTypeAwareRules(t *testing.T) {
155
155
  if len(residentLSPDiags) != 0 {
156
156
  t.Fatalf("unexpected resident LSP diagnostics: %#v", residentLSPDiags)
157
157
  }
158
- if got := residentLSP.tsProgram.Options().ConfigFilePath; got != shimtspath.ResolvePath(semanticConfig) {
158
+ if got := string(residentLSP.tsProgram.Options().ConfigFilePath); got != shimtspath.ResolvePath(semanticConfig) {
159
159
  t.Fatalf("resident LSP semantic config = %q, want %q", got, shimtspath.ResolvePath(semanticConfig))
160
160
  }
161
161
  resident.invalidate()
@@ -168,7 +168,7 @@ func TestLoadProgramKeepsCheckerPoolForTypeAwareRules(t *testing.T) {
168
168
  t.Fatalf("unexpected unmarked-wrapper diagnostics: %#v", unmarkedDiags)
169
169
  }
170
170
  defer unmarked.close()
171
- if got := unmarked.tsProgram.Options().ConfigFilePath; got != shimtspath.ResolvePath(generatedConfig) {
171
+ if got := string(unmarked.tsProgram.Options().ConfigFilePath); got != shimtspath.ResolvePath(generatedConfig) {
172
172
  t.Fatalf("unmarked wrapper inherited ambient semantic config = %q, want %q", got, shimtspath.ResolvePath(generatedConfig))
173
173
  }
174
174
  invalid, _, err := loadProgram(root, generatedConfig, loadProgramOptions{semanticConfigPath: "relative.json"})
package/linthost/lsp.go CHANGED
@@ -20,6 +20,7 @@ import (
20
20
  shimcore "github.com/microsoft/typescript-go/shim/core"
21
21
  shimparser "github.com/microsoft/typescript-go/shim/parser"
22
22
  shimscanner "github.com/microsoft/typescript-go/shim/scanner"
23
+ shimtspath "github.com/microsoft/typescript-go/shim/tspath"
23
24
 
24
25
  publicrule "github.com/samchon/ttsc/packages/lint/rule"
25
26
  )
@@ -602,7 +603,7 @@ func filterFindingsForPath(findings []*Finding, target string) []*Finding {
602
603
  if finding == nil || finding.File == nil {
603
604
  continue
604
605
  }
605
- if canonicalProjectPath("", realProjectPath(finding.File.FileName())) == target {
606
+ if canonicalProjectPath("", realProjectPath(string(finding.File.FileName()))) == target {
606
607
  out = append(out, finding)
607
608
  }
608
609
  }
@@ -631,7 +632,7 @@ func lspRelatedInformationForFinding(finding *Finding) []lspRelatedInformation {
631
632
  if finding == nil || len(finding.RelatedInformation) == 0 || finding.File == nil {
632
633
  return nil
633
634
  }
634
- uri := fileURL(finding.File.FileName())
635
+ uri := fileURL(string(finding.File.FileName()))
635
636
  out := make([]lspRelatedInformation, 0, len(finding.RelatedInformation))
636
637
  for _, item := range finding.RelatedInformation {
637
638
  out = append(out, lspRelatedInformation{
@@ -1185,9 +1186,13 @@ func lspFormatBuffer(content string, opts *lspCommandOptions) (*lspWorkspaceEdit
1185
1186
 
1186
1187
  text := content
1187
1188
  converged := false
1188
- // The tsgo parser asserts on normalized (forward-slash) absolute paths;
1189
- // `target` comes from filepath.Abs and carries backslashes on Windows.
1190
- parseName := filepath.ToSlash(target)
1189
+ // The parser takes a rooted file name: the normalized, forward-slash form of
1190
+ // `target`, which filePathFromURI made absolute.
1191
+ parseName, ok := shimtspath.TryRootedFilePathFromAbsolute(target)
1192
+ if !ok {
1193
+ fmt.Fprintf(os.Stderr, "@ttsc/lint: LSP format target is not a rooted path: %s\n", target)
1194
+ return nil, 2
1195
+ }
1191
1196
  for pass := 0; pass < maxFormatPasses; pass++ {
1192
1197
  file := shimparser.ParseSourceFile(shimast.SourceFileParseOptions{FileName: parseName}, text, scriptKind)
1193
1198
  if file == nil || len(file.Diagnostics()) > 0 || len(file.JSDiagnostics()) > 0 {
@@ -84,7 +84,7 @@ export class JsSafeCatch {
84
84
  }
85
85
  var file *shimast.SourceFile
86
86
  for _, candidate := range prog.userSourceFiles() {
87
- if filepath.Base(candidate.FileName()) == "main.ts" {
87
+ if filepath.Base(string(candidate.FileName())) == "main.ts" {
88
88
  file = candidate
89
89
  break
90
90
  }
@@ -228,7 +228,7 @@ function checkUncertainArray<U>(
228
228
  if diagnostic == nil || diagnostic.Code() != 2349 || diagnostic.File() == nil {
229
229
  continue
230
230
  }
231
- fileName := strings.ReplaceAll(diagnostic.File().FileName(), "\\", "/")
231
+ fileName := strings.ReplaceAll(string(diagnostic.File().FileName()), "\\", "/")
232
232
  if fileName != "main.ts" && !strings.HasSuffix(fileName, "/main.ts") {
233
233
  continue
234
234
  }