Skip to content

Commit eb8a109

Browse files
committed
Resolve only relative references in open files on syntax server
1 parent 1814e2a commit eb8a109

File tree

5 files changed

+134
-17
lines changed

5 files changed

+134
-17
lines changed

src/compiler/resolutionCache.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace ts {
1111

1212
invalidateResolutionsOfFailedLookupLocations(): boolean;
1313
invalidateResolutionOfFile(filePath: Path): void;
14+
removeRelativeNoResolveResolutionsOfFile(filePath: Path): boolean;
1415
removeResolutionsOfFile(filePath: Path): void;
1516
removeResolutionsFromProjectReferenceRedirects(filePath: Path): void;
1617
setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: ESMap<Path, readonly string[]>): void;
@@ -141,7 +142,21 @@ namespace ts {
141142
type GetResolutionWithResolvedFileName<T extends ResolutionWithFailedLookupLocations = ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName = ResolutionWithResolvedFileName> =
142143
(resolution: T) => R | undefined;
143144

144-
export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, logChangesWhenResolvingModule: boolean): ResolutionCache {
145+
export enum ResolutionKind {
146+
All,
147+
RelativeReferencesInOpenFileOnly
148+
}
149+
150+
const noResolveResolvedModule: ResolvedModuleWithFailedLookupLocations = {
151+
resolvedModule: undefined,
152+
failedLookupLocations: []
153+
};
154+
const noResolveResolvedTypeReferenceDirective: ResolvedTypeReferenceDirectiveWithFailedLookupLocations = {
155+
resolvedTypeReferenceDirective: undefined,
156+
failedLookupLocations: []
157+
};
158+
159+
export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string | undefined, resolutionKind: ResolutionKind, logChangesWhenResolvingModule: boolean): ResolutionCache {
145160
let filesWithChangedSetOfUnresolvedImports: Path[] | undefined;
146161
let filesWithInvalidatedResolutions: Set<Path> | undefined;
147162
let filesWithInvalidatedNonRelativeUnresolvedImports: ReadonlyESMap<Path, readonly string[]> | undefined;
@@ -206,6 +221,7 @@ namespace ts {
206221
hasChangedAutomaticTypeDirectiveNames: () => hasChangedAutomaticTypeDirectiveNames,
207222
invalidateResolutionOfFile,
208223
invalidateResolutionsOfFailedLookupLocations,
224+
removeRelativeNoResolveResolutionsOfFile,
209225
setFilesWithInvalidatedNonRelativeUnresolvedImports,
210226
createHasInvalidatedResolution,
211227
updateTypeRootsWatch,
@@ -341,11 +357,12 @@ namespace ts {
341357
shouldRetryResolution: (t: T) => boolean;
342358
reusedNames?: readonly string[];
343359
logChanges?: boolean;
360+
noResolveResolution: T;
344361
}
345362
function resolveNamesWithLocalCache<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName>({
346363
names, containingFile, redirectedReference,
347364
cache, perDirectoryCacheWithRedirects,
348-
loader, getResolutionWithResolvedFileName,
365+
loader, getResolutionWithResolvedFileName, noResolveResolution,
349366
shouldRetryResolution, reusedNames, logChanges
350367
}: ResolveNamesWithLocalCacheInput<T, R>): (R | undefined)[] {
351368
const path = resolutionHost.toPath(containingFile);
@@ -382,7 +399,10 @@ namespace ts {
382399
resolution = resolutionInDirectory;
383400
}
384401
else {
385-
resolution = loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference);
402+
resolution = resolutionKind === ResolutionKind.All ||
403+
(isExternalModuleNameRelative(name) && resolutionHost.fileIsOpen(path)) ?
404+
loader(name, containingFile, compilerOptions, resolutionHost.getCompilerHost?.() || resolutionHost, redirectedReference) :
405+
noResolveResolution;
386406
perDirectoryResolution.set(name, resolution);
387407
}
388408
resolutionsInFile.set(name, resolution);
@@ -441,6 +461,7 @@ namespace ts {
441461
loader: resolveTypeReferenceDirective,
442462
getResolutionWithResolvedFileName: getResolvedTypeReferenceDirective,
443463
shouldRetryResolution: resolution => resolution.resolvedTypeReferenceDirective === undefined,
464+
noResolveResolution: noResolveResolvedTypeReferenceDirective,
444465
});
445466
}
446467

@@ -455,7 +476,8 @@ namespace ts {
455476
getResolutionWithResolvedFileName: getResolvedModule,
456477
shouldRetryResolution: resolution => !resolution.resolvedModule || !resolutionExtensionIsTSOrJson(resolution.resolvedModule.extension),
457478
reusedNames,
458-
logChanges: logChangesWhenResolvingModule
479+
logChanges: logChangesWhenResolvingModule,
480+
noResolveResolution: noResolveResolvedModule,
459481
});
460482
}
461483

@@ -741,6 +763,32 @@ namespace ts {
741763
}
742764
}
743765

766+
function removeRelativeNoResolveResolutionsOfFileFromCache<T extends ResolutionWithFailedLookupLocations>(
767+
cache: ESMap<string, ESMap<string, T>>,
768+
filePath: Path,
769+
noResolveResolution: T,
770+
) {
771+
Debug.assert(resolutionKind === ResolutionKind.RelativeReferencesInOpenFileOnly);
772+
// Deleted file, stop watching failed lookups for all the resolutions in the file
773+
const resolutions = cache.get(filePath);
774+
if (!resolutions) return false;
775+
let invalidated = false;
776+
resolutions.forEach((resolution, name) => {
777+
if (resolution === noResolveResolution && isExternalModuleNameRelative(name)) {
778+
resolutions.delete(name);
779+
invalidated = true;
780+
}
781+
});
782+
return invalidated;
783+
}
784+
785+
function removeRelativeNoResolveResolutionsOfFile(filePath: Path) {
786+
let invalidated = removeRelativeNoResolveResolutionsOfFileFromCache(resolvedModuleNames, filePath, noResolveResolvedModule);
787+
invalidated = removeRelativeNoResolveResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath, noResolveResolvedTypeReferenceDirective) || invalidated;
788+
return invalidated;
789+
790+
}
791+
744792
function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ReadonlyESMap<Path, readonly string[]>) {
745793
Debug.assert(filesWithInvalidatedNonRelativeUnresolvedImports === filesMap || filesWithInvalidatedNonRelativeUnresolvedImports === undefined);
746794
filesWithInvalidatedNonRelativeUnresolvedImports = filesMap;

src/compiler/watchPublic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ namespace ts {
320320
configFileName ?
321321
getDirectoryPath(getNormalizedAbsolutePath(configFileName, currentDirectory)) :
322322
currentDirectory,
323+
ResolutionKind.All,
323324
/*logChangesWhenResolvingModule*/ false
324325
);
325326
// Resolve module using host module resolution strategy if provided otherwise use resolution cache to resolve module names

src/server/editorServices.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2967,7 +2967,15 @@ namespace ts.server {
29672967
let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info);
29682968
let defaultConfigProject: ConfiguredProject | undefined;
29692969
let retainProjects: ConfiguredProject[] | ConfiguredProject | undefined;
2970-
if (!project && !this.syntaxOnly) { // Checking syntaxOnly is an optimization
2970+
if (this.syntaxOnly) {
2971+
// Invalidate resolutions in the file since this file is now open
2972+
info.containingProjects.forEach(project => {
2973+
if (project.resolutionCache.removeRelativeNoResolveResolutionsOfFile(info.path)) {
2974+
project.markAsDirty();
2975+
}
2976+
});
2977+
}
2978+
else if (!project) { // Checking syntaxOnly is an optimization
29712979
configFileName = this.getConfigFileNameForFile(info);
29722980
if (configFileName) {
29732981
project = this.findConfiguredProjectByProjectName(configFileName);

src/server/project.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,10 +280,6 @@ namespace ts.server {
280280
}
281281

282282
this.languageServiceEnabled = true;
283-
if (projectService.syntaxOnly) {
284-
this.compilerOptions.noResolve = true;
285-
}
286-
287283
this.setInternalCompilerOptionsForEmittingJsFiles();
288284
const host = this.projectService.host;
289285
if (this.projectService.logger.loggingEnabled()) {
@@ -295,7 +291,12 @@ namespace ts.server {
295291
this.realpath = maybeBind(host, host.realpath);
296292

297293
// Use the current directory as resolution root only if the project created using current directory string
298-
this.resolutionCache = createResolutionCache(this, currentDirectory && this.currentDirectory, /*logChangesWhenResolvingModule*/ true);
294+
this.resolutionCache = createResolutionCache(
295+
this,
296+
currentDirectory && this.currentDirectory,
297+
projectService.syntaxOnly ? ResolutionKind.RelativeReferencesInOpenFileOnly : ResolutionKind.All,
298+
/*logChangesWhenResolvingModule*/ true
299+
);
299300
this.languageService = createLanguageService(this, this.documentRegistry, this.projectService.syntaxOnly);
300301
if (lastFileExceededProgramSize) {
301302
this.disableLanguageService(lastFileExceededProgramSize);

src/testRunner/unittests/tsserver/semanticOperationsOnSyntaxServer.ts

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,66 @@ namespace ts.projectSystem {
33
function setup() {
44
const file1: File = {
55
path: `${tscWatch.projectRoot}/a.ts`,
6-
content: `import { y } from "./b";
6+
content: `import { y, cc } from "./b";
7+
import { something } from "something";
78
class c { prop = "hello"; foo() { return this.prop; } }`
89
};
910
const file2: File = {
1011
path: `${tscWatch.projectRoot}/b.ts`,
11-
content: "export const y = 10;"
12+
content: `export { cc } from "./c";
13+
import { something } from "something";
14+
export const y = 10;`
15+
};
16+
const file3: File = {
17+
path: `${tscWatch.projectRoot}/c.ts`,
18+
content: `export const cc = 10;`
19+
};
20+
const something: File = {
21+
path: `${tscWatch.projectRoot}/node_modules/something/index.d.ts`,
22+
content: "export const something = 10;"
1223
};
1324
const configFile: File = {
1425
path: `${tscWatch.projectRoot}/tsconfig.json`,
1526
content: "{}"
1627
};
17-
const host = createServerHost([file1, file2, libFile, configFile]);
28+
const host = createServerHost([file1, file2, file3, something, libFile, configFile]);
1829
const session = createSession(host, { syntaxOnly: true, useSingleInferredProject: true });
19-
return { host, session, file1, file2, configFile };
30+
return { host, session, file1, file2, file3, something, configFile };
2031
}
2132

2233
it("open files are added to inferred project even if config file is present and semantic operations succeed", () => {
23-
const { host, session, file1, file2 } = setup();
34+
const { host, session, file1, file2, file3, something } = setup();
2435
const service = session.getProjectService();
2536
openFilesForSession([file1], session);
2637
checkNumberOfProjects(service, { inferredProjects: 1 });
2738
const project = service.inferredProjects[0];
28-
checkProjectActualFiles(project, [libFile.path, file1.path]); // Import is not resolved
39+
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]); // Relative import from open file is resolves but not non relative
2940
verifyCompletions();
41+
verifyGoToDefToB();
3042

3143
openFilesForSession([file2], session);
3244
checkNumberOfProjects(service, { inferredProjects: 1 });
33-
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path]);
45+
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path]);
3446
verifyCompletions();
47+
verifyGoToDefToB();
48+
verifyGoToDefToC();
49+
50+
openFilesForSession([file3], session);
51+
checkNumberOfProjects(service, { inferredProjects: 1 });
52+
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path]);
53+
54+
openFilesForSession([something], session);
55+
checkNumberOfProjects(service, { inferredProjects: 1 });
56+
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
57+
58+
// Close open files and verify resolutions
59+
closeFilesForSession([file3], session);
60+
checkNumberOfProjects(service, { inferredProjects: 1 });
61+
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
62+
63+
closeFilesForSession([file2], session);
64+
checkNumberOfProjects(service, { inferredProjects: 1 });
65+
checkProjectActualFiles(project, [libFile.path, file1.path, file2.path, file3.path, something.path]);
3566

3667
function verifyCompletions() {
3768
assert.isTrue(project.languageServiceEnabled);
@@ -62,6 +93,34 @@ class c { prop = "hello"; foo() { return this.prop; } }`
6293
source: undefined
6394
};
6495
}
96+
97+
function verifyGoToDefToB() {
98+
const response = session.executeCommandSeq<protocol.DefinitionAndBoundSpanRequest>({
99+
command: protocol.CommandTypes.DefinitionAndBoundSpan,
100+
arguments: protocolFileLocationFromSubstring(file1, "y")
101+
}).response as protocol.DefinitionInfoAndBoundSpan;
102+
assert.deepEqual(response, {
103+
definitions: [{
104+
file: file2.path,
105+
...protocolTextSpanWithContextFromSubstring({ fileText: file2.content, text: "y", contextText: "export const y = 10;" })
106+
}],
107+
textSpan: protocolTextSpanWithContextFromSubstring({ fileText: file1.content, text: "y" })
108+
});
109+
}
110+
111+
function verifyGoToDefToC() {
112+
const response = session.executeCommandSeq<protocol.DefinitionAndBoundSpanRequest>({
113+
command: protocol.CommandTypes.DefinitionAndBoundSpan,
114+
arguments: protocolFileLocationFromSubstring(file1, "cc")
115+
}).response as protocol.DefinitionInfoAndBoundSpan;
116+
assert.deepEqual(response, {
117+
definitions: [{
118+
file: file3.path,
119+
...protocolTextSpanWithContextFromSubstring({ fileText: file3.content, text: "cc", contextText: "export const cc = 10;" })
120+
}],
121+
textSpan: protocolTextSpanWithContextFromSubstring({ fileText: file1.content, text: "cc" })
122+
});
123+
}
65124
});
66125

67126
it("throws on unsupported commands", () => {

0 commit comments

Comments
 (0)