diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 0bfbd381792e5..387dec4177966 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -807,6 +807,8 @@ namespace ts { let mapFromFileToProjectReferenceRedirects: ESMap | undefined; let mapFromToProjectReferenceRedirectSource: ESMap | undefined; + let skippedTrippleSlashReferences: Set | undefined; + const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() && !options.disableSourceOfProjectReferenceRedirect; const { onProgramCreateComplete, fileExists } = updateHostForUseSourceOfProjectReferenceRedirect({ @@ -928,6 +930,7 @@ namespace ts { getSourceFiles: () => files, getMissingFilePaths: () => missingFilePaths!, // TODO: GH#18217 getRefFileMap: () => refFileMap, + getSkippedTrippleSlashReferences: () => skippedTrippleSlashReferences, getFilesByNameMap: () => filesByName, getCompilerOptions: () => options, getSyntacticDiagnostics, @@ -1269,6 +1272,7 @@ namespace ts { const oldSourceFiles = oldProgram.getSourceFiles(); const enum SeenPackageName { Exists, Modified } const seenPackageNames = new Map(); + const oldSkippedTrippleSlashReferences = oldProgram.getSkippedTrippleSlashReferences(); for (const oldSourceFile of oldSourceFiles) { let newSourceFile = host.getSourceFileByPath @@ -1341,6 +1345,11 @@ namespace ts { oldProgram.structureIsReused = StructureIsReused.SafeModules; } + if (oldSkippedTrippleSlashReferences?.has(oldSourceFile.path) && includeTripleslashReferencesFrom(newSourceFile)) { + // tripleslash reference resolution is now allowed + oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + // check imports and module augmentations collectExternalModuleReferences(newSourceFile); if (!arrayIsEqualTo(oldSourceFile.imports, newSourceFile.imports, moduleNameIsEqualTo)) { @@ -1428,6 +1437,7 @@ namespace ts { missingFilePaths = oldProgram.getMissingFilePaths(); refFileMap = oldProgram.getRefFileMap(); + skippedTrippleSlashReferences = oldSkippedTrippleSlashReferences; // update fileName -> file mapping Debug.assert(newSourceFiles.length === oldProgram.getSourceFiles().length); @@ -2647,7 +2657,15 @@ namespace ts { return projectReferenceRedirects.get(projectReferencePath) || undefined; } + function includeTripleslashReferencesFrom(file: SourceFile) { + return !host.includeTripleslashReferencesFrom || host.includeTripleslashReferencesFrom(file.originalFileName); + } + function processReferencedFiles(file: SourceFile, isDefaultLib: boolean) { + if (!includeTripleslashReferencesFrom(file)) { + (skippedTrippleSlashReferences ||= new Set()).add(file.path); + return; + } forEach(file.referencedFiles, (ref, index) => { const referencedFileName = resolveTripleslashReference(ref.fileName, file.originalFileName); processSourceFile( diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 9e82ab0e1379f..403675c24f901 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -11,6 +11,7 @@ namespace ts { invalidateResolutionsOfFailedLookupLocations(): boolean; invalidateResolutionOfFile(filePath: Path): void; + removeRelativeNoResolveResolutionsOfFile(filePath: Path): boolean; removeResolutionsOfFile(filePath: Path): void; removeResolutionsFromProjectReferenceRedirects(filePath: Path): void; setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: ESMap): void; @@ -141,7 +142,21 @@ namespace ts { type GetResolutionWithResolvedFileName = (resolution: T) => R | undefined; - export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, logChangesWhenResolvingModule: boolean): ResolutionCache { + export enum ResolutionKind { + All, + RelativeReferencesInOpenFileOnly + } + + const noResolveResolvedModule: ResolvedModuleWithFailedLookupLocations = { + resolvedModule: undefined, + failedLookupLocations: [] + }; + const noResolveResolvedTypeReferenceDirective: ResolvedTypeReferenceDirectiveWithFailedLookupLocations = { + resolvedTypeReferenceDirective: undefined, + failedLookupLocations: [] + }; + + export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, resolutionKind: ResolutionKind, logChangesWhenResolvingModule: boolean): ResolutionCache { let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; let filesWithInvalidatedResolutions: Set | undefined; let filesWithInvalidatedNonRelativeUnresolvedImports: ReadonlyESMap | undefined; @@ -206,6 +221,7 @@ namespace ts { hasChangedAutomaticTypeDirectiveNames: () => hasChangedAutomaticTypeDirectiveNames, invalidateResolutionOfFile, invalidateResolutionsOfFailedLookupLocations, + removeRelativeNoResolveResolutionsOfFile, setFilesWithInvalidatedNonRelativeUnresolvedImports, createHasInvalidatedResolution, updateTypeRootsWatch, @@ -341,11 +357,12 @@ namespace ts { shouldRetryResolution: (t: T) => boolean; reusedNames?: readonly string[]; logChanges?: boolean; + noResolveResolution: T; } function resolveNamesWithLocalCache({ names, containingFile, redirectedReference, cache, perDirectoryCacheWithRedirects, - loader, getResolutionWithResolvedFileName, + loader, getResolutionWithResolvedFileName, noResolveResolution, shouldRetryResolution, reusedNames, logChanges }: ResolveNamesWithLocalCacheInput): (R | undefined)[] { const path = resolutionHost.toPath(containingFile); @@ -382,7 +399,10 @@ namespace ts { resolution = resolutionInDirectory; } else { - resolution = loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference); + resolution = resolutionKind === ResolutionKind.All || + (isExternalModuleNameRelative(name) && resolutionHost.fileIsOpen(path)) ? + loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference) : + noResolveResolution; perDirectoryResolution.set(name, resolution); } resolutionsInFile.set(name, resolution); @@ -441,6 +461,7 @@ namespace ts { loader: resolveTypeReferenceDirective, getResolutionWithResolvedFileName: getResolvedTypeReferenceDirective, shouldRetryResolution: resolution => resolution.resolvedTypeReferenceDirective === undefined, + noResolveResolution: noResolveResolvedTypeReferenceDirective, }); } @@ -455,7 +476,8 @@ namespace ts { getResolutionWithResolvedFileName: getResolvedModule, shouldRetryResolution: resolution => !resolution.resolvedModule || !resolutionExtensionIsTSOrJson(resolution.resolvedModule.extension), reusedNames, - logChanges: logChangesWhenResolvingModule + logChanges: logChangesWhenResolvingModule, + noResolveResolution: noResolveResolvedModule, }); } @@ -741,6 +763,31 @@ namespace ts { } } + function removeRelativeNoResolveResolutionsOfFileFromCache( + cache: ESMap>, + filePath: Path, + noResolveResolution: T, + ) { + Debug.assert(resolutionKind === ResolutionKind.RelativeReferencesInOpenFileOnly); + // Deleted file, stop watching failed lookups for all the resolutions in the file + const resolutions = cache.get(filePath); + if (!resolutions) return false; + let invalidated = false; + resolutions.forEach((resolution, name) => { + if (resolution === noResolveResolution && isExternalModuleNameRelative(name)) { + resolutions.delete(name); + invalidated = true; + } + }); + return invalidated; + } + + function removeRelativeNoResolveResolutionsOfFile(filePath: Path) { + let invalidated = removeRelativeNoResolveResolutionsOfFileFromCache(resolvedModuleNames, filePath, noResolveResolvedModule); + invalidated = removeRelativeNoResolveResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath, noResolveResolvedTypeReferenceDirective) || invalidated; + return invalidated; + } + function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ReadonlyESMap) { Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined); filesWithInvalidatedNonRelativeUnresolvedImports = filesMap; diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f2887624b8762..0866873ea9dfb 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3687,6 +3687,8 @@ namespace ts { /* @internal */ getRefFileMap(): MultiMap | undefined; /* @internal */ + getSkippedTrippleSlashReferences(): Set | undefined; + /* @internal */ getFilesByNameMap(): ESMap; /** @@ -6226,6 +6228,7 @@ namespace ts { * This method is a companion for 'resolveModuleNames' and is used to resolve 'types' references to actual type declaration files */ resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[]; + /* @internal */ includeTripleslashReferencesFrom?(containingFile: string): boolean; getEnvironmentVariable?(name: string): string | undefined; /* @internal */ onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions, hasSourceFileByPath: boolean): void; /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; diff --git a/src/compiler/watchPublic.ts b/src/compiler/watchPublic.ts index 2673b11681050..e5f7cb60eddf1 100644 --- a/src/compiler/watchPublic.ts +++ b/src/compiler/watchPublic.ts @@ -320,6 +320,7 @@ namespace ts { configFileName ? getDirectoryPath(getNormalizedAbsolutePath(configFileName, currentDirectory)) : currentDirectory, + ResolutionKind.All, /*logChangesWhenResolvingModule*/ false ); // Resolve module using host module resolution strategy if provided otherwise use resolution cache to resolve module names diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0c0dd41f351ab..a97d049f45bd8 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -2967,7 +2967,15 @@ namespace ts.server { let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info); let defaultConfigProject: ConfiguredProject | undefined; let retainProjects: ConfiguredProject[] | ConfiguredProject | undefined; - if (!project && !this.syntaxOnly) { // Checking syntaxOnly is an optimization + if (this.syntaxOnly) { + // Invalidate resolutions in the file since this file is now open + info.containingProjects.forEach(project => { + if (project.resolutionCache.removeRelativeNoResolveResolutionsOfFile(info.path)) { + project.markAsDirty(); + } + }); + } + else if (!project) { // Checking syntaxOnly is an optimization configFileName = this.getConfigFileNameForFile(info); if (configFileName) { project = this.findConfiguredProjectByProjectName(configFileName); @@ -3047,6 +3055,10 @@ namespace ts.server { Debug.assert(this.openFiles.has(info.path)); this.assignOrphanScriptInfoToInferredProject(info, this.openFiles.get(info.path)); } + else if (this.syntaxOnly && info.cacheSourceFile?.sourceFile.referencedFiles.length) { + // This file was just opened and references in this file will previously not been resolved so schedule update + info.containingProjects.forEach(project => project.markAsDirty()); + } Debug.assert(!info.isOrphan()); return { configFileName, configFileErrors, retainProjects }; } diff --git a/src/server/project.ts b/src/server/project.ts index 7742a416d9dce..413616f5667fa 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -281,7 +281,6 @@ namespace ts.server { this.languageServiceEnabled = true; if (projectService.syntaxOnly) { - this.compilerOptions.noResolve = true; this.compilerOptions.types = []; } @@ -296,7 +295,12 @@ namespace ts.server { this.realpath = maybeBind(host, host.realpath); // Use the current directory as resolution root only if the project created using current directory string - this.resolutionCache = createResolutionCache(this, currentDirectory && this.currentDirectory, /*logChangesWhenResolvingModule*/ true); + this.resolutionCache = createResolutionCache( + this, + currentDirectory && this.currentDirectory, + projectService.syntaxOnly ? ResolutionKind.RelativeReferencesInOpenFileOnly : ResolutionKind.All, + /*logChangesWhenResolvingModule*/ true + ); this.languageService = createLanguageService(this, this.documentRegistry, this.projectService.syntaxOnly); if (lastFileExceededProgramSize) { this.disableLanguageService(lastFileExceededProgramSize); @@ -450,6 +454,11 @@ namespace ts.server { return this.resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile, redirectedReference); } + /*@internal*/ + includeTripleslashReferencesFrom(containingFile: string) { + return !this.projectService.syntaxOnly || this.fileIsOpen(this.toPath(containingFile)); + } + directoryExists(path: string): boolean { return this.directoryStructureHost.directoryExists!(path); // TODO: GH#18217 } diff --git a/src/services/services.ts b/src/services/services.ts index fa0f02a9b95df..04d6450d548ac 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1296,7 +1296,7 @@ namespace ts { getCurrentDirectory: () => currentDirectory, fileExists, readFile, - realpath: host.realpath && (path => host.realpath!(path)), + realpath: maybeBind(host, host.realpath), directoryExists: directoryName => { return directoryProbablyExists(directoryName, host); }, @@ -1309,21 +1309,13 @@ namespace ts { }, onReleaseOldSourceFile, hasInvalidatedResolution, - hasChangedAutomaticTypeDirectiveNames + hasChangedAutomaticTypeDirectiveNames, + includeTripleslashReferencesFrom: maybeBind(host, host.includeTripleslashReferencesFrom), + trace: maybeBind(host, host.trace), + resolveModuleNames: maybeBind(host, host.resolveModuleNames), + resolveTypeReferenceDirectives: maybeBind(host, host.resolveTypeReferenceDirectives), + useSourceOfProjectReferenceRedirect: maybeBind(host, host.useSourceOfProjectReferenceRedirect), }; - if (host.trace) { - compilerHost.trace = message => host.trace!(message); - } - - if (host.resolveModuleNames) { - compilerHost.resolveModuleNames = (...args) => host.resolveModuleNames!(...args); - } - if (host.resolveTypeReferenceDirectives) { - compilerHost.resolveTypeReferenceDirectives = (...args) => host.resolveTypeReferenceDirectives!(...args); - } - if (host.useSourceOfProjectReferenceRedirect) { - compilerHost.useSourceOfProjectReferenceRedirect = () => host.useSourceOfProjectReferenceRedirect!(); - } host.setCompilerHost?.(compilerHost); const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); diff --git a/src/services/types.ts b/src/services/types.ts index 73080249abdbb..9eee73f99d4d8 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -266,6 +266,7 @@ namespace ts { resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedModule | undefined)[]; getResolvedModuleWithFailedLookupLocationsFromCache?(modulename: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[]; + /* @internal */ includeTripleslashReferencesFrom?(containingFile: string): boolean; /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; /* @internal */ hasChangedAutomaticTypeDirectiveNames?: HasChangedAutomaticTypeDirectiveNames; /* @internal */ diff --git a/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts b/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts index 09b6df7c42ae8..e91acf222b659 100644 --- a/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts +++ b/src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts @@ -3,35 +3,66 @@ namespace ts.projectSystem { function setup() { const file1: File = { path: `${tscWatch.projectRoot}/a.ts`, - content: `import { y } from "./b"; + content: `import { y, cc } from "./b"; +import { something } from "something"; class c { prop = "hello"; foo() { return this.prop; } }` }; const file2: File = { path: `${tscWatch.projectRoot}/b.ts`, - content: "export const y = 10;" + content: `export { cc } from "./c"; +import { something } from "something"; + export const y = 10;` + }; + const file3: File = { + path: `${tscWatch.projectRoot}/c.ts`, + content: `export const cc = 10;` + }; + const something: File = { + path: `${tscWatch.projectRoot}/node_modules/something/index.d.ts`, + content: "export const something = 10;" }; const configFile: File = { path: `${tscWatch.projectRoot}/tsconfig.json`, content: "{}" }; - const host = createServerHost([file1, file2, libFile, configFile]); + const host = createServerHost([file1, file2, file3, something, libFile, configFile]); const session = createSession(host, { syntaxOnly: true, useSingleInferredProject: true }); - return { host, session, file1, file2, configFile }; + return { host, session, file1, file2, file3, something, configFile }; } it("open files are added to inferred project even if config file is present and semantic operations succeed", () => { - const { host, session, file1, file2 } = setup(); + const { host, session, file1, file2, file3, something } = setup(); const service = session.getProjectService(); openFilesForSession([file1], session); checkNumberOfProjects(service, { inferredProjects: 1 }); const project = service.inferredProjects[0]; - checkProjectActualFiles(project, [libFile.path, file1.path]); // Import is not resolved + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]); // Relative import from open file is resolves but not non relative verifyCompletions(); + verifyGoToDefToB(); openFilesForSession([file2], session); checkNumberOfProjects(service, { inferredProjects: 1 }); - checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]); + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path]); verifyCompletions(); + verifyGoToDefToB(); + verifyGoToDefToC(); + + openFilesForSession([file3], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path]); + + openFilesForSession([something], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]); + + // Close open files and verify resolutions + closeFilesForSession([file3], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]); + + closeFilesForSession([file2], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]); function verifyCompletions() { assert.isTrue(project.languageServiceEnabled); @@ -62,6 +93,34 @@ class c { prop = "hello"; foo() { return this.prop; } }` source: undefined }; } + + function verifyGoToDefToB() { + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: protocolFileLocationFromSubstring(file1, "y") + }).response as protocol.DefinitionInfoAndBoundSpan; + assert.deepEqual(response, { + definitions: [{ + file: file2.path, + ...protocolTextSpanWithContextFromSubstring({ fileText: file2.content, text: "y", contextText: "export const y = 10;" }) + }], + textSpan: protocolTextSpanWithContextFromSubstring({ fileText: file1.content, text: "y" }) + }); + } + + function verifyGoToDefToC() { + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.DefinitionAndBoundSpan, + arguments: protocolFileLocationFromSubstring(file1, "cc") + }).response as protocol.DefinitionInfoAndBoundSpan; + assert.deepEqual(response, { + definitions: [{ + file: file3.path, + ...protocolTextSpanWithContextFromSubstring({ fileText: file3.content, text: "cc", contextText: "export const cc = 10;" }) + }], + textSpan: protocolTextSpanWithContextFromSubstring({ fileText: file1.content, text: "cc" }) + }); + } }); it("throws on unsupported commands", () => { @@ -97,7 +156,7 @@ class c { prop = "hello"; foo() { return this.prop; } }` }); it("should not include auto type reference directives", () => { - const { host, session, file1 } = setup(); + const { host, session, file1, file2 } = setup(); const atTypes: File = { path: `/node_modules/@types/somemodule/index.d.ts`, content: "export const something = 10;" @@ -107,7 +166,52 @@ class c { prop = "hello"; foo() { return this.prop; } }` openFilesForSession([file1], session); checkNumberOfProjects(service, { inferredProjects: 1 }); const project = service.inferredProjects[0]; - checkProjectActualFiles(project, [libFile.path, file1.path]); // Should not contain atTypes + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]); // Should not contain atTypes + }); + + it("should not include referenced files from unopened files", () => { + const file1: File = { + path: `${tscWatch.projectRoot}/a.ts`, + content: `/// +/// +function fooA() { }` + }; + const file2: File = { + path: `${tscWatch.projectRoot}/b.ts`, + content: `/// +/// +function fooB() { }` + }; + const file3: File = { + path: `${tscWatch.projectRoot}/c.ts`, + content: `function fooC() { }` + }; + const something: File = { + path: `${tscWatch.projectRoot}/node_modules/something/index.d.ts`, + content: "function something() {}" + }; + const configFile: File = { + path: `${tscWatch.projectRoot}/tsconfig.json`, + content: "{}" + }; + const host = createServerHost([file1, file2, file3, something, libFile, configFile]); + const session = createSession(host, { syntaxOnly: true, useSingleInferredProject: true }); + const service = session.getProjectService(); + openFilesForSession([file1], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + const project = service.inferredProjects[0]; + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, something.path]); // Should not contains c + + openFilesForSession([file2], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + assert.isTrue(project.dirty); + project.updateGraph(); + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]); + + closeFilesForSession([file2], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + assert.isFalse(project.dirty); + checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]); }); }); }