From 72cbc12c9a5cb49ff331f3a3828a83c4ae4c5991 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 6 Sep 2017 22:08:42 -0700 Subject: [PATCH] Allow undefined/null to override all parameters (#18058) --- src/compiler/commandLineParser.ts | 32 ++++++++++------ src/compiler/types.ts | 2 +- .../unittests/configurationExtension.ts | 37 ++++++++++++++++++- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index c92d147f9a9..dc9c2ad35ed 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -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 && @@ -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: @@ -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); } @@ -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 @@ -1419,7 +1425,7 @@ namespace ts { function getFileNames(): ExpandResult { let fileNames: ReadonlyArray; - if (hasProperty(raw, "files")) { + if (hasProperty(raw, "files") && !isNullOrUndefined(raw["files"])) { if (isArray(raw["files"])) { fileNames = >raw["files"]; if (fileNames.length === 0) { @@ -1432,7 +1438,7 @@ namespace ts { } let includeSpecs: ReadonlyArray; - if (hasProperty(raw, "include")) { + if (hasProperty(raw, "include") && !isNullOrUndefined(raw["include"])) { if (isArray(raw["include"])) { includeSpecs = >raw["include"]; } @@ -1442,7 +1448,7 @@ namespace ts { } let excludeSpecs: ReadonlyArray; - if (hasProperty(raw, "exclude")) { + if (hasProperty(raw, "exclude") && !isNullOrUndefined(raw["exclude"])) { if (isArray(raw["exclude"])) { excludeSpecs = >raw["exclude"]; } @@ -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( @@ -1552,7 +1558,7 @@ namespace ts { host: ParseConfigHost, basePath: string, getCanonicalFileName: (fileName: string) => string, - configFileName: string, + configFileName: string | undefined, errors: Push ): ParsedTsconfig { if (hasProperty(json, "excludes")) { @@ -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 }; @@ -1582,7 +1589,7 @@ namespace ts { host: ParseConfigHost, basePath: string, getCanonicalFileName: (fileName: string) => string, - configFileName: string, + configFileName: string | undefined, errors: Push ): ParsedTsconfig { const options = getDefaultCompilerOptions(configFileName); @@ -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( value, host, - basePath, + newBase, getCanonicalFileName, errors, (message, arg0) => @@ -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 = option; if (listOption.element.isFilePath || typeof listOption.element.type !== "string") { @@ -1827,6 +1836,7 @@ namespace ts { } function convertJsonOptionOfCustomType(opt: CommandLineOptionOfCustomType, value: string, errors: Push) { + if (isNullOrUndefined(value)) return undefined; const key = value.toLowerCase(); const val = opt.type.get(key); if (val !== undefined) { @@ -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); } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 9d5bbf5b08e..b9d8d7a6319 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3583,7 +3583,7 @@ namespace ts { name: string; } - export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[]; + export type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[] | null | undefined; export interface CompilerOptions { /*@internal*/ all?: boolean; diff --git a/src/harness/unittests/configurationExtension.ts b/src/harness/unittests/configurationExtension.ts index 2d50d2cb2af..b46d98f1524 100644 --- a/src/harness/unittests/configurationExtension.ts +++ b/src/harness/unittests/configurationExtension.ts @@ -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": "", @@ -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] @@ -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") + ]); }); }); });