diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 1789b161e39..080b17b8cbe 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -787,7 +787,13 @@ namespace ts { // Key is a file name. Value is the (non-empty, or undefined) list of files that redirect to it. let redirectTargetsMap = createMultiMap(); - const filesByName = createMap(); + /** + * map with + * - SourceFile if present + * - false if sourceFile missing for source of project reference redirect + * - undefined otherwise + */ + const filesByName = createMap(); let missingFilePaths: ReadonlyArray | undefined; // stores 'filename -> file association' ignoring case // used to track cases when two file names differ only in casing @@ -854,7 +860,7 @@ namespace ts { } } - missingFilePaths = arrayFrom(filesByName.keys(), p => p).filter(p => !filesByName.get(p)); + missingFilePaths = arrayFrom(mapDefinedIterator(filesByName.entries(), ([path, file]) => file === undefined ? path as Path : undefined)); files = stableSort(processingDefaultLibFiles, compareDefaultLibFiles).concat(processingOtherFiles); processingDefaultLibFiles = undefined; processingOtherFiles = undefined; @@ -1567,7 +1573,7 @@ namespace ts { } function getSourceFileByPath(path: Path): SourceFile | undefined { - return filesByName.get(path); + return filesByName.get(path) || undefined; } function getDiagnosticsHelper( @@ -2104,7 +2110,7 @@ namespace ts { /** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */ function getSourceFileFromReference(referencingFile: SourceFile, ref: FileReference): SourceFile | undefined { - return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), fileName => filesByName.get(toPath(fileName))); + return getSourceFileFromReferenceWorker(resolveTripleslashReference(ref.fileName, referencingFile.fileName), fileName => filesByName.get(toPath(fileName)) || undefined); } function getSourceFileFromReferenceWorker( @@ -2235,7 +2241,7 @@ namespace ts { } } - return file; + return file || undefined; } let redirectedPath: Path | undefined; @@ -2327,9 +2333,12 @@ namespace ts { } function addFileToFilesByName(file: SourceFile | undefined, path: Path, redirectedPath: Path | undefined) { - filesByName.set(path, file); if (redirectedPath) { filesByName.set(redirectedPath, file); + filesByName.set(path, file || false); + } + else { + filesByName.set(path, file); } } diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index b5909d3b3d5..ba7c7761886 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -10692,7 +10692,11 @@ export function fn5() { } const mainTs: File = { path: `${mainLocation}/main.ts`, content: `import { - fn1, fn2, fn3, fn4, fn5 + fn1, + fn2, + fn3, + fn4, + fn5 } from '../dependency/fns' fn1(); @@ -10739,9 +10743,9 @@ fn5(); } // Returns request and expected Response, expected response when no map file - type SessionAction = [Partial, Response, Response?]; + type SessionAction = [Partial, Response, Response?, Response?]; function gotoDefintinionFromMainTs(fn: number): SessionAction { - const startSpan = { line: fn + 4, offset: 1 }; + const startSpan = { line: fn + 8, offset: 1 }; const textSpan: protocol.TextSpan = { start: startSpan, end: { line: startSpan.line, offset: startSpan.offset + 3 } }; const definitionSpan: protocol.FileSpan = { file: dependencyTs.path, start: { line: fn, offset: 17 }, end: { line: fn, offset: 20 } }; const declareSpaceLength = "declare ".length; @@ -10751,12 +10755,19 @@ fn5(); arguments: { file: mainTs.path, ...startSpan } }, { + // To dependency definitions: [definitionSpan], textSpan }, { + // To the dts definitions: [{ file: dtsPath, start: { line: fn, offset: definitionSpan.start.offset + declareSpaceLength }, end: { line: fn, offset: definitionSpan.end.offset + declareSpaceLength } }], textSpan + }, + { + // To import declaration + definitions: [{ file: mainTs.path, start: { line: fn + 1, offset: 5 }, end: { line: fn + 1, offset: 8 } }], + textSpan } ]; } @@ -10816,11 +10827,16 @@ fn5(); return { host, session }; } - function checkProject(session: TestSession) { + function checkProject(session: TestSession, noDts?: true) { const service = session.getProjectService(); checkNumberOfProjects(service, { configuredProjects: 1 + verifier.length }); configFiles.forEach((configFile, index) => { - checkProjectActualFiles(service.configuredProjects.get(configFile)!, expectedProjectActualFiles[index]); + checkProjectActualFiles( + service.configuredProjects.get(configFile)!, + noDts ? + expectedProjectActualFiles[index].filter(f => f.toLowerCase() !== dtsPath) : + expectedProjectActualFiles[index] + ); }); } @@ -10839,6 +10855,21 @@ fn5(); ); } + function verifyInfosWhenNoDtsFile(session: TestSession, host: TestServerHost) { + const dtsMapClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsMapPath ? f : undefined); + const dtsClosedInfo = firstDefined(closedInfos, f => f.toLowerCase() === dtsPath ? f : undefined); + verifyInfosWithRandom( + session, + host, + openInfos, + closedInfos.filter(f => f !== dtsMapClosedInfo && f !== dtsClosedInfo && f !== dependencyTs.path), + // When project actual file contains dts, it needs to be watched + dtsClosedInfo && verifier.length === 1 && expectedProjectActualFiles[0].some(f => f.toLowerCase() === dtsPath) ? + otherWatchedFiles.concat(dtsClosedInfo) : + otherWatchedFiles + ); + } + function verifyDocumentPositionMapper(session: TestSession, dependencyMap: server.ScriptInfo, documentPositionMapper: server.ScriptInfo["documentPositionMapper"], notEqual?: true) { assert.strictEqual(session.getProjectService().filenameToScriptInfo.get(dtsMapPath), dependencyMap); if (notEqual) { @@ -10850,24 +10881,29 @@ fn5(); } function action(actionGetter: SessionActionGetter, fn: number, session: TestSession) { - const [req, expectedResponse, expectedNoMapResponse] = actionGetter(fn); + const [req, expectedResponse, expectedNoMapResponse, expectedNoDtsResponse] = actionGetter(fn); const { response } = session.executeCommandSeq(req); - return { response, expectedResponse, expectedNoMapResponse }; + return { response, expectedResponse, expectedNoMapResponse, expectedNoDtsResponse }; } function firstAction(session: TestSession) { actionGetters.forEach(actionGetter => action(actionGetter, 1, session)); } - function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType, dtsInfo: server.ScriptInfo, isFirst: boolean) => void) { + function verifyAllFnActionWorker(session: TestSession, verifyAction: (result: ReturnType, dtsInfo: server.ScriptInfo | undefined, isFirst: boolean) => void, dtsAbsent?: true) { // action let isFirst = true; for (const actionGetter of actionGetters) { for (let fn = 1; fn <= 5; fn++) { const result = action(actionGetter, fn, session); const dtsInfo = session.getProjectService().filenameToScriptInfo.get(dtsPath); - assert.isDefined(dtsInfo); - verifyAction(result, dtsInfo!, isFirst); + if (dtsAbsent) { + assert.isUndefined(dtsInfo); + } + else { + assert.isDefined(dtsInfo); + } + verifyAction(result, dtsInfo, isFirst); isFirst = false; } } @@ -10884,7 +10920,7 @@ fn5(); verifyAllFnActionWorker(session, ({ response, expectedResponse }, dtsInfo, isFirst) => { assert.deepEqual(response, expectedResponse); verifyInfos(session, host); - assert.equal(dtsInfo.sourceMapFilePath, dtsMapPath); + assert.equal(dtsInfo!.sourceMapFilePath, dtsMapPath); if (isFirst) { if (dependencyMap) { verifyDocumentPositionMapper(session, dependencyMap, documentPositionMapper, firstDocumentPositionMapperNotEquals); @@ -10910,22 +10946,33 @@ fn5(); let sourceMapFilePath: server.ScriptInfo["sourceMapFilePath"]; // action verifyAllFnActionWorker(session, ({ response, expectedResponse, expectedNoMapResponse }, dtsInfo, isFirst) => { - assert.deepEqual(response, expectedNoMapResponse && verifier.length ? expectedNoMapResponse : expectedResponse); + assert.deepEqual(response, expectedNoMapResponse || expectedResponse); verifyInfosWhenNoMapFile(session, host, dependencyTsOK); assert.isUndefined(session.getProjectService().filenameToScriptInfo.get(dtsMapPath)); if (isFirst) { - assert.isNotString(dtsInfo.sourceMapFilePath); - assert.isNotFalse(dtsInfo.sourceMapFilePath); - assert.isDefined(dtsInfo.sourceMapFilePath); - sourceMapFilePath = dtsInfo.sourceMapFilePath; + assert.isNotString(dtsInfo!.sourceMapFilePath); + assert.isNotFalse(dtsInfo!.sourceMapFilePath); + assert.isDefined(dtsInfo!.sourceMapFilePath); + sourceMapFilePath = dtsInfo!.sourceMapFilePath; } else { - assert.equal(dtsInfo.sourceMapFilePath, sourceMapFilePath); + assert.equal(dtsInfo!.sourceMapFilePath, sourceMapFilePath); } }); return sourceMapFilePath; } + function verifyAllFnActionWithNoDts( + session: TestSession, + host: TestServerHost + ) { + // action + verifyAllFnActionWorker(session, ({ response, expectedResponse, expectedNoDtsResponse }) => { + assert.deepEqual(response, expectedNoDtsResponse || expectedResponse); + verifyInfosWhenNoDtsFile(session, host); + }, /*dtsAbsent*/ true); + } + function verifyScenarioWithChangesWorker( change: (host: TestServerHost, session: TestSession) => void, afterActionDocumentPositionMapperNotEquals: true | undefined, @@ -10996,6 +11043,21 @@ fn5(); verifyOnlyRandomInfos(session, host); } + function verifyMainScenarioAndScriptInfoCollectionWithNoDts(session: TestSession, host: TestServerHost) { + // Main scenario action + verifyAllFnActionWithNoDts(session, host); + + // Collecting at this point retains dependency.d.ts and map watcher + closeFilesForSession([randomFile], session); + openFilesForSession([randomFile], session); + verifyInfosWhenNoDtsFile(session, host); + + // Closing open file, removes dependencies too + closeFilesForSession([...openFiles, randomFile], session); + openFilesForSession([randomFile], session); + verifyOnlyRandomInfos(session, host); + } + it(mainScenario, () => { const { host, session } = openTsFile(); checkProject(session); @@ -11060,13 +11122,22 @@ fn5(); verifyMainScenarioAndScriptInfoCollectionWithNoMap(session, host, /*dependencyTsOKInScenario*/ true); }); }); + + describe("when .d.ts file is not present", () => { + it(mainScenario, () => { + const { host, session } = openTsFile(host => host.deleteFile(dtsLocation)); + checkProject(session, /*noDts*/ true); + + verifyMainScenarioAndScriptInfoCollectionWithNoDts(session, host); + }); + }); } const usageVerifier: DocumentPositionMapperVerifier = [ /*openFile*/ mainTs, /*expectedProjectActualFiles*/[mainTs.path, libFile.path, mainConfig.path, dtsPath], /*actionGetter*/ gotoDefintinionFromMainTs, - /*openFileLastLine*/ 10 + /*openFileLastLine*/ 14 ]; describe("from project that uses dependency", () => { const closedInfos = [dependencyTs.path, dependencyConfig.path, libFile.path, dtsPath, dtsMapLocation];