diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index a79070dde80f7..72d2213f6df75 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -308,7 +308,7 @@ namespace ts.server { } interface AssignProjectResult extends OpenConfiguredProjectResult { - defaultConfigProject: ConfiguredProject | undefined; + retainProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined; } interface FilePropertyReader { @@ -424,7 +424,52 @@ namespace ts.server { return !!(infoOrFileNameOrConfig as AncestorConfigFileInfo).configFileInfo; } - function forEachResolvedProjectReference( + /*@internal*/ + /** Kind of operation to perform to get project reference project */ + export enum ProjectReferenceProjectLoadKind { + /** Find existing project for project reference */ + Find, + /** Find existing project or create one for the project reference */ + FindCreate, + /** Find existing project or create and load it for the project reference */ + FindCreateLoad + } + + /*@internal*/ + export function forEachResolvedProjectReferenceProject( + project: ConfiguredProject, + cb: (child: ConfiguredProject, configFileName: NormalizedPath) => T | undefined, + projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind.Find | ProjectReferenceProjectLoadKind.FindCreate + ): T | undefined; + /*@internal*/ + export function forEachResolvedProjectReferenceProject( + project: ConfiguredProject, + cb: (child: ConfiguredProject, configFileName: NormalizedPath) => T | undefined, + projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind.FindCreateLoad, + reason: string + ): T | undefined; + export function forEachResolvedProjectReferenceProject( + project: ConfiguredProject, + cb: (child: ConfiguredProject, configFileName: NormalizedPath) => T | undefined, + projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind, + reason?: string + ): T | undefined { + return forEachResolvedProjectReference(project, ref => { + if (!ref) return undefined; + const configFileName = toNormalizedPath(ref.sourceFile.fileName); + const child = project.projectService.findConfiguredProjectByProjectName(configFileName) || ( + projectReferenceProjectLoadKind === ProjectReferenceProjectLoadKind.FindCreate ? + project.projectService.createConfiguredProject(configFileName) : + projectReferenceProjectLoadKind === ProjectReferenceProjectLoadKind.FindCreateLoad ? + project.projectService.createAndLoadConfiguredProject(configFileName, reason!) : + undefined + ); + return child && cb(child, configFileName); + }); + } + + /*@internal*/ + export function forEachResolvedProjectReference( project: ConfiguredProject, cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined ): T | undefined { @@ -486,6 +531,13 @@ namespace ts.server { return !info.isScriptOpen() && info.mTime !== undefined; } + /*@internal*/ + /** true if script info is part of project and is not in project because it is referenced from project reference source */ + export function projectContainsInfoDirectly(project: Project, info: ScriptInfo) { + return project.containsScriptInfo(info) && + !project.isSourceOfProjectReferenceRedirect(info.path); + } + /*@internal*/ export function updateProjectIfDirty(project: Project) { return project.dirty && project.updateGraph(); @@ -1686,8 +1738,12 @@ namespace ts.server { findDefaultConfiguredProject(info: ScriptInfo) { if (!info.isScriptOpen()) return undefined; const configFileName = this.getConfigFileNameForFile(info); - return configFileName && + const project = configFileName && this.findConfiguredProjectByProjectName(configFileName); + + return project?.isSolution() ? + project.getDefaultChildProjectFromSolution(info) : + project; } /** @@ -1737,7 +1793,8 @@ namespace ts.server { this.logger.endGroup(); } - private findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined { + /*@internal*/ + findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined { // make sure that casing of config file name is consistent const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName)); return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath); @@ -1871,7 +1928,8 @@ namespace ts.server { project.setTypeAcquisition(typeAcquisition); } - private createConfiguredProject(configFileName: NormalizedPath) { + /* @internal */ + createConfiguredProject(configFileName: NormalizedPath) { const cachedDirectoryStructureHost = createCachedDirectoryStructureHost(this.host, this.host.getCurrentDirectory(), this.host.useCaseSensitiveFileNames)!; // TODO: GH#18217 this.logger.info(`Opened configuration file ${configFileName}`); const project = new ConfiguredProject( @@ -1895,7 +1953,7 @@ namespace ts.server { } /* @internal */ - private createAndLoadConfiguredProject(configFileName: NormalizedPath, reason: string) { + createAndLoadConfiguredProject(configFileName: NormalizedPath, reason: string) { const project = this.createConfiguredProject(configFileName); this.loadConfiguredProject(project, reason); return project; @@ -2713,7 +2771,8 @@ namespace ts.server { const configFileName = this.getConfigFileNameForFile(info); if (configFileName) { const project = this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProject(configFileName); - if (!updatedProjects.has(configFileName)) { + if (!updatedProjects.has(project.canonicalConfigFilePath)) { + updatedProjects.set(project.canonicalConfigFilePath, true); if (delayReload) { project.pendingReload = ConfigFileProgramReloadLevel.Full; project.pendingReloadReason = reason; @@ -2722,8 +2781,21 @@ namespace ts.server { else { // reload from the disk this.reloadConfiguredProject(project, reason); + // If this is solution, reload the project till the reloaded project contains the script info directly + if (!project.containsScriptInfo(info) && project.isSolution()) { + forEachResolvedProjectReferenceProject( + project, + child => { + if (!updatedProjects.has(child.canonicalConfigFilePath)) { + updatedProjects.set(child.canonicalConfigFilePath, true); + this.reloadConfiguredProject(child, reason); + } + return projectContainsInfoDirectly(child, info); + }, + ProjectReferenceProjectLoadKind.FindCreate + ); + } } - updatedProjects.set(configFileName, true); } } }); @@ -2810,10 +2882,26 @@ namespace ts.server { const configFileName = this.getConfigFileNameForFile(originalFileInfo); if (!configFileName) return undefined; - const configuredProject = this.findConfiguredProjectByProjectName(configFileName) || + let configuredProject: ConfiguredProject | undefined = this.findConfiguredProjectByProjectName(configFileName) || this.createAndLoadConfiguredProject(configFileName, `Creating project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}`); - if (configuredProject === project) return originalLocation; updateProjectIfDirty(configuredProject); + + if (configuredProject.isSolution()) { + // Find the project that is referenced from this solution that contains the script info directly + configuredProject = forEachResolvedProjectReferenceProject( + configuredProject, + child => { + updateProjectIfDirty(child); + const info = this.getScriptInfo(fileName); + return info && projectContainsInfoDirectly(child, info) ? child : undefined; + }, + ProjectReferenceProjectLoadKind.FindCreateLoad, + `Creating project referenced in solution ${configuredProject.projectName} to find possible configured project for original file: ${originalFileInfo.fileName}${location !== originalLocation ? " for location: " + location.fileName : ""}` + ); + if (!configuredProject) return undefined; + if (configuredProject === project) return originalLocation; + } + // Keep this configured project as referenced from project addOriginalConfiguredProject(configuredProject); @@ -2860,6 +2948,7 @@ namespace ts.server { let configFileErrors: readonly Diagnostic[] | undefined; let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info); let defaultConfigProject: ConfiguredProject | undefined; + let retainProjects: ConfiguredProject[] | ConfiguredProject | undefined; if (!project && !this.syntaxOnly) { // Checking syntaxOnly is an optimization configFileName = this.getConfigFileNameForFile(info); if (configFileName) { @@ -2880,9 +2969,41 @@ namespace ts.server { // Ensure project is ready to check if it contains opened script info updateProjectIfDirty(project); } + defaultConfigProject = project; - // Create ancestor configured project - this.createAncestorProjects(info, defaultConfigProject); + retainProjects = defaultConfigProject; + + // If this configured project doesnt contain script info but + // it is solution with project references, try those project references + if (!project.containsScriptInfo(info) && project.isSolution()) { + forEachResolvedProjectReferenceProject( + project, + (child, childConfigFileName) => { + updateProjectIfDirty(child); + // Retain these projects + if (!isArray(retainProjects)) { + retainProjects = [project as ConfiguredProject, child]; + } + else { + retainProjects.push(child); + } + + // If script info belongs to this child project, use this as default config project + if (projectContainsInfoDirectly(child, info)) { + configFileName = childConfigFileName; + configFileErrors = child.getAllProjectErrors(); + this.sendConfigFileDiagEvent(child, info.fileName); + return child; + } + }, + ProjectReferenceProjectLoadKind.FindCreateLoad, + `Creating project referenced in solution ${project.projectName} to find possible configured project for ${info.fileName} to open` + ); + } + else { + // Create ancestor configured project + this.createAncestorProjects(info, defaultConfigProject || project); + } } } @@ -2902,7 +3023,7 @@ namespace ts.server { this.assignOrphanScriptInfoToInferredProject(info, this.openFiles.get(info.path)); } Debug.assert(!info.isOrphan()); - return { configFileName, configFileErrors, defaultConfigProject }; + return { configFileName, configFileErrors, retainProjects }; } private createAncestorProjects(info: ScriptInfo, project: ConfiguredProject) { @@ -2953,7 +3074,10 @@ namespace ts.server { if (forEachPotentialProjectReference( project, potentialRefPath => forProjects!.has(potentialRefPath) - )) { + ) || (project.isSolution() && forEachResolvedProjectReference( + project, + (_ref, resolvedPath) => forProjects!.has(resolvedPath) + ))) { // Load children this.ensureProjectChildren(project, seenProjects); } @@ -2966,19 +3090,15 @@ namespace ts.server { updateProjectIfDirty(project); // Create tree because project is uptodate we only care of resolved references - forEachResolvedProjectReference( + forEachResolvedProjectReferenceProject( project, - ref => { - if (!ref) return; - const configFileName = toNormalizedPath(ref.sourceFile.fileName); - const child = this.findConfiguredProjectByProjectName(configFileName) || - this.createAndLoadConfiguredProject(configFileName, `Creating project for reference of project: ${project.projectName}`); - this.ensureProjectChildren(child, seenProjects); - } + child => this.ensureProjectChildren(child, seenProjects), + ProjectReferenceProjectLoadKind.FindCreateLoad, + `Creating project for reference of project: ${project.projectName}` ); } - private cleanupAfterOpeningFile(toRetainConfigProjects: ConfiguredProject[] | ConfiguredProject | undefined) { + private cleanupAfterOpeningFile(toRetainConfigProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) { // This was postponed from closeOpenFile to after opening next file, // so that we can reuse the project if we need to right away this.removeOrphanConfiguredProjects(toRetainConfigProjects); @@ -3000,14 +3120,14 @@ namespace ts.server { openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult { const info = this.getOrCreateOpenScriptInfo(fileName, fileContent, scriptKind, hasMixedContent, projectRootPath); - const { defaultConfigProject, ...result } = this.assignProjectToOpenedScriptInfo(info); - this.cleanupAfterOpeningFile(defaultConfigProject); + const { retainProjects, ...result } = this.assignProjectToOpenedScriptInfo(info); + this.cleanupAfterOpeningFile(retainProjects); this.telemetryOnOpenFile(info); this.printProjects(); return result; } - private removeOrphanConfiguredProjects(toRetainConfiguredProjects: ConfiguredProject[] | ConfiguredProject | undefined) { + private removeOrphanConfiguredProjects(toRetainConfiguredProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined) { const toRemoveConfiguredProjects = cloneMap(this.configuredProjects); const markOriginalProjectsAsUsed = (project: Project) => { if (!project.isOrphan() && project.originalConfiguredProjects) { @@ -3206,9 +3326,9 @@ namespace ts.server { } // All the script infos now exist, so ok to go update projects for open files - let defaultConfigProjects: ConfiguredProject[] | undefined; + let retainProjects: readonly ConfiguredProject[] | undefined; if (openScriptInfos) { - defaultConfigProjects = mapDefined(openScriptInfos, info => this.assignProjectToOpenedScriptInfo(info).defaultConfigProject); + retainProjects = flatMap(openScriptInfos, info => this.assignProjectToOpenedScriptInfo(info).retainProjects); } // While closing files there could be open files that needed assigning new inferred projects, do it now @@ -3218,7 +3338,7 @@ namespace ts.server { if (openScriptInfos) { // Cleanup projects - this.cleanupAfterOpeningFile(defaultConfigProjects); + this.cleanupAfterOpeningFile(retainProjects); // Telemetry openScriptInfos.forEach(info => this.telemetryOnOpenFile(info)); this.printProjects(); diff --git a/src/server/project.ts b/src/server/project.ts index 05f73e4f07ce0..918f3b240f84c 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -2164,6 +2164,25 @@ namespace ts.server { this.externalProjectRefCount--; } + /* @internal */ + isSolution() { + return this.getRootFilesMap().size === 0 && + !this.canConfigFileJsonReportNoInputFiles; + } + + /* @internal */ + /** Find the configured project from the project references in this solution which contains the info directly */ + getDefaultChildProjectFromSolution(info: ScriptInfo) { + Debug.assert(this.isSolution()); + return forEachResolvedProjectReferenceProject( + this, + child => projectContainsInfoDirectly(child, info) ? + child : + undefined, + ProjectReferenceProjectLoadKind.Find + ); + } + /** Returns true if the project is needed by any of the open script info/external project */ /* @internal */ hasOpenRef() { @@ -2184,12 +2203,16 @@ namespace ts.server { return !!configFileExistenceInfo.openFilesImpactedByConfigFile.size; } + const isSolution = this.isSolution(); + // If there is no pending update for this project, // We know exact set of open files that get impacted by this configured project as the files in the project // The project is referenced only if open files impacted by this project are present in this project return forEachEntry( configFileExistenceInfo.openFilesImpactedByConfigFile, - (_value, infoPath) => this.containsScriptInfo(this.projectService.getScriptInfoForPath(infoPath as Path)!) + (_value, infoPath) => isSolution ? + !!this.getDefaultChildProjectFromSolution(this.projectService.getScriptInfoForPath(infoPath as Path)!) : + this.containsScriptInfo(this.projectService.getScriptInfoForPath(infoPath as Path)!) ) || false; } diff --git a/src/server/session.ts b/src/server/session.ts index d38e49adc146d..82f3e00e48a6c 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -431,10 +431,16 @@ namespace ts.server { if (initialLocation) { const defaultDefinition = getDefinitionLocation(defaultProject, initialLocation!); if (defaultDefinition) { + const getGeneratedDefinition = memoize(() => defaultProject.isSourceOfProjectReferenceRedirect(defaultDefinition.fileName) ? + defaultDefinition : + defaultProject.getLanguageService().getSourceMapper().tryGetGeneratedPosition(defaultDefinition)); + const getSourceDefinition = memoize(() => defaultProject.isSourceOfProjectReferenceRedirect(defaultDefinition.fileName) ? + defaultDefinition : + defaultProject.getLanguageService().getSourceMapper().tryGetSourcePosition(defaultDefinition)); projectService.loadAncestorProjectTree(seenProjects); projectService.forEachEnabledProject(project => { if (!addToSeen(seenProjects, project)) return; - const definition = mapDefinitionInProject(defaultDefinition, defaultProject, project); + const definition = mapDefinitionInProject(defaultDefinition, project, getGeneratedDefinition, getSourceDefinition); if (definition) { toDo = callbackProjectAndLocation({ project, location: definition as TLocation }, projectService, toDo, seenProjects, cb); } @@ -447,17 +453,21 @@ namespace ts.server { } } - function mapDefinitionInProject(definition: DocumentPosition | undefined, definingProject: Project, project: Project): DocumentPosition | undefined { + function mapDefinitionInProject( + definition: DocumentPosition, + project: Project, + getGeneratedDefinition: () => DocumentPosition | undefined, + getSourceDefinition: () => DocumentPosition | undefined + ): DocumentPosition | undefined { // If the definition is actually from the project, definition is correct as is - if (!definition || - project.containsFile(toNormalizedPath(definition.fileName)) && + if (project.containsFile(toNormalizedPath(definition.fileName)) && !isLocationProjectReferenceRedirect(project, definition)) { return definition; } - const mappedDefinition = definingProject.isSourceOfProjectReferenceRedirect(definition.fileName) ? - definition : - definingProject.getLanguageService().getSourceMapper().tryGetGeneratedPosition(definition); - return mappedDefinition && project.containsFile(toNormalizedPath(mappedDefinition.fileName)) ? mappedDefinition : undefined; + const generatedDefinition = getGeneratedDefinition(); + if (generatedDefinition && project.containsFile(toNormalizedPath(generatedDefinition.fileName))) return generatedDefinition; + const sourceDefinition = getSourceDefinition(); + return sourceDefinition && project.containsFile(toNormalizedPath(sourceDefinition.fileName)) ? sourceDefinition : undefined; } function isLocationProjectReferenceRedirect(project: Project, location: DocumentPosition | undefined) { diff --git a/src/testRunner/unittests/publicApi.ts b/src/testRunner/unittests/publicApi.ts index 917fbe590efe4..7f141f16c2705 100644 --- a/src/testRunner/unittests/publicApi.ts +++ b/src/testRunner/unittests/publicApi.ts @@ -1,4 +1,4 @@ -describe("Public APIs", () => { +describe("unittests:: Public APIs", () => { function verifyApi(fileName: string) { const builtFile = `built/local/${fileName}`; const api = `api/${fileName}`; @@ -32,7 +32,7 @@ describe("Public APIs", () => { }); }); -describe("Public APIs:: token to string", () => { +describe("unittests:: Public APIs:: token to string", () => { function assertDefinedTokenToString(initial: ts.SyntaxKind, last: ts.SyntaxKind) { for (let t = initial; t <= last; t++) { assert.isDefined(ts.tokenToString(t), `Expected tokenToString defined for ${ts.Debug.formatSyntaxKind(t)}`); @@ -47,13 +47,13 @@ describe("Public APIs:: token to string", () => { }); }); -describe("Public APIs:: createPrivateIdentifier", () => { +describe("unittests:: Public APIs:: createPrivateIdentifier", () => { it("throws when name doesn't start with #", () => { assert.throw(() => ts.createPrivateIdentifier("not"), "Debug Failure. First character of private identifier must be #: not"); }); }); -describe("Public APIs:: isPropertyName", () => { +describe("unittests:: Public APIs:: isPropertyName", () => { it("checks if a PrivateIdentifier is a valid property name", () => { const prop = ts.createPrivateIdentifier("#foo"); assert.isTrue(ts.isPropertyName(prop), "PrivateIdentifier must be a valid property name."); diff --git a/src/testRunner/unittests/tsserver/helpers.ts b/src/testRunner/unittests/tsserver/helpers.ts index ce776323879a4..ab24d9edc2faf 100644 --- a/src/testRunner/unittests/tsserver/helpers.ts +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -716,6 +716,69 @@ namespace ts.projectSystem { assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event)); } } + export function projectLoadingStartEvent(projectName: string, reason: string, seq?: number): protocol.ProjectLoadingStartEvent { + return { + seq: seq || 0, + type: "event", + event: server.ProjectLoadingStartEvent, + body: { projectName, reason } + }; + } + + export function projectLoadingFinishEvent(projectName: string, seq?: number): protocol.ProjectLoadingFinishEvent { + return { + seq: seq || 0, + type: "event", + event: server.ProjectLoadingFinishEvent, + body: { projectName } + }; + } + + export function projectInfoTelemetryEvent(seq?: number): protocol.TelemetryEvent { + return telemetryEvent(server.ProjectInfoTelemetryEvent, "", seq); + } + + function telemetryEvent(telemetryEventName: string, payload: any, seq?: number): protocol.TelemetryEvent { + return { + seq: seq || 0, + type: "event", + event: "telemetry", + body: { + telemetryEventName, + payload + } + }; + } + + export function configFileDiagEvent(triggerFile: string, configFile: string, diagnostics: protocol.DiagnosticWithFileName[], seq?: number): protocol.ConfigFileDiagnosticEvent { + return { + seq: seq || 0, + type: "event", + event: server.ConfigFileDiagEvent, + body: { + triggerFile, + configFile, + diagnostics + } + }; + } + + export function checkEvents(session: TestSession, expectedEvents: protocol.Event[]) { + const events = session.events; + assert.equal(events.length, expectedEvents.length, `Actual:: ${JSON.stringify(session.events, /*replacer*/ undefined, " ")}`); + expectedEvents.forEach((expectedEvent, index) => { + if (expectedEvent.event === "telemetry") { + // Ignore payload + const { body, ...actual } = events[index] as protocol.TelemetryEvent; + const { body: expectedBody, ...expected } = expectedEvent as protocol.TelemetryEvent; + assert.deepEqual(actual, expected, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); + assert.equal(body.telemetryEventName, expectedBody.telemetryEventName, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`); + } + else { + checkNthEvent(session, expectedEvent, index, index === expectedEvents.length); + } + }); + } export function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) { const events = session.events; diff --git a/src/testRunner/unittests/tsserver/projectReferences.ts b/src/testRunner/unittests/tsserver/projectReferences.ts index 8baa3a1f0cf63..ad7de88dc4cdb 100644 --- a/src/testRunner/unittests/tsserver/projectReferences.ts +++ b/src/testRunner/unittests/tsserver/projectReferences.ts @@ -1839,5 +1839,339 @@ bar(); // No new solutions/projects loaded checkNumberOfProjects(service, { configuredProjects: 1 }); }); + + describe("when default project is solution project", () => { + interface VerifySolutionScenario { + configRefs: string[]; + additionalFiles: readonly File[]; + additionalProjects: readonly { projectName: string, files: readonly string[] }[]; + expectedOpenEvents: protocol.Event[]; + expectedReloadEvents: protocol.Event[]; + expectedReferences: protocol.ReferencesResponseBody; + expectedReferencesFromDtsProject: protocol.ReferencesResponseBody; + } + const main: File = { + path: `${tscWatch.projectRoot}/src/main.ts`, + content: `import { foo } from 'helpers/functions'; +export { foo };` + }; + const helper: File = { + path: `${tscWatch.projectRoot}/src/helpers/functions.ts`, + content: `export const foo = 1;` + }; + const mainDts: File = { + path: `${tscWatch.projectRoot}/target/src/main.d.ts`, + content: `import { foo } from 'helpers/functions'; +export { foo }; +//# sourceMappingURL=main.d.ts.map` + }; + const mainDtsMap: File = { + path: `${tscWatch.projectRoot}/target/src/main.d.ts.map`, + content: `{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAExC,OAAO,EAAC,GAAG,EAAC,CAAC"}` + }; + const helperDts: File = { + path: `${tscWatch.projectRoot}/target/src/helpers/functions.d.ts`, + content: `export declare const foo = 1; +//# sourceMappingURL=functions.d.ts.map` + }; + const helperDtsMap: File = { + path: `${tscWatch.projectRoot}/target/src/helpers/functions.d.ts.map`, + content: `{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../../../src/helpers/functions.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,GAAG,IAAI,CAAC"}` + }; + const tsconfigIndirect3: File = { + path: `${tscWatch.projectRoot}/indirect3/tsconfig.json`, + content: JSON.stringify({ + compilerOptions: { + baseUrl: "../target/src/" + }, + }) + }; + const fileResolvingToMainDts: File = { + path: `${tscWatch.projectRoot}/indirect3/main.ts`, + content: `import { foo } from 'main'; +foo;` + }; + const tsconfigSrcPath = `${tscWatch.projectRoot}/tsconfig-src.json`; + const tsconfigPath = `${tscWatch.projectRoot}/tsconfig.json`; + function verifySolutionScenario({ + configRefs, additionalFiles, additionalProjects, + expectedOpenEvents, expectedReloadEvents, + expectedReferences, expectedReferencesFromDtsProject + }: VerifySolutionScenario) { + const tsconfigSrc: File = { + path: tsconfigSrcPath, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "./target/", + baseUrl: "./src/" + }, + include: ["./src/**/*"] + }) + }; + const tsconfig: File = { + path: tsconfigPath, + content: JSON.stringify({ + references: configRefs.map(path => ({ path })), + files: [] + }) + }; + const dummyFile: File = { + path: "/dummy/dummy.ts", + content: "let a = 10;" + }; + const host = createServerHost([ + tsconfigSrc, tsconfig, main, helper, + libFile, dummyFile, + mainDts, mainDtsMap, helperDts, helperDtsMap, + tsconfigIndirect3, fileResolvingToMainDts, + ...additionalFiles]); + const session = createSession(host, { canUseEvents: true }); + const service = session.getProjectService(); + service.openClientFile(main.path); + verifyProjects(/*includeConfigured*/ true, /*includeDummy*/ false); + checkEvents(session, expectedOpenEvents); + const info = service.getScriptInfoForPath(main.path as Path)!; + const project = service.configuredProjects.get(tsconfigSrc.path)!; + assert.equal(info.getDefaultProject(), project); + assert.equal(service.findDefaultConfiguredProject(info), project); + + // Verify errors + verifyGetErrRequest({ + session, + host, + expected: [ + { file: main, syntax: [], semantic: [], suggestion: [] }, + ] + }); + + // Verify collection of script infos + service.openClientFile(dummyFile.path); + verifyProjects(/*includeConfigured*/ true, /*includeDummy*/ true); + + service.closeClientFile(main.path); + service.closeClientFile(dummyFile.path); + service.openClientFile(dummyFile.path); + verifyProjects(/*includeConfigured*/ false, /*includeDummy*/ true); + + service.openClientFile(main.path); + service.closeClientFile(dummyFile.path); + service.openClientFile(dummyFile.path); + verifyProjects(/*includeConfigured*/ true, /*includeDummy*/ true); + + // Verify Reload projects + session.clearMessages(); + service.reloadProjects(); + checkEvents(session, expectedReloadEvents); + verifyProjects(/*includeConfigured*/ true, /*includeDummy*/ true); + + // Find all refs + const response = session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: protocolFileLocationFromSubstring(main, "foo", { index: 1 }) + }).response as protocol.ReferencesResponseBody; + assert.deepEqual(response, expectedReferences); + + service.closeClientFile(main.path); + service.closeClientFile(dummyFile.path); + + // Verify when declaration map references the file + service.openClientFile(fileResolvingToMainDts.path); + checkNumberOfProjects(service, { configuredProjects: 1 }); + checkProjectActualFiles(service.configuredProjects.get(tsconfigIndirect3.path)!, [tsconfigIndirect3.path, fileResolvingToMainDts.path, mainDts.path, helperDts.path, libFile.path]); + + // Find all refs from dts include + const response2 = session.executeCommandSeq({ + command: protocol.CommandTypes.References, + arguments: protocolFileLocationFromSubstring(fileResolvingToMainDts, "foo") + }).response as protocol.ReferencesResponseBody; + assert.deepEqual(response2, expectedReferencesFromDtsProject); + + function verifyProjects(includeConfigured: boolean, includeDummy: boolean) { + const inferredProjects = includeDummy ? 1 : 0; + const configuredProjects = includeConfigured ? additionalProjects.length + 2 : 0; + checkNumberOfProjects(service, { configuredProjects, inferredProjects }); + if (includeConfigured) { + checkProjectActualFiles(service.configuredProjects.get(tsconfigSrc.path)!, [tsconfigSrc.path, main.path, helper.path, libFile.path]); + checkProjectActualFiles(service.configuredProjects.get(tsconfig.path)!, [tsconfig.path]); + additionalProjects.forEach(({ projectName, files }) => + checkProjectActualFiles(service.configuredProjects.get(projectName)!, files)); + } + if (includeDummy) { + checkProjectActualFiles(service.inferredProjects[0], [dummyFile.path, libFile.path]); + } + } + } + + function expectedProjectLoadAndTelemetry(config: string, reason: string) { + return [ + projectLoadingStartEvent(config, reason), + projectLoadingFinishEvent(config), + projectInfoTelemetryEvent(), + ]; + } + + function expectedSolutionLoadAndTelemetry() { + return expectedProjectLoadAndTelemetry(tsconfigPath, `Creating possible configured project for ${main.path} to open`); + } + + function expectedProjectReferenceLoadAndTelemetry(config: string) { + return expectedProjectLoadAndTelemetry(config, `Creating project referenced in solution ${tsconfigPath} to find possible configured project for ${main.path} to open`); + } + + function expectedReloadEvent(config: string) { + return [ + projectLoadingStartEvent(config, `User requested reload projects`), + projectLoadingFinishEvent(config), + configFileDiagEvent(config, config, []) + ]; + } + + function expectedReferencesResponse(): protocol.ReferencesResponseBody { + return { + refs: [ + makeReferenceItem({ + file: main, + text: "foo", + contextText: `import { foo } from 'helpers/functions';`, + isDefinition: true, + isWriteAccess: true, + lineText: `import { foo } from 'helpers/functions';`, + }), + makeReferenceItem({ + file: main, + text: "foo", + options: { index: 1 }, + contextText: `export { foo };`, + isDefinition: true, + isWriteAccess: true, + lineText: `export { foo };`, + }), + makeReferenceItem({ + file: helper, + text: "foo", + contextText: `export const foo = 1;`, + isDefinition: true, + isWriteAccess: true, + lineText: `export const foo = 1;`, + }), + ], + symbolName: "foo", + symbolStartOffset: protocolLocationFromSubstring(main.content, "foo").offset, + symbolDisplayString: "(alias) const foo: 1\nexport foo" + }; + } + + function expectedIndirectRefs(indirect: File) { + return [ + makeReferenceItem({ + file: indirect, + text: "foo", + contextText: `import { foo } from 'main';`, + isDefinition: true, + isWriteAccess: true, + lineText: `import { foo } from 'main';`, + }), + makeReferenceItem({ + file: indirect, + text: "foo", + options: { index: 1 }, + isDefinition: false, + isWriteAccess: false, + lineText: `foo;`, + }), + ]; + } + + function getIndirectProject(postfix: string) { + const tsconfigIndirect: File = { + path: `${tscWatch.projectRoot}/tsconfig-indirect${postfix}.json`, + content: JSON.stringify({ + compilerOptions: { + composite: true, + outDir: "./target/", + baseUrl: "./src/" + }, + files: [`./indirect${postfix}/main.ts`], + references: [{ path: "./tsconfig-src.json" }] + }) + }; + const indirect: File = { + path: `${tscWatch.projectRoot}/indirect${postfix}/main.ts`, + content: fileResolvingToMainDts.content + }; + return { tsconfigIndirect, indirect }; + } + + it("when project is directly referenced by solution", () => { + const expectedReferences = expectedReferencesResponse(); + verifySolutionScenario({ + configRefs: ["./tsconfig-src.json"], + additionalFiles: emptyArray, + additionalProjects: emptyArray, + expectedOpenEvents: [ + ...expectedSolutionLoadAndTelemetry(), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigSrcPath), + configFileDiagEvent(main.path, tsconfigSrcPath, []) + ], + expectedReloadEvents: [ + ...expectedReloadEvent(tsconfigPath), + ...expectedReloadEvent(tsconfigSrcPath), + ], + expectedReferences, + expectedReferencesFromDtsProject: { + ...expectedReferences, + refs: [ + ...expectedIndirectRefs(fileResolvingToMainDts), + ...expectedReferences.refs + ], + symbolDisplayString: "(alias) const foo: 1\nimport foo", + } + }); + }); + + it("when project is indirectly referenced by solution", () => { + const { tsconfigIndirect, indirect } = getIndirectProject("1"); + const { tsconfigIndirect: tsconfigIndirect2, indirect: indirect2 } = getIndirectProject("2"); + const { refs, ...rest } = expectedReferencesResponse(); + verifySolutionScenario({ + configRefs: ["./tsconfig-indirect1.json", "./tsconfig-indirect2.json"], + additionalFiles: [tsconfigIndirect, indirect, tsconfigIndirect2, indirect2], + additionalProjects: [{ + projectName: tsconfigIndirect.path, + files: [tsconfigIndirect.path, main.path, helper.path, indirect.path, libFile.path] + }], + expectedOpenEvents: [ + ...expectedSolutionLoadAndTelemetry(), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigIndirect.path), + ...expectedProjectReferenceLoadAndTelemetry(tsconfigSrcPath), + configFileDiagEvent(main.path, tsconfigSrcPath, []) + ], + expectedReloadEvents: [ + ...expectedReloadEvent(tsconfigPath), + ...expectedReloadEvent(tsconfigIndirect.path), + ...expectedReloadEvent(tsconfigSrcPath), + ], + expectedReferences: { + refs: [ + ...refs, + ...expectedIndirectRefs(indirect), + ...expectedIndirectRefs(indirect2), + ], + ...rest + }, + expectedReferencesFromDtsProject: { + ...rest, + refs: [ + ...expectedIndirectRefs(fileResolvingToMainDts), + ...refs, + ...expectedIndirectRefs(indirect2), + ...expectedIndirectRefs(indirect), + ], + symbolDisplayString: "(alias) const foo: 1\nimport foo", + } + }); + }); + }); }); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 2cdbe454e68d2..066af48d907ab 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -9310,14 +9310,12 @@ declare namespace ts.server { */ private getConfigFileNameForFile; private printProjects; - private findConfiguredProjectByProjectName; private getConfiguredProjectByCanonicalConfigFilePath; private findExternalProjectByProjectName; /** Get a filename if the language service exceeds the maximum allowed program size; otherwise returns undefined. */ private getFilenameForExceededTotalSizeLimitForNonTsFiles; private createExternalProject; private addFilesToNonInferredProject; - private createConfiguredProject; private updateNonInferredProjectFiles; private updateRootAndOptionsOfNonInferredProject; private sendConfigFileDiagEvent;