Skip to content

Fix issue of program not being reused when host implements getSourceFileByPath #27483

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 3 commits into from
Oct 1, 2018
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
6 changes: 4 additions & 2 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2024,10 +2024,12 @@ namespace ts {
}
}

function createRedirectSourceFile(redirectTarget: SourceFile, unredirected: SourceFile, fileName: string, path: Path): SourceFile {
function createRedirectSourceFile(redirectTarget: SourceFile, unredirected: SourceFile, fileName: string, path: Path, resolvedPath: Path, originalFileName: string): SourceFile {
const redirect: SourceFile = Object.create(redirectTarget);
redirect.fileName = fileName;
redirect.path = path;
redirect.resolvedPath = resolvedPath;
redirect.originalFileName = originalFileName;
redirect.redirectInfo = { redirectTarget, unredirected };
Object.defineProperties(redirect, {
id: {
Expand Down Expand Up @@ -2118,7 +2120,7 @@ namespace ts {
if (fileFromPackageId) {
// Some other SourceFile already exists with this package name and version.
// Instead of creating a duplicate, just redirect to the existing one.
const dupFile = createRedirectSourceFile(fileFromPackageId, file!, fileName, path); // TODO: GH#18217
const dupFile = createRedirectSourceFile(fileFromPackageId, file!, fileName, path, toPath(fileName), originalFileName); // TODO: GH#18217
redirectTargetsMap.add(fileFromPackageId.path, fileName);
filesByName.set(path, dupFile);
sourceFileToPackageName.set(path, packageId.name);
Expand Down
127 changes: 64 additions & 63 deletions src/testRunner/unittests/reuseProgramStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ namespace ts {
return file;
}

export function createTestCompilerHost(texts: ReadonlyArray<NamedSourceText>, target: ScriptTarget, oldProgram?: ProgramWithSourceTexts): TestCompilerHost {
export function createTestCompilerHost(texts: ReadonlyArray<NamedSourceText>, target: ScriptTarget, oldProgram?: ProgramWithSourceTexts, useGetSourceFileByPath?: boolean) {
const files = arrayToMap(texts, t => t.name, t => {
if (oldProgram) {
let oldFile = <SourceFileWithText>oldProgram.getSourceFile(t.name);
Expand All @@ -117,55 +117,47 @@ namespace ts {
}
return createSourceFileWithText(t.name, t.text, target);
});
const useCaseSensitiveFileNames = sys && sys.useCaseSensitiveFileNames;
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
const trace: string[] = [];

return {
const result: TestCompilerHost = {
trace: s => trace.push(s),
getTrace: () => trace,
getSourceFile(fileName): SourceFile {
return files.get(fileName)!;
},
getDefaultLibFileName(): string {
return "lib.d.ts";
},
getSourceFile: fileName => files.get(fileName),
getDefaultLibFileName: () => "lib.d.ts",
writeFile: notImplemented,
getCurrentDirectory(): string {
return "";
},
getDirectories(): string[] {
return [];
},
getCanonicalFileName(fileName): string {
return sys && sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase();
},
useCaseSensitiveFileNames(): boolean {
return sys && sys.useCaseSensitiveFileNames;
},
getNewLine(): string {
return sys ? sys.newLine : newLine;
},
getCurrentDirectory: () => "",
getDirectories: () => [],
getCanonicalFileName,
useCaseSensitiveFileNames: () => useCaseSensitiveFileNames,
getNewLine: () => sys ? sys.newLine : newLine,
fileExists: fileName => files.has(fileName),
readFile: fileName => {
const file = files.get(fileName);
return file && file.text;
},
};
if (useGetSourceFileByPath) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well calculate filesByPath inside the if.

const filesByPath = mapEntries(files, (fileName, file) => [toPath(fileName, "", getCanonicalFileName), file]);
result.getSourceFileByPath = (_fileName, path) => filesByPath.get(path);
}
return result;
}

export function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions): ProgramWithSourceTexts {
const host = createTestCompilerHost(texts, options.target!);
export function newProgram(texts: NamedSourceText[], rootNames: string[], options: CompilerOptions, useGetSourceFileByPath?: boolean): ProgramWithSourceTexts {
const host = createTestCompilerHost(texts, options.target!, /*oldProgram*/ undefined, useGetSourceFileByPath);
const program = <ProgramWithSourceTexts>createProgram(rootNames, options, host);
program.sourceTexts = texts;
program.host = host;
return program;
}

export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: ReadonlyArray<string>, options: CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[]) {
export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: ReadonlyArray<string>, options: CompilerOptions, updater: (files: NamedSourceText[]) => void, newTexts?: NamedSourceText[], useGetSourceFileByPath?: boolean) {
if (!newTexts) {
newTexts = oldProgram.sourceTexts!.slice(0);
}
updater(newTexts);
const host = createTestCompilerHost(newTexts, options.target!, oldProgram);
const host = createTestCompilerHost(newTexts, options.target!, oldProgram, useGetSourceFileByPath);
const program = <ProgramWithSourceTexts>createProgram(rootNames, options, host, oldProgram);
program.sourceTexts = newTexts;
program.host = host;
Expand Down Expand Up @@ -809,7 +801,7 @@ namespace ts {
const root = "/a.ts";
const compilerOptions = { target, moduleResolution: ModuleResolutionKind.NodeJs };

function createRedirectProgram(options?: { bText: string, bVersion: string }): ProgramWithSourceTexts {
function createRedirectProgram(useGetSourceFileByPath: boolean, options?: { bText: string, bVersion: string }): ProgramWithSourceTexts {
const files: NamedSourceText[] = [
{
name: "/node_modules/a/index.d.ts",
Expand Down Expand Up @@ -841,55 +833,64 @@ namespace ts {
},
];

return newProgram(files, [root], compilerOptions);
return newProgram(files, [root], compilerOptions, useGetSourceFileByPath);
}

function updateRedirectProgram(program: ProgramWithSourceTexts, updater: (files: NamedSourceText[]) => void): ProgramWithSourceTexts {
return updateProgram(program, [root], compilerOptions, updater);
function updateRedirectProgram(program: ProgramWithSourceTexts, updater: (files: NamedSourceText[]) => void, useGetSourceFileByPath: boolean): ProgramWithSourceTexts {
return updateProgram(program, [root], compilerOptions, updater, /*newTexts*/ undefined, useGetSourceFileByPath);
}

it("No changes -> redirect not broken", () => {
const program1 = createRedirectProgram();
function verifyRedirects(useGetSourceFileByPath: boolean) {
it("No changes -> redirect not broken", () => {
const program1 = createRedirectProgram(useGetSourceFileByPath);

const program2 = updateRedirectProgram(program1, files => {
updateProgramText(files, root, "const x = 1;");
const program2 = updateRedirectProgram(program1, files => {
updateProgramText(files, root, "const x = 1;");
}, useGetSourceFileByPath);
assert.equal(program1.structureIsReused, StructureIsReused.Completely);
assert.lengthOf(program2.getSemanticDiagnostics(), 0);
});
assert.equal(program1.structureIsReused, StructureIsReused.Completely);
assert.lengthOf(program2.getSemanticDiagnostics(), 0);
});

it("Target changes -> redirect broken", () => {
const program1 = createRedirectProgram();
assert.lengthOf(program1.getSemanticDiagnostics(), 0);
it("Target changes -> redirect broken", () => {
const program1 = createRedirectProgram(useGetSourceFileByPath);
assert.lengthOf(program1.getSemanticDiagnostics(), 0);

const program2 = updateRedirectProgram(program1, files => {
updateProgramText(files, axIndex, "export default class X { private x: number; private y: number; }");
updateProgramText(files, axPackage, JSON.stringify('{ name: "x", version: "1.2.4" }'));
const program2 = updateRedirectProgram(program1, files => {
updateProgramText(files, axIndex, "export default class X { private x: number; private y: number; }");
updateProgramText(files, axPackage, JSON.stringify('{ name: "x", version: "1.2.4" }'));
}, useGetSourceFileByPath);
assert.equal(program1.structureIsReused, StructureIsReused.Not);
assert.lengthOf(program2.getSemanticDiagnostics(), 1);
});
assert.equal(program1.structureIsReused, StructureIsReused.Not);
assert.lengthOf(program2.getSemanticDiagnostics(), 1);
});

it("Underlying changes -> redirect broken", () => {
const program1 = createRedirectProgram();
it("Underlying changes -> redirect broken", () => {
const program1 = createRedirectProgram(useGetSourceFileByPath);

const program2 = updateRedirectProgram(program1, files => {
updateProgramText(files, bxIndex, "export default class X { private x: number; private y: number; }");
updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.4" }));
const program2 = updateRedirectProgram(program1, files => {
updateProgramText(files, bxIndex, "export default class X { private x: number; private y: number; }");
updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.4" }));
}, useGetSourceFileByPath);
assert.equal(program1.structureIsReused, StructureIsReused.Not);
assert.lengthOf(program2.getSemanticDiagnostics(), 1);
});
assert.equal(program1.structureIsReused, StructureIsReused.Not);
assert.lengthOf(program2.getSemanticDiagnostics(), 1);
});

it("Previously duplicate packages -> program structure not reused", () => {
const program1 = createRedirectProgram({ bVersion: "1.2.4", bText: "export = class X { private x: number; }" });
it("Previously duplicate packages -> program structure not reused", () => {
const program1 = createRedirectProgram(useGetSourceFileByPath, { bVersion: "1.2.4", bText: "export = class X { private x: number; }" });

const program2 = updateRedirectProgram(program1, files => {
updateProgramText(files, bxIndex, "export default class X { private x: number; }");
updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.3" }));
const program2 = updateRedirectProgram(program1, files => {
updateProgramText(files, bxIndex, "export default class X { private x: number; }");
updateProgramText(files, bxPackage, JSON.stringify({ name: "x", version: "1.2.3" }));
}, useGetSourceFileByPath);
assert.equal(program1.structureIsReused, StructureIsReused.Not);
assert.deepEqual(program2.getSemanticDiagnostics(), []);
});
assert.equal(program1.structureIsReused, StructureIsReused.Not);
assert.deepEqual(program2.getSemanticDiagnostics(), []);
}

describe("when host implements getSourceFile", () => {
verifyRedirects(/*useGetSourceFileByPath*/ false);
});
describe("when host implements getSourceFileByPath", () => {
verifyRedirects(/*useGetSourceFileByPath*/ true);
});
});
});
Expand Down