Resolve only relative references in open files on syntax server (#39476)

* Resolve only relative references in open files on syntax server

* Support resolving tripleslash references only in the open file

* Apply suggestions from code review

Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>

Co-authored-by: Daniel Rosenwasser <DanielRosenwasser@users.noreply.github.com>
This commit is contained in:
Sheetal Nandi
2020-07-16 14:31:24 -07:00
committed by GitHub
parent c07c885151
commit 667ba74c93
9 changed files with 218 additions and 31 deletions

View File

@@ -807,6 +807,8 @@ namespace ts {
let mapFromFileToProjectReferenceRedirects: ESMap<Path, Path> | undefined;
let mapFromToProjectReferenceRedirectSource: ESMap<Path, SourceOfProjectReferenceRedirect> | undefined;
let skippedTrippleSlashReferences: Set<Path> | 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<string, SeenPackageName>();
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(

View File

@@ -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<Path, readonly string[]>): void;
@@ -141,7 +142,21 @@ namespace ts {
type GetResolutionWithResolvedFileName<T extends ResolutionWithFailedLookupLocations = ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName = ResolutionWithResolvedFileName> =
(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<Path> | undefined;
let filesWithInvalidatedNonRelativeUnresolvedImports: ReadonlyESMap<Path, readonly string[]> | 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<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>({
names, containingFile, redirectedReference,
cache, perDirectoryCacheWithRedirects,
loader, getResolutionWithResolvedFileName,
loader, getResolutionWithResolvedFileName, noResolveResolution,
shouldRetryResolution, reusedNames, logChanges
}: ResolveNamesWithLocalCacheInput<T, R>): (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<T extends ResolutionWithFailedLookupLocations>(
cache: ESMap<Path, ESMap<string, T>>,
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<Path, readonly string[]>) {
Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined);
filesWithInvalidatedNonRelativeUnresolvedImports = filesMap;

View File

@@ -3687,6 +3687,8 @@ namespace ts {
/* @internal */
getRefFileMap(): MultiMap<Path, RefFile> | undefined;
/* @internal */
getSkippedTrippleSlashReferences(): Set<Path> | undefined;
/* @internal */
getFilesByNameMap(): ESMap<string, SourceFile | false | undefined>;
/**
@@ -6227,6 +6229,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;

View File

@@ -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

View File

@@ -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 };
}

View File

@@ -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
}

View File

@@ -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);

View File

@@ -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 */

View File

@@ -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<protocol.DefinitionAndBoundSpanRequest>({
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<protocol.DefinitionAndBoundSpanRequest>({
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: `///<reference path="b.ts"/>
///<reference path="${tscWatch.projectRoot}/node_modules/something/index.d.ts"/>
function fooA() { }`
};
const file2: File = {
path: `${tscWatch.projectRoot}/b.ts`,
content: `///<reference path="./c.ts"/>
///<reference path="${tscWatch.projectRoot}/node_modules/something/index.d.ts"/>
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]);
});
});
}