Skip to content

Commit 5e9c436

Browse files
authored
Handle auto import scenarios when using project references (#37482)
* Add test for project reference and auto import Test for #34677 * Add project reference redirect to the possible file name to import if file is source of project reference redirect Fixes the auto import suggestion when project is built * Use fileExists that mimics presence of project reference redirect file when trying to get auto import file name
1 parent ec95c27 commit 5e9c436

File tree

8 files changed

+145
-23
lines changed

8 files changed

+145
-23
lines changed

src/compiler/checker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4070,6 +4070,9 @@ namespace ts {
40704070
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks),
40714071
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
40724072
redirectTargetsMap: host.redirectTargetsMap,
4073+
getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName),
4074+
isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName),
4075+
fileExists: fileName => host.fileExists(fileName),
40734076
} : undefined },
40744077
encounteredError: false,
40754078
visitedTypes: undefined,

src/compiler/emitter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ namespace ts {
759759
getLibFileFromReference: notImplemented,
760760
isSourceFileFromExternalLibrary: returnFalse,
761761
getResolvedProjectReferenceToRedirect: returnUndefined,
762+
getProjectReferenceRedirect: returnUndefined,
762763
isSourceOfProjectReferenceRedirect: returnFalse,
763764
writeFile: (name, text, writeByteOrderMark) => {
764765
switch (name) {

src/compiler/moduleSpecifiers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,9 @@ namespace ts.moduleSpecifiers {
177177
): T | undefined {
178178
const getCanonicalFileName = hostGetCanonicalFileName(host);
179179
const cwd = host.getCurrentDirectory();
180+
const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined;
180181
const redirects = host.redirectTargetsMap.get(toPath(importedFileName, cwd, getCanonicalFileName)) || emptyArray;
181-
const importedFileNames = [importedFileName, ...redirects];
182+
const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects];
182183
const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd));
183184
if (!preferSymlinks) {
184185
const result = forEach(targets, cb);

src/compiler/program.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,7 @@ namespace ts {
809809

810810
const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() &&
811811
!options.disableSourceOfProjectReferenceRedirect;
812-
const onProgramCreateComplete = updateHostForUseSourceOfProjectReferenceRedirect({
812+
const { onProgramCreateComplete, fileExists } = updateHostForUseSourceOfProjectReferenceRedirect({
813813
compilerHost: host,
814814
useSourceOfProjectReferenceRedirect,
815815
toPath,
@@ -970,6 +970,7 @@ namespace ts {
970970
forEachResolvedProjectReference,
971971
isSourceOfProjectReferenceRedirect,
972972
emitBuildInfo,
973+
fileExists,
973974
getProbableSymlinks,
974975
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
975976
};
@@ -1479,6 +1480,7 @@ namespace ts {
14791480
getLibFileFromReference: program.getLibFileFromReference,
14801481
isSourceFileFromExternalLibrary,
14811482
getResolvedProjectReferenceToRedirect,
1483+
getProjectReferenceRedirect,
14821484
isSourceOfProjectReferenceRedirect,
14831485
getProbableSymlinks,
14841486
writeFile: writeFileCallback || (
@@ -3482,20 +3484,9 @@ namespace ts {
34823484
const originalGetDirectories = host.compilerHost.getDirectories;
34833485
const originalRealpath = host.compilerHost.realpath;
34843486

3487+
if (!host.useSourceOfProjectReferenceRedirect) return { onProgramCreateComplete: noop, fileExists };
34853488

3486-
if (!host.useSourceOfProjectReferenceRedirect) return noop;
3487-
3488-
// This implementation of fileExists checks if the file being requested is
3489-
// .d.ts file for the referenced Project.
3490-
// If it is it returns true irrespective of whether that file exists on host
3491-
host.compilerHost.fileExists = (file) => {
3492-
if (originalFileExists.call(host.compilerHost, file)) return true;
3493-
if (!host.getResolvedProjectReferences()) return false;
3494-
if (!isDeclarationFileName(file)) return false;
3495-
3496-
// Project references go to source file instead of .d.ts file
3497-
return fileOrDirectoryExistsUsingSource(file, /*isFile*/ true);
3498-
};
3489+
host.compilerHost.fileExists = fileExists;
34993490

35003491
if (originalDirectoryExists) {
35013492
// This implementation of directoryExists checks if the directory being requested is
@@ -3547,8 +3538,7 @@ namespace ts {
35473538
originalRealpath.call(host.compilerHost, s);
35483539
}
35493540

3550-
return onProgramCreateComplete;
3551-
3541+
return { onProgramCreateComplete, fileExists };
35523542

35533543
function onProgramCreateComplete() {
35543544
host.compilerHost.fileExists = originalFileExists;
@@ -3557,6 +3547,18 @@ namespace ts {
35573547
// DO not revert realpath as it could be used later
35583548
}
35593549

3550+
// This implementation of fileExists checks if the file being requested is
3551+
// .d.ts file for the referenced Project.
3552+
// If it is it returns true irrespective of whether that file exists on host
3553+
function fileExists(file: string) {
3554+
if (originalFileExists.call(host.compilerHost, file)) return true;
3555+
if (!host.getResolvedProjectReferences()) return false;
3556+
if (!isDeclarationFileName(file)) return false;
3557+
3558+
// Project references go to source file instead of .d.ts file
3559+
return fileOrDirectoryExistsUsingSource(file, /*isFile*/ true);
3560+
}
3561+
35603562
function fileExistsIfProjectReferenceDts(file: string) {
35613563
const source = host.getSourceOfProjectReferenceRedirect(file);
35623564
return source !== undefined ?

src/compiler/types.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3287,6 +3287,10 @@ namespace ts {
32873287
/*@internal*/ getProgramBuildInfo?(): ProgramBuildInfo | undefined;
32883288
/*@internal*/ emitBuildInfo(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult;
32893289
/*@internal*/ getProbableSymlinks(): ReadonlyMap<string>;
3290+
/**
3291+
* This implementation handles file exists to be true if file is source of project reference redirect when program is created using useSourceOfProjectReferenceRedirect
3292+
*/
3293+
/*@internal*/ fileExists(fileName: string): boolean;
32903294
}
32913295

32923296
/*@internal*/
@@ -6428,16 +6432,16 @@ namespace ts {
64286432
/*@internal*/
64296433
export interface ModuleSpecifierResolutionHost {
64306434
useCaseSensitiveFileNames?(): boolean;
6431-
fileExists?(path: string): boolean;
6435+
fileExists(path: string): boolean;
64326436
getCurrentDirectory(): string;
64336437
readFile?(path: string): string | undefined;
6434-
/* @internal */
64356438
getProbableSymlinks?(files: readonly SourceFile[]): ReadonlyMap<string>;
6436-
/* @internal */
64376439
getGlobalTypingsCacheLocation?(): string | undefined;
64386440

64396441
getSourceFiles(): readonly SourceFile[];
64406442
readonly redirectTargetsMap: RedirectTargetsMap;
6443+
getProjectReferenceRedirect(fileName: string): string | undefined;
6444+
isSourceOfProjectReferenceRedirect(fileName: string): boolean;
64416445
}
64426446

64436447
// Note: this used to be deprecated in our public API, but is still used internally

src/services/utilities.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1705,14 +1705,16 @@ namespace ts {
17051705
// Mix in `getProbableSymlinks` from Program when host doesn't have it
17061706
// in order for non-Project hosts to have a symlinks cache.
17071707
return {
1708-
fileExists: maybeBind(host, host.fileExists),
1708+
fileExists: fileName => program.fileExists(fileName),
17091709
getCurrentDirectory: () => host.getCurrentDirectory(),
17101710
readFile: maybeBind(host, host.readFile),
17111711
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
17121712
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks) || (() => program.getProbableSymlinks()),
17131713
getGlobalTypingsCacheLocation: maybeBind(host, host.getGlobalTypingsCacheLocation),
17141714
getSourceFiles: () => program.getSourceFiles(),
1715-
redirectTargetsMap: program.redirectTargetsMap
1715+
redirectTargetsMap: program.redirectTargetsMap,
1716+
getProjectReferenceRedirect: fileName => program.getProjectReferenceRedirect(fileName),
1717+
isSourceOfProjectReferenceRedirect: fileName => program.isSourceOfProjectReferenceRedirect(fileName),
17161718
};
17171719
}
17181720

src/testRunner/unittests/tsserver/projectReferenceErrors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ fnErr();
353353
{ line: 4, offset: 5 },
354354
{ line: 4, offset: 10 },
355355
Diagnostics.Module_0_has_no_exported_member_1,
356-
[`"../dependency/fns"`, "fnErr"],
356+
[`"../decls/fns"`, "fnErr"],
357357
"error",
358358
)
359359
],

src/testRunner/unittests/tsserver/projectReferences.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,5 +2173,114 @@ foo;`
21732173
});
21742174
});
21752175
});
2176+
2177+
describe("auto import with referenced project", () => {
2178+
function verifyAutoImport(built: boolean, disableSourceOfProjectReferenceRedirect?: boolean) {
2179+
const solnConfig: File = {
2180+
path: `${tscWatch.projectRoot}/tsconfig.json`,
2181+
content: JSON.stringify({
2182+
files: [],
2183+
references: [
2184+
{ path: "shared/src/library" },
2185+
{ path: "app/src/program" }
2186+
]
2187+
})
2188+
};
2189+
const sharedConfig: File = {
2190+
path: `${tscWatch.projectRoot}/shared/src/library/tsconfig.json`,
2191+
content: JSON.stringify({
2192+
compilerOptions: {
2193+
composite: true,
2194+
outDir: "../../bld/library"
2195+
}
2196+
})
2197+
};
2198+
const sharedIndex: File = {
2199+
path: `${tscWatch.projectRoot}/shared/src/library/index.ts`,
2200+
content: `export function foo() {}`
2201+
};
2202+
const sharedPackage: File = {
2203+
path: `${tscWatch.projectRoot}/shared/package.json`,
2204+
content: JSON.stringify({
2205+
name: "shared",
2206+
version: "1.0.0",
2207+
main: "bld/library/index.js",
2208+
types: "bld/library/index.d.ts"
2209+
})
2210+
};
2211+
const appConfig: File = {
2212+
path: `${tscWatch.projectRoot}/app/src/program/tsconfig.json`,
2213+
content: JSON.stringify({
2214+
compilerOptions: {
2215+
composite: true,
2216+
outDir: "../../bld/program",
2217+
disableSourceOfProjectReferenceRedirect
2218+
},
2219+
references: [
2220+
{ path: "../../../shared/src/library" }
2221+
]
2222+
})
2223+
};
2224+
const appBar: File = {
2225+
path: `${tscWatch.projectRoot}/app/src/program/bar.ts`,
2226+
content: `import {foo} from "shared";`
2227+
};
2228+
const appIndex: File = {
2229+
path: `${tscWatch.projectRoot}/app/src/program/index.ts`,
2230+
content: `foo`
2231+
};
2232+
const sharedSymlink: SymLink = {
2233+
path: `${tscWatch.projectRoot}/node_modules/shared`,
2234+
symLink: `${tscWatch.projectRoot}/shared`
2235+
};
2236+
const files = [solnConfig, sharedConfig, sharedIndex, sharedPackage, appConfig, appBar, appIndex, sharedSymlink, libFile];
2237+
const host = createServerHost(files);
2238+
if (built) {
2239+
const solutionBuilder = tscWatch.createSolutionBuilder(host, [solnConfig.path], {});
2240+
solutionBuilder.build();
2241+
host.clearOutput();
2242+
}
2243+
const session = createSession(host);
2244+
openFilesForSession([appIndex], session);
2245+
const response = session.executeCommandSeq<protocol.CodeFixRequest>({
2246+
command: protocol.CommandTypes.GetCodeFixes,
2247+
arguments: {
2248+
file: appIndex.path,
2249+
startLine: 1,
2250+
startOffset: 1,
2251+
endLine: 1,
2252+
endOffset: 4,
2253+
errorCodes: [Diagnostics.Cannot_find_name_0.code],
2254+
}
2255+
}).response as protocol.CodeFixAction[];
2256+
assert.deepEqual(response, [
2257+
{
2258+
fixName: "import",
2259+
description: `Import 'foo' from module "shared"`,
2260+
changes: [{
2261+
fileName: appIndex.path,
2262+
textChanges: [{
2263+
start: { line: 1, offset: 1 },
2264+
end: { line: 1, offset: 1 },
2265+
newText: 'import { foo } from "shared";\n\n',
2266+
}],
2267+
}],
2268+
commands: undefined,
2269+
fixAllDescription: undefined,
2270+
fixId: undefined
2271+
}
2272+
]);
2273+
}
2274+
2275+
it("when project is built", () => {
2276+
verifyAutoImport(/*built*/ true);
2277+
});
2278+
it("when project is not built", () => {
2279+
verifyAutoImport(/*built*/ false);
2280+
});
2281+
it("when disableSourceOfProjectReferenceRedirect is true", () => {
2282+
verifyAutoImport(/*built*/ true, /*disableSourceOfProjectReferenceRedirect*/ true);
2283+
});
2284+
});
21762285
});
21772286
}

0 commit comments

Comments
 (0)