From d6ff1a7241dab125fc1258d3f59416641ccf8ff8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 29 Aug 2018 12:23:52 -0700 Subject: [PATCH] Move parsing of build options to commandLineParsing so it can be tested and it lines with other commandline parsing --- src/compiler/commandLineParser.ts | 124 ++++++++++++++--- .../unittests/commandLineParsing.ts | 116 ++++++++++++++++ src/tsc/tsc.ts | 125 +++--------------- 3 files changed, 241 insertions(+), 124 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index f1dd4093956..e4c4edcb4db 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -62,9 +62,7 @@ namespace ts { /* @internal */ export const libMap = createMapFromEntries(libEntries); - /* @internal */ - export const optionDeclarations: CommandLineOption[] = [ - // CommandLine only options + const commonOptionsWithBuild: CommandLineOption[] = [ { name: "help", shortName: "h", @@ -78,6 +76,27 @@ namespace ts { shortName: "?", type: "boolean" }, + { + name: "preserveWatchOutput", + type: "boolean", + showInSimplifiedHelpView: false, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Whether_to_keep_outdated_console_output_in_watch_mode_instead_of_clearing_the_screen, + }, + { + name: "watch", + shortName: "w", + type: "boolean", + showInSimplifiedHelpView: true, + category: Diagnostics.Command_line_Options, + description: Diagnostics.Watch_input_files, + }, + ]; + + /* @internal */ + export const optionDeclarations: CommandLineOption[] = [ + // CommandLine only options + ...commonOptionsWithBuild, { name: "all", type: "boolean", @@ -125,21 +144,6 @@ namespace ts { category: Diagnostics.Command_line_Options, description: Diagnostics.Stylize_errors_and_messages_using_color_and_context_experimental }, - { - name: "preserveWatchOutput", - type: "boolean", - showInSimplifiedHelpView: false, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Whether_to_keep_outdated_console_output_in_watch_mode_instead_of_clearing_the_screen, - }, - { - name: "watch", - shortName: "w", - type: "boolean", - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Watch_input_files, - }, // Basic { @@ -754,6 +758,38 @@ namespace ts { } ]; + /* @internal */ + export const buildOpts: CommandLineOption[] = [ + ...commonOptionsWithBuild, + { + name: "verbose", + shortName: "v", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Enable_verbose_logging, + type: "boolean" + }, + { + name: "dry", + shortName: "d", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, + type: "boolean" + }, + { + name: "force", + shortName: "f", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, + type: "boolean" + }, + { + name: "clean", + category: Diagnostics.Command_line_Options, + description: Diagnostics.Delete_the_outputs_of_all_projects, + type: "boolean" + } + ]; + /* @internal */ export const typeAcquisitionDeclarations: CommandLineOption[] = [ { @@ -997,6 +1033,58 @@ namespace ts { return optionNameMap.get(optionName); } + /*@internal*/ + export interface ParsedBuildCommand { + buildOptions: BuildOptions; + projects: string[]; + errors: ReadonlyArray; + } + + /*@internal*/ + export function parseBuildCommand(args: string[]): ParsedBuildCommand { + let buildOptionNameMap: OptionNameMap | undefined; + const returnBuildOptionNameMap = () => (buildOptionNameMap || (buildOptionNameMap = createOptionNameMap(buildOpts))); + + const buildOptions: BuildOptions = {}; + const projects: string[] = []; + let errors: Diagnostic[] | undefined; + for (const arg of args) { + if (arg.charCodeAt(0) === CharacterCodes.minus) { + const opt = getOptionDeclarationFromName(returnBuildOptionNameMap, arg.slice(arg.charCodeAt(1) === CharacterCodes.minus ? 2 : 1), /*allowShort*/ true); + if (opt) { + buildOptions[opt.name as keyof BuildOptions] = true; + } + else { + (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.Unknown_build_option_0, arg)); + } + } + else { + // Not a flag, parse as filename + projects.push(arg); + } + } + + if (projects.length === 0) { + // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." + projects.push("."); + } + + // Nonsensical combinations + if (buildOptions.clean && buildOptions.force) { + (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); + } + if (buildOptions.clean && buildOptions.verbose) { + (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); + } + if (buildOptions.clean && buildOptions.watch) { + (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); + } + if (buildOptions.watch && buildOptions.dry) { + (errors || (errors = [])).push(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); + } + + return { buildOptions, projects, errors: errors || emptyArray }; + } function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); diff --git a/src/testRunner/unittests/commandLineParsing.ts b/src/testRunner/unittests/commandLineParsing.ts index 7145b9e5ec4..7ac9504f1f8 100644 --- a/src/testRunner/unittests/commandLineParsing.ts +++ b/src/testRunner/unittests/commandLineParsing.ts @@ -366,4 +366,120 @@ namespace ts { }); }); }); + + describe("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); + + const parsedErrors = parsed.errors; + const expectedErrors = expectedParsedBuildCommand.errors; + assert.isTrue(parsedErrors.length === expectedErrors.length, `Expected error: ${JSON.stringify(expectedErrors)}. Actual error: ${JSON.stringify(parsedErrors)}.`); + for (let i = 0; i < parsedErrors.length; i++) { + const parsedError = parsedErrors[i]; + const expectedError = expectedErrors[i]; + assert.equal(parsedError.code, expectedError.code); + assert.equal(parsedError.category, expectedError.category); + assert.equal(parsedError.messageText, expectedError.messageText); + } + + const parsedProjects = parsed.projects; + const expectedProjects = expectedParsedBuildCommand.projects; + assert.deepEqual(parsedProjects, expectedProjects, `Expected projects: [${JSON.stringify(expectedProjects)}]. Actual projects: [${JSON.stringify(parsedProjects)}].`); + } + it("parse build without any options ", () => { + // --lib es6 0.ts + assertParseResult([], + { + errors: [], + projects: ["."], + buildOptions: {} + }); + }); + + it("Parse multiple options", () => { + // --lib es5,es2015.symbol.wellknown 0.ts + assertParseResult(["--verbose", "--force", "tests"], + { + errors: [], + projects: ["tests"], + buildOptions: { verbose: true, force: true } + }); + }); + + it("Parse option with invalid option ", () => { + // --lib es5,invalidOption 0.ts + assertParseResult(["--verbose", "--invalidOption"], + { + errors: [{ + messageText: "Unknown build option '--invalidOption'.", + category: Diagnostics.Unknown_build_option_0.category, + code: Diagnostics.Unknown_build_option_0.code, + file: undefined, + start: undefined, + length: undefined, + }], + projects: ["."], + buildOptions: { verbose: true } + }); + }); + + it("Parse multiple flags with input projects at the end", () => { + // --lib es5,es2015.symbol.wellknown --target es5 0.ts + assertParseResult(["--force", "--verbose", "src", "tests"], + { + errors: [], + projects: ["src", "tests"], + buildOptions: { force: true, verbose: true } + }); + }); + + it("Parse multiple flags with input projects in the middle", () => { + // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown + assertParseResult(["--force", "src", "tests", "--verbose"], + { + errors: [], + projects: ["src", "tests"], + buildOptions: { force: true, verbose: true } + }); + }); + + it("Parse multiple flags with input projects in the beginning", () => { + // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown + assertParseResult(["src", "tests", "--force", "--verbose"], + { + errors: [], + projects: ["src", "tests"], + buildOptions: { force: true, verbose: true } + }); + }); + + describe("Combining options that make no sense together", () => { + function verifyInvalidCombination(flag1: keyof BuildOptions, flag2: keyof BuildOptions) { + it(`--${flag1} and --${flag2} together is invalid`, () => { + // --module commonjs --target es5 0.ts --lib es5,es2015.symbol.wellknown + assertParseResult([`--${flag1}`, `--${flag2}`], + { + errors: [{ + messageText: `Options '${flag1}' and '${flag2}' cannot be combined.`, + category: Diagnostics.Options_0_and_1_cannot_be_combined.category, + code: Diagnostics.Options_0_and_1_cannot_be_combined.code, + file: undefined, + start: undefined, + length: undefined, + }], + projects: ["."], + buildOptions: { [flag1]: true, [flag2]: true } + }); + }); + } + + verifyInvalidCombination("clean", "force"); + verifyInvalidCombination("clean", "verbose"); + verifyInvalidCombination("clean", "watch"); + verifyInvalidCombination("watch", "dry"); + }); + }); } diff --git a/src/tsc/tsc.ts b/src/tsc/tsc.ts index 3e0b9a631f8..d3966071265 100644 --- a/src/tsc/tsc.ts +++ b/src/tsc/tsc.ts @@ -165,80 +165,10 @@ namespace ts { } function performBuild(args: string[]): number | undefined { - const buildOpts: CommandLineOption[] = [ - { - name: "help", - shortName: "h", - type: "boolean", - showInSimplifiedHelpView: true, - category: Diagnostics.Command_line_Options, - description: Diagnostics.Print_this_message, - }, - { - name: "help", - shortName: "?", - type: "boolean" - }, - { - name: "verbose", - shortName: "v", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Enable_verbose_logging, - type: "boolean" - }, - { - name: "dry", - shortName: "d", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Show_what_would_be_built_or_deleted_if_specified_with_clean, - type: "boolean" - }, - { - name: "force", - shortName: "f", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Build_all_projects_including_those_that_appear_to_be_up_to_date, - type: "boolean" - }, - { - name: "clean", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Delete_the_outputs_of_all_projects, - type: "boolean" - }, - { - name: "watch", - shortName:"w", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Watch_input_files, - type: "boolean" - }, - { - name: "preserveWatchOutput", - type: "boolean", - category: Diagnostics.Command_line_Options, - description: Diagnostics.Whether_to_keep_outdated_console_output_in_watch_mode_instead_of_clearing_the_screen, - }, - ]; - let buildOptionNameMap: OptionNameMap | undefined; - const returnBuildOptionNameMap = () => (buildOptionNameMap || (buildOptionNameMap = createOptionNameMap(buildOpts))); - - const buildOptions: BuildOptions = {}; - const projects: string[] = []; - for (const arg of args) { - if (arg.charCodeAt(0) === CharacterCodes.minus) { - const opt = getOptionDeclarationFromName(returnBuildOptionNameMap, arg.slice(arg.charCodeAt(1) === CharacterCodes.minus ? 2 : 1), /*allowShort*/ true); - if (opt) { - buildOptions[opt.name as keyof BuildOptions] = true; - } - else { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Unknown_build_option_0, arg)); - } - } - else { - // Not a flag, parse as filename - addProject(arg); - } + const { buildOptions, projects: buildProjects, errors } = parseBuildCommand(args); + if (errors.length > 0) { + errors.forEach(reportDiagnostic); + return ExitStatus.DiagnosticsPresent_OutputsSkipped; } if (buildOptions.help) { @@ -249,6 +179,21 @@ namespace ts { // Update to pretty if host supports it updateReportDiagnostic(); + const projects = mapDefined(buildProjects, project => { + const fileName = resolvePath(sys.getCurrentDirectory(), project); + const refPath = resolveProjectReferencePath(sys, { path: fileName }); + if (!sys.fileExists(refPath)) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); + return undefined; + } + return refPath; + }); + + if (projects.length === 0) { + printVersion(); + printHelp(buildOpts, "--build "); + return ExitStatus.Success; + } if (!sys.getModifiedTime || !sys.setModifiedTime || (buildOptions.clean && !sys.deleteFile)) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--build")); @@ -258,29 +203,6 @@ namespace ts { reportWatchModeWithoutSysSupport(); } - // Nonsensical combinations - if (buildOptions.clean && buildOptions.force) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force")); - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - if (buildOptions.clean && buildOptions.verbose) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose")); - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - if (buildOptions.clean && buildOptions.watch) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch")); - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - if (buildOptions.watch && buildOptions.dry) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry")); - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - - if (projects.length === 0) { - // tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ." - addProject("."); - } - // TODO: change this to host if watch => watchHost otherwiue without wathc const builder = createSolutionBuilder(createSolutionBuilderWithWatchHost(sys, reportDiagnostic, createBuilderStatusReporter(sys, shouldBePretty()), createWatchStatusReporter()), projects, buildOptions); if (buildOptions.clean) { @@ -294,15 +216,6 @@ namespace ts { } return builder.buildAllProjects(); - - function addProject(projectSpecification: string) { - const fileName = resolvePath(sys.getCurrentDirectory(), projectSpecification); - const refPath = resolveProjectReferencePath(sys, { path: fileName }); - if (!sys.fileExists(refPath)) { - return reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, fileName)); - } - projects.push(refPath); - } } function performCompilation(rootNames: string[], projectReferences: ReadonlyArray | undefined, options: CompilerOptions, configFileParsingDiagnostics?: ReadonlyArray) {