Skip to content

Commit ba39113

Browse files
authored
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
1 parent b41eb1b commit ba39113

File tree

7 files changed

+592
-44
lines changed

7 files changed

+592
-44
lines changed

Diff for: src/server/editorServices.ts

+149-29
Large diffs are not rendered by default.

Diff for: src/server/project.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -2005,6 +2005,25 @@ namespace ts.server {
20052005
this.externalProjectRefCount--;
20062006
}
20072007

2008+
/* @internal */
2009+
isSolution() {
2010+
return this.getRootFilesMap().size === 0 &&
2011+
!this.canConfigFileJsonReportNoInputFiles;
2012+
}
2013+
2014+
/* @internal */
2015+
/** Find the configured project from the project references in this solution which contains the info directly */
2016+
getDefaultChildProjectFromSolution(info: ScriptInfo) {
2017+
Debug.assert(this.isSolution());
2018+
return forEachResolvedProjectReferenceProject(
2019+
this,
2020+
child => projectContainsInfoDirectly(child, info) ?
2021+
child :
2022+
undefined,
2023+
ProjectReferenceProjectLoadKind.Find
2024+
);
2025+
}
2026+
20082027
/** Returns true if the project is needed by any of the open script info/external project */
20092028
/* @internal */
20102029
hasOpenRef() {
@@ -2025,12 +2044,16 @@ namespace ts.server {
20252044
return !!configFileExistenceInfo.openFilesImpactedByConfigFile.size;
20262045
}
20272046

2047+
const isSolution = this.isSolution();
2048+
20282049
// If there is no pending update for this project,
20292050
// We know exact set of open files that get impacted by this configured project as the files in the project
20302051
// The project is referenced only if open files impacted by this project are present in this project
20312052
return forEachEntry(
20322053
configFileExistenceInfo.openFilesImpactedByConfigFile,
2033-
(_value, infoPath) => this.containsScriptInfo(this.projectService.getScriptInfoForPath(infoPath as Path)!)
2054+
(_value, infoPath) => isSolution ?
2055+
!!this.getDefaultChildProjectFromSolution(this.projectService.getScriptInfoForPath(infoPath as Path)!) :
2056+
this.containsScriptInfo(this.projectService.getScriptInfoForPath(infoPath as Path)!)
20342057
) || false;
20352058
}
20362059

Diff for: src/server/session.ts

+18-8
Original file line numberDiff line numberDiff line change
@@ -431,10 +431,16 @@ namespace ts.server {
431431
if (initialLocation) {
432432
const defaultDefinition = getDefinitionLocation(defaultProject, initialLocation!);
433433
if (defaultDefinition) {
434+
const getGeneratedDefinition = memoize(() => defaultProject.isSourceOfProjectReferenceRedirect(defaultDefinition.fileName) ?
435+
defaultDefinition :
436+
defaultProject.getLanguageService().getSourceMapper().tryGetGeneratedPosition(defaultDefinition));
437+
const getSourceDefinition = memoize(() => defaultProject.isSourceOfProjectReferenceRedirect(defaultDefinition.fileName) ?
438+
defaultDefinition :
439+
defaultProject.getLanguageService().getSourceMapper().tryGetSourcePosition(defaultDefinition));
434440
projectService.loadAncestorProjectTree(seenProjects);
435441
projectService.forEachEnabledProject(project => {
436442
if (!addToSeen(seenProjects, project)) return;
437-
const definition = mapDefinitionInProject(defaultDefinition, defaultProject, project);
443+
const definition = mapDefinitionInProject(defaultDefinition, project, getGeneratedDefinition, getSourceDefinition);
438444
if (definition) {
439445
toDo = callbackProjectAndLocation<TLocation>({ project, location: definition as TLocation }, projectService, toDo, seenProjects, cb);
440446
}
@@ -447,17 +453,21 @@ namespace ts.server {
447453
}
448454
}
449455

450-
function mapDefinitionInProject(definition: DocumentPosition | undefined, definingProject: Project, project: Project): DocumentPosition | undefined {
456+
function mapDefinitionInProject(
457+
definition: DocumentPosition,
458+
project: Project,
459+
getGeneratedDefinition: () => DocumentPosition | undefined,
460+
getSourceDefinition: () => DocumentPosition | undefined
461+
): DocumentPosition | undefined {
451462
// If the definition is actually from the project, definition is correct as is
452-
if (!definition ||
453-
project.containsFile(toNormalizedPath(definition.fileName)) &&
463+
if (project.containsFile(toNormalizedPath(definition.fileName)) &&
454464
!isLocationProjectReferenceRedirect(project, definition)) {
455465
return definition;
456466
}
457-
const mappedDefinition = definingProject.isSourceOfProjectReferenceRedirect(definition.fileName) ?
458-
definition :
459-
definingProject.getLanguageService().getSourceMapper().tryGetGeneratedPosition(definition);
460-
return mappedDefinition && project.containsFile(toNormalizedPath(mappedDefinition.fileName)) ? mappedDefinition : undefined;
467+
const generatedDefinition = getGeneratedDefinition();
468+
if (generatedDefinition && project.containsFile(toNormalizedPath(generatedDefinition.fileName))) return generatedDefinition;
469+
const sourceDefinition = getSourceDefinition();
470+
return sourceDefinition && project.containsFile(toNormalizedPath(sourceDefinition.fileName)) ? sourceDefinition : undefined;
461471
}
462472

463473
function isLocationProjectReferenceRedirect(project: Project, location: DocumentPosition | undefined) {

Diff for: src/testRunner/unittests/publicApi.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
describe("Public APIs", () => {
1+
describe("unittests:: Public APIs", () => {
22
function verifyApi(fileName: string) {
33
const builtFile = `built/local/${fileName}`;
44
const api = `api/${fileName}`;
@@ -32,7 +32,7 @@ describe("Public APIs", () => {
3232
});
3333
});
3434

35-
describe("Public APIs:: token to string", () => {
35+
describe("unittests:: Public APIs:: token to string", () => {
3636
function assertDefinedTokenToString(initial: ts.SyntaxKind, last: ts.SyntaxKind) {
3737
for (let t = initial; t <= last; t++) {
3838
assert.isDefined(ts.tokenToString(t), `Expected tokenToString defined for ${ts.Debug.formatSyntaxKind(t)}`);
@@ -47,13 +47,13 @@ describe("Public APIs:: token to string", () => {
4747
});
4848
});
4949

50-
describe("Public APIs:: createPrivateIdentifier", () => {
50+
describe("unittests:: Public APIs:: createPrivateIdentifier", () => {
5151
it("throws when name doesn't start with #", () => {
5252
assert.throw(() => ts.createPrivateIdentifier("not"), "Debug Failure. First character of private identifier must be #: not");
5353
});
5454
});
5555

56-
describe("Public APIs:: isPropertyName", () => {
56+
describe("unittests:: Public APIs:: isPropertyName", () => {
5757
it("checks if a PrivateIdentifier is a valid property name", () => {
5858
const prop = ts.createPrivateIdentifier("#foo");
5959
assert.isTrue(ts.isPropertyName(prop), "PrivateIdentifier must be a valid property name.");

Diff for: src/testRunner/unittests/tsserver/helpers.ts

+63
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,69 @@ namespace ts.projectSystem {
716716
assert.isFalse(event.event.endsWith("Diag"), JSON.stringify(event));
717717
}
718718
}
719+
export function projectLoadingStartEvent(projectName: string, reason: string, seq?: number): protocol.ProjectLoadingStartEvent {
720+
return {
721+
seq: seq || 0,
722+
type: "event",
723+
event: server.ProjectLoadingStartEvent,
724+
body: { projectName, reason }
725+
};
726+
}
727+
728+
export function projectLoadingFinishEvent(projectName: string, seq?: number): protocol.ProjectLoadingFinishEvent {
729+
return {
730+
seq: seq || 0,
731+
type: "event",
732+
event: server.ProjectLoadingFinishEvent,
733+
body: { projectName }
734+
};
735+
}
736+
737+
export function projectInfoTelemetryEvent(seq?: number): protocol.TelemetryEvent {
738+
return telemetryEvent(server.ProjectInfoTelemetryEvent, "", seq);
739+
}
740+
741+
function telemetryEvent(telemetryEventName: string, payload: any, seq?: number): protocol.TelemetryEvent {
742+
return {
743+
seq: seq || 0,
744+
type: "event",
745+
event: "telemetry",
746+
body: {
747+
telemetryEventName,
748+
payload
749+
}
750+
};
751+
}
752+
753+
export function configFileDiagEvent(triggerFile: string, configFile: string, diagnostics: protocol.DiagnosticWithFileName[], seq?: number): protocol.ConfigFileDiagnosticEvent {
754+
return {
755+
seq: seq || 0,
756+
type: "event",
757+
event: server.ConfigFileDiagEvent,
758+
body: {
759+
triggerFile,
760+
configFile,
761+
diagnostics
762+
}
763+
};
764+
}
765+
766+
export function checkEvents(session: TestSession, expectedEvents: protocol.Event[]) {
767+
const events = session.events;
768+
assert.equal(events.length, expectedEvents.length, `Actual:: ${JSON.stringify(session.events, /*replacer*/ undefined, " ")}`);
769+
expectedEvents.forEach((expectedEvent, index) => {
770+
if (expectedEvent.event === "telemetry") {
771+
// Ignore payload
772+
const { body, ...actual } = events[index] as protocol.TelemetryEvent;
773+
const { body: expectedBody, ...expected } = expectedEvent as protocol.TelemetryEvent;
774+
assert.deepEqual(actual, expected, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`);
775+
assert.equal(body.telemetryEventName, expectedBody.telemetryEventName, `Expected ${JSON.stringify(expectedEvent)} at ${index} in ${JSON.stringify(events)}`);
776+
}
777+
else {
778+
checkNthEvent(session, expectedEvent, index, index === expectedEvents.length);
779+
}
780+
});
781+
}
719782

720783
export function checkNthEvent(session: TestSession, expectedEvent: protocol.Event, index: number, isMostRecent: boolean) {
721784
const events = session.events;

0 commit comments

Comments
 (0)