From 6e134de1c8d3851615accaacba15901c82a3370d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 19 Mar 2020 12:02:39 -0700 Subject: [PATCH 1/3] Add test for project reference and auto import Test for #34677 --- .../unittests/tsserver/projectReferences.ts | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index ad7de88dc4cdb..0509eba17c7b3 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -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({ + 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); + }); + }); }); } From a6a4e7d5df71220be32802cda2bd374866497cd6 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 19 Mar 2020 12:34:42 -0700 Subject: [PATCH 2/3] 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 --- src/compiler/checker.ts | 2 ++ src/compiler/emitter.ts | 1 + src/compiler/moduleSpecifiers.ts | 3 ++- src/compiler/program.ts | 1 + src/compiler/types.ts | 2 ++ src/services/utilities.ts | 4 +++- src/testRunner/unittests/tsserver/projectReferenceErrors.ts | 2 +- 7 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 663ea4aa02b4a..96eb70ccfae4d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4070,6 +4070,8 @@ 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) } : undefined }, encounteredError: false, visitedTypes: undefined, diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 08104e7ffe9b9..51767c8e9af23 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -759,6 +759,7 @@ namespace ts { getLibFileFromReference: notImplemented, isSourceFileFromExternalLibrary: returnFalse, getResolvedProjectReferenceToRedirect: returnUndefined, + getProjectReferenceRedirect: returnUndefined, isSourceOfProjectReferenceRedirect: returnFalse, writeFile: (name, text, writeByteOrderMark) => { switch (name) { diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index f0ea43ed68b41..57df15b5ee02d 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -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); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index e3f3e27a8272f..b28ef578c6ebd 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1479,6 +1479,7 @@ namespace ts { getLibFileFromReference: program.getLibFileFromReference, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect, + getProjectReferenceRedirect, isSourceOfProjectReferenceRedirect, getProbableSymlinks, writeFile: writeFileCallback || ( diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 893ee47c3dfa2..e0809b5b0025c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6438,6 +6438,8 @@ namespace ts { 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 diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 58a39726589a9..ba41d2282c35e 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1712,7 +1712,9 @@ namespace ts { 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), }; } diff --git a/src/testRunner/unittests/tsserver/projectReferenceErrors.ts b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts index a034d41dc5213..fd6a833a22a32 100644 --- a/src/testRunner/unittests/tsserver/projectReferenceErrors.ts +++ b/src/testRunner/unittests/tsserver/projectReferenceErrors.ts @@ -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", ) ], From 0e6c547681314770fab036423d6baefebe272814 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 19 Mar 2020 15:09:15 -0700 Subject: [PATCH 3/3] Use fileExists that mimics presence of project reference redirect file when trying to get auto import file name --- src/compiler/checker.ts | 3 ++- src/compiler/program.ts | 33 +++++++++++++++++---------------- src/compiler/types.ts | 8 +++++--- src/services/utilities.ts | 2 +- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 96eb70ccfae4d..3040059231ef3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4071,7 +4071,8 @@ namespace ts { useCaseSensitiveFileNames: maybeBind(host, host.useCaseSensitiveFileNames), redirectTargetsMap: host.redirectTargetsMap, getProjectReferenceRedirect: fileName => host.getProjectReferenceRedirect(fileName), - isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName) + isSourceOfProjectReferenceRedirect: fileName => host.isSourceOfProjectReferenceRedirect(fileName), + fileExists: fileName => host.fileExists(fileName), } : undefined }, encounteredError: false, visitedTypes: undefined, diff --git a/src/compiler/program.ts b/src/compiler/program.ts index b28ef578c6ebd..d06ee2cd0205a 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -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(), }; @@ -3483,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 @@ -3548,8 +3538,7 @@ namespace ts { originalRealpath.call(host.compilerHost, s); } - return onProgramCreateComplete; - + return { onProgramCreateComplete, fileExists }; function onProgramCreateComplete() { host.compilerHost.fileExists = originalFileExists; @@ -3558,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 ? diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e0809b5b0025c..9f3cd6dcb8627 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3287,6 +3287,10 @@ namespace ts { /*@internal*/ getProgramBuildInfo?(): ProgramBuildInfo | undefined; /*@internal*/ emitBuildInfo(writeFile?: WriteFileCallback, cancellationToken?: CancellationToken): EmitResult; /*@internal*/ getProbableSymlinks(): ReadonlyMap; + /** + * 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,12 +6432,10 @@ 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; - /* @internal */ getGlobalTypingsCacheLocation?(): string | undefined; getSourceFiles(): readonly SourceFile[]; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index ba41d2282c35e..2c012483322d7 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1705,7 +1705,7 @@ 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),