mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
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:
+149
-29
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user