diff --git a/tsc/internal/api/module_resolution.go b/tsc/internal/api/module_resolution.go index 7df844f5e3875..ba903fab3b0cd 100644 --- a/tsc/internal/api/module_resolution.go +++ b/tsc/internal/api/module_resolution.go @@ -11,7 +11,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/module" - "github.com/microsoft/TypeScript/tsc/internal/packagejson" "github.com/microsoft/TypeScript/tsc/internal/project" "github.com/microsoft/TypeScript/tsc/internal/tspath" ) @@ -20,7 +19,6 @@ type moduleResolverFactory struct { registration *moduleResolverRegistration session *Session conn ipc.Conn - ctx context.Context currentDirectory string } @@ -41,6 +39,7 @@ type callbackModuleResolver struct { } func (f *moduleResolverFactory) NewResolver( + ctx context.Context, options module.ResolverOptions, ) (module.Resolver, func()) { options.CompilerOptions = f.registration.compilerOptions @@ -55,7 +54,7 @@ func (f *moduleResolverFactory) NewResolver( var resolver module.Resolver = &callbackModuleResolver{ registration: f.registration, conn: f.conn, - ctx: f.ctx, + ctx: ctx, currentDirectory: f.currentDirectory, programResolutionContextID: contextID, fallbackResolver: fallback, @@ -128,21 +127,8 @@ func (p *callbackModuleResolver) ResolveTypeReferenceDirective( return p.fallbackResolver.ResolveTypeReferenceDirective(typeReferenceDirectiveName, containingFile, resolutionMode, redirectedReference) } -func (p *callbackModuleResolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { - return p.fallbackResolver.GetPackageScopeForPath(directory) -} - -func (p *callbackModuleResolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { - p.fallbackResolver.PackageJsonCacheEntries(f) -} - -func (p *callbackModuleResolver) ResolvePackageDirectory( - moduleName string, - containingFile string, - resolutionMode core.ResolutionMode, - redirectedReference module.ResolvedProjectReference, -) *module.ResolvedModule { - return p.fallbackResolver.ResolvePackageDirectory(moduleName, containingFile, resolutionMode, redirectedReference) +func (p *callbackModuleResolver) GetResolutionData() *module.ResolutionData { + return p.fallbackResolver.GetResolutionData() } func compileModuleResolutionSpec(spec *ModuleResolutionSpec, currentDirectory string, useCaseSensitive bool) (*module.StaticResolutions, error) { @@ -226,7 +212,7 @@ func moduleResolutionTraceToStrings(trace []module.DiagAndArgs) []string { }) } -func (s *Session) moduleResolverFactory(ctx context.Context, options *CreateProgramOptions) (project.ModuleResolverFactory, error) { +func (s *Session) moduleResolverFactory(options *CreateProgramOptions) (project.ModuleResolverFactory, error) { if options.ModuleResolver == 0 { return nil, nil } @@ -243,7 +229,6 @@ func (s *Session) moduleResolverFactory(ctx context.Context, options *CreateProg registration: data, session: s, conn: s.conn, - ctx: ctx, currentDirectory: s.GetCurrentDirectory(), }, nil } diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index d6f39a7df04f1..a70119ca47883 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -1370,7 +1370,7 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq if programParams.Options != nil { request.ProjectReferences = programParams.Options.ProjectReferences request.ConfigFileParsingDiagnostics = core.Map(programParams.Options.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }) - factory, err := s.moduleResolverFactory(ctx, programParams.Options) + factory, err := s.moduleResolverFactory(programParams.Options) if err != nil { return nil, err } @@ -1405,7 +1405,7 @@ func (s *Session) toAPISnapshotRequest(ctx context.Context, changes *SnapshotReq if programParams.Options != nil { request.ProjectReferences = programParams.Options.ProjectReferences request.ConfigFileParsingDiagnostics = core.Map(programParams.Options.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }) - factory, err := s.moduleResolverFactory(ctx, programParams.Options) + factory, err := s.moduleResolverFactory(programParams.Options) if err != nil { return nil, err } diff --git a/tsc/internal/api/session_module_resolution_test.go b/tsc/internal/api/session_module_resolution_test.go index 215d8b926b90e..47359c752679c 100644 --- a/tsc/internal/api/session_module_resolution_test.go +++ b/tsc/internal/api/session_module_resolution_test.go @@ -13,15 +13,17 @@ import ( ) type failingModuleResolutionConn struct { - calls int + calls int + contexts []context.Context } func (c *failingModuleResolutionConn) Run(context.Context) error { return nil } -func (c *failingModuleResolutionConn) Call(context.Context, string, any) (json.Value, error) { +func (c *failingModuleResolutionConn) Call(ctx context.Context, _ string, _ any) (json.Value, error) { c.calls++ + c.contexts = append(c.contexts, ctx) return nil, errors.New("callback error") } @@ -238,10 +240,9 @@ func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { registration: registration, session: session, conn: conn, - ctx: context.Background(), currentDirectory: "/", } - provider, cleanup := factory.NewResolver(module.ResolverOptions{ + provider, cleanup := factory.NewResolver(context.Background(), module.ResolverOptions{ Host: session, CompilerOptions: core.EmptyCompilerOptions, }) @@ -255,6 +256,35 @@ func TestModuleResolutionCallbackErrorsAreReturned(t *testing.T) { assert.Equal(t, len(session.programResolutionContexts), 0) } +func TestModuleResolutionFactoryUsesCurrentContext(t *testing.T) { + t.Parallel() + projectSession, _ := projecttestutil.Setup(map[string]any{}) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + conn := &failingModuleResolutionConn{} + factory := &moduleResolverFactory{ + registration: &moduleResolverRegistration{id: 1, resolveModuleNameCallback: "resolveModuleName/1"}, + session: session, + conn: conn, + currentDirectory: "/", + } + oldContext, cancel := context.WithCancel(context.Background()) + defer cancel() + for _, ctx := range []context.Context{oldContext, t.Context()} { + resolver, cleanup := factory.NewResolver(ctx, module.ResolverOptions{ + Host: session, + CompilerOptions: core.EmptyCompilerOptions, + }) + _, _, err := resolver.ResolveModuleNameFromDirectory("pkg", "/src", core.ResolutionModeESM) + assert.ErrorContains(t, err, "callback error") + assert.Equal(t, conn.contexts[len(conn.contexts)-1], ctx) + cleanup() + cancel() + } + assert.Equal(t, len(session.programResolutionContexts), 0) +} + func TestModuleResolutionCallbackErrorRejectsLanguageServerUpdate(t *testing.T) { t.Parallel() @@ -290,6 +320,7 @@ func TestModuleResolutionCallbackErrorRejectsLanguageServerUpdate(t *testing.T) }}, }) assert.ErrorContains(t, err, "callback error") + assert.Equal(t, len(session.programResolutionContexts), 0) assert.Assert(t, projectSession.Snapshot() == baseSnapshot) assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 0) } diff --git a/tsc/internal/compiler/emitHost.go b/tsc/internal/compiler/emitHost.go index de6c2dde62742..75112fa032561 100644 --- a/tsc/internal/compiler/emitHost.go +++ b/tsc/internal/compiler/emitHost.go @@ -136,8 +136,3 @@ func (host *emitHost) IsSourceFileFromExternalLibrary(file *ast.SourceFile) bool func (host *emitHost) GetSymlinkCache() *symlinks.KnownSymlinks { return host.program.GetSymlinkCache() } - -func (host *emitHost) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode) *module.ResolvedModule { - resolved, _, _ := host.program.resolver.ResolveModuleName(moduleName, containingFile, resolutionMode, nil) - return resolved -} diff --git a/tsc/internal/compiler/fileInclude.go b/tsc/internal/compiler/fileInclude.go index 1b23b73359ed2..e9a1f3b0bce07 100644 --- a/tsc/internal/compiler/fileInclude.go +++ b/tsc/internal/compiler/fileInclude.go @@ -2,7 +2,6 @@ package compiler import ( "fmt" - "sync" "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" @@ -28,16 +27,12 @@ const ( ) type FileIncludeReason struct { - kind fileIncludeKind - data any - - // Uses relative file name - relativeFileNameDiag *ast.Diagnostic - relativeFileNameDiagOnce sync.Once - - // Uses file name as is - diag *ast.Diagnostic - diagOnce sync.Once + kind fileIncludeKind + index int + isDefaultLib bool + referencedFile *referencedFileData + automaticTypeDirective *automaticTypeDirectiveFileData + canonicalSourceFile tspath.Path } type referencedFileData struct { @@ -80,12 +75,11 @@ type automaticTypeDirectiveFileData struct { } func (r *FileIncludeReason) asIndex() int { - return r.data.(int) + return r.index } func (r *FileIncludeReason) asLibFileIndex() (int, bool) { - index, ok := r.data.(int) - return index, ok + return r.index, !r.isDefaultLib } func (r *FileIncludeReason) isReferencedFile() bool { @@ -93,11 +87,11 @@ func (r *FileIncludeReason) isReferencedFile() bool { } func (r *FileIncludeReason) asReferencedFileData() *referencedFileData { - return r.data.(*referencedFileData) + return r.referencedFile } func (r *FileIncludeReason) asAutomaticTypeDirectiveFileData() *automaticTypeDirectiveFileData { - return r.data.(*automaticTypeDirectiveFileData) + return r.automaticTypeDirective } func (r *FileIncludeReason) getReferencedLocation(program *Program) *referenceFileLocation { @@ -152,19 +146,18 @@ func (r *FileIncludeReason) getReferencedLocation(program *Program) *referenceFi } func (r *FileIncludeReason) toDiagnostic(program *Program, relativeFileName bool) *ast.Diagnostic { - if relativeFileName { - r.relativeFileNameDiagOnce.Do(func() { - r.relativeFileNameDiag = r.computeDiagnostic(program, func(fileName string) string { - return tspath.GetRelativePathFromDirectory(program.GetCurrentDirectory(), fileName, program.comparePathsOptions) - }) - }) - return r.relativeFileNameDiag - } else { - r.diagOnce.Do(func() { - r.diag = r.computeDiagnostic(program, func(fileName string) string { return fileName }) - }) - return r.diag + key := includeReasonDiagnosticKey{reason: r, relativeFileName: relativeFileName} + if diagnostic, ok := program.includeProcessor.reasonDiagnostics.Load(key); ok { + return diagnostic } + diagnostic := r.computeDiagnostic(program, func(fileName string) string { + if relativeFileName { + return tspath.GetRelativePathFromDirectory(program.GetCurrentDirectory(), fileName, program.comparePathsOptions) + } + return fileName + }) + diagnostic, _ = program.includeProcessor.reasonDiagnostics.LoadOrStore(key, diagnostic) + return diagnostic } func (r *FileIncludeReason) computeDiagnostic(program *Program, toFileName func(string) string) *ast.Diagnostic { @@ -214,7 +207,7 @@ func (r *FileIncludeReason) computeDiagnostic(program *Program, toFileName func( return ast.NewCompilerDiagnostic(diagnostics.Default_library) } case fileIncludeKindContentMapperSupplemental: - canonical := program.GetSourceFileByPath(r.data.(tspath.Path)) + canonical := program.GetSourceFileByPath(r.canonicalSourceFile) return ast.NewCompilerDiagnostic(diagnostics.Supplemental_virtual_file_produced_by_the_content_mapper_for_file_0, toFileName(canonical.FileName())) default: panic(fmt.Sprintf("unknown reason: %v", r.kind)) diff --git a/tsc/internal/compiler/fileloader.go b/tsc/internal/compiler/fileloader.go index f8bba578a534a..9782b2664506e 100644 --- a/tsc/internal/compiler/fileloader.go +++ b/tsc/internal/compiler/fileloader.go @@ -41,11 +41,13 @@ type LibFile struct { type sourceFileFromReferenceDiagnostic struct { message *diagnostics.Message - args []any + args []string } type fileLoader struct { - opts ProgramOptions + opts ProgramConfig + host CompilerHost + tracing *tracing.Tracing resolver module.Resolver defaultLibraryPath string comparePathsOptions tspath.ComparePathsOptions @@ -62,8 +64,7 @@ type fileLoader struct { factoryMu sync.Mutex factory ast.NodeFactory - projectReferenceFileMapper *projectReferenceFileMapper - dtsDirectories collections.Set[tspath.Path] + projectReferences *projectReferenceFileMapperBuilder pathForLibFileCache collections.SyncMap[string, *LibFile] pathForLibFileResolutions collections.SyncMap[tspath.Path, *libResolution] @@ -111,8 +112,7 @@ func (r *redirectsFile) Path() tspath.Path { } type processedFiles struct { - resolver module.Resolver - files []*ast.SourceFile + files []*ast.SourceFile // duplicateSourceFiles tracks parsed files loaded during program construction // that were later dropped from the final program, such as losing filename // casing variants for the same path or files hidden behind package redirect @@ -130,7 +130,7 @@ type processedFiles struct { libFiles map[tspath.Path]*LibFile // List of present unsupported extensions sourceFilesFoundSearchingNodeModules collections.Set[tspath.Path] - includeProcessor *includeProcessor + fileIncludeData // if file was included using source file and its output is actually part of program // this contains mapping from output to source file outputFileToProjectReferenceSource map[tspath.Path]string @@ -140,7 +140,6 @@ type processedFiles struct { redirectFilesByPath map[tspath.Path]*redirectsFile // Program-level diagnostics reported when a content mapper fails fatally (reported once per mapper). contentMapperDiagnostics []*ast.Diagnostic - moduleResolutionError error finishedProcessing bool } @@ -152,7 +151,7 @@ type jsxRuntimeImportSpecifier struct { func processAllProgramFiles( opts ProgramOptions, singleThreaded bool, -) processedFiles { +) (processedFiles, *module.ResolutionData, error) { compilerOptions := opts.Config.CompilerOptions() rootFiles := opts.Config.FileNames() supportedExtensions := tsoptions.GetSupportedExtensions(compilerOptions, opts.Config.ContentMapperExtensions()) @@ -162,7 +161,9 @@ func processAllProgramFiles( maxNodeModuleJsDepth = *p } loader := fileLoader{ - opts: opts, + opts: opts.ProgramConfig, + host: opts.Host, + tracing: opts.Tracing, defaultLibraryPath: tspath.GetNormalizedAbsolutePath(opts.Host.DefaultLibraryPath(), opts.Host.GetCurrentDirectory()), comparePathsOptions: tspath.ComparePathsOptions{ UseCaseSensitiveFileNames: opts.Host.FS().UseCaseSensitiveFileNames(), @@ -179,7 +180,7 @@ func processAllProgramFiles( } loader.addProjectReferenceTasks(singleThreaded) resolverOptions := module.ResolverOptions{ - Host: loader.projectReferenceFileMapper.host, + Host: loader.projectReferences.host, CompilerOptions: compilerOptions, TypingsLocation: opts.TypingsLocation, ProjectName: opts.ProjectName, @@ -194,19 +195,19 @@ func processAllProgramFiles( defer opts.Tracing.Push(tracing.PhaseProgram, "processRootFiles", map[string]any{"count": len(rootFiles)}, false)() } for index, rootFile := range rootFiles { - loader.addRootFileTask(rootFile, nil, &FileIncludeReason{kind: fileIncludeKindRootFile, data: index}) + loader.addRootFileTask(rootFile, nil, &FileIncludeReason{kind: fileIncludeKindRootFile, index: index}) } if len(rootFiles) > 0 && compilerOptions.NoLib.IsFalseOrUnknown() { if compilerOptions.Lib == nil { name := tsoptions.GetDefaultLibFileName(compilerOptions) libFile := loader.pathForLibFile(name) - loader.addRootTask(libFile.path, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile}) + loader.addRootTask(libFile.path, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile, isDefaultLib: true}) } else { for index, lib := range compilerOptions.Lib { if name, ok := tsoptions.GetLibFileName(lib); ok { libFile := loader.pathForLibFile(name) - loader.addRootTask(libFile.path, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile, data: index}) + loader.addRootTask(libFile.path, libFile, &FileIncludeReason{kind: fileIncludeKindLibFile, index: index}) } // !!! error on unknown name } @@ -219,19 +220,15 @@ func processAllProgramFiles( loader.filesParser.parse(&loader, loader.rootTasks) - // Clear out loader and host to ensure its not used post program creation - loader.projectReferenceFileMapper.loader = nil - loader.projectReferenceFileMapper.host = nil - - return loader.filesParser.getProcessedFiles(&loader) + return loader.filesParser.getProcessedFiles(&loader), loader.resolver.GetResolutionData(), loader.moduleResolutionError } func (p *fileLoader) toPath(file string) tspath.Path { - return tspath.ToPath(file, p.opts.Host.GetCurrentDirectory(), p.opts.Host.FS().UseCaseSensitiveFileNames()) + return tspath.ToPath(file, p.host.GetCurrentDirectory(), p.host.FS().UseCaseSensitiveFileNames()) } func (p *fileLoader) addRootTask(fileName string, libFile *LibFile, includeReason *FileIncludeReason) { - absPath := tspath.GetNormalizedAbsolutePath(fileName, p.opts.Host.GetCurrentDirectory()) + absPath := tspath.GetNormalizedAbsolutePath(fileName, p.host.GetCurrentDirectory()) if p.opts.Config.CompilerOptions().AllowNonTsExtensions.IsTrue() || tspath.HasExtension(absPath) { p.rootTasks = append(p.rootTasks, &parseTask{ normalizedFilePath: absPath, @@ -242,7 +239,7 @@ func (p *fileLoader) addRootTask(fileName string, libFile *LibFile, includeReaso } func (p *fileLoader) addRootFileTask(fileName string, libFile *LibFile, includeReason *FileIncludeReason) { - currDir := p.opts.Host.GetCurrentDirectory() + currDir := p.host.GetCurrentDirectory() absPath := tspath.GetNormalizedAbsolutePath(fileName, currDir) containingFile := currDir if p.opts.Config.ConfigFile != nil { @@ -259,7 +256,7 @@ func (p *fileLoader) addRootFileTask(fileName string, libFile *LibFile, includeR rootTask.failedLookup = true rootTask.processingDiagnostics = []*processingDiagnostic{{ kind: processingDiagnosticKindExplainingFileInclude, - data: &includeExplainingDiagnostic{ + explanation: &includeExplainingDiagnostic{ diagnosticReason: includeReason, message: diagnostic.message, args: diagnostic.args, @@ -275,7 +272,7 @@ func (p *fileLoader) addAutomaticTypeDirectiveTasks() { if compilerOptions.ConfigFilePath != "" { containingDirectory = tspath.GetDirectoryPath(compilerOptions.ConfigFilePath) } else { - containingDirectory = p.opts.Host.GetCurrentDirectory() + containingDirectory = p.host.GetCurrentDirectory() } containingFileName := tspath.CombinePaths(containingDirectory, module.InferredTypesContainingFile) p.rootTasks = append(p.rootTasks, &parseTask{ @@ -290,7 +287,7 @@ func (p *fileLoader) resolveAutomaticTypeDirectives(containingFileName string) ( typeResolutionsTrace []module.DiagAndArgs, pDiagnostics []*processingDiagnostic, ) { - automaticTypeDirectiveNames := module.GetAutomaticTypeDirectiveNames(p.opts.Config.CompilerOptions(), p.opts.Host) + automaticTypeDirectiveNames := module.GetAutomaticTypeDirectiveNames(p.opts.Config.CompilerOptions(), p.host) if len(automaticTypeDirectiveNames) != 0 { toParse = make([]resolvedRef, 0, len(automaticTypeDirectiveNames)) typeResolutionsInFile = make(module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], len(automaticTypeDirectiveNames)) @@ -300,8 +297,8 @@ func (p *fileLoader) resolveAutomaticTypeDirectives(containingFileName string) ( resolutionMode := core.ResolutionModeNone resolved, trace := p.resolver.ResolveTypeReferenceDirective(name, containingFileName, resolutionMode, nil) var traceDone func() - if p.opts.Tracing != nil { - traceDone = p.opts.Tracing.Push(tracing.PhaseProgram, "processTypeReferenceDirective", map[string]any{"directive": name, "hasResolved": resolved.IsResolved(), "refKind": int(fileIncludeKindAutomaticTypeDirectiveFile)}, false) + if p.tracing != nil { + traceDone = p.tracing.Push(tracing.PhaseProgram, "processTypeReferenceDirective", map[string]any{"directive": name, "hasResolved": resolved.IsResolved(), "refKind": int(fileIncludeKindAutomaticTypeDirectiveFile)}, false) } typeResolutionsInFile[module.ModeAwareCacheKey{Name: name, Mode: resolutionMode}] = resolved typeResolutionsTrace = append(typeResolutionsTrace, trace...) @@ -311,21 +308,21 @@ func (p *fileLoader) resolveAutomaticTypeDirectives(containingFileName string) ( increaseDepth: resolved.IsExternalLibraryImport, elideOnDepth: false, includeReason: &FileIncludeReason{ - kind: fileIncludeKindAutomaticTypeDirectiveFile, - data: &automaticTypeDirectiveFileData{name, resolved.PackageId}, + kind: fileIncludeKindAutomaticTypeDirectiveFile, + automaticTypeDirective: &automaticTypeDirectiveFileData{name, resolved.PackageId}, }, packageId: resolved.PackageId, }) } else { pDiagnostics = append(pDiagnostics, &processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, - data: &includeExplainingDiagnostic{ + explanation: &includeExplainingDiagnostic{ diagnosticReason: &FileIncludeReason{ - kind: fileIncludeKindAutomaticTypeDirectiveFile, - data: &automaticTypeDirectiveFileData{typeReference: name}, + kind: fileIncludeKindAutomaticTypeDirectiveFile, + automaticTypeDirective: &automaticTypeDirectiveFileData{typeReference: name}, }, message: diagnostics.Cannot_find_type_definition_file_for_0, - args: []any{name}, + args: []string{name}, }, }) } @@ -338,9 +335,12 @@ func (p *fileLoader) resolveAutomaticTypeDirectives(containingFileName string) ( } func (p *fileLoader) addProjectReferenceTasks(singleThreaded bool) { - p.projectReferenceFileMapper = &projectReferenceFileMapper{ - opts: p.opts, - host: p.opts.Host, + p.projectReferences = &projectReferenceFileMapperBuilder{ + projectReferenceFileMapper: &projectReferenceFileMapper{ + config: p.opts.Config, + useSourceOfProjectReference: p.opts.canUseProjectReferenceSource(), + }, + host: p.host, } projectReferences := p.opts.Config.ResolvedProjectReferencePaths() if len(projectReferences) == 0 { @@ -388,7 +388,7 @@ func (p *fileLoader) loadSourceFileMetaData(fileName string) ast.SourceFileMetaD } } - packageJsonScope := p.resolver.GetPackageScopeForPath(tspath.GetDirectoryPath(fileName)) + packageJsonScope := p.resolver.GetResolutionData().NewResolver(p.projectReferences.host).GetPackageScopeForPath(tspath.GetDirectoryPath(fileName)) moduleResolutionKind := p.opts.Config.CompilerOptions().GetModuleResolutionKind() var packageJsonType, packageJsonDirectory string @@ -411,11 +411,11 @@ func (p *fileLoader) loadSourceFileMetaData(fileName string) ast.SourceFileMetaD } func (p *fileLoader) parseSourceFile(t *parseTask) *ast.SourceFile { - if p.opts.Tracing != nil { - defer p.opts.Tracing.Push(tracing.PhaseParse, "createSourceFile", map[string]any{"path": t.normalizedFilePath}, true)() + if p.tracing != nil { + defer p.tracing.Push(tracing.PhaseParse, "createSourceFile", map[string]any{"path": t.normalizedFilePath}, true)() } path := p.toPath(t.normalizedFilePath) - options := p.projectReferenceFileMapper.getCompilerOptionsForFile(t) + options := p.projectReferences.getCompilerOptionsForFile(t) parseOptions := ast.SourceFileParseOptions{ FileName: t.normalizedFilePath, Path: path, @@ -424,7 +424,7 @@ func (p *fileLoader) parseSourceFile(t *parseTask) *ast.SourceFile { if tspath.FileExtensionIsOneOf(t.normalizedFilePath, p.contentMapperExtensions) { return p.parseContentMappedFile(parseOptions) } - return p.opts.Host.GetSourceFile(parseOptions) + return p.host.GetSourceFile(parseOptions) } // parseContentMappedFile produces a content-mapped virtual source file via the host's content @@ -443,7 +443,7 @@ func (p *fileLoader) parseContentMappedFile(opts ast.SourceFileParseOptions) *as // The mapper failed initialization or exceeded its failure budget; add the file empty without re-reporting. return p.emptyContentMappedFile(opts, mapper.Identity(), transformIdentity) } - files, err := p.opts.Host.GetContentMappedSourceFiles(opts, mapper) + files, err := p.host.GetContentMappedSourceFiles(opts, mapper) if err != nil { sourceFile := p.emptyContentMappedFile(opts, mapper.Identity(), transformIdentity) if transformError, ok := errors.AsType[*contentmapper.TransformError](err); ok && transformError.Kind == contentmapper.TransformErrorKindInitialize { @@ -576,7 +576,7 @@ func contentMapperMappingDiagnostic(file *ast.SourceFile, label string, problem // empty module rather than triggering a "cannot find module" error. It is still marked as content-mapped // so it is excluded from emit like a successfully mapped file. func (p *fileLoader) getContentMapperTransformIdentity(mapper *contentmapper.Mapper) string { - if project := p.opts.Host.ContentMapperProject(); project != nil { + if project := p.host.ContentMapperProject(); project != nil { if identity, err := project.Identity(mapper); err == nil { return identity } @@ -585,7 +585,7 @@ func (p *fileLoader) getContentMapperTransformIdentity(mapper *contentmapper.Map } func (p *fileLoader) emptyContentMappedFile(opts ast.SourceFileParseOptions, mapperIdentity string, transformIdentity string) *ast.SourceFile { - content, _ := p.opts.Host.FS().ReadFile(opts.FileName) + content, _ := p.host.FS().ReadFile(opts.FileName) sourceFile := parser.ParseSourceFile(opts, "", core.ScriptKindTS) sourceFile.SetContentMapperInfo(ast.ContentMapperSourceFileInfo{ ContentMapper: mapperIdentity, @@ -697,40 +697,40 @@ func (p *fileLoader) getSourceFileFromReference( diagnosticFileName := tspath.NormalizeSlashes(referenceText) if tspath.HasExtension(fileName) { - canonicalFileName := tspath.GetCanonicalFileName(fileName, p.opts.Host.FS().UseCaseSensitiveFileNames()) + canonicalFileName := tspath.GetCanonicalFileName(fileName, p.host.FS().UseCaseSensitiveFileNames()) if !allowNonTsExtensions && !p.isSupportedExtension(canonicalFileName) { if tspath.HasJSFileExtension(canonicalFileName) { - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, args: []any{diagnosticFileName}} + return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, args: []string{diagnosticFileName}} } - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, args: []any{diagnosticFileName, "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}} + return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, args: []string{diagnosticFileName, "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}} } - if !p.opts.Host.FS().FileExists(fileName) { - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []any{diagnosticFileName}} + if !p.host.FS().FileExists(fileName) { + return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []string{diagnosticFileName}} } - if includeReason.isReferencedFile() && tspath.GetCanonicalFileName(containingFile, p.opts.Host.FS().UseCaseSensitiveFileNames()) == canonicalFileName { + if includeReason.isReferencedFile() && tspath.GetCanonicalFileName(containingFile, p.host.FS().UseCaseSensitiveFileNames()) == canonicalFileName { return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.A_file_cannot_have_a_reference_to_itself} } return fileName, nil } - if allowNonTsExtensions && p.opts.Host.FS().FileExists(fileName) { + if allowNonTsExtensions && p.host.FS().FileExists(fileName) { return fileName, nil } if allowNonTsExtensions { - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []any{diagnosticFileName}} + return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.File_0_not_found, args: []string{diagnosticFileName}} } for _, ext := range p.supportedExtensions[0] { candidate := fileName + ext - if p.opts.Host.FS().FileExists(candidate) { + if p.host.FS().FileExists(candidate) { return candidate, nil } } - return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.Could_not_resolve_the_path_0_with_the_extensions_Colon_1, args: []any{diagnosticFileName, "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}} + return "", &sourceFileFromReferenceDiagnostic{message: diagnostics.Could_not_resolve_the_path_0_with_the_extensions_Colon_1, args: []string{diagnosticFileName, "'" + strings.Join(core.Flatten(p.supportedExtensions), "', '") + "'"}} } func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containingFile string, index int) (*resolvedRef, *processingDiagnostic) { @@ -743,7 +743,7 @@ func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containi normalizedFileName := tspath.NormalizePath(referencedFileName) includeReason := &FileIncludeReason{ kind: fileIncludeKindReferenceFile, - data: &referencedFileData{ + referencedFile: &referencedFileData{ file: p.toPath(containingFile), index: index, }, @@ -758,7 +758,7 @@ func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containi if diagnostic != nil { return nil, &processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, - data: &includeExplainingDiagnostic{ + explanation: &includeExplainingDiagnostic{ diagnosticReason: includeReason, message: diagnostic.message, args: diagnostic.args, @@ -777,25 +777,25 @@ func (p *fileLoader) resolveTypeReferenceDirectives(t *parseTask) { if len(file.TypeReferenceDirectives) == 0 { return } - if p.opts.Tracing != nil { - defer p.opts.Tracing.Push(tracing.PhaseProgram, "resolveTypeReferenceDirectiveNamesWorker", map[string]any{"containingFileName": file.FileName()}, false)() + if p.tracing != nil { + defer p.tracing.Push(tracing.PhaseProgram, "resolveTypeReferenceDirectiveNamesWorker", map[string]any{"containingFileName": file.FileName()}, false)() } meta := t.metadata typeResolutionsInFile := make(module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], len(file.TypeReferenceDirectives)) var typeResolutionsTrace []module.DiagAndArgs for index, ref := range file.TypeReferenceDirectives { - redirect, fileName := p.projectReferenceFileMapper.getRedirectForResolution(file) + redirect, fileName := p.projectReferences.getRedirectForResolution(file) resolutionMode := getModeForTypeReferenceDirectiveInFile(ref, file, meta, module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect)) resolved, trace := p.resolver.ResolveTypeReferenceDirective(ref.FileName, fileName, resolutionMode, redirect) var traceDone func() - if p.opts.Tracing != nil { - traceDone = p.opts.Tracing.Push(tracing.PhaseProgram, "processTypeReferenceDirective", map[string]any{"directive": ref.FileName, "hasResolved": resolved.IsResolved(), "refKind": int(fileIncludeKindTypeReferenceDirective), "refPath": string(t.path)}, false) + if p.tracing != nil { + traceDone = p.tracing.Push(tracing.PhaseProgram, "processTypeReferenceDirective", map[string]any{"directive": ref.FileName, "hasResolved": resolved.IsResolved(), "refKind": int(fileIncludeKindTypeReferenceDirective), "refPath": string(t.path)}, false) } typeResolutionsInFile[module.ModeAwareCacheKey{Name: ref.FileName, Mode: resolutionMode}] = resolved includeReason := &FileIncludeReason{ kind: fileIncludeKindTypeReferenceDirective, - data: &referencedFileData{ + referencedFile: &referencedFileData{ file: t.path, index: index, }, @@ -812,8 +812,8 @@ func (p *fileLoader) resolveTypeReferenceDirectives(t *parseTask) { }, nil) } else { t.processingDiagnostics = append(t.processingDiagnostics, &processingDiagnostic{ - kind: processingDiagnosticKindUnknownReference, - data: includeReason, + kind: processingDiagnosticKindUnknownReference, + reason: includeReason, }) } if traceDone != nil { @@ -828,8 +828,8 @@ func (p *fileLoader) resolveTypeReferenceDirectives(t *parseTask) { const externalHelpersModuleNameText = "tslib" // TODO(jakebailey): dedupe func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { - if p.opts.Tracing != nil { - defer p.opts.Tracing.Push(tracing.PhaseProgram, "resolveModuleNamesWorker", map[string]any{"containingFileName": t.file.FileName()}, false)() + if p.tracing != nil { + defer p.tracing.Push(tracing.PhaseProgram, "resolveModuleNamesWorker", map[string]any{"containingFileName": t.file.FileName()}, false)() } file := t.file meta := t.metadata @@ -839,7 +839,7 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { isJavaScriptFile := ast.IsSourceFileJS(file) isExternalModuleFile := ast.IsExternalModule(file) - redirect, fileName := p.projectReferenceFileMapper.getRedirectForResolution(file) + redirect, fileName := p.projectReferences.getRedirectForResolution(file) optionsForFile := module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect) if isJavaScriptFile || (!file.IsDeclarationFile && (optionsForFile.GetIsolatedModules() || isExternalModuleFile)) { if optionsForFile.ImportHelpers.IsTrue() { @@ -908,7 +908,7 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { resolvedFileName := resolvedModule.ResolvedFileName isFromNodeModulesSearch := resolvedModule.IsExternalLibraryImport // Don't treat redirected files as JS files. - isJsFile := !resolvedModule.ResolvedUsingExtraExtensions && !tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && p.projectReferenceFileMapper.getRedirectParsedCommandLineForResolution(ast.NewHasFileName(resolvedFileName, p.toPath(resolvedFileName))) == nil + isJsFile := !resolvedModule.ResolvedUsingExtraExtensions && !tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && p.projectReferences.getRedirectParsedCommandLineForResolution(ast.NewHasFileName(resolvedFileName, p.toPath(resolvedFileName))) == nil isJsFileFromNodeModules := isFromNodeModulesSearch && isJsFile && strings.Contains(resolvedFileName, "/node_modules/") // add file to program only if: @@ -932,7 +932,7 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(t *parseTask) { elideOnDepth: isJsFileFromNodeModules, includeReason: &FileIncludeReason{ kind: fileIncludeKindImport, - data: &referencedFileData{ + referencedFile: &referencedFileData{ file: t.path, index: importIndex, synthetic: core.IfElse(importIndex < 0, entry, nil), @@ -967,7 +967,7 @@ func (p *fileLoader) pathForLibFile(name string) *LibFile { replaced := false if !p.opts.SkipModuleResolution && p.opts.Config.CompilerOptions().LibReplacement.IsTrue() && name != "lib.d.ts" { libraryName := getLibraryNameFromLibFileName(name) - resolveFrom := getInferredLibraryNameResolveFrom(p.opts.Config.CompilerOptions(), p.opts.Host.GetCurrentDirectory(), name) + resolveFrom := getInferredLibraryNameResolveFrom(p.opts.Config.CompilerOptions(), p.host.GetCurrentDirectory(), name) resolution, trace := p.resolveLibrary(libraryName, resolveFrom) if resolution.IsResolved() { path = resolution.ResolvedFileName @@ -985,7 +985,7 @@ func (p *fileLoader) pathForLibFile(name string) *LibFile { } func (p *fileLoader) resolveLibrary(libraryName, resolveFrom string) (*module.ResolvedModule, []module.DiagAndArgs) { - if tr := p.opts.Tracing; tr != nil { + if tr := p.tracing; tr != nil { defer tr.Push(tracing.PhaseProgram, "resolveLibrary", map[string]any{"resolveFrom": resolveFrom}, false)() } resolved, trace, err := p.resolver.ResolveModuleName(libraryName, resolveFrom, core.ModuleKindCommonJS, nil) diff --git a/tsc/internal/compiler/filesparser.go b/tsc/internal/compiler/filesparser.go index dc232f819fa7f..d692d932e4aaa 100644 --- a/tsc/internal/compiler/filesparser.go +++ b/tsc/internal/compiler/filesparser.go @@ -67,10 +67,10 @@ func (t *parseTask) load(loader *fileLoader) { // exists only to carry its processing diagnostic, so nothing is parsed. return } - if loader.opts.Tracing != nil { - defer loader.opts.Tracing.Push(tracing.PhaseProgram, "findSourceFile", map[string]any{"fileName": t.normalizedFilePath}, false)() + if loader.tracing != nil { + defer loader.tracing.Push(tracing.PhaseProgram, "findSourceFile", map[string]any{"fileName": t.normalizedFilePath}, false)() } - redirect := loader.projectReferenceFileMapper.getParseFileRedirect(t) + redirect := loader.projectReferences.getParseFileRedirect(t) if redirect != "" { t.redirect(loader, redirect) return @@ -80,24 +80,24 @@ func (t *parseTask) load(loader *fileLoader) { compilerOptions := loader.opts.Config.CompilerOptions() allowNonTsExtensions := compilerOptions.AllowNonTsExtensions.IsTrue() if !allowNonTsExtensions { - canonicalFileName := tspath.GetCanonicalFileName(t.normalizedFilePath, loader.opts.Host.FS().UseCaseSensitiveFileNames()) + canonicalFileName := tspath.GetCanonicalFileName(t.normalizedFilePath, loader.host.FS().UseCaseSensitiveFileNames()) if !loader.isSupportedExtension(canonicalFileName) { if tspath.HasJSFileExtension(canonicalFileName) { t.processingDiagnostics = append(t.processingDiagnostics, &processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, - data: &includeExplainingDiagnostic{ + explanation: &includeExplainingDiagnostic{ diagnosticReason: t.includeReason, message: diagnostics.File_0_is_a_JavaScript_file_Did_you_mean_to_enable_the_allowJs_option, - args: []any{t.normalizedFilePath}, + args: []string{t.normalizedFilePath}, }, }) } else { t.processingDiagnostics = append(t.processingDiagnostics, &processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, - data: &includeExplainingDiagnostic{ + explanation: &includeExplainingDiagnostic{ diagnosticReason: t.includeReason, message: diagnostics.File_0_has_an_unsupported_extension_The_only_supported_extensions_are_1, - args: []any{t.normalizedFilePath, "'" + strings.Join(core.Flatten(loader.supportedExtensions), "', '") + "'"}, + args: []string{t.normalizedFilePath, "'" + strings.Join(core.Flatten(loader.supportedExtensions), "', '") + "'"}, }, }) } @@ -148,7 +148,7 @@ func (t *parseTask) load(loader *fileLoader) { for index, lib := range file.LibReferenceDirectives { includeReason := &FileIncludeReason{ kind: fileIncludeKindLibReferenceDirective, - data: &referencedFileData{ + referencedFile: &referencedFileData{ file: t.path, index: index, }, @@ -161,8 +161,8 @@ func (t *parseTask) load(loader *fileLoader) { }, libFile) } else { t.processingDiagnostics = append(t.processingDiagnostics, &processingDiagnostic{ - kind: processingDiagnosticKindUnknownReference, - data: includeReason, + kind: processingDiagnosticKindUnknownReference, + reason: includeReason, }) } } @@ -175,8 +175,8 @@ func (t *parseTask) load(loader *fileLoader) { file: supplemental, isContentMapperSupplemental: true, includeReason: &FileIncludeReason{ - kind: fileIncludeKindContentMapperSupplemental, - data: t.path, + kind: fileIncludeKindContentMapperSupplemental, + canonicalSourceFile: t.path, }, }) } @@ -193,8 +193,8 @@ func (t *parseTask) redirect(loader *fileLoader, fileName string) { } func (t *parseTask) loadAutomaticTypeDirectives(loader *fileLoader) { - if loader.opts.Tracing != nil { - defer loader.opts.Tracing.Push(tracing.PhaseProgram, "processTypeReferences", nil, false)() + if loader.tracing != nil { + defer loader.tracing.Push(tracing.PhaseProgram, "processTypeReferences", nil, false)() } toParseTypeRefs, typeResolutionsInFile, typeResolutionsTrace, pDiagnostics := loader.resolveAutomaticTypeDirectives(t.normalizedFilePath) t.typeResolutionsInFile = typeResolutionsInFile @@ -344,7 +344,7 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { tasksSeenByNameIgnoreCase = make(map[string]*parseTask, totalFileCount) } - includeProcessor := &includeProcessor{ + includeData := &fileIncludeData{ fileIncludeReasons: make(map[tspath.Path][]*FileIncludeReason, totalFileCount), } var outputFileToProjectReferenceSource map[tspath.Path]string @@ -386,7 +386,7 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { if task.loadedTask != nil { task = task.loadedTask } - w.addIncludeReason(includeProcessor, task, includeReason) + w.addIncludeReason(includeData, task, includeReason) } data, _ := w.taskDataByPath.Load(task.path) if !task.loaded { @@ -420,7 +420,7 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { checkedAbsolutePath := tspath.GetNormalizedAbsolutePathWithoutRoot(checkedName, loader.comparePathsOptions.CurrentDirectory) inputAbsolutePath := tspath.GetNormalizedAbsolutePathWithoutRoot(task.normalizedFilePath, loader.comparePathsOptions.CurrentDirectory) if checkedAbsolutePath != inputAbsolutePath { - includeProcessor.addProcessingDiagnosticsForFileCasing(task.path, checkedName, task.normalizedFilePath, includeReason) + includeData.addProcessingDiagnosticsForFileCasing(task.path, checkedName, task.normalizedFilePath, includeReason) } } continue @@ -431,17 +431,17 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { if tasksSeenByNameIgnoreCase != nil { pathLowerCase := tspath.ToFileNameLowerCase(string(task.path)) if taskByIgnoreCase, ok := tasksSeenByNameIgnoreCase[pathLowerCase]; ok { - includeProcessor.addProcessingDiagnosticsForFileCasing(taskByIgnoreCase.path, taskByIgnoreCase.normalizedFilePath, task.normalizedFilePath, includeReason) + includeData.addProcessingDiagnosticsForFileCasing(taskByIgnoreCase.path, taskByIgnoreCase.normalizedFilePath, task.normalizedFilePath, includeReason) } else { tasksSeenByNameIgnoreCase[pathLowerCase] = task } } for _, trace := range task.typeResolutionsTrace { - loader.opts.Host.Trace(trace.Message, trace.Args...) + loader.host.Trace(trace.Message, trace.Args...) } for _, trace := range task.resolutionsTrace { - loader.opts.Host.Trace(trace.Message, trace.Args...) + loader.host.Trace(trace.Message, trace.Args...) } file := task.file @@ -497,7 +497,7 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { if task.isForAutomaticTypeDirective { typeResolutionsInFile[task.path] = task.typeResolutionsInFile if len(task.processingDiagnostics) > 0 { - includeProcessor.processingDiagnostics = append(includeProcessor.processingDiagnostics, task.processingDiagnostics...) + includeData.processingDiagnostics = append(includeData.processingDiagnostics, task.processingDiagnostics...) } continue } @@ -505,7 +505,7 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { path := task.path if len(task.processingDiagnostics) > 0 { - includeProcessor.processingDiagnostics = append(includeProcessor.processingDiagnostics, task.processingDiagnostics...) + includeData.processingDiagnostics = append(includeData.processingDiagnostics, task.processingDiagnostics...) } if file == nil { @@ -558,17 +558,16 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { module.ModeAwareCacheKey{Name: value.libraryName, Mode: core.ModuleKindCommonJS}: value.resolution, } for _, trace := range value.trace { - loader.opts.Host.Trace(trace.Message, trace.Args...) + loader.host.Trace(trace.Message, trace.Args...) } } return processedFiles{ finishedProcessing: true, - resolver: loader.resolver, files: allFiles, duplicateSourceFiles: duplicateSourceFiles, filesByPath: filesByPath, - projectReferenceFileMapper: loader.projectReferenceFileMapper, + projectReferenceFileMapper: loader.projectReferences.projectReferenceFileMapper, resolvedModules: resolvedModules, typeResolutionsInFile: typeResolutionsInFile, sourceFileMetaDatas: sourceFileMetaDatas, @@ -577,16 +576,15 @@ func (w *filesParser) getProcessedFiles(loader *fileLoader) processedFiles { sourceFilesFoundSearchingNodeModules: sourceFilesFoundSearchingNodeModules, libFiles: libFilesMap, missingFiles: missingFiles, - includeProcessor: includeProcessor, + fileIncludeData: *includeData, outputFileToProjectReferenceSource: outputFileToProjectReferenceSource, redirectTargetsMap: redirectTargetsMap, redirectFilesByPath: redirectFilesByPath, contentMapperDiagnostics: loader.contentMapperDiagnostics, - moduleResolutionError: loader.moduleResolutionError, } } -func (w *filesParser) addIncludeReason(includeProcessor *includeProcessor, task *parseTask, reason *FileIncludeReason) { +func (w *filesParser) addIncludeReason(includeProcessor *fileIncludeData, task *parseTask, reason *FileIncludeReason) { if task.redirectedParseTask != nil { w.addIncludeReason(includeProcessor, task.redirectedParseTask, reason) } else if task.loaded { diff --git a/tsc/internal/compiler/includeprocessor.go b/tsc/internal/compiler/includeprocessor.go index 30c958a1eaf77..ff717f55774ab 100644 --- a/tsc/internal/compiler/includeprocessor.go +++ b/tsc/internal/compiler/includeprocessor.go @@ -12,10 +12,13 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/tspath" ) -type includeProcessor struct { +type fileIncludeData struct { fileIncludeReasons map[tspath.Path][]*FileIncludeReason processingDiagnostics []*processingDiagnostic +} +type includeProcessor struct { + reasonDiagnostics collections.SyncMap[includeReasonDiagnosticKey, *ast.Diagnostic] reasonToReferenceLocation collections.SyncMap[*FileIncludeReason, *referenceFileLocation] includeReasonToRelatedInfo collections.SyncMap[*FileIncludeReason, *ast.Diagnostic] redirectAndFileFormat collections.SyncMap[tspath.Path, []*ast.Diagnostic] @@ -25,17 +28,15 @@ type includeProcessor struct { compilerOptionsSyntaxOnce sync.Once } -func updateFileIncludeProcessor(p *Program) { - p.includeProcessor = &includeProcessor{ - fileIncludeReasons: p.includeProcessor.fileIncludeReasons, - processingDiagnostics: p.includeProcessor.processingDiagnostics, - } +type includeReasonDiagnosticKey struct { + reason *FileIncludeReason + relativeFileName bool } func (i *includeProcessor) getDiagnostics(p *Program) *ast.DiagnosticsCollection { i.computedDiagnosticsOnce.Do(func() { i.computedDiagnostics = &ast.DiagnosticsCollection{} - for _, d := range i.processingDiagnostics { + for _, d := range p.processingDiagnostics { i.computedDiagnostics.Add(d.toDiagnostic(p)) } for _, resolutions := range p.resolvedModules { @@ -56,31 +57,31 @@ func (i *includeProcessor) getDiagnostics(p *Program) *ast.DiagnosticsCollection return i.computedDiagnostics } -func (i *includeProcessor) addProcessingDiagnostic(d ...*processingDiagnostic) { +func (i *fileIncludeData) addProcessingDiagnostic(d ...*processingDiagnostic) { i.processingDiagnostics = append(i.processingDiagnostics, d...) } -func (i *includeProcessor) addProcessingDiagnosticsForFileCasing(file tspath.Path, existingCasing string, currentCasing string, reason *FileIncludeReason) { +func (i *fileIncludeData) addProcessingDiagnosticsForFileCasing(file tspath.Path, existingCasing string, currentCasing string, reason *FileIncludeReason) { if !reason.isReferencedFile() && slices.ContainsFunc(i.fileIncludeReasons[file], func(r *FileIncludeReason) bool { return r.isReferencedFile() }) { i.addProcessingDiagnostic(&processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, - data: &includeExplainingDiagnostic{ + explanation: &includeExplainingDiagnostic{ file: file, diagnosticReason: reason, message: diagnostics.Already_included_file_name_0_differs_from_file_name_1_only_in_casing, - args: []any{existingCasing, currentCasing}, + args: []string{existingCasing, currentCasing}, }, }) } else { i.addProcessingDiagnostic(&processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, - data: &includeExplainingDiagnostic{ + explanation: &includeExplainingDiagnostic{ file: file, diagnosticReason: reason, message: diagnostics.File_name_0_differs_from_already_included_file_name_1_only_in_casing, - args: []any{currentCasing, existingCasing}, + args: []string{currentCasing, existingCasing}, }, }) } diff --git a/tsc/internal/compiler/processingDiagnostic.go b/tsc/internal/compiler/processingDiagnostic.go index 0dbe2dab6ff03..4c245e7a7319d 100644 --- a/tsc/internal/compiler/processingDiagnostic.go +++ b/tsc/internal/compiler/processingDiagnostic.go @@ -20,23 +20,24 @@ const ( ) type processingDiagnostic struct { - kind processingDiagnosticKind - data any + kind processingDiagnosticKind + reason *FileIncludeReason + explanation *includeExplainingDiagnostic } func (d *processingDiagnostic) asFileIncludeReason() *FileIncludeReason { - return d.data.(*FileIncludeReason) + return d.reason } type includeExplainingDiagnostic struct { file tspath.Path diagnosticReason *FileIncludeReason message *diagnostics.Message - args []any + args []string } func (d *processingDiagnostic) asIncludeExplainingDiagnostic() *includeExplainingDiagnostic { - return d.data.(*includeExplainingDiagnostic) + return d.explanation } func (d *processingDiagnostic) toDiagnostic(program *Program) *ast.Diagnostic { @@ -98,7 +99,7 @@ func (d *processingDiagnostic) createDiagnosticExplainingFile(program *Program) // !!! todo sheetal caching if diag.file != "" { - reasons := program.includeProcessor.fileIncludeReasons[diag.file] + reasons := program.fileIncludeReasons[diag.file] includeDetails = make([]*ast.Diagnostic, 0, len(reasons)) for _, reason := range reasons { processInclude(reason) @@ -119,11 +120,12 @@ func (d *processingDiagnostic) createDiagnosticExplainingFile(program *Program) } var result *ast.Diagnostic + args := core.Map(diag.args, func(arg string) any { return arg }) if preferredLocation != nil { - result = program.includeProcessor.getReferenceLocation(preferredLocation, program).diagnosticAt(diag.message, diag.args...) + result = program.includeProcessor.getReferenceLocation(preferredLocation, program).diagnosticAt(diag.message, args...) } if result == nil { - result = ast.NewCompilerDiagnostic(diag.message, diag.args...) + result = ast.NewCompilerDiagnostic(diag.message, args...) } if chain != nil { result.SetMessageChain(chain) diff --git a/tsc/internal/compiler/program.go b/tsc/internal/compiler/program.go index c81fb423908d0..c0fb38b3bff6f 100644 --- a/tsc/internal/compiler/program.go +++ b/tsc/internal/compiler/program.go @@ -35,21 +35,33 @@ import ( ) type ProgramOptions struct { - Host CompilerHost + ProgramConfig + ProgramHosts + ProgramFactories +} + +type ProgramConfig struct { Config *tsoptions.ParsedCommandLine UseSourceOfProjectReference bool SingleThreaded core.Tristate - CreateCheckerPool func(*Program) CheckerPool TypingsLocation string ProjectName string - Tracing *tracing.Tracing - CreateModuleResolver func(options module.ResolverOptions) module.Resolver // SkipModuleResolution avoids all module and type reference resolution while // still collecting import metadata needed for emit. SkipModuleResolution bool } -func (p *ProgramOptions) canUseProjectReferenceSource() bool { +type ProgramHosts struct { + Host CompilerHost + Tracing *tracing.Tracing +} + +type ProgramFactories struct { + CreateCheckerPool func(*Program) CheckerPool + CreateModuleResolver func(module.ResolverOptions) module.Resolver +} + +func (p *ProgramConfig) canUseProjectReferenceSource() bool { return p.UseSourceOfProjectReference && !p.Config.CompilerOptions().DisableSourceOfProjectReferenceRedirect.IsTrue() } @@ -83,8 +95,12 @@ type packageNamesInfo struct { } type Program struct { - opts ProgramOptions - checkerPool CheckerPool // always set; used as fallback for project system pools + opts ProgramConfig + hosts ProgramHosts + resolutionData *module.ResolutionData + checkerPool CheckerPool // always set; used as fallback for project system pools + includeProcessor includeProcessor + moduleResolutionError error // compilerCheckerPool is set only when the built-in compiler checker pool is in use // (i.e. CreateCheckerPool was not provided). It enables grouped parallel iteration, @@ -136,7 +152,7 @@ func (p *Program) GetCurrentDirectory() string { } func (p *Program) ContentMapperProject() contentmapper.Project { - return p.opts.Host.ContentMapperProject() + return p.hosts.Host.ContentMapperProject() } // GetGlobalTypingsCacheLocation implements checker.Program. @@ -146,7 +162,7 @@ func (p *Program) GetGlobalTypingsCacheLocation() string { // GetNearestAncestorDirectoryWithPackageJson implements checker.Program. func (p *Program) GetNearestAncestorDirectoryWithPackageJson(dirname string) string { - scoped := p.resolver.GetPackageScopeForPath(dirname) + scoped := p.newResolver().GetPackageScopeForPath(dirname) if scoped != nil && scoped.Exists() { return scoped.PackageDirectory } @@ -156,7 +172,7 @@ func (p *Program) GetNearestAncestorDirectoryWithPackageJson(dirname string) str // GetPackageJsonInfo implements checker.Program. func (p *Program) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheEntry { directory := tspath.GetDirectoryPath(pkgJsonPath) - scoped := p.resolver.GetPackageScopeForPath(directory) + scoped := p.newResolver().GetPackageScopeForPath(directory) if scoped != nil && scoped.Exists() && scoped.PackageDirectory == directory { return scoped } @@ -165,7 +181,11 @@ func (p *Program) GetPackageJsonInfo(pkgJsonPath string) *packagejson.InfoCacheE // PackageJsonCacheEntries iterates on all package json cache entries. func (p *Program) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { - p.resolver.PackageJsonCacheEntries(f) + p.resolutionData.PackageJsonCacheEntries(f) +} + +func (p *Program) newResolver() *module.DefaultResolver { + return p.resolutionData.NewResolver(p.projectReferenceFileMapper.resolutionHost(p.hosts.Host)) } // GetRedirectTargets returns the list of file paths that redirect to the given path. @@ -283,12 +303,12 @@ func (p *Program) GetSourceFileFromReference(origin *ast.SourceFile, ref *ast.Fi } func NewProgram(opts ProgramOptions) *Program { - p := &Program{opts: opts} - if p.opts.Tracing != nil { - defer p.opts.Tracing.Push(tracing.PhaseProgram, "createProgram", map[string]any{"configFilePath": opts.Config.CompilerOptions().ConfigFilePath}, true)() + p := &Program{opts: opts.ProgramConfig, hosts: opts.ProgramHosts} + if opts.Tracing != nil { + defer opts.Tracing.Push(tracing.PhaseProgram, "createProgram", map[string]any{"configFilePath": opts.Config.CompilerOptions().ConfigFilePath}, true)() } - p.processedFiles = processAllProgramFiles(p.opts, p.SingleThreaded()) - p.initCheckerPool() + p.processedFiles, p.resolutionData, p.moduleResolutionError = processAllProgramFiles(opts, p.SingleThreaded()) + p.initCheckerPool(opts.CreateCheckerPool) p.verifyCompilerOptions() p.collectContentMapperOptionDiagnostics() return p @@ -296,8 +316,6 @@ func NewProgram(opts ProgramOptions) *Program { // Return an updated program for which it is known that only the file with the given path has changed. // In addition to a new program, return a boolean indicating whether the data of the old program was reused. -// createCheckerPool, if non-nil, overrides the CreateCheckerPool stored in the old program's options, -// ensuring each caller uses a fresh closure and avoiding data races on captured variables. // The returned *ast.SourceFile is the changed file as acquired through newHost; it is nil // only if the host cannot locate the file (e.g. it was deleted). Callers that manage // host-side parse caches must release this exact pointer when the old program could not be @@ -311,15 +329,12 @@ func (p *Program) UpdateProgram( if result, newFile, reused := p.ReuseProgram(changedFilePath, newHost, createCheckerPool, createModuleResolver); reused { return result, newFile, true } else { - newOpts := p.opts - newOpts.Host = newHost - if createCheckerPool != nil { - newOpts.CreateCheckerPool = createCheckerPool - } - if createModuleResolver != nil { - newOpts.CreateModuleResolver = createModuleResolver - } - return NewProgram(newOpts), newFile, false + return NewProgram(ProgramOptions{ + ProgramConfig: p.opts, + Host: newHost, + CreateCheckerPool: createCheckerPool, + CreateModuleResolver: createModuleResolver, + }), newFile, false } } @@ -335,14 +350,6 @@ func (p *Program) ReuseProgram( createCheckerPool func(*Program) CheckerPool, createModuleResolver func(module.ResolverOptions) module.Resolver, ) (*Program, *ast.SourceFile, bool) { - newOpts := p.opts - newOpts.Host = newHost - if createCheckerPool != nil { - newOpts.CreateCheckerPool = createCheckerPool - } - if createModuleResolver != nil { - newOpts.CreateModuleResolver = createModuleResolver - } oldFile := p.filesByPath[changedFilePath] var newFile *ast.SourceFile var oldSupplementalFiles []*ast.SourceFile @@ -351,7 +358,7 @@ func (p *Program) ReuseProgram( // Content-mapped files are produced by running an external transform, which a plain reparse can't // reproduce. Re-run the transform through the host; any failure (or a missing file) falls back to // a full rebuild so the file loader's failure policy runs. - mapper := newOpts.Config.GetContentMapperForFileName(oldFile.FileName()) + mapper := p.opts.Config.GetContentMapperForFileName(oldFile.FileName()) var err error files, transformErr := newHost.GetContentMappedSourceFiles(oldFile.ParseOptions(), mapper) newFile, err = files.Canonical, transformErr @@ -374,7 +381,7 @@ func (p *Program) ReuseProgram( return nil, newFile, false } - if !p.canReplaceFileInProgram(oldFile, newFile) { + if p.moduleResolutionError != nil || !p.canReplaceFileInProgram(oldFile, newFile) { return nil, newFile, false } // Cloning does not recompute synthetic helper or JSX-runtime import bookkeeping. Fall back to a full @@ -403,7 +410,9 @@ func (p *Program) ReuseProgram( } // TODO: reverify compiler options when config has changed? result := &Program{ - opts: newOpts, + opts: p.opts, + hosts: ProgramHosts{Host: newHost}, + resolutionData: p.resolutionData.Clone(), comparePathsOptions: p.comparePathsOptions, processedFiles: p.processedFiles, usesUriStyleNodeCoreModules: p.usesUriStyleNodeCoreModules, @@ -414,7 +423,6 @@ func (p *Program) ReuseProgram( result.unresolvedImports.tryReuse(&p.unresolvedImports) result.knownSymlinks.tryReuse(&p.knownSymlinks) result.packageNames.tryReuse(&p.packageNames) - result.initCheckerPool() index := core.FindIndex(result.files, func(file *ast.SourceFile) bool { return file.Path() == newFile.Path() }) result.files = slices.Clone(result.files) result.files[index] = newFile @@ -428,19 +436,19 @@ func (p *Program) ReuseProgram( result.filesByPath[newSupplemental.Path()] = newSupplemental } } - updateFileIncludeProcessor(result) + result.initCheckerPool(createCheckerPool) return result, newFile, true } -func (p *Program) initCheckerPool() { +func (p *Program) initCheckerPool(create func(*Program) CheckerPool) { if !p.finishedProcessing { panic("Program must finish processing files before initializing checker pool") } - if p.opts.CreateCheckerPool != nil { - p.checkerPool = p.opts.CreateCheckerPool(p) + if create != nil { + p.checkerPool = create(p) } else { - pool := newCheckerPoolWithTracing(p, p.opts.Tracing) + pool := newCheckerPoolWithTracing(p, p.hosts.Tracing) p.checkerPool = pool p.compilerCheckerPool = pool } @@ -527,8 +535,8 @@ func (p *Program) GetContentMapper(file *ast.SourceFile) *contentmapper.Mapper { func (p *Program) ContentMapperExtensions() []string { return p.opts.Config.ContentMapperExtensions() } func (p *Program) CommandLine() *tsoptions.ParsedCommandLine { return p.opts.Config } -func (p *Program) Host() CompilerHost { return p.opts.Host } -func (p *Program) Tracing() *tracing.Tracing { return p.opts.Tracing } +func (p *Program) Host() CompilerHost { return p.hosts.Host } +func (p *Program) Tracing() *tracing.Tracing { return p.hosts.Tracing } func (p *Program) GetConfigFileParsingDiagnostics() []*ast.Diagnostic { return slices.Clip(p.opts.Config.GetConfigFileParsingDiagnostics()) } @@ -576,8 +584,8 @@ func (p *Program) BindSourceFiles() { for _, file := range p.files { if !file.IsBound() { wg.Queue(func() { - if p.opts.Tracing != nil { - defer p.opts.Tracing.Push(tracing.PhaseBind, "bindSourceFile", map[string]any{"path": string(file.Path())}, true)() + if p.hosts.Tracing != nil { + defer p.hosts.Tracing.Push(tracing.PhaseBind, "bindSourceFile", map[string]any{"path": string(file.Path())}, true)() } binder.BindSourceFile(file) }) @@ -1083,12 +1091,12 @@ func (p *Program) verifyCompilerOptions() { rootPath = canonical.Path() } if sourceFileMayBeEmitted(file, p, false, false) && !rootPaths.Has(rootPath) { - p.includeProcessor.addProcessingDiagnostic(&processingDiagnostic{ + p.addProcessingDiagnostic(&processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, - data: &includeExplainingDiagnostic{ + explanation: &includeExplainingDiagnostic{ file: file.Path(), message: diagnostics.File_0_is_not_listed_within_the_file_list_of_project_1_Projects_must_list_all_files_or_use_an_include_pattern, - args: []any{file.FileName(), configFilePath()}, + args: []string{file.FileName(), configFilePath()}, }, }) } @@ -1819,12 +1827,12 @@ func (p *Program) checkSourceFilesBelongToPath(sourceFiles []string, rootDirecto for _, file := range sourceFiles { absoluteSourceFilePath := tspath.GetCanonicalFileName(tspath.GetNormalizedAbsolutePath(file, p.GetCurrentDirectory()), p.UseCaseSensitiveFileNames()) if !tspath.ContainsPath(rootDirectory, file, p.comparePathsOptions) { - p.includeProcessor.addProcessingDiagnostic(&processingDiagnostic{ + p.addProcessingDiagnostic(&processingDiagnostic{ kind: processingDiagnosticKindExplainingFileInclude, - data: &includeExplainingDiagnostic{ + explanation: &includeExplainingDiagnostic{ file: tspath.Path(absoluteSourceFilePath), message: diagnostics.File_0_is_not_under_rootDir_1_rootDir_is_expected_to_contain_all_source_files, - args: []any{file, rootDirectory}, + args: []string{file, rootDirectory}, }, }) allFilesBelongToPath = false @@ -1865,7 +1873,7 @@ type SourceMapEmitResult struct { } func (p *Program) Emit(ctx context.Context, options EmitOptions) *EmitResult { - if tr := p.opts.Tracing; tr != nil { + if tr := p.hosts.Tracing; tr != nil { defer tr.Push(tracing.PhaseEmit, "emit", nil, true)() } @@ -1900,7 +1908,7 @@ func (p *Program) Emit(ctx context.Context, options EmitOptions) *EmitResult { emitOnly: options.EmitOnly, forceEmit: options.ForceEmit, writeFile: options.WriteFile, - tr: p.opts.Tracing, + tr: p.hosts.Tracing, } emitters = append(emitters, emitter) wg.Queue(func() { @@ -2107,7 +2115,7 @@ func (p *Program) GetSourceFiles() []*ast.SourceFile { // Testing only func (p *Program) GetIncludeReasons() map[tspath.Path][]*FileIncludeReason { - return p.includeProcessor.fileIncludeReasons + return p.fileIncludeReasons } // Testing only @@ -2124,7 +2132,7 @@ func (p *Program) ExplainFiles(w io.Writer, locale locale.Locale) { filesExplained := 0 explainFile := func(file ast.HasFileName) { fmt.Fprintln(w, toRelativeFileName(file.FileName())) - for _, reason := range p.includeProcessor.fileIncludeReasons[file.Path()] { + for _, reason := range p.fileIncludeReasons[file.Path()] { fmt.Fprintln(w, " ", reason.toDiagnostic(p, true).Localize(locale)) } for _, diag := range p.includeProcessor.explainRedirectAndImpliedFormat(p, file.Path(), toRelativeFileName) { @@ -2225,6 +2233,7 @@ func (p *Program) DeepImportPackageNames() *collections.Set[string] { func (p *Program) collectPackageNames() *packageNamesInfo { return p.packageNames.getValue(func() *packageNamesInfo { + resolver := p.newResolver() packageNames := &packageNamesInfo{&collections.Set[string]{}, &collections.Set[string]{}, &collections.Set[string]{}} for _, file := range p.files { if p.IsSourceFileDefaultLibrary(file.Path()) || p.IsSourceFileFromExternalLibrary(file) || strings.Contains(file.FileName(), "/node_modules/") { @@ -2247,7 +2256,7 @@ func (p *Program) collectPackageNames() *packageNamesInfo { name := resolvedModule.PackageId.Name if name == "" { // 2. GetPackageScopeForPath - get name from package.json in the package directory - if packageScope := p.resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName); packageScope != nil && packageScope.Exists() { + if packageScope := resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName); packageScope != nil && packageScope.Exists() { if scopeName, ok := packageScope.Contents.Name.GetValue(); ok { name = scopeName } @@ -2265,7 +2274,7 @@ func (p *Program) collectPackageNames() *packageNamesInfo { // map, so auto-import can only find them via recursive directory search. _, rest := module.ParsePackageName(imp.Text()) if rest != "" { - if scope := p.resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName); scope != nil && scope.Exists() && !scope.Contents.Exports.IsPresent() { + if scope := resolver.GetPackageScopeForPath(resolvedModule.ResolvedFileName); scope != nil && scope.Exists() && !scope.Contents.Exports.IsPresent() { packageNames.deepImportPackages.Add(module.GetPackageNameFromTypesPackageName(name)) } } @@ -2299,6 +2308,7 @@ func (p *Program) HasTSFile() bool { func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { return p.knownSymlinks.getValue(func() *symlinks.KnownSymlinks { + resolver := p.newResolver() knownSymlinks := symlinks.NewKnownSymlink(p.GetCurrentDirectory(), p.UseCaseSensitiveFileNames()) // Resolved modules store realpath information when they're resolved inside node_modules @@ -2334,7 +2344,7 @@ func (p *Program) GetSymlinkCache() *symlinks.KnownSymlinks { } } - if packageResolution := p.resolver.ResolvePackageDirectory(dep, packageJsonName, core.ResolutionModeCommonJS, nil); packageResolution.IsResolved() && packageResolution.OriginalPath != "" { + if packageResolution := resolver.ResolvePackageDirectory(dep, packageJsonName, core.ResolutionModeCommonJS, nil); packageResolution.IsResolved() && packageResolution.OriginalPath != "" { knownSymlinks.ProcessResolution( tspath.CombinePaths(packageResolution.OriginalPath, "package.json"), tspath.CombinePaths(packageResolution.ResolvedFileName, "package.json"), diff --git a/tsc/internal/compiler/program_test.go b/tsc/internal/compiler/program_test.go index 01ab79a363be1..185d83e20570d 100644 --- a/tsc/internal/compiler/program_test.go +++ b/tsc/internal/compiler/program_test.go @@ -1,16 +1,24 @@ -package compiler_test +package compiler import ( "fmt" + "maps" "path/filepath" + "reflect" "slices" "strings" "testing" + "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/bundled" - "github.com/microsoft/TypeScript/tsc/internal/compiler" + "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/module" + "github.com/microsoft/TypeScript/tsc/internal/packagejson" "github.com/microsoft/TypeScript/tsc/internal/repo" + "github.com/microsoft/TypeScript/tsc/internal/symlinks" + "github.com/microsoft/TypeScript/tsc/internal/testutil" + "github.com/microsoft/TypeScript/tsc/internal/tracing" "github.com/microsoft/TypeScript/tsc/internal/tsoptions" "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/osvfs" @@ -18,6 +26,207 @@ import ( "gotest.tools/v3/assert" ) +func TestProgramSharedData(t *testing.T) { + t.Parallel() + // These types use tagged or JSON payloads. + leaves := []reflect.Type{ + reflect.TypeFor[ast.Node](), + reflect.TypeFor[ast.SourceFile](), + reflect.TypeFor[ast.Diagnostic](), + reflect.TypeFor[tsoptions.ParsedCommandLine](), + reflect.TypeFor[packagejson.PackageJson](), + } + for _, typ := range []reflect.Type{ + reflect.TypeFor[ProgramConfig](), + reflect.TypeFor[module.ResolutionData](), + reflect.TypeFor[processedFiles](), + reflect.TypeFor[lazyValue[collections.Set[string]]](), + reflect.TypeFor[lazyValue[symlinks.KnownSymlinks]](), + reflect.TypeFor[lazyValue[packageNamesInfo]](), + } { + assert.NilError(t, testutil.CheckDataOnly(typ, leaves)) + } + for _, typ := range []reflect.Type{ + reflect.TypeFor[ProgramOptions](), + reflect.TypeFor[ProgramHosts](), + reflect.TypeFor[ProgramFactories](), + reflect.TypeFor[module.DefaultResolver](), + reflect.TypeFor[fileLoader](), + reflect.TypeFor[projectReferenceFileMapperBuilder](), + reflect.TypeFor[func()](), + reflect.TypeFor[any](), + reflect.TypeFor[chan int](), + reflect.TypeFor[collections.SyncMap[string, func()]](), + reflect.TypeFor[map[string][]struct{ owner any }](), + } { + assert.ErrorContains(t, testutil.CheckDataOnly(typ, leaves), "shared data must not retain hosts") + } + // Retained state may contain these runtime dependencies, but not factories. + retainedLeaves := append(leaves, + reflect.TypeFor[CompilerHost](), + reflect.TypeFor[tracing.Tracing](), + reflect.TypeFor[CheckerPool](), + reflect.TypeFor[checkerPool](), + reflect.TypeFor[error](), + ) + assert.NilError(t, testutil.CheckDataOnly(reflect.TypeFor[Program](), retainedLeaves)) + assert.ErrorContains(t, testutil.CheckDataOnly(reflect.TypeFor[ProgramOptions](), retainedLeaves), "shared data must not retain hosts") + assert.ErrorContains(t, testutil.CheckDataOnly(reflect.TypeFor[ProgramFactories](), retainedLeaves), "shared data must not retain hosts") + for field := range reflect.TypeFor[ProgramFactories]().Fields() { + assert.Equal(t, field.Type.Kind(), reflect.Func, "factory field %s must not hold retained hosts", field.Name) + } +} + +func TestIncludeReasonDiagnosticsAreProgramLocal(t *testing.T) { + t.Parallel() + opts := ProgramConfig{Config: &tsoptions.ParsedCommandLine{}} + oldProgram := &Program{opts: opts} + newProgram := &Program{opts: opts} + reason := &FileIncludeReason{kind: fileIncludeKindRootFile} + for _, relative := range []bool{false, true} { + oldDiagnostic := reason.toDiagnostic(oldProgram, relative) + newDiagnostic := reason.toDiagnostic(newProgram, relative) + assert.Equal(t, reason.toDiagnostic(oldProgram, relative), oldDiagnostic) + assert.Equal(t, reason.toDiagnostic(newProgram, relative), newDiagnostic) + assert.Assert(t, oldDiagnostic != newDiagnostic) + } +} + +func TestProgramHostsAndFactories(t *testing.T) { + t.Parallel() + files := map[string]any{ + "/src/tsconfig.json": `{"compilerOptions":{"noLib":true,"module":"nodenext"},"files":["index.ts"]}`, + "/src/index.ts": `/// +import { value } from "./dep.js"; export const result = value;`, + "/src/dep.ts": "export const value = 1;", + "/src/node_modules/@types/dep/index.d.ts": "export {};", + } + host := NewCompilerHost("/", vfstest.FromMap(files, true), "", nil, nil, nil) + config, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/src/tsconfig.json", nil, nil, host, nil) + assert.Equal(t, len(diagnostics), 0) + var pools, resolvers int + tr := new(tracing.Tracing) + p := NewProgram(ProgramOptions{ + Config: config, + Host: host, + Tracing: tr, + CreateCheckerPool: func(p *Program) CheckerPool { + pools++ + assert.Equal(t, p.Tracing(), tr) + return newCheckerPoolWithTracing(p, p.Tracing()) + }, + CreateModuleResolver: func(options module.ResolverOptions) module.Resolver { + resolvers++ + resolverFiles := maps.Clone(files) + resolverFiles["/factory-only/package.json"] = `{"name":"factory-host"}` + options.Host = NewCompilerHost("/", vfstest.FromMap(resolverFiles, true), "", nil, nil, nil) + return module.NewResolver(options) + }, + }) + assert.Equal(t, pools, 1) + assert.Equal(t, resolvers, 1) + assert.Equal(t, p.Tracing(), tr) + assert.Assert(t, p.GetPackageJsonInfo("/factory-only/package.json") == nil, "lazy lookups must not retain the factory's host") + oldFile := p.GetSourceFile("/src/index.ts") + resolved := p.GetResolvedModuleFromModuleSpecifier(oldFile, oldFile.Imports()[0]) + assert.Assert(t, resolved.IsResolved()) + resolvedTypeRef := p.GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(oldFile.TypeReferenceDirectives[0], oldFile) + assert.Assert(t, resolvedTypeRef.IsResolved()) + + newFiles := maps.Clone(files) + newFiles["/src/index.ts"] = "\n" + files["/src/index.ts"].(string) + newFiles["/probe/package.json"] = `{"name":"new-host"}` + newHost := NewCompilerHost("/", vfstest.FromMap(newFiles, true), "", nil, nil, nil) + cloned, changed, reused := p.ReuseProgram("/src/index.ts", newHost, + func(p *Program) CheckerPool { + pools++ + assert.Equal(t, p.Host(), newHost) + assert.Assert(t, p.Tracing() == nil) + assert.Equal(t, p.GetSourceFile("/src/index.ts").Text(), newFiles["/src/index.ts"].(string)) + return newCheckerPoolWithTracing(p, p.Tracing()) + }, + func(module.ResolverOptions) module.Resolver { + t.Fatal("cloning must reuse resolution data without invoking construction callbacks") + return nil + }, + ) + assert.Assert(t, reused) + assert.Assert(t, cloned.Tracing() == nil) + assert.Equal(t, p.Tracing(), tr) + assert.Equal(t, changed, cloned.GetSourceFile("/src/index.ts")) + assert.Equal(t, cloned.GetResolvedModuleFromModuleSpecifier(changed, changed.Imports()[0]), resolved) + assert.Equal(t, cloned.GetResolvedTypeReferenceDirectiveFromTypeReferenceDirective(changed.TypeReferenceDirectives[0], changed), resolvedTypeRef) + assert.Assert(t, cloned.resolutionData != p.resolutionData) + assert.Assert(t, cloned.GetCheckerPool() != p.GetCheckerPool()) + assert.Assert(t, cloned.GetPackageJsonInfo("/probe/package.json") != nil) + assert.Assert(t, p.GetPackageJsonInfo("/probe/package.json") == nil, "new lazy lookups must not populate the old generation's cache") + assert.Equal(t, pools, 2) + assert.Equal(t, resolvers, 1) + + defaults, _, reused := cloned.ReuseProgram("/src/index.ts", newHost, nil, nil) + assert.Assert(t, reused) + assert.Assert(t, defaults.compilerCheckerPool != nil) + assert.Assert(t, defaults.Tracing() == nil) + assert.Assert(t, defaults.compilerCheckerPool.tracing == nil) + assert.Equal(t, pools, 2) + + traced := NewProgram(ProgramOptions{Config: config, Host: newHost, Tracing: tr}) + assert.Equal(t, traced.Tracing(), tr) + assert.Equal(t, traced.compilerCheckerPool.tracing, tr) + + newFiles["/src/index.ts"] = `import "./other.js";` + newFiles["/src/other.ts"] = "export {};" + rebuildHost := NewCompilerHost("/", vfstest.FromMap(newFiles, true), "", nil, nil, nil) + rebuilt, _, reused := p.UpdateProgram("/src/index.ts", rebuildHost, nil, nil) + assert.Assert(t, !reused) + assert.Assert(t, rebuilt.compilerCheckerPool != nil) + assert.Equal(t, rebuilt.Host(), rebuildHost) + assert.Assert(t, rebuilt.Tracing() == nil) + assert.Assert(t, rebuilt.compilerCheckerPool.tracing == nil) + assert.Assert(t, rebuilt.GetSourceFile("/src/other.ts") != nil) + assert.Equal(t, pools, 2) + assert.Equal(t, resolvers, 1) +} + +func TestClonedProgramProjectReferenceResolution(t *testing.T) { + t.Parallel() + for _, preserveSymlinks := range []bool{false, true} { + t.Run(map[bool]string{false: "realpaths", true: "preserveSymlinks"}[preserveSymlinks], func(t *testing.T) { + t.Parallel() + files := map[string]any{ + "/src/tsconfig.json": `{"compilerOptions":{"noLib":true,"module":"nodenext"},"files":["index.ts"],"references":[{"path":"../reference"}]}`, + "/src/index.ts": `import { value } from "reference"; export const result = value;`, + "/src/node_modules/reference": vfstest.Symlink("/reference"), + "/reference/tsconfig.json": `{"compilerOptions":{"composite":true,"outDir":"dist"},"files":["index.ts"]}`, + "/reference/package.json": `{"name":"reference","version":"1.0.0","types":"dist/index.d.ts"}`, + "/reference/index.ts": "export const value = 1;", + } + host := NewCompilerHost("/", vfstest.FromMap(files, true), "", nil, nil, nil) + config, diagnostics := tsoptions.GetParsedCommandLineOfConfigFile("/src/tsconfig.json", &core.CompilerOptions{ + PreserveSymlinks: core.BoolToTristate(preserveSymlinks), + }, nil, host, nil) + assert.Equal(t, len(diagnostics), 0) + p := NewProgram(ProgramOptions{Config: config, Host: host, UseSourceOfProjectReference: true}) + assert.Assert(t, p.GetSourceFile("/reference/index.ts") != nil) + assert.Assert(t, !host.FS().FileExists("/reference/dist/index.d.ts")) + newFiles := maps.Clone(files) + newFiles["/src/index.ts"] = "\n" + files["/src/index.ts"].(string) + newFiles["/probe/package.json"] = `{"name":"new-host"}` + newHost := NewCompilerHost("/", vfstest.FromMap(newFiles, true), "", nil, nil, nil) + cloned, _, reused := p.ReuseProgram("/src/index.ts", newHost, nil, nil) + assert.Assert(t, reused) + assert.Equal(t, cloned.projectReferenceFileMapper, p.projectReferenceFileMapper) + assert.Assert(t, cloned.GetSourceFile("/reference/index.ts") != nil) + assert.Assert(t, cloned.GetPackageJsonInfo("/probe/package.json") != nil) + // Resolve again to exercise the new .d.ts-faking host. + resolved, _, err := cloned.newResolver().ResolveModuleName("reference", "/src/nested/probe.ts", core.ModuleKindCommonJS, nil) + assert.NilError(t, err) + assert.Assert(t, resolved.IsResolved()) + assert.Assert(t, strings.HasSuffix(resolved.ResolvedFileName, "/dist/index.d.ts")) + }) + } +} + type testFile struct { fileName string contents string @@ -248,14 +457,14 @@ func TestProgram(t *testing.T) { opts := core.CompilerOptions{Target: testCase.target} - program := compiler.NewProgram(compiler.ProgramOptions{ + program := NewProgram(ProgramOptions{ Config: &tsoptions.ParsedCommandLine{ ParsedConfig: &tsoptions.ParsedOptions{ FileNames: []string{"c:/dev/src/index.ts"}, CompilerOptions: &opts, }, }, - Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil, nil), + Host: NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil, nil), }) actualFiles := []string{} @@ -288,14 +497,14 @@ func TestIncludeProcessorDiagnosticsWithMissingFileCasing(t *testing.T) { // List both casings as root files. The first one (/src/MyFile.ts) will fail // to load because it does not exist on the case-sensitive filesystem. - program := compiler.NewProgram(compiler.ProgramOptions{ + program := NewProgram(ProgramOptions{ Config: &tsoptions.ParsedCommandLine{ ParsedConfig: &tsoptions.ParsedOptions{ FileNames: []string{"/src/MyFile.ts", "/src/myFile.ts"}, CompilerOptions: &opts, }, }, - Host: compiler.NewCompilerHost("/", fs, bundled.LibPath(), nil, nil, nil), + Host: NewCompilerHost("/", fs, bundled.LibPath(), nil, nil, nil), }) // GetProgramDiagnostics triggers getDiagnostics which processes all @@ -330,18 +539,18 @@ func BenchmarkNewProgram(b *testing.B) { } opts := core.CompilerOptions{Target: testCase.target} - programOpts := compiler.ProgramOptions{ + programOpts := ProgramOptions{ Config: &tsoptions.ParsedCommandLine{ ParsedConfig: &tsoptions.ParsedOptions{ FileNames: []string{"c:/dev/src/index.ts"}, CompilerOptions: &opts, }, }, - Host: compiler.NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil, nil), + Host: NewCompilerHost("c:/dev/src", fs, bundled.LibPath(), nil, nil, nil), } for b.Loop() { - compiler.NewProgram(programOpts) + NewProgram(programOpts) } }) } @@ -349,16 +558,16 @@ func BenchmarkNewProgram(b *testing.B) { b.Run("compiler", func(b *testing.B) { rootPath := tspath.NormalizeSlashes(filepath.Join(repo.TestDataPath(), "fixtures/compiler")) fs := bundled.WrapFS(osvfs.FS()) - host := compiler.NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil, nil) + host := NewCompilerHost(rootPath, fs, bundled.LibPath(), nil, nil, nil) parsed, errors := tsoptions.GetParsedCommandLineOfConfigFile(tspath.CombinePaths(rootPath, "tsconfig.json"), nil, nil, host, nil) assert.Equal(b, len(errors), 0, "Expected no errors in parsed command line") - opts := compiler.ProgramOptions{ + opts := ProgramOptions{ Config: parsed, Host: host, } for b.Loop() { - compiler.NewProgram(opts) + NewProgram(opts) } }) } diff --git a/tsc/internal/compiler/projectreferencedtsfakinghost.go b/tsc/internal/compiler/projectreferencedtsfakinghost.go index 0f1d426ba30d9..d92c86229752c 100644 --- a/tsc/internal/compiler/projectreferencedtsfakinghost.go +++ b/tsc/internal/compiler/projectreferencedtsfakinghost.go @@ -4,7 +4,6 @@ import ( "strings" "time" - "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/symlinks" @@ -14,23 +13,22 @@ import ( ) type projectReferenceDtsFakingHost struct { - host CompilerHost - fs *cachedvfs.FS + currentDirectory string + fs *cachedvfs.FS } var _ module.ResolutionHost = (*projectReferenceDtsFakingHost)(nil) -func newProjectReferenceDtsFakingHost(loader *fileLoader) module.ResolutionHost { +func newProjectReferenceDtsFakingHost(host module.ResolutionHost, references *projectReferenceFileMapper) module.ResolutionHost { // Create a new host that will fake the dts files - host := &projectReferenceDtsFakingHost{ - host: loader.opts.Host, + return &projectReferenceDtsFakingHost{ + currentDirectory: host.GetCurrentDirectory(), fs: cachedvfs.From(&projectReferenceDtsFakingVfs{ - projectReferenceFileMapper: loader.projectReferenceFileMapper, - dtsDirectories: loader.dtsDirectories, + host: host, + projectReferenceFileMapper: references, knownSymlinks: symlinks.KnownSymlinks{}, }), } - return host } // FS implements module.ResolutionHost. @@ -40,12 +38,12 @@ func (h *projectReferenceDtsFakingHost) FS() vfs.FS { // GetCurrentDirectory implements module.ResolutionHost. func (h *projectReferenceDtsFakingHost) GetCurrentDirectory() string { - return h.host.GetCurrentDirectory() + return h.currentDirectory } type projectReferenceDtsFakingVfs struct { + host module.ResolutionHost projectReferenceFileMapper *projectReferenceFileMapper - dtsDirectories collections.Set[tspath.Path] knownSymlinks symlinks.KnownSymlinks } @@ -53,12 +51,12 @@ var _ vfs.FS = (*projectReferenceDtsFakingVfs)(nil) // UseCaseSensitiveFileNames implements vfs.FS. func (fs *projectReferenceDtsFakingVfs) UseCaseSensitiveFileNames() bool { - return fs.projectReferenceFileMapper.opts.Host.FS().UseCaseSensitiveFileNames() + return fs.host.FS().UseCaseSensitiveFileNames() } // FileExists implements vfs.FS. func (fs *projectReferenceDtsFakingVfs) FileExists(path string) bool { - if fs.projectReferenceFileMapper.opts.Host.FS().FileExists(path) { + if fs.host.FS().FileExists(path) { return true } if !tspath.IsDeclarationFileName(path) { @@ -71,7 +69,7 @@ func (fs *projectReferenceDtsFakingVfs) FileExists(path string) bool { // ReadFile implements vfs.FS. func (fs *projectReferenceDtsFakingVfs) ReadFile(path string) (contents string, ok bool) { // Dont need to override as we cannot mimick read file - return fs.projectReferenceFileMapper.opts.Host.FS().ReadFile(path) + return fs.host.FS().ReadFile(path) } // WriteFile implements vfs.FS. @@ -96,7 +94,7 @@ func (fs *projectReferenceDtsFakingVfs) Chtimes(path string, aTime time.Time, mT // DirectoryExists implements vfs.FS. func (fs *projectReferenceDtsFakingVfs) DirectoryExists(path string) bool { - if fs.projectReferenceFileMapper.opts.Host.FS().DirectoryExists(path) { + if fs.host.FS().DirectoryExists(path) { fs.handleDirectoryCouldBeSymlink(path) return true } @@ -119,11 +117,11 @@ func (fs *projectReferenceDtsFakingVfs) Realpath(path string) string { if ok { return result } - return fs.projectReferenceFileMapper.opts.Host.FS().Realpath(path) + return fs.host.FS().Realpath(path) } func (fs *projectReferenceDtsFakingVfs) toPath(path string) tspath.Path { - return tspath.ToPath(path, fs.projectReferenceFileMapper.opts.Host.GetCurrentDirectory(), fs.UseCaseSensitiveFileNames()) + return tspath.ToPath(path, fs.host.GetCurrentDirectory(), fs.UseCaseSensitiveFileNames()) } func (fs *projectReferenceDtsFakingVfs) handleDirectoryCouldBeSymlink(directory string) { @@ -194,7 +192,7 @@ func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrD if exists = fileOrDirectoryExistsUsingSource(string(knownDirectoryLink.RealPath) + relative).IsTrue(); exists { if isFile { // Store the real path for the file - absolutePath := tspath.GetNormalizedAbsolutePath(fileOrDirectory, fs.projectReferenceFileMapper.opts.Host.GetCurrentDirectory()) + absolutePath := tspath.GetNormalizedAbsolutePath(fileOrDirectory, fs.host.GetCurrentDirectory()) fs.knownSymlinks.SetFile( absolutePath, fileOrDirectoryPath, @@ -211,14 +209,14 @@ func (fs *projectReferenceDtsFakingVfs) fileOrDirectoryExistsUsingSource(fileOrD func (fs *projectReferenceDtsFakingVfs) fileExistsIfProjectReferenceDts(file string) core.Tristate { source := fs.projectReferenceFileMapper.getProjectReferenceFromOutputDts(fs.toPath(file)) if source != nil { - return core.IfElse(fs.projectReferenceFileMapper.opts.Host.FS().FileExists(source.Source), core.TSTrue, core.TSFalse) + return core.IfElse(fs.host.FS().FileExists(source.Source), core.TSTrue, core.TSFalse) } return core.TSUnknown } func (fs *projectReferenceDtsFakingVfs) directoryExistsIfProjectReferenceDeclDir(dir string) core.Tristate { dirPath := fs.toPath(dir) - for declDirPath := range fs.dtsDirectories.Keys() { + for declDirPath := range fs.projectReferenceFileMapper.dtsDirectories.Keys() { if dirPath.ContainsPath(declDirPath) || declDirPath.ContainsPath(dirPath) { return core.TSTrue } diff --git a/tsc/internal/compiler/projectreferencefilemapper.go b/tsc/internal/compiler/projectreferencefilemapper.go index 7f2ef37141c91..3f01c109a5202 100644 --- a/tsc/internal/compiler/projectreferencefilemapper.go +++ b/tsc/internal/compiler/projectreferencefilemapper.go @@ -12,9 +12,9 @@ import ( ) type projectReferenceFileMapper struct { - opts ProgramOptions - host module.ResolutionHost - loader *fileLoader // Only present during populating the mapper and parsing, released after that + config *tsoptions.ParsedCommandLine + useSourceOfProjectReference bool + dtsDirectories collections.Set[tspath.Path] configToProjectReference map[tspath.Path]*tsoptions.ParsedCommandLine // All the resolved references needed referencesInConfigFile map[tspath.Path][]tspath.Path // Map of config file to its references @@ -25,19 +25,31 @@ type projectReferenceFileMapper struct { realpathDtsToSource collections.SyncMap[tspath.Path, *tsoptions.SourceOutputAndProjectReference] } +type projectReferenceFileMapperBuilder struct { + *projectReferenceFileMapper + host module.ResolutionHost +} + +func (mapper *projectReferenceFileMapper) resolutionHost(host module.ResolutionHost) module.ResolutionHost { + if mapper.useSourceOfProjectReference && len(mapper.outputDtsToProjectReference) != 0 { + return newProjectReferenceDtsFakingHost(host, mapper) + } + return host +} + func (mapper *projectReferenceFileMapper) rootConfigPath() tspath.Path { - if mapper.opts.Config.ConfigFile == nil { + if mapper.config.ConfigFile == nil { return "" } - return mapper.opts.Config.ConfigFile.SourceFile.Path() + return mapper.config.ConfigFile.SourceFile.Path() } func (mapper *projectReferenceFileMapper) getParseFileRedirect(file ast.HasFileName) string { - if mapper.opts.canUseProjectReferenceSource() { + if mapper.useSourceOfProjectReference { // Map to source file from project reference source := mapper.getProjectReferenceFromOutputDts(file.Path()) if source == nil { - source = mapper.getSourceToDtsIfSymlink(file) + source, _ = mapper.realpathDtsToSource.Load(file.Path()) } if source != nil { return source.Source @@ -74,12 +86,12 @@ func (mapper *projectReferenceFileMapper) getProjectReferenceFromOutputDts(path } func (mapper *projectReferenceFileMapper) isSourceFromProjectReference(path tspath.Path) bool { - return mapper.opts.canUseProjectReferenceSource() && mapper.getProjectReferenceFromSource(path) != nil + return mapper.useSourceOfProjectReference && mapper.getProjectReferenceFromSource(path) != nil } func (mapper *projectReferenceFileMapper) getCompilerOptionsForFile(file ast.HasFileName) *core.CompilerOptions { redirect := mapper.getRedirectParsedCommandLineForResolution(file) - return module.GetCompilerOptionsWithRedirect(mapper.opts.Config.CompilerOptions(), redirect) + return module.GetCompilerOptionsWithRedirect(mapper.config.CompilerOptions(), redirect) } func (mapper *projectReferenceFileMapper) getRedirectParsedCommandLineForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine { @@ -101,7 +113,7 @@ func (mapper *projectReferenceFileMapper) getRedirectForResolution(file ast.HasF return resultFromDts.Resolved, resultFromDts.Source } - realpathDtsToSource := mapper.getSourceToDtsIfSymlink(file) + realpathDtsToSource, _ := mapper.realpathDtsToSource.Load(path) if realpathDtsToSource != nil { return realpathDtsToSource.Resolved, realpathDtsToSource.Source } @@ -116,14 +128,14 @@ func (mapper *projectReferenceFileMapper) getResolvedReferenceFor(path tspath.Pa func (mapper *projectReferenceFileMapper) rangeResolvedProjectReference( f func(path tspath.Path, config *tsoptions.ParsedCommandLine, parent *tsoptions.ParsedCommandLine, index int) bool, ) bool { - if len(mapper.opts.Config.ProjectReferences()) == 0 { + if len(mapper.config.ProjectReferences()) == 0 { return false } seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile)) rootConfigPath := mapper.rootConfigPath() seenRef.Add(rootConfigPath) refs := mapper.referencesInConfigFile[rootConfigPath] - return mapper.rangeResolvedReferenceWorker(refs, f, mapper.opts.Config, seenRef) + return mapper.rangeResolvedReferenceWorker(refs, f, mapper.config, seenRef) } func (mapper *projectReferenceFileMapper) rangeResolvedReferenceWorker( @@ -157,36 +169,54 @@ func (mapper *projectReferenceFileMapper) rangeResolvedProjectReferenceInChildCo seenRef := collections.NewSetWithSizeHint[tspath.Path](len(mapper.referencesInConfigFile)) seenRef.Add(childConfig.ConfigFile.SourceFile.Path()) refs := mapper.referencesInConfigFile[childConfig.ConfigFile.SourceFile.Path()] - return mapper.rangeResolvedReferenceWorker(refs, f, mapper.opts.Config, seenRef) + return mapper.rangeResolvedReferenceWorker(refs, f, mapper.config, seenRef) +} + +func (builder *projectReferenceFileMapperBuilder) getParseFileRedirect(file ast.HasFileName) string { + if builder.useSourceOfProjectReference && builder.getProjectReferenceFromOutputDts(file.Path()) == nil { + builder.resolveSymlink(file) + } + return builder.projectReferenceFileMapper.getParseFileRedirect(file) +} + +func (builder *projectReferenceFileMapperBuilder) getRedirectForResolution(file ast.HasFileName) (*tsoptions.ParsedCommandLine, string) { + if builder.getProjectReferenceFromSource(file.Path()) == nil && builder.getProjectReferenceFromOutputDts(file.Path()) == nil { + builder.resolveSymlink(file) + } + return builder.projectReferenceFileMapper.getRedirectForResolution(file) +} + +func (builder *projectReferenceFileMapperBuilder) getCompilerOptionsForFile(file ast.HasFileName) *core.CompilerOptions { + redirect, _ := builder.getRedirectForResolution(file) + return module.GetCompilerOptionsWithRedirect(builder.config.CompilerOptions(), redirect) +} + +func (builder *projectReferenceFileMapperBuilder) getRedirectParsedCommandLineForResolution(file ast.HasFileName) *tsoptions.ParsedCommandLine { + redirect, _ := builder.getRedirectForResolution(file) + return redirect } -func (mapper *projectReferenceFileMapper) getSourceToDtsIfSymlink(file ast.HasFileName) *tsoptions.SourceOutputAndProjectReference { +func (builder *projectReferenceFileMapperBuilder) resolveSymlink(file ast.HasFileName) { // If preserveSymlinks is true, module resolution wont jump the symlink // but the resolved real path may be the .d.ts from project reference // Note:: Currently we try the real path only if the // file is from node_modules to avoid having to run real path on all file paths path := file.Path() - realpathDtsToSource, ok := mapper.realpathDtsToSource.Load(path) + _, ok := builder.realpathDtsToSource.Load(path) if ok { - return realpathDtsToSource + return } - if mapper.loader != nil && mapper.opts.Config.CompilerOptions().PreserveSymlinks == core.TSTrue { + if len(builder.config.ResolvedProjectReferencePaths()) != 0 && builder.config.CompilerOptions().PreserveSymlinks == core.TSTrue { fileName := file.FileName() if !strings.Contains(fileName, "/node_modules/") { - mapper.realpathDtsToSource.Store(path, nil) + builder.realpathDtsToSource.Store(path, nil) } else { - realDeclarationPath := mapper.loader.toPath(mapper.host.FS().Realpath(fileName)) + realDeclarationPath := tspath.ToPath(builder.host.FS().Realpath(fileName), builder.host.GetCurrentDirectory(), builder.host.FS().UseCaseSensitiveFileNames()) if realDeclarationPath == path { - mapper.realpathDtsToSource.Store(path, nil) + builder.realpathDtsToSource.Store(path, nil) } else { - realpathDtsToSource := mapper.getProjectReferenceFromOutputDts(realDeclarationPath) - if realpathDtsToSource != nil { - mapper.realpathDtsToSource.Store(path, realpathDtsToSource) - return realpathDtsToSource - } - mapper.realpathDtsToSource.Store(path, nil) + builder.realpathDtsToSource.Store(path, builder.getProjectReferenceFromOutputDts(realDeclarationPath)) } } } - return nil } diff --git a/tsc/internal/compiler/projectreferenceparser.go b/tsc/internal/compiler/projectreferenceparser.go index 9bf23abed9d7b..f94cc566d6fc4 100644 --- a/tsc/internal/compiler/projectreferenceparser.go +++ b/tsc/internal/compiler/projectreferenceparser.go @@ -18,10 +18,10 @@ type projectReferenceParseTask struct { func (t *projectReferenceParseTask) parse(projectReferenceParser *projectReferenceParser) { loader := projectReferenceParser.loader - if tr := loader.opts.Tracing; tr != nil { + if tr := loader.tracing; tr != nil { defer tr.Push(tracing.PhaseParse, "parseJsonSourceFileConfigFileContent", map[string]any{"path": t.configName}, false)() } - t.resolved = loader.opts.Host.GetResolvedProjectReference(t.configName, loader.toPath(t.configName)) + t.resolved = loader.host.GetResolvedProjectReference(t.configName, loader.toPath(t.configName)) if t.resolved == nil { return } @@ -46,7 +46,6 @@ type projectReferenceParser struct { } func (p *projectReferenceParser) parse(tasks []*projectReferenceParseTask) { - p.loader.projectReferenceFileMapper.loader = p.loader p.start(tasks) p.wg.RunAndWait() p.initMapper(tasks) @@ -69,14 +68,12 @@ func (p *projectReferenceParser) start(tasks []*projectReferenceParseTask) { func (p *projectReferenceParser) initMapper(tasks []*projectReferenceParseTask) { totalReferences := p.tasksByFileName.Size() + 1 - p.loader.projectReferenceFileMapper.configToProjectReference = make(map[tspath.Path]*tsoptions.ParsedCommandLine, totalReferences) - p.loader.projectReferenceFileMapper.referencesInConfigFile = make(map[tspath.Path][]tspath.Path, totalReferences) - p.loader.projectReferenceFileMapper.sourceToProjectReference = make(map[tspath.Path]*tsoptions.SourceOutputAndProjectReference) - p.loader.projectReferenceFileMapper.outputDtsToProjectReference = make(map[tspath.Path]*tsoptions.SourceOutputAndProjectReference) - p.loader.projectReferenceFileMapper.referencesInConfigFile[p.loader.projectReferenceFileMapper.rootConfigPath()] = p.initMapperWorker(tasks, &collections.Set[*projectReferenceParseTask]{}) - if p.loader.projectReferenceFileMapper.opts.canUseProjectReferenceSource() && len(p.loader.projectReferenceFileMapper.outputDtsToProjectReference) != 0 { - p.loader.projectReferenceFileMapper.host = newProjectReferenceDtsFakingHost(p.loader) - } + p.loader.projectReferences.configToProjectReference = make(map[tspath.Path]*tsoptions.ParsedCommandLine, totalReferences) + p.loader.projectReferences.referencesInConfigFile = make(map[tspath.Path][]tspath.Path, totalReferences) + p.loader.projectReferences.sourceToProjectReference = make(map[tspath.Path]*tsoptions.SourceOutputAndProjectReference) + p.loader.projectReferences.outputDtsToProjectReference = make(map[tspath.Path]*tsoptions.SourceOutputAndProjectReference) + p.loader.projectReferences.referencesInConfigFile[p.loader.projectReferences.rootConfigPath()] = p.initMapperWorker(tasks, &collections.Set[*projectReferenceParseTask]{}) + p.loader.projectReferences.host = p.loader.projectReferences.resolutionHost(p.loader.host) } func (p *projectReferenceParser) initMapperWorker(tasks []*projectReferenceParseTask, seen *collections.Set[*projectReferenceParseTask]) []tspath.Path { @@ -91,25 +88,25 @@ func (p *projectReferenceParser) initMapperWorker(tasks []*projectReferenceParse if !seen.AddIfAbsent(task) { continue } - p.loader.projectReferenceFileMapper.configToProjectReference[path] = task.resolved - if task.resolved != nil && p.loader.projectReferenceFileMapper.opts.Config.ConfigFile != task.resolved.ConfigFile { + p.loader.projectReferences.configToProjectReference[path] = task.resolved + if task.resolved != nil && p.loader.projectReferences.config.ConfigFile != task.resolved.ConfigFile { // Map current task's files first, before recursing into subtasks. // This matches TypeScript's behavior where child project references // overwrite parent entries when a file belongs to multiple projects. - maps.Copy(p.loader.projectReferenceFileMapper.sourceToProjectReference, task.resolved.SourceToProjectReference()) - maps.Copy(p.loader.projectReferenceFileMapper.outputDtsToProjectReference, task.resolved.OutputDtsToProjectReference()) - if p.loader.projectReferenceFileMapper.opts.canUseProjectReferenceSource() { + maps.Copy(p.loader.projectReferences.sourceToProjectReference, task.resolved.SourceToProjectReference()) + maps.Copy(p.loader.projectReferences.outputDtsToProjectReference, task.resolved.OutputDtsToProjectReference()) + if p.loader.projectReferences.useSourceOfProjectReference { declDir := task.resolved.CompilerOptions().DeclarationDir if declDir == "" { declDir = task.resolved.CompilerOptions().OutDir } if declDir != "" { - p.loader.dtsDirectories.Add(p.loader.toPath(declDir)) + p.loader.projectReferences.dtsDirectories.Add(p.loader.toPath(declDir)) } } } referencesInConfig := p.initMapperWorker(task.subTasks, seen) - p.loader.projectReferenceFileMapper.referencesInConfigFile[path] = referencesInConfig + p.loader.projectReferences.referencesInConfigFile[path] = referencesInConfig } return results } diff --git a/tsc/internal/module/cache.go b/tsc/internal/module/cache.go index 5b125abc0ada0..0b179431bc686 100644 --- a/tsc/internal/module/cache.go +++ b/tsc/internal/module/cache.go @@ -4,6 +4,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/packagejson" + "github.com/microsoft/TypeScript/tsc/internal/tspath" ) type ModeAwareCache[T any] map[ModeAwareCacheKey]T @@ -59,28 +60,44 @@ func (c *parsedPatternsCache) Get(pathMappings *collections.OrderedMap[string, [ return patterns } -type caches struct { - packageJsonInfoCache *packagejson.InfoCache +type ResolutionData struct { + compilerOptions *core.CompilerOptions + typingsLocation string + projectName string + extraExtensions []string - moduleResolutionCache moduleResolutionCache - typeRefDirectiveResolutionCache typeRefDirectiveResolutionCache + packageJsonInfoCache *packagejson.InfoCache +} - // Cached representations for `core.CompilerOptions.paths`, keyed by the - // path mappings themselves. This does not handle other path patterns such - // as `typesVersions`. - parsedPatternsForPaths parsedPatternsCache +func newResolutionData(opts ResolverOptions) *ResolutionData { + data := &ResolutionData{ + compilerOptions: opts.CompilerOptions, + typingsLocation: opts.TypingsLocation, + projectName: opts.ProjectName, + extraExtensions: opts.ExtraExtensions, + packageJsonInfoCache: opts.PackageJsonCache, + } + if data.packageJsonInfoCache == nil { + data.packageJsonInfoCache = packagejson.NewInfoCache(opts.Host.GetCurrentDirectory(), opts.Host.FS().UseCaseSensitiveFileNames()) + } + return data } -func newCaches( - currentDirectory string, - useCaseSensitiveFileNames bool, - options *core.CompilerOptions, -) caches { - return caches{ - packageJsonInfoCache: packagejson.NewInfoCache(currentDirectory, useCaseSensitiveFileNames), +// Clone copies the package-json cache table without copying its entries. +func (c *ResolutionData) Clone() *ResolutionData { + return &ResolutionData{ + compilerOptions: c.compilerOptions, + typingsLocation: c.typingsLocation, + projectName: c.projectName, + extraExtensions: c.extraExtensions, + packageJsonInfoCache: c.packageJsonInfoCache.Clone(), } } +func (c *ResolutionData) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { + c.packageJsonInfoCache.Range(f) +} + func getRedirectConfigName(redirect ResolvedProjectReference) string { if redirect == nil { return "" diff --git a/tsc/internal/module/resolver.go b/tsc/internal/module/resolver.go index 367884c90698a..df358896fd5bf 100644 --- a/tsc/internal/module/resolver.go +++ b/tsc/internal/module/resolver.go @@ -146,13 +146,17 @@ func GetCompilerOptionsWithRedirect(compilerOptions *core.CompilerOptions, redir } type DefaultResolver struct { - caches - host ResolutionHost - compilerOptions *core.CompilerOptions - typingsLocation string - projectName string - extraExtensions []string + *ResolutionData + host ResolutionHost // reportDiagnostic: DiagnosticReporter + + moduleResolutionCache moduleResolutionCache + typeRefDirectiveResolutionCache typeRefDirectiveResolutionCache + + // Cached representations for `core.CompilerOptions.paths`, keyed by the + // path mappings themselves. This does not handle other path patterns such + // as `typesVersions`. + parsedPatternsForPaths parsedPatternsCache } type ResolverOptions struct { @@ -165,19 +169,18 @@ type ResolverOptions struct { } func NewResolver(opts ResolverOptions) *DefaultResolver { - r := &DefaultResolver{ - host: opts.Host, - compilerOptions: opts.CompilerOptions, - typingsLocation: opts.TypingsLocation, - projectName: opts.ProjectName, - extraExtensions: opts.ExtraExtensions, - } - if opts.PackageJsonCache != nil { - r.packageJsonInfoCache = opts.PackageJsonCache - } else { - r.caches = newCaches(opts.Host.GetCurrentDirectory(), opts.Host.FS().UseCaseSensitiveFileNames(), opts.CompilerOptions) + return newResolutionData(opts).NewResolver(opts.Host) +} + +func (r *DefaultResolver) GetResolutionData() *ResolutionData { + return r.ResolutionData +} + +func (d *ResolutionData) NewResolver(host ResolutionHost) *DefaultResolver { + return &DefaultResolver{ + ResolutionData: d, + host: host, } - return r } func (r *DefaultResolver) newTraceBuilder() *tracer { @@ -191,10 +194,6 @@ func (r *DefaultResolver) GetPackageScopeForPath(directory string) *packagejson. return (&resolutionState{compilerOptions: r.compilerOptions, resolver: r}).getPackageScopeForPath(directory) } -func (r *DefaultResolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { - r.caches.packageJsonInfoCache.Range(f) -} - func (r *tracer) traceResolutionUsingProjectReference(redirectedReference ResolvedProjectReference) { if redirectedReference != nil && redirectedReference.CompilerOptions() != nil { r.write(diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.ConfigName()) diff --git a/tsc/internal/module/resolver_test.go b/tsc/internal/module/resolver_test.go index 9de8cf9c8c223..b616515151219 100644 --- a/tsc/internal/module/resolver_test.go +++ b/tsc/internal/module/resolver_test.go @@ -1,15 +1,20 @@ -package module_test +package module import ( + "reflect" "strings" "sync" "sync/atomic" "testing" + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/core" - "github.com/microsoft/TypeScript/tsc/internal/module" + "github.com/microsoft/TypeScript/tsc/internal/packagejson" + "github.com/microsoft/TypeScript/tsc/internal/testutil" "github.com/microsoft/TypeScript/tsc/internal/vfs" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" + "gotest.tools/v3/assert" ) type resolutionHostStub struct { @@ -20,6 +25,14 @@ type resolutionHostStub struct { func (h *resolutionHostStub) FS() vfs.FS { return h.fs } func (h *resolutionHostStub) GetCurrentDirectory() string { return h.cwd } +func TestResolverSharedData(t *testing.T) { + t.Parallel() + assert.NilError(t, testutil.CheckDataOnly(reflect.TypeFor[ResolutionData](), []reflect.Type{ + reflect.TypeFor[ast.Diagnostic](), + reflect.TypeFor[packagejson.PackageJson](), + })) +} + // Regression test for https://github.com/microsoft/TypeScript/tsc/issues/3526. // // Resolving a node_modules import with a trailing slash (e.g. `pkg/`) must @@ -39,7 +52,7 @@ func TestResolveModuleNameTrailingSlash(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(module.ResolverOptions{Host: host, CompilerOptions: opts}) + resolver := NewResolver(ResolverOptions{Host: host, CompilerOptions: opts}) for _, name := range []string{"pkg", "pkg/"} { r, _, _ := resolver.ResolveModuleName(name, "/repo/src/file.ts", core.ModuleKindESNext, nil) @@ -49,6 +62,51 @@ func TestResolveModuleNameTrailingSlash(t *testing.T) { } } +func TestResolutionDataCaches(t *testing.T) { + t.Parallel() + oldHost := &resolutionHostStub{cwd: "/", fs: vfstest.FromMap(map[string]string{ + "/src/node_modules/pkg/package.json": `{"name":"pkg","types":"index.d.ts"}`, + "/src/node_modules/pkg/index.d.ts": "export const value: number;", + }, true)} + newHost := &resolutionHostStub{cwd: "/", fs: vfstest.FromMap(map[string]string{ + "/src/node_modules/pkg/package.json": `{"name":"pkg","types":"index.d.ts"}`, + "/src/node_modules/pkg/index.d.ts": "export const value: number;", + "/new/package.json": `{"name":"new"}`, + "/missing-first/package.json": `{"name":"missing-first"}`, + }, true)} + resolver := NewResolver(ResolverOptions{Host: oldHost, CompilerOptions: &core.CompilerOptions{Module: core.ModuleKindNodeNext}}) + resolved, _, err := resolver.ResolveModuleName("pkg", "/src/index.ts", core.ModuleKindCommonJS, nil) + assert.NilError(t, err) + assert.Assert(t, resolved.IsResolved()) + cached, _, err := resolver.ResolveModuleName("pkg", "/src/index.ts", core.ModuleKindCommonJS, nil) + assert.NilError(t, err) + assert.Equal(t, cached, resolved) + + resolver.ResolveTypeReferenceDirective("missing", "/src/index.ts", core.ModuleKindCommonJS, nil) + paths := collections.NewOrderedMapWithSizeHint[string, []string](1) + paths.Set("alias/*", []string{"./*"}) + resolver.getParsedPatternsForPaths(&core.CompilerOptions{Paths: paths}) + assert.Equal(t, resolver.moduleResolutionCache.cache.Size(), 1) + assert.Equal(t, resolver.typeRefDirectiveResolutionCache.cache.Size(), 1) + assert.Equal(t, resolver.parsedPatternsForPaths.cache.Size(), 1) + + data := resolver.GetResolutionData() + rebound := data.NewResolver(oldHost) + assert.Equal(t, rebound.moduleResolutionCache.cache.Size(), 0) + assert.Equal(t, rebound.typeRefDirectiveResolutionCache.cache.Size(), 0) + assert.Equal(t, rebound.parsedPatternsForPaths.cache.Size(), 0) + + clone := data.Clone().NewResolver(newHost) + assert.Equal(t, clone.moduleResolutionCache.cache.Size(), 0) + assert.Equal(t, clone.typeRefDirectiveResolutionCache.cache.Size(), 0) + assert.Equal(t, clone.parsedPatternsForPaths.cache.Size(), 0) + assert.Equal(t, clone.GetPackageScopeForPath("/src/node_modules/pkg"), resolver.GetPackageScopeForPath("/src/node_modules/pkg")) + assert.Assert(t, clone.GetPackageScopeForPath("/new").Exists()) + assert.Assert(t, !resolver.GetPackageScopeForPath("/new").Exists()) + assert.Assert(t, !resolver.GetPackageScopeForPath("/missing-first").Exists()) + assert.Assert(t, clone.GetPackageScopeForPath("/missing-first").Exists()) +} + // blockingFS wraps a vfs.FS and forces FileExists calls for `targetPath` to // block on `gate` until released. Each caller sends on `arrived` when it // reaches the gate. This is used to deterministically reproduce the @@ -168,7 +226,7 @@ func TestResolveModuleNameTrailingSlashRace(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(module.ResolverOptions{Host: host, CompilerOptions: opts}) + resolver := NewResolver(ResolverOptions{Host: host, CompilerOptions: opts}) type resolutionResult struct { name string @@ -240,7 +298,7 @@ func TestResolveSubpathNilContentsRace(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(module.ResolverOptions{Host: host, CompilerOptions: opts}) + resolver := NewResolver(ResolverOptions{Host: host, CompilerOptions: opts}) var panicked atomic.Bool type resolutionResult struct { @@ -322,7 +380,7 @@ func TestParseNodeModuleFromPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - if got := module.ParseNodeModuleFromPath(tt.path, tt.isFolder); got != tt.want { + if got := ParseNodeModuleFromPath(tt.path, tt.isFolder); got != tt.want { t.Errorf("ParseNodeModuleFromPath(%q, %v) = %q, want %q", tt.path, tt.isFolder, got, tt.want) } }) @@ -363,7 +421,7 @@ func TestResolvePeerDependencyNilContentsRace(t *testing.T) { Module: core.ModuleKindESNext, Target: core.ScriptTargetESNext, } - resolver := module.NewResolver(module.ResolverOptions{Host: host, CompilerOptions: opts}) + resolver := NewResolver(ResolverOptions{Host: host, CompilerOptions: opts}) var panicked atomic.Bool type resolutionResult struct { diff --git a/tsc/internal/module/staticresolver.go b/tsc/internal/module/staticresolver.go index 34a55ba2c9f00..15c530fe636e0 100644 --- a/tsc/internal/module/staticresolver.go +++ b/tsc/internal/module/staticresolver.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/microsoft/TypeScript/tsc/internal/core" - "github.com/microsoft/TypeScript/tsc/internal/packagejson" "github.com/microsoft/TypeScript/tsc/internal/tspath" ) @@ -137,19 +136,6 @@ func (r *StaticResolver) ResolveTypeReferenceDirective( return r.fallback.ResolveTypeReferenceDirective(typeReferenceDirectiveName, containingFile, resolutionMode, redirectedReference) } -func (r *StaticResolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry { - return r.fallback.GetPackageScopeForPath(directory) -} - -func (r *StaticResolver) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) { - r.fallback.PackageJsonCacheEntries(f) -} - -func (r *StaticResolver) ResolvePackageDirectory( - moduleName string, - containingFile string, - resolutionMode core.ResolutionMode, - redirectedReference ResolvedProjectReference, -) *ResolvedModule { - return r.fallback.ResolvePackageDirectory(moduleName, containingFile, resolutionMode, redirectedReference) +func (r *StaticResolver) GetResolutionData() *ResolutionData { + return r.fallback.GetResolutionData() } diff --git a/tsc/internal/module/staticresolver_test.go b/tsc/internal/module/staticresolver_test.go index a93c711158389..93d3a60e359d9 100644 --- a/tsc/internal/module/staticresolver_test.go +++ b/tsc/internal/module/staticresolver_test.go @@ -1,10 +1,9 @@ -package module_test +package module import ( "testing" "github.com/microsoft/TypeScript/tsc/internal/core" - "github.com/microsoft/TypeScript/tsc/internal/module" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" "gotest.tools/v3/assert" ) @@ -16,7 +15,7 @@ func TestStaticResolver(t *testing.T) { "/repo/node_modules/fallback/package.json": `{"name":"fallback","types":"index.d.ts"}`, "/repo/node_modules/fallback/index.d.ts": "export {};", }, true) - fallback := module.NewResolver(module.ResolverOptions{ + fallback := NewResolver(ResolverOptions{ Host: &resolutionHostStub{fs: fs, cwd: "/repo"}, CompilerOptions: &core.CompilerOptions{ Module: core.ModuleKindESNext, @@ -24,12 +23,12 @@ func TestStaticResolver(t *testing.T) { }, }) esm := core.ResolutionModeESM - resolutions, err := module.NewStaticResolutions( - []module.StaticResolutionEntry{ - {ModuleName: "provided", Result: &module.ResolvedModule{ResolvedFileName: "/global.d.ts"}}, - {ModuleName: "provided", ContainingDirectory: "/repo/src", Result: &module.ResolvedModule{ResolvedFileName: "/directory.d.ts"}}, - {ModuleName: "provided", ResolutionMode: &esm, Result: &module.ResolvedModule{ResolvedFileName: "/esm.d.ts"}}, - {ModuleName: "provided", ContainingDirectory: "/repo/src", ResolutionMode: &esm, Result: &module.ResolvedModule{ResolvedFileName: "/directory-esm.d.ts"}}, + resolutions, err := NewStaticResolutions( + []StaticResolutionEntry{ + {ModuleName: "provided", Result: &ResolvedModule{ResolvedFileName: "/global.d.ts"}}, + {ModuleName: "provided", ContainingDirectory: "/repo/src", Result: &ResolvedModule{ResolvedFileName: "/directory.d.ts"}}, + {ModuleName: "provided", ResolutionMode: &esm, Result: &ResolvedModule{ResolvedFileName: "/esm.d.ts"}}, + {ModuleName: "provided", ContainingDirectory: "/repo/src", ResolutionMode: &esm, Result: &ResolvedModule{ResolvedFileName: "/directory-esm.d.ts"}}, {ModuleName: "unresolved"}, }, true, @@ -37,7 +36,7 @@ func TestStaticResolver(t *testing.T) { true, ) assert.NilError(t, err) - resolver := module.NewStaticResolver(fallback, resolutions) + resolver := NewStaticResolver(fallback, resolutions) tests := []struct { name string diff --git a/tsc/internal/module/types.go b/tsc/internal/module/types.go index 00d9f0f3b786b..a84b599caf41a 100644 --- a/tsc/internal/module/types.go +++ b/tsc/internal/module/types.go @@ -7,7 +7,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/core" - "github.com/microsoft/TypeScript/tsc/internal/packagejson" "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs" ) @@ -35,14 +34,7 @@ type Resolver interface { resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference, ) (*ResolvedTypeReferenceDirective, []DiagAndArgs) - GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry - PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) - ResolvePackageDirectory( - moduleName string, - containingFile string, - resolutionMode core.ResolutionMode, - redirectedReference ResolvedProjectReference, - ) *ResolvedModule + GetResolutionData() *ResolutionData } type ModeAwareCacheKey struct { diff --git a/tsc/internal/packagejson/cache.go b/tsc/internal/packagejson/cache.go index e0c1fa3688102..15b5bc2244843 100644 --- a/tsc/internal/packagejson/cache.go +++ b/tsc/internal/packagejson/cache.go @@ -179,6 +179,15 @@ func NewInfoCache(currentDirectory string, useCaseSensitiveFileNames bool) *Info } } +func (p *InfoCache) Clone() *InfoCache { + clone := NewInfoCache(p.currentDirectory, p.useCaseSensitiveFileNames) + p.cache.Range(func(key tspath.Path, value *InfoCacheEntry) bool { + clone.cache.Store(key, value) + return true + }) + return clone +} + func (p *InfoCache) Get(packageJsonPath string) *InfoCacheEntry { key := tspath.ToPath(packageJsonPath, p.currentDirectory, p.useCaseSensitiveFileNames) if value, ok := p.cache.Load(key); ok { diff --git a/tsc/internal/project/project.go b/tsc/internal/project/project.go index 79954831210c9..8c25a98838e3c 100644 --- a/tsc/internal/project/project.go +++ b/tsc/internal/project/project.go @@ -501,10 +501,6 @@ func (p *Project) CreateProgram() CreateProgramResult { var programCloned bool var newProgram *compiler.Program - // Define a fresh CreateCheckerPool closure for this call. Each invocation of - // CreateProgram must use its own closure so that concurrent goroutines cloning - // the same project never share a captured variable through a stale closure - // stored in the old program's options. createCheckerPool := func(program *compiler.Program) compiler.CheckerPool { return newCheckerPool(p.host.sessionOptions.CheckerPoolOptions, program, p.log) } @@ -513,7 +509,7 @@ func (p *Project) CreateProgram() CreateProgramResult { if p.moduleResolverFactory == nil { return module.NewResolver(options) } - resolver, cleanup := p.moduleResolverFactory.NewResolver(options) + resolver, cleanup := p.moduleResolverFactory.NewResolver(p.host.builder.ctx, options) cleanupModuleResolver = cleanup return resolver } diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index 6040a639a2697..ab9d2645e01f3 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -329,7 +329,7 @@ type APICreateProgramRequest struct { } type ModuleResolverFactory interface { - NewResolver(options module.ResolverOptions) (module.Resolver, func()) + NewResolver(ctx context.Context, options module.ResolverOptions) (module.Resolver, func()) } type APIReconfigureProgramRequest struct { diff --git a/tsc/internal/testutil/testutil.go b/tsc/internal/testutil/testutil.go index 9aa3c7a8abc0a..7390f22f7fd0c 100644 --- a/tsc/internal/testutil/testutil.go +++ b/tsc/internal/testutil/testutil.go @@ -1,9 +1,12 @@ package testutil import ( + "fmt" "os" + "reflect" "runtime/debug" "strconv" + "strings" "sync" "testing" @@ -47,3 +50,50 @@ var testProgramIsSingleThreaded = sync.OnceValue(func() bool { func TestProgramIsSingleThreaded() bool { return testProgramIsSingleThreaded() } + +// CheckDataOnly rejects opaque references except within explicitly allowed leaf types. +func CheckDataOnly(root reflect.Type, leaves []reflect.Type) error { + seen := make(map[reflect.Type]bool) + for _, leaf := range leaves { + seen[leaf] = true + } + for _, leaf := range []reflect.Type{ + reflect.TypeFor[sync.Mutex](), + reflect.TypeFor[sync.RWMutex](), + reflect.TypeFor[sync.Once](), + } { + seen[leaf] = true + } + var visit func(reflect.Type, string) error + visit = func(typ reflect.Type, path string) error { + if seen[typ] { + return nil + } + seen[typ] = true + switch typ.Kind() { + case reflect.Func, reflect.Interface, reflect.Chan, reflect.UnsafePointer: + return fmt.Errorf("%s contains %s; shared data must not retain hosts or opaque payloads", path, typ) + case reflect.Pointer, reflect.Slice, reflect.Array: + return visit(typ.Elem(), path+"[]") + case reflect.Map: + if err := visit(typ.Key(), path+"[key]"); err != nil { + return err + } + return visit(typ.Elem(), path+"[value]") + case reflect.Struct: + for field := range typ.Fields() { + // SyncMap's zero-length arrays expose K and V for inspection. + if typ.PkgPath() == "github.com/microsoft/TypeScript/tsc/internal/collections" && + strings.HasPrefix(typ.Name(), "SyncMap[") && + field.Name == "m" && field.Type == reflect.TypeFor[sync.Map]() { + continue + } + if err := visit(field.Type, path+"."+field.Name); err != nil { + return err + } + } + } + return nil + } + return visit(root, root.String()) +}