TypeScript/src/testRunner/unittests/programApi.ts
Sheetal Nandi 2eca17d7c1
Keep track of why files are in the program (#40011)
* --explainFiles currently hardcoded

* Move configFileSpecs to configFile so it can be used in program later

* Explain root file inclusion reason and explain include files in the log

* Baseline explainFiles

* Fix incorrectly reporting of file list two times in --b mode

* Fix unnecessary new lines in output represented incorretly in the baseline

* More tests

* More cleaning up

* Keep listing files in same order as list files, just add explaination

* Fix double listing of file names when the program has errors

* Make diagnostic chains for file include reason

* Add explaination for the file include to diagnostics for program

* Harness ls incorrectly adding tsconfig as the root file

* Fix incorrect use of path for calculating absolute path

* Fix the root file in fourslash

* Test project service options merge

* Add config file name to matched by include explaination

* Add test for when the file changes and program is reused completely but related file information is reattached to correct location

* Handle file preprocessing diagnostics updates when program is reused and related information location changes

* Moved types to types.ts

* Refactoring and cleanup

* More cleanup

* More refatoring

* Handle synthetic imports

* Baselines after merge
2020-12-08 16:10:05 -08:00

224 lines
13 KiB
TypeScript

namespace ts {
function verifyMissingFilePaths(missingPaths: readonly Path[], expected: readonly string[]) {
assert.isDefined(missingPaths);
const map = new Set(expected);
for (const missing of missingPaths) {
const value = map.has(missing);
assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`);
map.delete(missing);
}
const notFound = arrayFrom(mapDefinedIterator(map.keys(), k => map.has(k) ? k : undefined));
assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`);
}
describe("unittests:: Program.getMissingFilePaths", () => {
const options: CompilerOptions = {
noLib: true,
};
const emptyFileName = "empty.ts";
const emptyFileRelativePath = "./" + emptyFileName;
const emptyFile = new documents.TextDocument(emptyFileName, "");
const referenceFileName = "reference.ts";
const referenceFileRelativePath = "./" + referenceFileName;
const referenceFile = new documents.TextDocument(referenceFileName,
"/// <reference path=\"d:/imaginary/nonexistent1.ts\"/>\n" + // Absolute
"/// <reference path=\"./nonexistent2.ts\"/>\n" + // Relative
"/// <reference path=\"nonexistent3.ts\"/>\n" + // Unqualified
"/// <reference path=\"nonexistent4\"/>\n" // No extension
);
const testCompilerHost = new fakes.CompilerHost(
vfs.createFromFileSystem(
Harness.IO,
/*ignoreCase*/ true,
{ documents: [emptyFile, referenceFile], cwd: "d:\\pretend\\" }),
{ newLine: NewLineKind.LineFeed });
it("handles no missing root files", () => {
const program = createProgram([emptyFileRelativePath], options, testCompilerHost);
const missing = program.getMissingFilePaths();
verifyMissingFilePaths(missing, []);
});
it("handles missing root file", () => {
const program = createProgram(["./nonexistent.ts"], options, testCompilerHost);
const missing = program.getMissingFilePaths();
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path
});
it("handles multiple missing root files", () => {
const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost);
const missing = program.getMissingFilePaths();
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
});
it("handles a mix of present and missing root files", () => {
const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost);
const missing = program.getMissingFilePaths();
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
});
it("handles repeatedly specified root files", () => {
const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost);
const missing = program.getMissingFilePaths();
verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]);
});
it("normalizes file paths", () => {
const program0 = createProgram(["./nonexistent.ts", "./NONEXISTENT.ts"], options, testCompilerHost);
const program1 = createProgram(["./NONEXISTENT.ts", "./nonexistent.ts"], options, testCompilerHost);
const missing0 = program0.getMissingFilePaths();
const missing1 = program1.getMissingFilePaths();
assert.equal(missing0.length, 1);
assert.deepEqual(missing0, missing1);
});
it("handles missing triple slash references", () => {
const program = createProgram([referenceFileRelativePath], options, testCompilerHost);
const missing = program.getMissingFilePaths();
verifyMissingFilePaths(missing, [
// From absolute reference
"d:/imaginary/nonexistent1.ts",
// From relative reference
"d:/pretend/nonexistent2.ts",
// From unqualified reference
"d:/pretend/nonexistent3.ts",
// From no-extension reference
"d:/pretend/nonexistent4.d.ts",
"d:/pretend/nonexistent4.ts",
"d:/pretend/nonexistent4.tsx"
]);
});
it("should not have missing file paths", () => {
const testSource = `
class Foo extends HTMLElement {
bar: string = 'baz';
}`;
const host: CompilerHost = {
getSourceFile: (fileName: string, languageVersion: ScriptTarget, _onError?: (message: string) => void) => {
return fileName === "test.ts" ? createSourceFile(fileName, testSource, languageVersion) : undefined;
},
getDefaultLibFileName: () => "",
writeFile: (_fileName, _content) => { throw new Error("unsupported"); },
getCurrentDirectory: () => sys.getCurrentDirectory(),
getCanonicalFileName: fileName => sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
getNewLine: () => sys.newLine,
useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames,
fileExists: fileName => fileName === "test.ts",
readFile: fileName => fileName === "test.ts" ? testSource : undefined,
resolveModuleNames: (_moduleNames: string[], _containingFile: string) => { throw new Error("unsupported"); },
getDirectories: _path => { throw new Error("unsupported"); },
};
const program = createProgram(["test.ts"], { module: ModuleKind.ES2015 }, host);
assert(program.getSourceFiles().length === 1, "expected 'getSourceFiles' length to be 1");
assert(program.getMissingFilePaths().length === 0, "expected 'getMissingFilePaths' length to be 0");
assert((program.getFileProcessingDiagnostics()?.length || 0) === 0, "expected 'getFileProcessingDiagnostics' length to be 0");
});
});
describe("unittests:: Program.isSourceFileFromExternalLibrary", () => {
it("works on redirect files", () => {
// In this example '/node_modules/foo/index.d.ts' will redirect to '/node_modules/bar/node_modules/foo/index.d.ts'.
const a = new documents.TextDocument("/a.ts", 'import * as bar from "bar"; import * as foo from "foo";');
const bar = new documents.TextDocument("/node_modules/bar/index.d.ts", 'import * as foo from "foo";');
const fooPackageJsonText = '{ "name": "foo", "version": "1.2.3" }';
const fooIndexText = "export const x: number;";
const barFooPackage = new documents.TextDocument("/node_modules/bar/node_modules/foo/package.json", fooPackageJsonText);
const barFooIndex = new documents.TextDocument("/node_modules/bar/node_modules/foo/index.d.ts", fooIndexText);
const fooPackage = new documents.TextDocument("/node_modules/foo/package.json", fooPackageJsonText);
const fooIndex = new documents.TextDocument("/node_modules/foo/index.d.ts", fooIndexText);
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [a, bar, barFooPackage, barFooIndex, fooPackage, fooIndex], cwd: "/" });
const program = createProgram(["/a.ts"], emptyOptions, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed }));
assertIsExternal(program, [a, bar, barFooIndex, fooIndex], f => f !== a);
});
it('works on `/// <reference types="" />`', () => {
const a = new documents.TextDocument("/a.ts", '/// <reference types="foo" />');
const fooIndex = new documents.TextDocument("/node_modules/foo/index.d.ts", "declare const foo: number;");
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [a, fooIndex], cwd: "/" });
const program = createProgram(["/a.ts"], emptyOptions, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed }));
assertIsExternal(program, [a, fooIndex], f => f !== a);
});
function assertIsExternal(program: Program, files: readonly documents.TextDocument[], isExternalExpected: (file: documents.TextDocument) => boolean): void {
for (const file of files) {
const actual = program.isSourceFileFromExternalLibrary(program.getSourceFile(file.file)!);
const expected = isExternalExpected(file);
assert.equal(actual, expected, `Expected ${file.file} isSourceFileFromExternalLibrary to be ${expected}, got ${actual}`);
}
}
});
describe("unittests:: Program.getNodeCount / Program.getIdentifierCount", () => {
it("works on projects that have .json files", () => {
const main = new documents.TextDocument("/main.ts", 'export { version } from "./package.json";');
const pkg = new documents.TextDocument("/package.json", '{"version": "1.0.0"}');
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main, pkg], cwd: "/" });
const program = createProgram(["/main.ts"], { resolveJsonModule: true }, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed }));
const json = program.getSourceFile("/package.json")!;
assert.equal(json.scriptKind, ScriptKind.JSON);
assert.isNumber(json.nodeCount);
assert.isNumber(json.identifierCount);
assert.isNotNaN(program.getNodeCount());
assert.isNotNaN(program.getIdentifierCount());
});
});
describe("unittests:: programApi:: Program.getDiagnosticsProducingTypeChecker / Program.getSemanticDiagnostics", () => {
it("does not produce errors on `as const` it would not normally produce on the command line", () => {
const main = new documents.TextDocument("/main.ts", "0 as const");
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main], cwd: "/" });
const program = createProgram(["/main.ts"], {}, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed }));
const typeChecker = program.getDiagnosticsProducingTypeChecker();
const sourceFile = program.getSourceFile("main.ts")!;
typeChecker.getTypeAtLocation(((sourceFile.statements[0] as ExpressionStatement).expression as AsExpression).type);
const diag = program.getSemanticDiagnostics();
assert.isEmpty(diag);
});
it("getSymbolAtLocation does not cause additional error to be added on module resolution failure", () => {
const main = new documents.TextDocument("/main.ts", "import \"./module\";");
const mod = new documents.TextDocument("/module.d.ts", "declare const foo: any;");
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main, mod], cwd: "/" });
const program = createProgram(["/main.ts"], {}, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed }));
const sourceFile = program.getSourceFile("main.ts")!;
const typeChecker = program.getDiagnosticsProducingTypeChecker();
typeChecker.getSymbolAtLocation((sourceFile.statements[0] as ImportDeclaration).moduleSpecifier);
assert.isEmpty(program.getSemanticDiagnostics());
});
});
describe("unittests:: programApi:: CompilerOptions relative paths", () => {
it("resolves relative paths by getCurrentDirectory", () => {
const main = new documents.TextDocument("/main.ts", "import \"module\";");
const mod = new documents.TextDocument("/lib/module.ts", "declare const foo: any;");
const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { documents: [main, mod], cwd: "/" });
const program = createProgram(["./main.ts"], {
paths: { "*": ["./lib/*"] }
}, new fakes.CompilerHost(fs, { newLine: NewLineKind.LineFeed }));
assert.isEmpty(program.getConfigFileParsingDiagnostics());
assert.isEmpty(program.getGlobalDiagnostics());
assert.isEmpty(program.getSemanticDiagnostics());
});
});
}