Fix diagnostic reporting for empty files in tsconfig

This commit is contained in:
christian 2018-09-03 22:57:26 -04:00
parent 45101491c0
commit c87ca2f1ab
8 changed files with 132 additions and 9 deletions

View file

@ -1843,7 +1843,9 @@ namespace ts {
if (hasProperty(raw, "files") && !isNullOrUndefined(raw.files)) {
if (isArray(raw.files)) {
filesSpecs = <ReadonlyArray<string>>raw.files;
if (filesSpecs.length === 0) {
const hasReferences = hasProperty(raw, "references") && !isNullOrUndefined(raw.references);
const hasZeroOrNoReferences = !hasReferences || raw.references.length === 0;
if (filesSpecs.length === 0 && hasZeroOrNoReferences) {
createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json");
}
}
@ -2067,11 +2069,6 @@ namespace ts {
createDiagnosticForNodeInSourceFile(sourceFile, valueNode, message, arg0)
);
return;
case "files":
if ((<ReadonlyArray<string>>value).length === 0) {
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueNode, Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json"));
}
return;
}
},
onSetUnknownOptionKeyValueInRoot(key: string, keyNode: PropertyName, _value: CompilerOptionsValue, _valueNode: Expression) {
@ -2081,6 +2078,13 @@ namespace ts {
}
};
const json = convertToObjectWorker(sourceFile, errors, /*returnValue*/ true, getTsconfigRootOptionsMap(), optionsIterator);
const hasZeroFiles = json && json.files && json.files.length === 0;
const hasZeroOrNoReferences = !(json && json.references) || json.references.length === 0;
if (hasZeroFiles && hasZeroOrNoReferences) {
errors.push(createCompilerDiagnostic(Diagnostics.The_files_list_in_config_file_0_is_empty, sourceFile.fileName));
}
if (!typeAcquisition) {
if (typingOptionstypeAcquisition) {
typeAcquisition = (typingOptionstypeAcquisition.enableAutoDiscovery !== undefined) ?

View file

@ -946,7 +946,6 @@ namespace ts {
context.projectStatus.setValue(proj, { type: UpToDateStatusType.Unbuildable, reason: "Config file errors" });
return resultFlags;
}
if (configFile.fileNames.length === 0) {
// Nothing to build - must be a solution file, basically
return BuildResultFlags.None;
@ -956,7 +955,8 @@ namespace ts {
projectReferences: configFile.projectReferences,
host,
rootNames: configFile.fileNames,
options: configFile.options
options: configFile.options,
configFileParsingDiagnostics: configFile.errors,
};
const program = createProgram(programOptions);
@ -1149,7 +1149,6 @@ namespace ts {
const queue = graph.buildQueue;
reportBuildQueue(graph);
let anyFailed = false;
for (const next of queue) {
const proj = configFileCache.parseConfigFile(next);
@ -1157,11 +1156,15 @@ namespace ts {
anyFailed = true;
break;
}
// report errors early when using continue or break statements
const errors = proj.errors;
const status = getUpToDateStatus(proj);
verboseReportProjectStatus(next, status);
const projName = proj.options.configFilePath!;
if (status.type === UpToDateStatusType.UpToDate && !context.options.force) {
reportErrors(errors);
// Up to date, skip
if (defaultOptions.dry) {
// In a dry build, inform the user of this fact
@ -1171,17 +1174,20 @@ namespace ts {
}
if (status.type === UpToDateStatusType.UpToDateWithUpstreamTypes && !context.options.force) {
reportErrors(errors);
// Fake build
updateOutputTimestamps(proj);
continue;
}
if (status.type === UpToDateStatusType.UpstreamBlocked) {
reportErrors(errors);
if (context.options.verbose) reportStatus(Diagnostics.Skipping_build_of_project_0_because_its_dependency_1_has_errors, projName, status.upstreamProjectName);
continue;
}
if (status.type === UpToDateStatusType.ContainerOnly) {
reportErrors(errors);
// Do nothing
continue;
}
@ -1193,6 +1199,10 @@ namespace ts {
return anyFailed ? ExitStatus.DiagnosticsPresent_OutputsSkipped : ExitStatus.Success;
}
function reportErrors(errors: Diagnostic[]) {
errors.forEach((err) => host.reportDiagnostic(err));
}
/**
* Report the build ordering inferred from the current project graph if we're in verbose mode
*/

View file

@ -292,6 +292,48 @@ namespace ts {
});
}
export namespace EmptyFiles {
const projFs = loadProjectFromDisk("tests/projects/empty-files");
const allExpectedOutputs = [
"/src/core/index.js",
"/src/core/index.d.ts",
"/src/core/index.d.ts.map",
];
describe("tsbuild - empty files option in tsconfig", () => {
it("has empty files diagnostic when files is empty and no references are provided", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
const builder = createSolutionBuilder(host, ["/src/no-references"], { dry: false, force: false, verbose: false });
host.clearDiagnostics();
builder.buildAllProjects();
host.assertDiagnosticMessages(Diagnostics.The_files_list_in_config_file_0_is_empty);
// Check for outputs to not be written.
for (const output of allExpectedOutputs) {
assert(!fs.existsSync(output), `Expect file ${output} to not exist`);
}
});
it("does not have empty files diagnostic when files is empty and references are provided", () => {
const fs = projFs.shadow();
const host = new fakes.SolutionBuilderHost(fs);
const builder = createSolutionBuilder(host, ["/src/with-references"], { dry: false, force: false, verbose: false });
host.clearDiagnostics();
builder.buildAllProjects();
host.assertDiagnosticMessages(/*empty*/);
// Check for outputs to be written.
for (const output of allExpectedOutputs) {
assert(fs.existsSync(output), `Expect file ${output} to exist`);
}
});
});
}
describe("tsbuild - graph-ordering", () => {
let host: fakes.SolutionBuilderHost | undefined;
const deps: [string, string][] = [

View file

@ -61,6 +61,19 @@ namespace ts {
}
}
function assertParseFileDiagnosticsExclusion(jsonText: string, configFileName: string, basePath: string, allFileList: string[], expectedExcludedDiagnosticCode: number) {
{
const parsed = getParsedCommandJson(jsonText, configFileName, basePath, allFileList);
assert.isTrue(parsed.errors.length >= 0);
assert.isTrue(parsed.errors.findIndex(e => e.code === expectedExcludedDiagnosticCode) === -1, `Expected error code ${expectedExcludedDiagnosticCode} to not be in ${JSON.stringify(parsed.errors)}`);
}
{
const parsed = getParsedCommandJsonNode(jsonText, configFileName, basePath, allFileList);
assert.isTrue(parsed.errors.length >= 0);
assert.isTrue(parsed.errors.findIndex(e => e.code === expectedExcludedDiagnosticCode) === -1, `Expected error code ${expectedExcludedDiagnosticCode} to not be in ${JSON.stringify(parsed.errors)}`);
}
}
it("returns empty config for file with only whitespaces", () => {
assertParseResult("", { config : {} });
assertParseResult(" ", { config : {} });
@ -274,6 +287,32 @@ namespace ts {
"files": []
}`;
assertParseFileDiagnostics(content,
"/apath/tsconfig.json",
"tests/cases/unittests",
["/apath/a.ts"],
Diagnostics.The_files_list_in_config_file_0_is_empty.code,
/*noLocation*/ true);
});
it("generates errors for empty files list when no references are provided", () => {
const content = `{
"files": [],
"references": []
}`;
assertParseFileDiagnostics(content,
"/apath/tsconfig.json",
"tests/cases/unittests",
["/apath/a.ts"],
Diagnostics.The_files_list_in_config_file_0_is_empty.code,
/*noLocation*/ true);
});
it("does not generate errors for empty files list when one or more references are provided", () => {
const content = `{
"files": [],
"references": [{ "path": "/apath" }]
}`;
assertParseFileDiagnosticsExclusion(content,
"/apath/tsconfig.json",
"tests/cases/unittests",
["/apath/a.ts"],

View file

@ -0,0 +1 @@
export function multiply(a: number, b: number) { return a * b; }

View file

@ -0,0 +1,7 @@
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true
}
}

View file

@ -0,0 +1,9 @@
{
"references": [],
"files": [],
"compilerOptions": {
"composite": true,
"declaration": true,
"forceConsistentCasingInFileNames": true
}
}

View file

@ -0,0 +1,11 @@
{
"references": [
{ "path": "../core" },
],
"files": [],
"compilerOptions": {
"composite": true,
"declaration": true,
"forceConsistentCasingInFileNames": true
}
}