From 0ac96580d5ccd72df0786d991724232c3129008a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 18 Sep 2018 16:21:05 -0700 Subject: [PATCH] Resolve project references transitively --- src/compiler/commandLineParser.ts | 1 + src/compiler/program.ts | 161 +++++++++++------- src/compiler/types.ts | 3 +- src/server/project.ts | 8 +- src/testRunner/unittests/tsbuild.ts | 50 ++++-- .../reference/api/tsserverlibrary.d.ts | 7 +- tests/baselines/reference/api/typescript.d.ts | 3 +- tests/projects/transitiveReferences/a.ts | 1 + tests/projects/transitiveReferences/b.ts | 2 + tests/projects/transitiveReferences/c.ts | 2 + .../transitiveReferences/tsconfig.a.json | 1 + .../transitiveReferences/tsconfig.b.json | 1 + .../transitiveReferences/tsconfig.c.json | 1 + 13 files changed, 163 insertions(+), 78 deletions(-) create mode 100644 tests/projects/transitiveReferences/a.ts create mode 100644 tests/projects/transitiveReferences/b.ts create mode 100644 tests/projects/transitiveReferences/c.ts create mode 100644 tests/projects/transitiveReferences/tsconfig.a.json create mode 100644 tests/projects/transitiveReferences/tsconfig.b.json create mode 100644 tests/projects/transitiveReferences/tsconfig.c.json diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 58f1e8fdaa3..b1cc4ef5b27 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1293,6 +1293,7 @@ namespace ts { const result = parseJsonText(configFileName, configFileText); const cwd = host.getCurrentDirectory(); + result.path = toPath(configFileName, cwd, createGetCanonicalFileName(host.useCaseSensitiveFileNames)); return parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 17a10137d20..6dff51559b8 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -454,6 +454,8 @@ namespace ts { return false; } + let seenResolvedRefs: ResolvedProjectReference[] | undefined; + // If project references dont match if (!arrayIsEqualTo(program.getProjectReferences(), projectReferences, projectReferenceUptoDate)) { return false; @@ -496,11 +498,29 @@ namespace ts { if (!projectReferenceIsEqualTo(oldRef, newRef)) { return false; } - const oldResolvedRef = program!.getResolvedProjectReferences()![index]; + return resolvedProjectReferenceUptoDate(program!.getResolvedProjectReferences()![index], oldRef); + } + + function resolvedProjectReferenceUptoDate(oldResolvedRef: ResolvedProjectReference | undefined, oldRef: ProjectReference): boolean { if (oldResolvedRef) { + if (contains(seenResolvedRefs, oldResolvedRef)) { + // Assume true + return true; + } + // If sourceFile for the oldResolvedRef existed, check the version for uptodate - return sourceFileVersionUptoDate(oldResolvedRef.sourceFile); + if (!sourceFileVersionUptoDate(oldResolvedRef.sourceFile)) { + return false; + } + + // Add to seen before checking the referenced paths of this config file + (seenResolvedRefs || (seenResolvedRefs = [])).push(oldResolvedRef); + + // If child project references are upto date, this project reference is uptodate + return !forEach(oldResolvedRef.references, (childResolvedRef, index) => + !resolvedProjectReferenceUptoDate(childResolvedRef, oldResolvedRef.commandLine.projectReferences![index])); } + // In old program, not able to resolve project reference path, // so if config file doesnt exist, it is uptodate. return !fileExists(resolveProjectReferencePath(oldRef)); @@ -662,8 +682,8 @@ namespace ts { const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap() : undefined; // A parallel array to projectReferences storing the results of reading in the referenced tsconfig files - let resolvedProjectReferences: (ResolvedProjectReference | undefined)[] | undefined = projectReferences ? [] : undefined; - let projectReferenceRedirects: ParsedCommandLine[] | undefined; + let resolvedProjectReferences: ReadonlyArray | undefined; + let projectReferenceRedirects: Map | undefined; const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); const structuralIsReused = tryReuseStructureFromOldProgram(); @@ -672,16 +692,16 @@ namespace ts { processingOtherFiles = []; if (projectReferences) { - for (const ref of projectReferences) { - const parsedRef = parseProjectReferenceConfigFile(ref); - resolvedProjectReferences!.push(parsedRef); + if (!resolvedProjectReferences) { + resolvedProjectReferences = projectReferences.map(parseProjectReferenceConfigFile); + } + for (const parsedRef of resolvedProjectReferences) { if (parsedRef) { const out = parsedRef.commandLine.options.outFile || parsedRef.commandLine.options.out; if (out) { const dtsOutfile = changeExtension(out, ".d.ts"); processSourceFile(dtsOutfile, /*isDefaultLib*/ false, /*ignoreNoDefaultLib*/ false, /*packageId*/ undefined); } - addProjectReferenceRedirects(parsedRef.commandLine); } } } @@ -740,6 +760,12 @@ namespace ts { // unconditionally set oldProgram to undefined to prevent it from being captured in closure oldProgram = undefined; + // Do not use our own command line for projectReferenceRedirects + if (projectReferenceRedirects) { + Debug.assert(!!options.configFilePath); + const path = toPath(options.configFilePath!); + projectReferenceRedirects.delete(path); + } program = { getRootFileNames: () => rootNames, @@ -1001,6 +1027,48 @@ namespace ts { } } + function canReuseProjectReferences( + newProjectReferences: ReadonlyArray | undefined, + oldProjectReferences: ReadonlyArray | undefined, + oldResolvedReferences: ReadonlyArray | undefined): boolean { + // If array of references is changed, we cant resue old program + if (!arrayIsEqualTo(oldProjectReferences!, newProjectReferences, projectReferenceIsEqualTo)) { + return false; + } + + // Check the json files for the project references + if (newProjectReferences) { + // Resolved project referenced should be array if projectReferences provided are array + Debug.assert(!!oldResolvedReferences); + for (let i = 0; i < newProjectReferences.length; i++) { + const oldRef = oldResolvedReferences![i]; + const newRef = parseProjectReferenceConfigFile(newProjectReferences[i]); + if (oldRef) { + if (!newRef || newRef.sourceFile !== oldRef.sourceFile) { + // Resolved project reference has gone missing or changed + return false; + } + + // If the transitive references can be reused then only this reference can be reused + if (!canReuseProjectReferences(newRef.commandLine.projectReferences, oldRef.commandLine.projectReferences, oldRef.references)) { + return false; + } + } + else { + // A previously-unresolved reference may be resolved now + if (newRef !== undefined) { + return false; + } + } + } + } + else { + // Resolved project referenced should be undefined if projectReferences is undefined + Debug.assert(!oldResolvedReferences); + } + return true; + } + function tryReuseStructureFromOldProgram(): StructureIsReused { if (!oldProgram) { return StructureIsReused.Not; @@ -1026,39 +1094,10 @@ namespace ts { } // Check if any referenced project tsconfig files are different - - // If array of references is changed, we cant resue old program - const oldProjectReferences = oldProgram.getProjectReferences(); - if (!arrayIsEqualTo(oldProjectReferences!, projectReferences, projectReferenceIsEqualTo)) { + if (!canReuseProjectReferences(projectReferences, oldProgram.getProjectReferences(), oldProgram.getResolvedProjectReferences())) { return oldProgram.structureIsReused = StructureIsReused.Not; } - - // Check the json files for the project references - const oldRefs = oldProgram.getResolvedProjectReferences(); - if (projectReferences) { - // Resolved project referenced should be array if projectReferences provided are array - Debug.assert(!!oldRefs); - for (let i = 0; i < projectReferences.length; i++) { - const oldRef = oldRefs![i]; - const newRef = parseProjectReferenceConfigFile(projectReferences[i]); - if (oldRef) { - if (!newRef || newRef.sourceFile !== oldRef.sourceFile) { - // Resolved project reference has gone missing or changed - return oldProgram.structureIsReused = StructureIsReused.Not; - } - } - else { - // A previously-unresolved reference may be resolved now - if (newRef !== undefined) { - return oldProgram.structureIsReused = StructureIsReused.Not; - } - } - } - } - else { - // Resolved project referenced should be undefined if projectReferences is undefined - Debug.assert(!oldRefs); - } + resolvedProjectReferences = oldProgram.getResolvedProjectReferences(); // check if program source files has changed in the way that can affect structure of the program const newSourceFiles: SourceFile[] = []; @@ -1248,14 +1287,6 @@ namespace ts { fileProcessingDiagnostics.reattachFileDiagnostics(modifiedFile.newFile); } resolvedTypeReferenceDirectives = oldProgram.getResolvedTypeReferenceDirectives(); - resolvedProjectReferences = oldProgram.getResolvedProjectReferences(); - if (resolvedProjectReferences) { - resolvedProjectReferences.forEach(ref => { - if (ref) { - addProjectReferenceRedirects(ref.commandLine); - } - }); - } sourceFileToPackageName = oldProgram.sourceFileToPackageName; redirectTargetsMap = oldProgram.redirectTargetsMap; @@ -2182,22 +2213,22 @@ namespace ts { function getProjectReferenceRedirect(fileName: string): string | undefined { // Ignore dts or any of the non ts files - if (!projectReferenceRedirects || fileExtensionIs(fileName, Extension.Dts) || !fileExtensionIsOneOf(fileName, supportedTSExtensions)) { + if (!resolvedProjectReferences || !resolvedProjectReferences.length || fileExtensionIs(fileName, Extension.Dts) || !fileExtensionIsOneOf(fileName, supportedTSExtensions)) { return undefined; } // If this file is produced by a referenced project, we need to rewrite it to // look in the output folder of the referenced project rather than the input - return forEach(projectReferenceRedirects, referencedProject => { + return forEachEntry(projectReferenceRedirects!, referencedProject => { // not input file from the referenced project, ignore - if (!contains(referencedProject.fileNames, fileName, isSameFile)) { + if (!referencedProject || !contains(referencedProject.commandLine.fileNames, fileName, isSameFile)) { return undefined; } - const out = referencedProject.options.outFile || referencedProject.options.out; + const out = referencedProject.commandLine.options.outFile || referencedProject.commandLine.options.out; return out ? changeExtension(out, Extension.Dts) : - getOutputDeclarationFileName(fileName, referencedProject); + getOutputDeclarationFileName(fileName, referencedProject.commandLine); }); } @@ -2388,22 +2419,34 @@ namespace ts { return allFilesBelongToPath; } - function parseProjectReferenceConfigFile(ref: ProjectReference): { commandLine: ParsedCommandLine, sourceFile: SourceFile } | undefined { + function parseProjectReferenceConfigFile(ref: ProjectReference): ResolvedProjectReference | undefined { + if (!projectReferenceRedirects) { + projectReferenceRedirects = createMap(); + } + // The actual filename (i.e. add "/tsconfig.json" if necessary) const refPath = resolveProjectReferencePath(ref); + const sourceFilePath = toPath(refPath); + const fromCache = projectReferenceRedirects.get(sourceFilePath); + if (fromCache !== undefined) { + return fromCache || undefined; + } + // An absolute path pointing to the containing directory of the config file const basePath = getNormalizedAbsolutePath(getDirectoryPath(refPath), host.getCurrentDirectory()); const sourceFile = host.getSourceFile(refPath, ScriptTarget.JSON) as JsonSourceFile | undefined; if (sourceFile === undefined) { + projectReferenceRedirects.set(sourceFilePath, false); return undefined; } - sourceFile.path = toPath(refPath); + sourceFile.path = sourceFilePath; const commandLine = parseJsonSourceFileConfigFileContent(sourceFile, configParsingHost, basePath, /*existingOptions*/ undefined, refPath); - return { commandLine, sourceFile }; - } - - function addProjectReferenceRedirects(referencedProject: ParsedCommandLine) { - (projectReferenceRedirects || (projectReferenceRedirects = [])).push(referencedProject); + const resolvedRef: ResolvedProjectReference = { commandLine, sourceFile }; + projectReferenceRedirects.set(sourceFilePath, resolvedRef); + if (commandLine.projectReferences) { + resolvedRef.references = commandLine.projectReferences.map(parseProjectReferenceConfigFile); + } + return resolvedRef; } function verifyCompilerOptions() { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a091228813d..bc35588488c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2829,7 +2829,7 @@ namespace ts { /* @internal */ getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined; getProjectReferences(): ReadonlyArray | undefined; - getResolvedProjectReferences(): (ResolvedProjectReference | undefined)[] | undefined; + getResolvedProjectReferences(): ReadonlyArray | undefined; /*@internal*/ getProjectReferenceRedirect(fileName: string): string | undefined; } @@ -2839,6 +2839,7 @@ namespace ts { export interface ResolvedProjectReference { commandLine: ParsedCommandLine; sourceFile: SourceFile; + references?: ReadonlyArray; } /* @internal */ diff --git a/src/server/project.ts b/src/server/project.ts index 8689c5e7e3d..164f0fe7c34 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -281,8 +281,8 @@ namespace ts.server { return this.projectStateVersion.toString(); } - getProjectReferences(): ReadonlyArray { - return emptyArray; + getProjectReferences(): ReadonlyArray | undefined { + return undefined; } getScriptFileNames() { @@ -1391,8 +1391,8 @@ namespace ts.server { return asNormalizedPath(this.getProjectName()); } - getProjectReferences(): ReadonlyArray { - return this.projectReferences || emptyArray; + getProjectReferences(): ReadonlyArray | undefined { + return this.projectReferences; } updateReferences(refs: ReadonlyArray | undefined) { diff --git a/src/testRunner/unittests/tsbuild.ts b/src/testRunner/unittests/tsbuild.ts index 36feb68a223..bb219ced37c 100644 --- a/src/testRunner/unittests/tsbuild.ts +++ b/src/testRunner/unittests/tsbuild.ts @@ -321,16 +321,6 @@ export class cNew {}`); "/src/tests/index.ts" ]); - function getLibs() { - return [ - "/lib/lib.d.ts", - "/lib/lib.es5.d.ts", - "/lib/lib.dom.d.ts", - "/lib/lib.webworker.importscripts.d.ts", - "/lib/lib.scripthost.d.ts" - ]; - } - function getCoreOutputs() { return [ "/src/core/index.d.ts", @@ -376,6 +366,36 @@ export class cNew {}`); } }); }); + + describe("tsbuild - when project reference is referenced transitively", () => { + const projFs = loadProjectFromDisk("tests/projects/transitiveReferences"); + const allExpectedOutputs = [ + "/src/a.js", "/src/a.d.ts", + "/src/b.js", "/src/b.d.ts", + "/src/c.js" + ]; + it("verify that it builds correctly", () => { + const fs = projFs.shadow(); + const host = new fakes.SolutionBuilderHost(fs); + const builder = createSolutionBuilder(host, ["/src/tsconfig.c.json"], { listFiles: true }); + builder.buildAllProjects(); + host.assertDiagnosticMessages(/*empty*/); + for (const output of allExpectedOutputs) { + assert(fs.existsSync(output), `Expect file ${output} to exist`); + } + assert.deepEqual(host.traces, [ + ...getLibs(), + "/src/a.ts", + ...getLibs(), + "/src/a.d.ts", + "/src/b.ts", + ...getLibs(), + "/src/a.d.ts", + "/src/b.d.ts", + "/src/c.ts" + ]); + }); + }); } export namespace OutFile { @@ -584,4 +604,14 @@ export class cNew {}`); fs.makeReadonly(); return fs; } + + function getLibs() { + return [ + "/lib/lib.d.ts", + "/lib/lib.es5.d.ts", + "/lib/lib.dom.d.ts", + "/lib/lib.webworker.importscripts.d.ts", + "/lib/lib.scripthost.d.ts" + ]; + } } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index a90f40f9ac7..942b3978e75 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1812,11 +1812,12 @@ declare namespace ts { isSourceFileFromExternalLibrary(file: SourceFile): boolean; isSourceFileDefaultLibrary(file: SourceFile): boolean; getProjectReferences(): ReadonlyArray | undefined; - getResolvedProjectReferences(): (ResolvedProjectReference | undefined)[] | undefined; + getResolvedProjectReferences(): ReadonlyArray | undefined; } interface ResolvedProjectReference { commandLine: ParsedCommandLine; sourceFile: SourceFile; + references?: ReadonlyArray; } interface CustomTransformers { /** Custom transformers to evaluate before built-in .js transformations. */ @@ -8114,7 +8115,7 @@ declare namespace ts.server { getCompilerOptions(): CompilerOptions; getNewLine(): string; getProjectVersion(): string; - getProjectReferences(): ReadonlyArray; + getProjectReferences(): ReadonlyArray | undefined; getScriptFileNames(): string[]; private getOrCreateScriptInfoAndAttachToProject; getScriptKind(fileName: string): ScriptKind; @@ -8231,7 +8232,7 @@ declare namespace ts.server { */ updateGraph(): boolean; getConfigFilePath(): NormalizedPath; - getProjectReferences(): ReadonlyArray; + getProjectReferences(): ReadonlyArray | undefined; updateReferences(refs: ReadonlyArray | undefined): void; enablePlugins(): void; /** diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d6bd508999d..ee9ce336ad9 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1812,11 +1812,12 @@ declare namespace ts { isSourceFileFromExternalLibrary(file: SourceFile): boolean; isSourceFileDefaultLibrary(file: SourceFile): boolean; getProjectReferences(): ReadonlyArray | undefined; - getResolvedProjectReferences(): (ResolvedProjectReference | undefined)[] | undefined; + getResolvedProjectReferences(): ReadonlyArray | undefined; } interface ResolvedProjectReference { commandLine: ParsedCommandLine; sourceFile: SourceFile; + references?: ReadonlyArray; } interface CustomTransformers { /** Custom transformers to evaluate before built-in .js transformations. */ diff --git a/tests/projects/transitiveReferences/a.ts b/tests/projects/transitiveReferences/a.ts new file mode 100644 index 00000000000..56d3f8e39a0 --- /dev/null +++ b/tests/projects/transitiveReferences/a.ts @@ -0,0 +1 @@ +export class A {} diff --git a/tests/projects/transitiveReferences/b.ts b/tests/projects/transitiveReferences/b.ts new file mode 100644 index 00000000000..619dd224835 --- /dev/null +++ b/tests/projects/transitiveReferences/b.ts @@ -0,0 +1,2 @@ +import {A} from './a'; +export const b = new A(); diff --git a/tests/projects/transitiveReferences/c.ts b/tests/projects/transitiveReferences/c.ts new file mode 100644 index 00000000000..b436b3db99c --- /dev/null +++ b/tests/projects/transitiveReferences/c.ts @@ -0,0 +1,2 @@ +import {b} from './b'; +console.log(b); \ No newline at end of file diff --git a/tests/projects/transitiveReferences/tsconfig.a.json b/tests/projects/transitiveReferences/tsconfig.a.json new file mode 100644 index 00000000000..74fe1ef8b56 --- /dev/null +++ b/tests/projects/transitiveReferences/tsconfig.a.json @@ -0,0 +1 @@ +{"compilerOptions": {"composite": true}, "files": ["a.ts"]} diff --git a/tests/projects/transitiveReferences/tsconfig.b.json b/tests/projects/transitiveReferences/tsconfig.b.json new file mode 100644 index 00000000000..f4734997f73 --- /dev/null +++ b/tests/projects/transitiveReferences/tsconfig.b.json @@ -0,0 +1 @@ +{"compilerOptions": {"composite": true}, "files": ["b.ts"], "references": [{"path": "tsconfig.a.json"}]} diff --git a/tests/projects/transitiveReferences/tsconfig.c.json b/tests/projects/transitiveReferences/tsconfig.c.json new file mode 100644 index 00000000000..1f919a04cd9 --- /dev/null +++ b/tests/projects/transitiveReferences/tsconfig.c.json @@ -0,0 +1 @@ +{"files": ["c.ts"], "references": [{"path": "tsconfig.b.json"}]}