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
This commit is contained in:
Sheetal Nandi 2020-02-26 15:26:26 -08:00 committed by GitHub
parent 05c9ec3f12
commit d07761fe39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 384 additions and 48 deletions

View file

@ -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(<CommandLineOptionOfCustomType>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(<CommandLineOptionOfCustomType>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) {

View file

@ -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",

View file

@ -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",

View file

@ -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<number | string>;
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<number | string>", () => {
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;

View file

@ -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"],
});
});
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;