Skip to content

Fix the issue when file is attached to project because its a root file name but program contains instead its d.ts #33636

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ namespace ts.server {
const memGetDefinition = memoize(getDefinition);
projectService.forEachEnabledProject(project => {
if (!addToSeen(seenProjects, project.projectName)) return;
const definition = getDefinitionInProject(memGetDefinition(), defaultProject, project);
const definition = mapDefinitionInProject(memGetDefinition(), defaultProject, project);
if (definition) {
toDo = callbackProjectAndLocation<TLocation>({ project, location: definition as TLocation }, projectService, toDo, seenProjects, cb);
}
Expand All @@ -446,22 +446,46 @@ namespace ts.server {
}
}

function getDefinitionInProject(definition: DocumentPosition | undefined, definingProject: Project, project: Project): DocumentPosition | undefined {
if (!definition || project.containsFile(toNormalizedPath(definition.fileName))) return definition;
function mapDefinitionInProject(definition: DocumentPosition | undefined, definingProject: Project, project: Project): DocumentPosition | undefined {
// If the definition is actually from the project, definition is correct as is
if (!definition ||
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;
}

function isLocationProjectReferenceRedirect(project: Project, location: DocumentPosition | undefined) {
if (!location) return false;
const program = project.getLanguageService().getProgram();
if (!program) return false;
const sourceFile = program.getSourceFile(location.fileName);

// It is possible that location is attached to project but
// the program actually includes its redirect instead.
// This happens when rootFile in project is one of the file from referenced project
// Thus root is attached but program doesnt have the actual .ts file but .d.ts
// If this is not the file we were actually looking, return rest of the toDo
return !!sourceFile &&
sourceFile.resolvedPath !== sourceFile.path &&
sourceFile.resolvedPath !== project.toPath(location.fileName);
}

function callbackProjectAndLocation<TLocation extends DocumentPosition | undefined>(
projectAndLocation: ProjectAndLocation<TLocation>,
projectService: ProjectService,
toDo: ProjectAndLocation<TLocation>[] | undefined,
seenProjects: Map<true>,
cb: CombineProjectOutputCallback<TLocation>,
): ProjectAndLocation<TLocation>[] | undefined {
if (projectAndLocation.project.getCancellationToken().isCancellationRequested()) return undefined; // Skip rest of toDo if cancelled
const { project, location } = projectAndLocation;
if (project.getCancellationToken().isCancellationRequested()) return undefined; // Skip rest of toDo if cancelled
// If this is not the file we were actually looking, return rest of the toDo
if (isLocationProjectReferenceRedirect(project, location)) return toDo;
cb(projectAndLocation, (project, location) => {
seenProjects.set(projectAndLocation.project.projectName, true);
const originalLocation = projectService.getOriginalLocationEnsuringConfiguredProject(project, location);
Expand All @@ -475,8 +499,8 @@ namespace ts.server {
}
const symlinkedProjectsMap = projectService.getSymlinkedProjects(originalScriptInfo);
if (symlinkedProjectsMap) {
symlinkedProjectsMap.forEach((symlinkedProjects) => {
for (const symlinkedProject of symlinkedProjects) addToTodo({ project: symlinkedProject, location: originalLocation as TLocation }, toDo!, seenProjects);
symlinkedProjectsMap.forEach((symlinkedProjects, symlinkedPath) => {
for (const symlinkedProject of symlinkedProjects) addToTodo({ project: symlinkedProject, location: { fileName: symlinkedPath, pos: originalLocation.pos } as TLocation }, toDo!, seenProjects);
});
}
return originalLocation === location ? undefined : originalLocation;
Expand Down
126 changes: 124 additions & 2 deletions src/testRunner/unittests/tsserver/projectReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace ts.projectSystem {
// ts build should succeed
const solutionBuilder = tscWatch.createSolutionBuilder(host, rootNames, {});
solutionBuilder.build();
assert.equal(host.getOutput().length, 0);
assert.equal(host.getOutput().length, 0, JSON.stringify(host.getOutput(), /*replacer*/ undefined, " "));

return host;
}
Expand Down Expand Up @@ -1166,7 +1166,7 @@ ${dependencyTs.content}`);
dtsFileCreated: "main",
dtsFileDeleted: ["noDts", noDts => ({
// Map collection after file open
closedInfos: noDts.closedInfos.concat(dtsMapLocation) ,
closedInfos: noDts.closedInfos.concat(dtsMapLocation),
expectsMap: true
})],
dependencyChange: ["change", () => ({
Expand All @@ -1179,6 +1179,128 @@ ${dependencyTs.content}`);
});
});

describe("when root file is file from referenced project", () => {
function verify(disableSourceOfProjectReferenceRedirect: boolean) {
const projectLocation = `/user/username/projects/project`;
const commonConfig: File = {
path: `${projectLocation}/src/common/tsconfig.json`,
content: JSON.stringify({
compilerOptions: {
composite: true,
declarationMap: true,
outDir: "../../out",
baseUrl: "..",
disableSourceOfProjectReferenceRedirect
},
include: ["./**/*"]
})
};
const keyboardTs: File = {
path: `${projectLocation}/src/common/input/keyboard.ts`,
content: `function bar() { return "just a random function so .d.ts location doesnt match"; }
export function evaluateKeyboardEvent() { }`
};
const keyboardTestTs: File = {
path: `${projectLocation}/src/common/input/keyboard.test.ts`,
content: `import { evaluateKeyboardEvent } from 'common/input/keyboard';
function testEvaluateKeyboardEvent() {
return evaluateKeyboardEvent();
}
`
};
const srcConfig: File = {
path: `${projectLocation}/src/tsconfig.json`,
content: JSON.stringify({
compilerOptions: {
composite: true,
declarationMap: true,
outDir: "../out",
baseUrl: ".",
paths: {
"common/*": ["./common/*"],
},
tsBuildInfoFile: "../out/src.tsconfig.tsbuildinfo",
disableSourceOfProjectReferenceRedirect
},
include: ["./**/*"],
references: [
{ path: "./common" }
]
})
};
const terminalTs: File = {
path: `${projectLocation}/src/terminal.ts`,
content: `import { evaluateKeyboardEvent } from 'common/input/keyboard';
function foo() {
return evaluateKeyboardEvent();
}
`
};
const host = createHost(
[commonConfig, keyboardTs, keyboardTestTs, srcConfig, terminalTs, libFile],
[srcConfig.path]
);
const session = createSession(host);
openFilesForSession([keyboardTs, terminalTs], session);

const searchStr = "evaluateKeyboardEvent";
const importStr = `import { evaluateKeyboardEvent } from 'common/input/keyboard';`;
const result = session.executeCommandSeq<protocol.ReferencesRequest>({
command: protocol.CommandTypes.References,
arguments: protocolFileLocationFromSubstring(keyboardTs, searchStr)
}).response as protocol.ReferencesResponseBody;
assert.deepEqual(result, {
refs: [
makeReferenceItem({
file: keyboardTs,
text: searchStr,
contextText: `export function evaluateKeyboardEvent() { }`,
isDefinition: true,
lineText: `export function evaluateKeyboardEvent() { }`
}),
makeReferenceItem({
file: keyboardTestTs,
text: searchStr,
contextText: importStr,
isDefinition: true,
lineText: importStr
}),
makeReferenceItem({
file: keyboardTestTs,
text: searchStr,
options: { index: 1 },
isDefinition: false,
lineText: ` return evaluateKeyboardEvent();`
}),
makeReferenceItem({
file: terminalTs,
text: searchStr,
contextText: importStr,
isDefinition: true,
lineText: importStr
}),
makeReferenceItem({
file: terminalTs,
text: searchStr,
options: { index: 1 },
isDefinition: false,
lineText: ` return evaluateKeyboardEvent();`
}),
],
symbolName: searchStr,
symbolStartOffset: protocolLocationFromSubstring(keyboardTs.content, searchStr).offset,
symbolDisplayString: "function evaluateKeyboardEvent(): void"
});
}

it(`when using declaration file maps to navigate between projects`, () => {
verify(/*disableSourceOfProjectReferenceRedirect*/ true);
});
it(`when using original source files in the project`, () => {
verify(/*disableSourceOfProjectReferenceRedirect*/ false);
});
});

it("reusing d.ts files from composite and non composite projects", () => {
const projectLocation = "/user/username/projects/myproject";
const configA: File = {
Expand Down