Skip to content

Allow undefined/null to override any compiler option #18058

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
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
32 changes: 21 additions & 11 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,7 +1057,7 @@ namespace ts {
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnosticMessage, keyText));
}
const value = convertPropertyValueToJson(element.initializer, option);
if (typeof keyText !== "undefined" && typeof value !== "undefined") {
if (typeof keyText !== "undefined") {
result[keyText] = value;
// Notify key value set, if user asked for it
if (jsonConversionNotifier &&
Expand Down Expand Up @@ -1104,7 +1104,7 @@ namespace ts {
return false;

case SyntaxKind.NullKeyword:
reportInvalidOptionValue(!!option);
reportInvalidOptionValue(option && option.name === "extends"); // "extends" is the only option we don't allow null/undefined for
return null; // tslint:disable-line:no-null-keyword

case SyntaxKind.StringLiteral:
Expand Down Expand Up @@ -1189,6 +1189,7 @@ namespace ts {

function isCompilerOptionsValue(option: CommandLineOption, value: any): value is CompilerOptionsValue {
if (option) {
if (isNullOrUndefined(value)) return true; // All options are undefinable/nullable
if (option.type === "list") {
return isArray(value);
}
Expand Down Expand Up @@ -1379,6 +1380,11 @@ namespace ts {
}
}

function isNullOrUndefined(x: any): x is null | undefined {
// tslint:disable-next-line:no-null-keyword
return x === undefined || x === null;
}

/**
* Parse the contents of a config file from json or json source file (tsconfig.json).
* @param json The contents of the config file to parse
Expand Down Expand Up @@ -1419,7 +1425,7 @@ namespace ts {

function getFileNames(): ExpandResult {
let fileNames: ReadonlyArray<string>;
if (hasProperty(raw, "files")) {
if (hasProperty(raw, "files") && !isNullOrUndefined(raw["files"])) {
if (isArray(raw["files"])) {
fileNames = <ReadonlyArray<string>>raw["files"];
if (fileNames.length === 0) {
Expand All @@ -1432,7 +1438,7 @@ namespace ts {
}

let includeSpecs: ReadonlyArray<string>;
if (hasProperty(raw, "include")) {
if (hasProperty(raw, "include") && !isNullOrUndefined(raw["include"])) {
if (isArray(raw["include"])) {
includeSpecs = <ReadonlyArray<string>>raw["include"];
}
Expand All @@ -1442,7 +1448,7 @@ namespace ts {
}

let excludeSpecs: ReadonlyArray<string>;
if (hasProperty(raw, "exclude")) {
if (hasProperty(raw, "exclude") && !isNullOrUndefined(raw["exclude"])) {
if (isArray(raw["exclude"])) {
excludeSpecs = <ReadonlyArray<string>>raw["exclude"];
}
Expand All @@ -1461,7 +1467,7 @@ namespace ts {
includeSpecs = ["**/*"];
}

const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions, sourceFile);
const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, configFileName ? getDirectoryPath(toPath(configFileName, basePath, createGetCanonicalFileName(host.useCaseSensitiveFileNames))) : basePath, options, host, errors, extraFileExtensions, sourceFile);

if (result.fileNames.length === 0 && !hasProperty(raw, "files") && resolutionStack.length === 0) {
errors.push(
Expand Down Expand Up @@ -1552,7 +1558,7 @@ namespace ts {
host: ParseConfigHost,
basePath: string,
getCanonicalFileName: (fileName: string) => string,
configFileName: string,
configFileName: string | undefined,
errors: Push<Diagnostic>
): ParsedTsconfig {
if (hasProperty(json, "excludes")) {
Expand All @@ -1571,7 +1577,8 @@ namespace ts {
errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string"));
}
else {
extendedConfigPath = getExtendsConfigPath(json.extends, host, basePath, getCanonicalFileName, errors, createCompilerDiagnostic);
const newBase = configFileName ? getDirectoryPath(toPath(configFileName, basePath, getCanonicalFileName)) : basePath;
extendedConfigPath = getExtendsConfigPath(json.extends, host, newBase, getCanonicalFileName, errors, createCompilerDiagnostic);
}
}
return { raw: json, options, typeAcquisition, extendedConfigPath };
Expand All @@ -1582,7 +1589,7 @@ namespace ts {
host: ParseConfigHost,
basePath: string,
getCanonicalFileName: (fileName: string) => string,
configFileName: string,
configFileName: string | undefined,
errors: Push<Diagnostic>
): ParsedTsconfig {
const options = getDefaultCompilerOptions(configFileName);
Expand All @@ -1603,10 +1610,11 @@ namespace ts {
onSetValidOptionKeyValueInRoot(key: string, _keyNode: PropertyName, value: CompilerOptionsValue, valueNode: Expression) {
switch (key) {
case "extends":
const newBase = configFileName ? getDirectoryPath(toPath(configFileName, basePath, getCanonicalFileName)) : basePath;
extendedConfigPath = getExtendsConfigPath(
<string>value,
host,
basePath,
newBase,
getCanonicalFileName,
errors,
(message, arg0) =>
Expand Down Expand Up @@ -1803,6 +1811,7 @@ namespace ts {
}

function normalizeOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue {
if (isNullOrUndefined(value)) return undefined;
if (option.type === "list") {
const listOption = <CommandLineOptionOfListType>option;
if (listOption.element.isFilePath || typeof listOption.element.type !== "string") {
Expand All @@ -1827,6 +1836,7 @@ namespace ts {
}

function convertJsonOptionOfCustomType(opt: CommandLineOptionOfCustomType, value: string, errors: Push<Diagnostic>) {
if (isNullOrUndefined(value)) return undefined;
const key = value.toLowerCase();
const val = opt.type.get(key);
if (val !== undefined) {
Expand Down Expand Up @@ -1977,7 +1987,7 @@ namespace ts {
// remove a literal file.
if (fileNames) {
for (const fileName of fileNames) {
const file = combinePaths(basePath, fileName);
const file = getNormalizedAbsolutePath(fileName, basePath);
literalFileMap.set(keyMapper(file), file);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3568,7 +3568,7 @@ namespace ts {
name: string;
}

export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | PluginImport[];
export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike<string[]> | PluginImport[] | null | undefined;

export interface CompilerOptions {
/*@internal*/ all?: boolean;
Expand Down
37 changes: 36 additions & 1 deletion src/harness/unittests/configurationExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,23 @@ namespace ts {
},
include: ["../supplemental.*"]
},
"/dev/configs/third.json": {
extends: "./second",
compilerOptions: {
// tslint:disable-next-line:no-null-keyword
module: null
},
include: ["../supplemental.*"]
},
"/dev/configs/fourth.json": {
extends: "./third",
compilerOptions: {
module: "system"
},
// tslint:disable-next-line:no-null-keyword
include: null,
files: ["../main.ts"]
},
"/dev/extends.json": { extends: 42 },
"/dev/extends2.json": { extends: "configs/base" },
"/dev/main.ts": "",
Expand Down Expand Up @@ -106,7 +123,7 @@ namespace ts {
}
}

describe("Configuration Extension", () => {
describe("configurationExtension", () => {
forEach<[string, string, Utils.MockParseConfigHost], void>([
["under a case insensitive host", caseInsensitiveBasePath, caseInsensitiveHost],
["under a case sensitive host", caseSensitiveBasePath, caseSensitiveHost]
Expand Down Expand Up @@ -206,6 +223,24 @@ namespace ts {
category: DiagnosticCategory.Error,
messageText: `A path in an 'extends' option must be relative or rooted, but 'configs/base' is not.`
}]);

testSuccess("can overwrite compiler options using extended 'null'", "configs/third.json", {
allowJs: true,
noImplicitAny: true,
strictNullChecks: true,
module: undefined // Technically, this is distinct from the key never being set; but within the compiler we don't make the distinction
}, [
combinePaths(basePath, "supplemental.ts")
]);

testSuccess("can overwrite top-level options using extended 'null'", "configs/fourth.json", {
allowJs: true,
noImplicitAny: true,
strictNullChecks: true,
module: ModuleKind.System
}, [
combinePaths(basePath, "main.ts")
]);
});
});
});
Expand Down