From d07761fe3944f48c7518cf4199a82466a664a347 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 26 Feb 2020 15:26:26 -0800 Subject: [PATCH] Allow --composite false or --composite null on the command line (#36997) * Add tests for specifying composite as command line option * Allow passing --composite false on commandline * Add test to verify tsc --composite false from command line * Handle "undefined" as option value to be set to undefined for that option * Support "null" as option to be converted to undefined which is normally end result from our config file as well * Support null as option for any tsconfig only option as well, and dont support undefined * Fix public api test case * Validates objects instead of stringify result * Add composite true to base source --- src/compiler/commandLineParser.ts | 96 ++++++--- src/compiler/diagnosticMessages.json | 6 +- src/testRunner/tsconfig.json | 1 + .../unittests/config/commandLineParsing.ts | 199 ++++++++++++++++-- src/testRunner/unittests/tsc/composite.ts | 85 ++++++++ ...and-line-but-has-tsbuild-info-in-config.js | 11 + ...and-line-but-has-tsbuild-info-in-config.js | 12 ++ ...setting-composite-false-on-command-line.js | 11 + ...-setting-composite-null-on-command-line.js | 11 + 9 files changed, 384 insertions(+), 48 deletions(-) create mode 100644 src/testRunner/unittests/tsc/composite.ts create mode 100644 tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-and-tsbuildinfo-as-null-on-command-line-but-has-tsbuild-info-in-config.js create mode 100644 tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-on-command-line-but-has-tsbuild-info-in-config.js create mode 100644 tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-on-command-line.js create mode 100644 tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-null-on-command-line.js diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index e425445fc3..72bc87d4ee 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1164,11 +1164,13 @@ namespace ts { } } - interface OptionsBase { + /*@internal*/ + export interface OptionsBase { [option: string]: CompilerOptionsValue | TsConfigSourceFile | undefined; } - interface ParseCommandLineWorkerDiagnostics extends DidYouMeanOptionsDiagnostics { + /*@internal*/ + export interface ParseCommandLineWorkerDiagnostics extends DidYouMeanOptionsDiagnostics { getOptionsNameMap: () => OptionsNameMap; optionTypeMismatchDiagnostic: DiagnosticMessage; } @@ -1189,7 +1191,8 @@ namespace ts { createDiagnostics(diagnostics.unknownOptionDiagnostic, unknownOptionErrorText || unknownOption); } - function parseCommandLineWorker( + /*@internal*/ + export function parseCommandLineWorker( diagnostics: ParseCommandLineWorkerDiagnostics, commandLine: readonly string[], readFile?: (path: string) => string | undefined) { @@ -1279,7 +1282,25 @@ namespace ts { errors: Diagnostic[] ) { if (opt.isTSConfigOnly) { - errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file, opt.name)); + const optValue = args[i]; + if (optValue === "null") { + options[opt.name] = undefined; + i++; + } + else if (opt.type === "boolean") { + if (optValue === "false") { + options[opt.name] = false; + i++; + } + else { + if (optValue === "true") i++; + errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_false_or_null_on_command_line, opt.name)); + } + } + else { + errors.push(createCompilerDiagnostic(Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, opt.name)); + if (optValue && !startsWith(optValue, "-")) i++; + } } else { // Check to see if no argument was provided (e.g. "--locale" is the last command-line argument). @@ -1287,42 +1308,49 @@ namespace ts { errors.push(createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name, getCompilerOptionValueTypeString(opt))); } - switch (opt.type) { - case "number": - options[opt.name] = parseInt(args[i]); - i++; - break; - case "boolean": - // boolean flag has optional value true, false, others - const optValue = args[i]; - options[opt.name] = optValue !== "false"; - // consume next argument as boolean flag value - if (optValue === "false" || optValue === "true") { + if (args[i] !== "null") { + switch (opt.type) { + case "number": + options[opt.name] = parseInt(args[i]); i++; - } - break; - case "string": - options[opt.name] = args[i] || ""; - i++; - break; - case "list": - const result = parseListTypeOption(opt, args[i], errors); - options[opt.name] = result || []; - if (result) { + break; + case "boolean": + // boolean flag has optional value true, false, others + const optValue = args[i]; + options[opt.name] = optValue !== "false"; + // consume next argument as boolean flag value + if (optValue === "false" || optValue === "true") { + i++; + } + break; + case "string": + options[opt.name] = args[i] || ""; i++; - } - break; - // If not a primitive, the possible types are specified in what is effectively a map of options. - default: - options[opt.name] = parseCustomTypeOption(opt, args[i], errors); - i++; - break; + break; + case "list": + const result = parseListTypeOption(opt, args[i], errors); + options[opt.name] = result || []; + if (result) { + i++; + } + break; + // If not a primitive, the possible types are specified in what is effectively a map of options. + default: + options[opt.name] = parseCustomTypeOption(opt, args[i], errors); + i++; + break; + } + } + else { + options[opt.name] = undefined; + i++; } } return i; } - const compilerOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { + /*@internal*/ + export const compilerOptionsDidYouMeanDiagnostics: ParseCommandLineWorkerDiagnostics = { getOptionsNameMap, optionDeclarations, unknownOptionDiagnostic: Diagnostics.Unknown_compiler_option_0, @@ -2170,7 +2198,7 @@ namespace ts { } function convertToOptionValueWithAbsolutePaths(option: CommandLineOption | undefined, value: CompilerOptionsValue, toAbsolutePath: (path: string) => string) { - if (option) { + if (option && !isNullOrUndefined(value)) { if (option.type === "list") { const values = value as readonly (string | number)[]; if (option.element.isFilePath && values.length) { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 4ee407ce1c..d64e8d6ee8 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3642,7 +3642,7 @@ "category": "Message", "code": 6061 }, - "Option '{0}' can only be specified in 'tsconfig.json' file.": { + "Option '{0}' can only be specified in 'tsconfig.json' file or set to 'null' on command line.": { "category": "Error", "code": 6064 }, @@ -4296,6 +4296,10 @@ "category": "Error", "code": 6229 }, + "Option '{0}' can only be specified in 'tsconfig.json' file or set to 'false' or 'null' on command line.": { + "category": "Error", + "code": 6230 + }, "Projects to reference": { "category": "Message", diff --git a/src/testRunner/tsconfig.json b/src/testRunner/tsconfig.json index 909b2fe8c4..5fdc05ae1c 100644 --- a/src/testRunner/tsconfig.json +++ b/src/testRunner/tsconfig.json @@ -126,6 +126,7 @@ "unittests/tsbuild/transitiveReferences.ts", "unittests/tsbuild/watchEnvironment.ts", "unittests/tsbuild/watchMode.ts", + "unittests/tsc/composite.ts", "unittests/tsc/declarationEmit.ts", "unittests/tsc/incremental.ts", "unittests/tsc/listFilesOnly.ts", diff --git a/src/testRunner/unittests/config/commandLineParsing.ts b/src/testRunner/unittests/config/commandLineParsing.ts index 47e66bac19..8dbbffaf20 100644 --- a/src/testRunner/unittests/config/commandLineParsing.ts +++ b/src/testRunner/unittests/config/commandLineParsing.ts @@ -1,11 +1,9 @@ namespace ts { describe("unittests:: config:: commandLineParsing:: parseCommandLine", () => { - function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine) { - const parsed = parseCommandLine(commandLine); - const parsedCompilerOptions = JSON.stringify(parsed.options); - const expectedCompilerOptions = JSON.stringify(expectedParsedCommandLine.options); - assert.equal(parsedCompilerOptions, expectedCompilerOptions); + function assertParseResult(commandLine: string[], expectedParsedCommandLine: ParsedCommandLine, workerDiagnostic?: () => ParseCommandLineWorkerDiagnostics) { + const parsed = parseCommandLineWorker(workerDiagnostic?.() || compilerOptionsDidYouMeanDiagnostics, commandLine); + assert.deepEqual(parsed.options, expectedParsedCommandLine.options); assert.deepEqual(parsed.watchOptions, expectedParsedCommandLine.watchOptions); const parsedErrors = parsed.errors; @@ -120,7 +118,7 @@ namespace ts { length: undefined, }], fileNames: ["0.ts"], - options: {} + options: { jsx: undefined } }); }); @@ -146,7 +144,7 @@ namespace ts { length: undefined, }], fileNames: ["0.ts"], - options: {} + options: { module: undefined } }); }); @@ -172,7 +170,7 @@ namespace ts { length: undefined, }], fileNames: ["0.ts"], - options: {} + options: { newLine: undefined } }); }); @@ -198,7 +196,7 @@ namespace ts { length: undefined, }], fileNames: ["0.ts"], - options: {} + options: { target: undefined } }); }); @@ -224,7 +222,7 @@ namespace ts { length: undefined, }], fileNames: ["0.ts"], - options: {} + options: { moduleResolution: undefined } }); }); @@ -414,6 +412,183 @@ namespace ts { }); }); + describe("parses command line null for tsconfig only option", () => { + interface VerifyNull { + optionName: string; + nonNullValue?: string; + workerDiagnostic?: () => ParseCommandLineWorkerDiagnostics; + diagnosticMessage: DiagnosticMessage; + } + function verifyNull({ optionName, nonNullValue, workerDiagnostic, diagnosticMessage }: VerifyNull) { + it("allows setting it to null", () => { + assertParseResult( + [`--${optionName}`, "null", "0.ts"], + { + errors: [], + fileNames: ["0.ts"], + options: { [optionName]: undefined } + }, + workerDiagnostic + ); + }); + + if (nonNullValue) { + it("errors if non null value is passed", () => { + assertParseResult( + [`--${optionName}`, nonNullValue, "0.ts"], + { + errors: [{ + messageText: formatStringFromArgs(diagnosticMessage.message, [optionName]), + category: diagnosticMessage.category, + code: diagnosticMessage.code, + file: undefined, + start: undefined, + length: undefined + }], + fileNames: ["0.ts"], + options: {} + }, + workerDiagnostic + ); + }); + } + + it("errors if its followed by another option", () => { + assertParseResult( + ["0.ts", "--strictNullChecks", `--${optionName}`], + { + errors: [{ + messageText: formatStringFromArgs(diagnosticMessage.message, [optionName]), + category: diagnosticMessage.category, + code: diagnosticMessage.code, + file: undefined, + start: undefined, + length: undefined + }], + fileNames: ["0.ts"], + options: { strictNullChecks: true } + }, + workerDiagnostic + ); + }); + + it("errors if its last option", () => { + assertParseResult( + ["0.ts", `--${optionName}`], + { + errors: [{ + messageText: formatStringFromArgs(diagnosticMessage.message, [optionName]), + category: diagnosticMessage.category, + code: diagnosticMessage.code, + file: undefined, + start: undefined, + length: undefined + }], + fileNames: ["0.ts"], + options: {} + }, + workerDiagnostic + ); + }); + } + + interface VerifyNullNonIncludedOption { + type: () => "string" | "number" | Map; + nonNullValue?: string; + } + function verifyNullNonIncludedOption({ type, nonNullValue }: VerifyNullNonIncludedOption) { + verifyNull({ + optionName: "optionName", + nonNullValue, + diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line, + workerDiagnostic: () => { + const optionDeclarations = [ + ...compilerOptionsDidYouMeanDiagnostics.optionDeclarations, + { + name: "optionName", + type: type(), + isTSConfigOnly: true, + category: Diagnostics.Basic_Options, + description: Diagnostics.Enable_project_compilation, + } + ]; + return { + ...compilerOptionsDidYouMeanDiagnostics, + optionDeclarations, + getOptionsNameMap: () => createOptionNameMap(optionDeclarations) + }; + } + }); + } + + describe("option of type boolean", () => { + it("allows setting it to false", () => { + assertParseResult( + ["--composite", "false", "0.ts"], + { + errors: [], + fileNames: ["0.ts"], + options: { composite: false } + } + ); + }); + + verifyNull({ + optionName: "composite", + nonNullValue: "true", + diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_false_or_null_on_command_line + }); + }); + + describe("option of type object", () => { + verifyNull({ + optionName: "paths", + diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line + }); + }); + + describe("option of type list", () => { + verifyNull({ + optionName: "rootDirs", + nonNullValue: "abc,xyz", + diagnosticMessage: Diagnostics.Option_0_can_only_be_specified_in_tsconfig_json_file_or_set_to_null_on_command_line + }); + }); + + describe("option of type string", () => { + verifyNullNonIncludedOption({ + type: () => "string", + nonNullValue: "hello" + }); + }); + + describe("option of type number", () => { + verifyNullNonIncludedOption({ + type: () => "number", + nonNullValue: "10" + }); + }); + + describe("option of type Map", () => { + verifyNullNonIncludedOption({ + type: () => createMapFromTemplate({ + node: ModuleResolutionKind.NodeJs, + classic: ModuleResolutionKind.Classic, + }), + nonNullValue: "node" + }); + }); + }); + + it("allows tsconfig only option to be set to null", () => { + assertParseResult(["--composite", "null", "-tsBuildInfoFile", "null", "0.ts"], + { + errors: [], + fileNames: ["0.ts"], + options: { composite: undefined, tsBuildInfoFile: undefined } + }); + }); + describe("Watch options", () => { it("parse --watchFile", () => { assertParseResult(["--watchFile", "UseFsEvents", "0.ts"], @@ -487,9 +662,7 @@ namespace ts { describe("unittests:: config:: commandLineParsing:: parseBuildOptions", () => { function assertParseResult(commandLine: string[], expectedParsedBuildCommand: ParsedBuildCommand) { const parsed = parseBuildCommand(commandLine); - const parsedBuildOptions = JSON.stringify(parsed.buildOptions); - const expectedBuildOptions = JSON.stringify(expectedParsedBuildCommand.buildOptions); - assert.equal(parsedBuildOptions, expectedBuildOptions); + assert.deepEqual(parsed.buildOptions, expectedParsedBuildCommand.buildOptions); assert.deepEqual(parsed.watchOptions, expectedParsedBuildCommand.watchOptions); const parsedErrors = parsed.errors; diff --git a/src/testRunner/unittests/tsc/composite.ts b/src/testRunner/unittests/tsc/composite.ts new file mode 100644 index 0000000000..8eb0ea3bd8 --- /dev/null +++ b/src/testRunner/unittests/tsc/composite.ts @@ -0,0 +1,85 @@ +namespace ts { + describe("unittests:: tsc:: composite::", () => { + verifyTsc({ + scenario: "composite", + subScenario: "when setting composite false on command line", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` + { + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "composite": true, + }, + "include": [ + "src/**/*.ts" + ] + }`, + }), + commandLineArgs: ["--composite", "false", "--p", "src/project"], + }); + + verifyTsc({ + scenario: "composite", + subScenario: "when setting composite null on command line", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` + { + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "composite": true, + }, + "include": [ + "src/**/*.ts" + ] + }`, + }), + commandLineArgs: ["--composite", "null", "--p", "src/project"], + }); + + verifyTsc({ + scenario: "composite", + subScenario: "when setting composite false on command line but has tsbuild info in config", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` + { + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "composite": true, + "tsBuildInfoFile": "tsconfig.json.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ] + }`, + }), + commandLineArgs: ["--composite", "false", "--p", "src/project"], + }); + + verifyTsc({ + scenario: "composite", + subScenario: "when setting composite false and tsbuildinfo as null on command line but has tsbuild info in config", + fs: () => loadProjectFromFiles({ + "/src/project/src/main.ts": "export const x = 10;", + "/src/project/tsconfig.json": Utils.dedent` + { + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "composite": true, + "tsBuildInfoFile": "tsconfig.json.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ] + }`, + }), + commandLineArgs: ["--composite", "false", "--p", "src/project", "--tsBuildInfoFile", "null"], + }); + }); +} diff --git a/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-and-tsbuildinfo-as-null-on-command-line-but-has-tsbuild-info-in-config.js b/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-and-tsbuildinfo-as-null-on-command-line-but-has-tsbuild-info-in-config.js new file mode 100644 index 0000000000..8b1e90d965 --- /dev/null +++ b/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-and-tsbuildinfo-as-null-on-command-line-but-has-tsbuild-info-in-config.js @@ -0,0 +1,11 @@ +//// [/lib/initial-buildOutput.txt] +/lib/tsc --composite false --p src/project --tsBuildInfoFile null +exitCode:: ExitStatus.Success + + +//// [/src/project/src/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.x = 10; + + diff --git a/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-on-command-line-but-has-tsbuild-info-in-config.js b/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-on-command-line-but-has-tsbuild-info-in-config.js new file mode 100644 index 0000000000..dc7ed0590b --- /dev/null +++ b/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-on-command-line-but-has-tsbuild-info-in-config.js @@ -0,0 +1,12 @@ +//// [/lib/initial-buildOutput.txt] +/lib/tsc --composite false --p src/project +src/project/tsconfig.json(6,9): error TS5069: Option 'tsBuildInfoFile' cannot be specified without specifying option 'incremental' or option 'composite'. +exitCode:: ExitStatus.DiagnosticsPresent_OutputsGenerated + + +//// [/src/project/src/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.x = 10; + + diff --git a/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-on-command-line.js b/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-on-command-line.js new file mode 100644 index 0000000000..878fe45b3f --- /dev/null +++ b/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-false-on-command-line.js @@ -0,0 +1,11 @@ +//// [/lib/initial-buildOutput.txt] +/lib/tsc --composite false --p src/project +exitCode:: ExitStatus.Success + + +//// [/src/project/src/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.x = 10; + + diff --git a/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-null-on-command-line.js b/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-null-on-command-line.js new file mode 100644 index 0000000000..c30cd727af --- /dev/null +++ b/tests/baselines/reference/tsc/composite/initial-build/when-setting-composite-null-on-command-line.js @@ -0,0 +1,11 @@ +//// [/lib/initial-buildOutput.txt] +/lib/tsc --composite null --p src/project +exitCode:: ExitStatus.Success + + +//// [/src/project/src/main.js] +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.x = 10; + +