Skip to content

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

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
Aug 14, 2020
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
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);
});
});
}