Skip to content

Commit

Permalink
Ensure file, include and exclude specs used are strings (#40041)
Browse files Browse the repository at this point in the history
* Test displaying failure when specs used are not strings

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

* Feedback
  • Loading branch information
sheetalkamat authored and DanielRosenwasser committed Aug 14, 2020
1 parent edaa8aa commit 1030455
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 68 deletions.
137 changes: 73 additions & 64 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

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");
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 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) {
let includeSpecs = toPropValue(getSpecsFromRaw("include"));

const excludeOfRaw = getSpecsFromRaw("exclude");
let excludeSpecs = toPropValue(excludeOfRaw);
if (excludeOfRaw === "no-prop" && raw.compilerOptions) {
const outDir = raw.compilerOptions.outDir;
const declarationDir = raw.compilerOptions.declarationDir;

Expand All @@ -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) {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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.
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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));
Expand Down
9 changes: 5 additions & 4 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}

Expand Down
31 changes: 31 additions & 0 deletions src/testRunner/unittests/config/tsconfigParsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
}

0 comments on commit 1030455

Please sign in to comment.