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
This commit is contained in:
Sheetal Nandi
2020-03-19 16:53:44 -07:00
committed by GitHub
parent ec95c27b4d
commit 5e9c43607f
8 changed files with 145 additions and 23 deletions

View File

@@ -4070,6 +4070,9 @@ namespace ts {
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks),
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
redirectTargetsMap: host.redirectTargetsMap,
getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName),
isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName),
fileExists: fileName => host.fileExists(fileName),
} : undefined },
encounteredError: false,
visitedTypes: undefined,

View File

@@ -759,6 +759,7 @@ namespace ts {
getLibFileFromReference: notImplemented,
isSourceFileFromExternalLibrary: returnFalse,
getResolvedProjectReferenceToRedirect: returnUndefined,
getProjectReferenceRedirect: returnUndefined,
isSourceOfProjectReferenceRedirect: returnFalse,
writeFile: (name, text, writeByteOrderMark) => {
switch (name) {

View File

@@ -177,8 +177,9 @@ namespace ts.moduleSpecifiers {
): T | undefined {
const getCanonicalFileName = hostGetCanonicalFileName(host);
const cwd = host.getCurrentDirectory();
const referenceRedirect = host.isSourceOfProjectReferenceRedirect(importedFileName) ? host.getProjectReferenceRedirect(importedFileName) : undefined;
const redirects = host.redirectTargetsMap.get(toPath(importedFileName, cwd, getCanonicalFileName)) || emptyArray;
const importedFileNames = [importedFileName, ...redirects];
const importedFileNames = [...(referenceRedirect ? [referenceRedirect] : emptyArray), importedFileName, ...redirects];
const targets = importedFileNames.map(f => getNormalizedAbsolutePath(f, cwd));
if (!preferSymlinks) {
const result = forEach(targets, cb);

View File

@@ -809,7 +809,7 @@ namespace ts {
const useSourceOfProjectReferenceRedirect = !!host.useSourceOfProjectReferenceRedirect?.() &&
!options.disableSourceOfProjectReferenceRedirect;
const onProgramCreateComplete = updateHostForUseSourceOfProjectReferenceRedirect({
const { onProgramCreateComplete, fileExists } = updateHostForUseSourceOfProjectReferenceRedirect({
compilerHost: host,
useSourceOfProjectReferenceRedirect,
toPath,
@@ -970,6 +970,7 @@ namespace ts {
forEachResolvedProjectReference,
isSourceOfProjectReferenceRedirect,
emitBuildInfo,
fileExists,
getProbableSymlinks,
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
};
@@ -1479,6 +1480,7 @@ namespace ts {
getLibFileFromReference: program.getLibFileFromReference,
isSourceFileFromExternalLibrary,
getResolvedProjectReferenceToRedirect,
getProjectReferenceRedirect,
isSourceOfProjectReferenceRedirect,
getProbableSymlinks,
writeFile: writeFileCallback || (
@@ -3482,20 +3484,9 @@ namespace ts {
const originalGetDirectories = host.compilerHost.getDirectories;
const originalRealpath = host.compilerHost.realpath;
if (!host.useSourceOfProjectReferenceRedirect) return { onProgramCreateComplete: noop, fileExists };
if (!host.useSourceOfProjectReferenceRedirect) return noop;
// This implementation of fileExists checks if the file being requested is
// .d.ts file for the referenced Project.
// If it is it returns true irrespective of whether that file exists on host
host.compilerHost.fileExists = (file) => {
if (originalFileExists.call(host.compilerHost, file)) return true;
if (!host.getResolvedProjectReferences()) return false;
if (!isDeclarationFileName(file)) return false;
// Project references go to source file instead of .d.ts file
return fileOrDirectoryExistsUsingSource(file, /*isFile*/ true);
};
host.compilerHost.fileExists = fileExists;
if (originalDirectoryExists) {
// This implementation of directoryExists checks if the directory being requested is
@@ -3547,8 +3538,7 @@ namespace ts {
originalRealpath.call(host.compilerHost, s);
}
return onProgramCreateComplete;
return { onProgramCreateComplete, fileExists };
function onProgramCreateComplete() {
host.compilerHost.fileExists = originalFileExists;
@@ -3557,6 +3547,18 @@ namespace ts {
// DO not revert realpath as it could be used later
}
// This implementation of fileExists checks if the file being requested is
// .d.ts file for the referenced Project.
// If it is it returns true irrespective of whether that file exists on host
function fileExists(file: string) {
if (originalFileExists.call(host.compilerHost, file)) return true;
if (!host.getResolvedProjectReferences()) return false;
if (!isDeclarationFileName(file)) return false;
// Project references go to source file instead of .d.ts file
return fileOrDirectoryExistsUsingSource(file, /*isFile*/ true);
}
function fileExistsIfProjectReferenceDts(file: string) {
const source = host.getSourceOfProjectReferenceRedirect(file);
return source !== undefined ?

View File

@@ -3287,6 +3287,10 @@ namespace ts {
/*@internal*/ getProgramBuildInfo?(): ProgramBuildInfo | undefined;
/*@internal*/ emitBuildInfo(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult;
/*@internal*/ getProbableSymlinks(): ReadonlyMap<string>;
/**
* This implementation handles file exists to be true if file is source of project reference redirect when program is created using useSourceOfProjectReferenceRedirect
*/
/*@internal*/ fileExists(fileName: string): boolean;
}
/*@internal*/
@@ -6428,16 +6432,16 @@ namespace ts {
/*@internal*/
export interface ModuleSpecifierResolutionHost {
useCaseSensitiveFileNames?(): boolean;
fileExists?(path: string): boolean;
fileExists(path: string): boolean;
getCurrentDirectory(): string;
readFile?(path: string): string | undefined;
/* @internal */
getProbableSymlinks?(files: readonly SourceFile[]): ReadonlyMap<string>;
/* @internal */
getGlobalTypingsCacheLocation?(): string | undefined;
getSourceFiles(): readonly SourceFile[];
readonly redirectTargetsMap: RedirectTargetsMap;
getProjectReferenceRedirect(fileName: string): string | undefined;
isSourceOfProjectReferenceRedirect(fileName: string): boolean;
}
// Note: this used to be deprecated in our public API, but is still used internally

View File

@@ -1705,14 +1705,16 @@ namespace ts {
// Mix in `getProbableSymlinks` from Program when host doesn't have it
// in order for non-Project hosts to have a symlinks cache.
return {
fileExists: maybeBind(host, host.fileExists),
fileExists: fileName => program.fileExists(fileName),
getCurrentDirectory: () => host.getCurrentDirectory(),
readFile: maybeBind(host, host.readFile),
useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames),
getProbableSymlinks: maybeBind(host, host.getProbableSymlinks) || (() => program.getProbableSymlinks()),
getGlobalTypingsCacheLocation: maybeBind(host, host.getGlobalTypingsCacheLocation),
getSourceFiles: () => program.getSourceFiles(),
redirectTargetsMap: program.redirectTargetsMap
redirectTargetsMap: program.redirectTargetsMap,
getProjectReferenceRedirect: fileName => program.getProjectReferenceRedirect(fileName),
isSourceOfProjectReferenceRedirect: fileName => program.isSourceOfProjectReferenceRedirect(fileName),
};
}

View File

@@ -353,7 +353,7 @@ fnErr();
{ line: 4, offset: 5 },
{ line: 4, offset: 10 },
Diagnostics.Module_0_has_no_exported_member_1,
[`"../dependency/fns"`, "fnErr"],
[`"../decls/fns"`, "fnErr"],
"error",
)
],

View File

@@ -2173,5 +2173,114 @@ foo;`
});
});
});
describe("auto import with referenced project", () => {
function verifyAutoImport(built: boolean, disableSourceOfProjectReferenceRedirect?: boolean) {
const solnConfig: File = {
path: `${tscWatch.projectRoot}/tsconfig.json`,
content: JSON.stringify({
files: [],
references: [
{ path: "shared/src/library" },
{ path: "app/src/program" }
]
})
};
const sharedConfig: File = {
path: `${tscWatch.projectRoot}/shared/src/library/tsconfig.json`,
content: JSON.stringify({
compilerOptions: {
composite: true,
outDir: "../../bld/library"
}
})
};
const sharedIndex: File = {
path: `${tscWatch.projectRoot}/shared/src/library/index.ts`,
content: `export function foo() {}`
};
const sharedPackage: File = {
path: `${tscWatch.projectRoot}/shared/package.json`,
content: JSON.stringify({
name: "shared",
version: "1.0.0",
main: "bld/library/index.js",
types: "bld/library/index.d.ts"
})
};
const appConfig: File = {
path: `${tscWatch.projectRoot}/app/src/program/tsconfig.json`,
content: JSON.stringify({
compilerOptions: {
composite: true,
outDir: "../../bld/program",
disableSourceOfProjectReferenceRedirect
},
references: [
{ path: "../../../shared/src/library" }
]
})
};
const appBar: File = {
path: `${tscWatch.projectRoot}/app/src/program/bar.ts`,
content: `import {foo} from "shared";`
};
const appIndex: File = {
path: `${tscWatch.projectRoot}/app/src/program/index.ts`,
content: `foo`
};
const sharedSymlink: SymLink = {
path: `${tscWatch.projectRoot}/node_modules/shared`,
symLink: `${tscWatch.projectRoot}/shared`
};
const files = [solnConfig, sharedConfig, sharedIndex, sharedPackage, appConfig, appBar, appIndex, sharedSymlink, libFile];
const host = createServerHost(files);
if (built) {
const solutionBuilder = tscWatch.createSolutionBuilder(host, [solnConfig.path], {});
solutionBuilder.build();
host.clearOutput();
}
const session = createSession(host);
openFilesForSession([appIndex], session);
const response = session.executeCommandSeq<protocol.CodeFixRequest>({
command: protocol.CommandTypes.GetCodeFixes,
arguments: {
file: appIndex.path,
startLine: 1,
startOffset: 1,
endLine: 1,
endOffset: 4,
errorCodes: [Diagnostics.Cannot_find_name_0.code],
}
}).response as protocol.CodeFixAction[];
assert.deepEqual(response, [
{
fixName: "import",
description: `Import 'foo' from module "shared"`,
changes: [{
fileName: appIndex.path,
textChanges: [{
start: { line: 1, offset: 1 },
end: { line: 1, offset: 1 },
newText: 'import { foo } from "shared";\n\n',
}],
}],
commands: undefined,
fixAllDescription: undefined,
fixId: undefined
}
]);
}
it("when project is built", () => {
verifyAutoImport(/*built*/ true);
});
it("when project is not built", () => {
verifyAutoImport(/*built*/ false);
});
it("when disableSourceOfProjectReferenceRedirect is true", () => {
verifyAutoImport(/*built*/ true, /*disableSourceOfProjectReferenceRedirect*/ true);
});
});
});
}