Skip to content

Commit c95cffe

Browse files
authored
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
1 parent 0ed523b commit c95cffe

File tree

3 files changed

+109
-68
lines changed

3 files changed

+109
-68
lines changed

Diff for: src/compiler/commandLineParser.ts

+73-64
Original file line numberDiff line numberDiff line change
@@ -2308,53 +2308,48 @@ namespace ts {
23082308
};
23092309

23102310
function getFileNames(): ExpandResult {
2311-
let filesSpecs: readonly string[] | undefined;
2312-
if (hasProperty(raw, "files") && !isNullOrUndefined(raw.files)) {
2313-
if (isArray(raw.files)) {
2314-
filesSpecs = <readonly string[]>raw.files;
2315-
const hasReferences = hasProperty(raw, "references") && !isNullOrUndefined(raw.references);
2316-
const hasZeroOrNoReferences = !hasReferences || raw.references.length === 0;
2317-
const hasExtends = hasProperty(raw, "extends");
2318-
if (filesSpecs.length === 0 && hasZeroOrNoReferences && !hasExtends) {
2319-
if (sourceFile) {
2320-
const fileName = configFileName || "tsconfig.json";
2321-
const diagnosticMessage = Diagnostics.The_files_list_in_config_file_0_is_empty;
2322-
const nodeValue = firstDefined(getTsConfigPropArray(sourceFile, "files"), property => property.initializer);
2323-
const error = nodeValue
2324-
? createDiagnosticForNodeInSourceFile(sourceFile, nodeValue, diagnosticMessage, fileName)
2325-
: createCompilerDiagnostic(diagnosticMessage, fileName);
2326-
errors.push(error);
2327-
}
2328-
else {
2329-
createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json");
2330-
}
2311+
const referencesOfRaw = getPropFromRaw<ProjectReference>("references", element => typeof element === "object", "object");
2312+
if (isArray(referencesOfRaw)) {
2313+
for (const ref of referencesOfRaw) {
2314+
if (typeof ref.path !== "string") {
2315+
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string");
2316+
}
2317+
else {
2318+
(projectReferences || (projectReferences = [])).push({
2319+
path: getNormalizedAbsolutePath(ref.path, basePath),
2320+
originalPath: ref.path,
2321+
prepend: ref.prepend,
2322+
circular: ref.circular
2323+
});
23312324
}
2332-
}
2333-
else {
2334-
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "files", "Array");
23352325
}
23362326
}
23372327

2338-
let includeSpecs: readonly string[] | undefined;
2339-
if (hasProperty(raw, "include") && !isNullOrUndefined(raw.include)) {
2340-
if (isArray(raw.include)) {
2341-
includeSpecs = <readonly string[]>raw.include;
2342-
}
2343-
else {
2344-
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "include", "Array");
2328+
const filesSpecs = toPropValue(getSpecsFromRaw("files"));
2329+
if (filesSpecs) {
2330+
const hasZeroOrNoReferences = referencesOfRaw === "no-prop" || isArray(referencesOfRaw) && referencesOfRaw.length === 0;
2331+
const hasExtends = hasProperty(raw, "extends");
2332+
if (filesSpecs.length === 0 && hasZeroOrNoReferences && !hasExtends) {
2333+
if (sourceFile) {
2334+
const fileName = configFileName || "tsconfig.json";
2335+
const diagnosticMessage = Diagnostics.The_files_list_in_config_file_0_is_empty;
2336+
const nodeValue = firstDefined(getTsConfigPropArray(sourceFile, "files"), property => property.initializer);
2337+
const error = nodeValue
2338+
? createDiagnosticForNodeInSourceFile(sourceFile, nodeValue, diagnosticMessage, fileName)
2339+
: createCompilerDiagnostic(diagnosticMessage, fileName);
2340+
errors.push(error);
2341+
}
2342+
else {
2343+
createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json");
2344+
}
23452345
}
23462346
}
23472347

2348-
let excludeSpecs: readonly string[] | undefined;
2349-
if (hasProperty(raw, "exclude") && !isNullOrUndefined(raw.exclude)) {
2350-
if (isArray(raw.exclude)) {
2351-
excludeSpecs = <readonly string[]>raw.exclude;
2352-
}
2353-
else {
2354-
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "exclude", "Array");
2355-
}
2356-
}
2357-
else if (raw.compilerOptions) {
2348+
let includeSpecs = toPropValue(getSpecsFromRaw("include"));
2349+
2350+
const excludeOfRaw = getSpecsFromRaw("exclude");
2351+
let excludeSpecs = toPropValue(excludeOfRaw);
2352+
if (excludeOfRaw === "no-prop" && raw.compilerOptions) {
23582353
const outDir = raw.compilerOptions.outDir;
23592354
const declarationDir = raw.compilerOptions.declarationDir;
23602355

@@ -2372,28 +2367,33 @@ namespace ts {
23722367
errors.push(getErrorForNoInputFiles(result.spec, configFileName));
23732368
}
23742369

2375-
if (hasProperty(raw, "references") && !isNullOrUndefined(raw.references)) {
2376-
if (isArray(raw.references)) {
2377-
for (const ref of raw.references) {
2378-
if (typeof ref.path !== "string") {
2379-
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "reference.path", "string");
2380-
}
2381-
else {
2382-
(projectReferences || (projectReferences = [])).push({
2383-
path: getNormalizedAbsolutePath(ref.path, basePath),
2384-
originalPath: ref.path,
2385-
prepend: ref.prepend,
2386-
circular: ref.circular
2387-
});
2388-
}
2370+
return result;
2371+
}
2372+
2373+
type PropOfRaw<T> = readonly T[] | "not-array" | "no-prop";
2374+
function toPropValue<T>(specResult: PropOfRaw<T>) {
2375+
return isArray(specResult) ? specResult : undefined;
2376+
}
2377+
2378+
function getSpecsFromRaw(prop: "files" | "include" | "exclude"): PropOfRaw<string> {
2379+
return getPropFromRaw(prop, isString, "string");
2380+
}
2381+
2382+
function getPropFromRaw<T>(prop: "files" | "include" | "exclude" | "references", validateElement: (value: unknown) => boolean, elementTypeName: string): PropOfRaw<T> {
2383+
if (hasProperty(raw, prop) && !isNullOrUndefined(raw[prop])) {
2384+
if (isArray(raw[prop])) {
2385+
const result = raw[prop];
2386+
if (!sourceFile && !every(result, validateElement)) {
2387+
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, elementTypeName));
23892388
}
2389+
return result;
23902390
}
23912391
else {
2392-
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "references", "Array");
2392+
createCompilerDiagnosticOnlyIfJson(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, prop, "Array");
2393+
return "not-array";
23932394
}
23942395
}
2395-
2396-
return result;
2396+
return "no-prop";
23972397
}
23982398

23992399
function createCompilerDiagnosticOnlyIfJson(message: DiagnosticMessage, arg0?: string, arg1?: string) {
@@ -2941,7 +2941,15 @@ namespace ts {
29412941
// new entries in these paths.
29422942
const wildcardDirectories = getWildcardDirectories(validatedIncludeSpecs, validatedExcludeSpecs, basePath, host.useCaseSensitiveFileNames);
29432943

2944-
const spec: ConfigFileSpecs = { filesSpecs, includeSpecs, excludeSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories };
2944+
const spec: ConfigFileSpecs = {
2945+
filesSpecs,
2946+
includeSpecs,
2947+
excludeSpecs,
2948+
validatedFilesSpec: filter(filesSpecs, isString),
2949+
validatedIncludeSpecs,
2950+
validatedExcludeSpecs,
2951+
wildcardDirectories
2952+
};
29452953
return getFileNamesFromConfigSpecs(spec, basePath, options, host, extraFileExtensions);
29462954
}
29472955

@@ -2980,7 +2988,7 @@ namespace ts {
29802988
// file map with a possibly case insensitive key. We use this map to store paths matched
29812989
// via wildcard of *.json kind
29822990
const wildCardJsonFileMap = new Map<string, string>();
2983-
const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec;
2991+
const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec;
29842992

29852993
// Rather than requery this for each file and filespec, we query the supported extensions
29862994
// once and store it on the expansion context.
@@ -2989,8 +2997,8 @@ namespace ts {
29892997

29902998
// Literal files are always included verbatim. An "include" or "exclude" specification cannot
29912999
// remove a literal file.
2992-
if (filesSpecs) {
2993-
for (const fileName of filesSpecs) {
3000+
if (validatedFilesSpec) {
3001+
for (const fileName of validatedFilesSpec) {
29943002
const file = getNormalizedAbsolutePath(fileName, basePath);
29953003
literalFileMap.set(keyMapper(file), file);
29963004
}
@@ -3056,14 +3064,14 @@ namespace ts {
30563064
useCaseSensitiveFileNames: boolean,
30573065
currentDirectory: string
30583066
): boolean {
3059-
const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs } = spec;
3067+
const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = spec;
30603068
if (!length(validatedIncludeSpecs) || !length(validatedExcludeSpecs)) return false;
30613069

30623070
basePath = normalizePath(basePath);
30633071

30643072
const keyMapper = createGetCanonicalFileName(useCaseSensitiveFileNames);
3065-
if (filesSpecs) {
3066-
for (const fileName of filesSpecs) {
3073+
if (validatedFilesSpec) {
3074+
for (const fileName of validatedFilesSpec) {
30673075
if (keyMapper(getNormalizedAbsolutePath(fileName, basePath)) === pathToCheck) return false;
30683076
}
30693077
}
@@ -3077,6 +3085,7 @@ namespace ts {
30773085

30783086
function validateSpecs(specs: readonly string[], errors: Push<Diagnostic>, allowTrailingRecursion: boolean, jsonSourceFile: TsConfigSourceFile | undefined, specKey: string): readonly string[] {
30793087
return specs.filter(spec => {
3088+
if (!isString(spec)) return false;
30803089
const diag = specToDiagnostic(spec, allowTrailingRecursion);
30813090
if (diag !== undefined) {
30823091
errors.push(createDiagnostic(diag, spec));

Diff for: src/compiler/types.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -5880,13 +5880,14 @@ namespace ts {
58805880
/**
58815881
* Present to report errors (user specified specs), validatedIncludeSpecs are used for file name matching
58825882
*/
5883-
includeSpecs?: readonly string[];
5883+
includeSpecs: readonly string[] | undefined;
58845884
/**
58855885
* Present to report errors (user specified specs), validatedExcludeSpecs are used for file name matching
58865886
*/
5887-
excludeSpecs?: readonly string[];
5888-
validatedIncludeSpecs?: readonly string[];
5889-
validatedExcludeSpecs?: readonly string[];
5887+
excludeSpecs: readonly string[] | undefined;
5888+
validatedFilesSpec: readonly string[] | undefined;
5889+
validatedIncludeSpecs: readonly string[] | undefined;
5890+
validatedExcludeSpecs: readonly string[] | undefined;
58905891
wildcardDirectories: MapLike<WatchDirectoryFlags>;
58915892
}
58925893

Diff for: src/testRunner/unittests/config/tsconfigParsing.ts

+31
Original file line numberDiff line numberDiff line change
@@ -381,5 +381,36 @@ namespace ts {
381381
const parsed = getParsedCommandJsonNode(jsonText, "/apath/tsconfig.json", "tests/cases/unittests", ["/apath/a.ts"]);
382382
assert.isTrue(parsed.errors.length >= 0);
383383
});
384+
385+
it("generates errors when files is not string", () => {
386+
assertParseFileDiagnostics(
387+
JSON.stringify({
388+
files: [{
389+
compilerOptions: {
390+
experimentalDecorators: true,
391+
allowJs: true
392+
}
393+
}]
394+
}),
395+
"/apath/tsconfig.json",
396+
"tests/cases/unittests",
397+
["/apath/a.ts"],
398+
Diagnostics.Compiler_option_0_requires_a_value_of_type_1.code,
399+
/*noLocation*/ true);
400+
});
401+
402+
it("generates errors when include is not string", () => {
403+
assertParseFileDiagnostics(
404+
JSON.stringify({
405+
include: [
406+
["./**/*.ts"]
407+
]
408+
}),
409+
"/apath/tsconfig.json",
410+
"tests/cases/unittests",
411+
["/apath/a.ts"],
412+
Diagnostics.Compiler_option_0_requires_a_value_of_type_1.code,
413+
/*noLocation*/ true);
414+
});
384415
});
385416
}

0 commit comments

Comments
 (0)