Ensure file, include and exclude specs used are strings (#40041)

* Test displaying failure when specs used are not strings

* Ensure specs used are strings
Fixes #38164, #39856

* Feedback
This commit is contained in:
Sheetal Nandi 2020-08-13 17:08:20 -07:00 committed by Daniel Rosenwasser
parent edaa8aa9c0
commit 10304555e7
3 changed files with 110 additions and 69 deletions

View File

@ -2308,53 +2308,48 @@ namespace ts {
};
function getFileNames(): ExpandResult {
let filesSpecs: readonly string[] | undefined;
if (hasProperty(raw, "files") && !isNullOrUndefined(raw.files)) {
if (isArray(raw.files)) {
filesSpecs = <readonly string[]>raw.files;
const hasReferences = hasProperty(raw, "references") && !isNullOrUndefined(raw.references);
const hasZeroOrNoReferences = !hasReferences || raw.references.length === 0;
const hasExtends = hasProperty(raw, "extends");
if (filesSpecs.length === 0 && hasZeroOrNoReferences && !hasExtends) {
if (sourceFile) {
const fileName = configFileName || "tsconfig.json";
const diagnosticMessage = Diagnostics.The_files_list_in_config_file_0_is_empty;
const nodeValue = firstDefined(getTsConfigPropArray(sourceFile, "files"), property => property.initializer);
const error = nodeValue
? createDiagnosticForNodeInSourceFile(sourceFile, nodeValue, diagnosticMessage, fileName)
: createCompilerDiagnostic(diagnosticMessage, fileName);
errors.push(error);
}
else {
createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json");
}
const referencesOfRaw = getPropFromRaw<ProjectReference>("references", element => typeof element === "object", "object");
if (isArray(referencesOfRaw)) {
for (const ref of referencesOfRaw) {
if (typeof ref.path !== "string") {
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string");
}
else {
(projectReferences || (projectReferences = [])).push({
path: getNormalizedAbsolutePath(ref.path, basePath),
originalPath: ref.path,
prepend: ref.prepend,
circular: ref.circular
});
}
}
else {
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "files", "Array");
}
const filesSpecs = toPropValue(getSpecsFromRaw("files"));
if (filesSpecs) {
const hasZeroOrNoReferences = referencesOfRaw === "no-prop" || isArray(referencesOfRaw) && referencesOfRaw.length === 0;
const hasExtends = hasProperty(raw, "extends");
if (filesSpecs.length === 0 && hasZeroOrNoReferences && !hasExtends) {
if (sourceFile) {
const fileName = configFileName || "tsconfig.json";
const diagnosticMessage = Diagnostics.The_files_list_in_config_file_0_is_empty;
const nodeValue = firstDefined(getTsConfigPropArray(sourceFile, "files"), property => property.initializer);
const error = nodeValue
? createDiagnosticForNodeInSourceFile(sourceFile, nodeValue, diagnosticMessage, fileName)
: createCompilerDiagnostic(diagnosticMessage, fileName);
errors.push(error);
}
else {
createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json");
}
}
}
let includeSpecs: readonly string[] | undefined;
if (hasProperty(raw, "include") && !isNullOrUndefined(raw.include)) {
if (isArray(raw.include)) {
includeSpecs = <readonly string[]>raw.include;
}
else {
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "include", "Array");
}
}
let includeSpecs = toPropValue(getSpecsFromRaw("include"));
let excludeSpecs: readonly string[] | undefined;
if (hasProperty(raw, "exclude") && !isNullOrUndefined(raw.exclude)) {
if (isArray(raw.exclude)) {
excludeSpecs = <readonly string[]>raw.exclude;
}
else {
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array");
}
}
else if (raw.compilerOptions) {
const excludeOfRaw = getSpecsFromRaw("exclude");
let excludeSpecs = toPropValue(excludeOfRaw);
if (excludeOfRaw === "no-prop" && raw.compilerOptions) {
const outDir = raw.compilerOptions.outDir;
const declarationDir = raw.compilerOptions.declarationDir;
@ -2372,28 +2367,33 @@ namespace ts {
errors.push(getErrorForNoInputFiles(result.spec, configFileName));
}
if (hasProperty(raw, "references") && !isNullOrUndefined(raw.references)) {
if (isArray(raw.references)) {
for (const ref of raw.references) {
if (typeof ref.path !== "string") {
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string");
}
else {
(projectReferences || (projectReferences = [])).push({
path: getNormalizedAbsolutePath(ref.path, basePath),
originalPath: ref.path,
prepend: ref.prepend,
circular: ref.circular
});
}
return result;
}
type PropOfRaw<T> = readonly T[] | "not-array" | "no-prop";
function toPropValue<T>(specResult: PropOfRaw<T>) {
return isArray(specResult) ? specResult : undefined;
}
function getSpecsFromRaw(prop: "files" | "include" | "exclude"): PropOfRaw<string> {
return getPropFromRaw(prop, isString, "string");
}
function getPropFromRaw<T>(prop: "files" | "include" | "exclude" | "references", validateElement: (value: unknown) => boolean, elementTypeName: string): PropOfRaw<T> {
if (hasProperty(raw, prop) && !isNullOrUndefined(raw[prop])) {
if (isArray(raw[prop])) {
const result = raw[prop];
if (!sourceFile && !every(result, validateElement)) {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, elementTypeName));
}
return result;
}
else {
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "references", "Array");
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, "Array");
return "not-array";
}
}
return result;
return "no-prop";
}
function createCompilerDiagnosticOnlyIfJson(message: DiagnosticMessage, arg0?: string, arg1?: string) {
@ -2941,7 +2941,15 @@ namespace ts {
// new entries in these paths.
const wildcardDirectories = getWildcardDirectories(validatedIncludeSpecs, validatedExcludeSpecs, basePath, host.useCaseSensitiveFileNames);
const spec: ConfigFileSpecs = { filesSpecs, includeSpecs, excludeSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories };
const spec: ConfigFileSpecs = {
filesSpecs,
includeSpecs,
excludeSpecs,
validatedFilesSpec: filter(filesSpecs, isString),
validatedIncludeSpecs,
validatedExcludeSpecs,
wildcardDirectories
};
return getFileNamesFromConfigSpecs(spec, basePath, options, host, extraFileExtensions);
}
@ -2980,7 +2988,7 @@ namespace ts {
// file map with a possibly case insensitive key. We use this map to store paths matched
// via wildcard of *.json kind
const wildCardJsonFileMap = new Map<string, string>();
const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec;
const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec;
// Rather than requery this for each file and filespec, we query the supported extensions
// once and store it on the expansion context.
@ -2989,8 +2997,8 @@ namespace ts {
// Literal files are always included verbatim. An "include" or "exclude" specification cannot
// remove a literal file.
if (filesSpecs) {
for (const fileName of filesSpecs) {
if (validatedFilesSpec) {
for (const fileName of validatedFilesSpec) {
const file = getNormalizedAbsolutePath(fileName, basePath);
literalFileMap.set(keyMapper(file), file);
}
@ -3056,14 +3064,14 @@ namespace ts {
useCaseSensitiveFileNames: boolean,
currentDirectory: string
): boolean {
const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs } = spec;
const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = spec;
if (!length(validatedIncludeSpecs) || !length(validatedExcludeSpecs)) return false;
basePath = normalizePath(basePath);
const keyMapper = createGetCanonicalFileName(useCaseSensitiveFileNames);
if (filesSpecs) {
for (const fileName of filesSpecs) {
if (validatedFilesSpec) {
for (const fileName of validatedFilesSpec) {
if (keyMapper(getNormalizedAbsolutePath(fileName, basePath)) === pathToCheck) return false;
}
}
@ -3077,6 +3085,7 @@ namespace ts {
function validateSpecs(specs: readonly string[], errors: Push<Diagnostic>, allowTrailingRecursion: boolean, jsonSourceFile: TsConfigSourceFile | undefined, specKey: string): readonly string[] {
return specs.filter(spec => {
if (!isString(spec)) return false;
const diag = specToDiagnostic(spec, allowTrailingRecursion);
if (diag !== undefined) {
errors.push(createDiagnostic(diag, spec));

View File

@ -5880,13 +5880,14 @@ namespace ts {
/**
* Present to report errors (user specified specs), validatedIncludeSpecs are used for file name matching
*/
includeSpecs?: readonly string[];
includeSpecs: readonly string[] | undefined;
/**
* Present to report errors (user specified specs), validatedExcludeSpecs are used for file name matching
*/
excludeSpecs?: readonly string[];
validatedIncludeSpecs?: readonly string[];
validatedExcludeSpecs?: readonly string[];
excludeSpecs: readonly string[] | undefined;
validatedFilesSpec: readonly string[] | undefined;
validatedIncludeSpecs: readonly string[] | undefined;
validatedExcludeSpecs: readonly string[] | undefined;
wildcardDirectories: MapLike<WatchDirectoryFlags>;
}

View File

@ -381,5 +381,36 @@ namespace ts {
const parsed = getParsedCommandJsonNode(jsonText, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"]);
assert.isTrue(parsed.errors.length >= 0);
});
it("generates errors when files is not string", () => {
assertParseFileDiagnostics(
JSON.stringify({
files: [{
compilerOptions: {
experimentalDecorators: true,
allowJs: true
}
}]
}),
"/apath/tsconfig.json",
"tests/cases/unittests",
["/apath/a.ts"],
Diagnostics.Compiler_option_0_requires_a_value_of_type_1.code,
/*noLocation*/ true);
});
it("generates errors when include is not string", () => {
assertParseFileDiagnostics(
JSON.stringify({
include: [
["./**/*.ts"]
]
}),
"/apath/tsconfig.json",
"tests/cases/unittests",
["/apath/a.ts"],
Diagnostics.Compiler_option_0_requires_a_value_of_type_1.code,
/*noLocation*/ true);
});
});
}