Handle when default project for file is solution with file actually referenced by one of the project references (#37239)

* Add test where file from referenced project of solution belongs to inferred project instead of referenced project

* Try to find project from project references if the default config project is solution
Fixes #36708

* Add test to verify the correct collection of projects

* Handle when default config project is indirectly referenced in the solution

* Include public API tests in unittests

* Make sure default project for script info is calculated correctly

* Handle reload projects

* Ensure to load solution project tree when project is referenced by solution

* Find all refs when the file is referenced via d.ts

* Some comments per feedback

* Fix typo
This commit is contained in:
Sheetal Nandi
2020-03-13 10:46:58 -07:00
committed by GitHub
parent b41eb1bc61
commit ba39113ab9
7 changed files with 592 additions and 44 deletions
+149 -29
View File
@@ -308,7 +308,7 @@ namespace ts.server {
}
interface AssignProjectResult extends OpenConfiguredProjectResult {
defaultConfigProject: ConfiguredProject | undefined;
retainProjects: readonly ConfiguredProject[] | ConfiguredProject | undefined;
}
interface FilePropertyReader<T> {
@@ -424,7 +424,52 @@ namespace ts.server {
return !!(infoOrFileNameOrConfig as AncestorConfigFileInfo).configFileInfo;
}
function forEachResolvedProjectReference<T>(
/*@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<T>(
project: ConfiguredProject,
cb: (child: ConfiguredProject, configFileName: NormalizedPath) => T | undefined,
projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind.Find | ProjectReferenceProjectLoadKind.FindCreate
): T | undefined;
/*@internal*/
export function forEachResolvedProjectReferenceProject<T>(
project: ConfiguredProject,
cb: (child: ConfiguredProject, configFileName: NormalizedPath) => T | undefined,
projectReferenceProjectLoadKind: ProjectReferenceProjectLoadKind.FindCreateLoad,
reason: string
): T | undefined;
export function forEachResolvedProjectReferenceProject<T>(
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<T>(
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;
}
/**
@@ -1735,7 +1791,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);
@@ -1869,7 +1926,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(
@@ -1893,7 +1951,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;
@@ -2711,7 +2769,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;
@@ -2720,8 +2779,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);
}
}
});
@@ -2808,10 +2880,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);
@@ -2858,6 +2946,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) {
@@ -2878,9 +2967,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);
}
}
}
@@ -2900,7 +3021,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) {
@@ -2951,7 +3072,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);
}
@@ -2964,19 +3088,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);
@@ -2998,14 +3118,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) {
@@ -3204,9 +3324,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
@@ -3216,7 +3336,7 @@ namespace ts.server {
if (openScriptInfos) {
// Cleanup projects
this.cleanupAfterOpeningFile(defaultConfigProjects);
this.cleanupAfterOpeningFile(retainProjects);
// Telemetry
openScriptInfos.forEach(info => this.telemetryOnOpenFile(info));
this.printProjects();
+24 -1
View File
@@ -2005,6 +2005,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() {
@@ -2025,12 +2044,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;
}
+18 -8
View File
@@ -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<TLocation>({ 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) {
+4 -4
View File
@@ -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.");
@@ -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;
@@ -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<protocol.ReferencesRequest>({
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<protocol.ReferencesRequest>({
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",
}
});
});
});
});
}
-2
View File
@@ -9349,14 +9349,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;