diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index c9517440ac6ed..4bcc1e48c8f19 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1087,12 +1087,15 @@ namespace ts { [option: string]: CompilerOptionsValue | undefined; } - /** Tuple with error messages for 'unknown compiler option', 'option requires type' */ - type ParseCommandLineWorkerDiagnostics = [DiagnosticMessage, DiagnosticMessage]; + interface ParseCommandLineWorkerDiagnostics { + unknownOptionDiagnostic: DiagnosticMessage, + unknownDidYouMeanDiagnostic: DiagnosticMessage, + optionTypeMismatchDiagnostic: DiagnosticMessage + } function parseCommandLineWorker( getOptionNameMap: () => OptionNameMap, - [unknownOptionDiagnostic, optionTypeMismatchDiagnostic]: ParseCommandLineWorkerDiagnostics, + diagnostics: ParseCommandLineWorkerDiagnostics, commandLine: readonly string[], readFile?: (path: string) => string | undefined) { const options = {} as OptionsBase; @@ -1123,7 +1126,7 @@ namespace ts { else { // Check to see if no argument was provided (e.g. "--locale" is the last command-line argument). if (!args[i] && opt.type !== "boolean") { - errors.push(createCompilerDiagnostic(optionTypeMismatchDiagnostic, opt.name)); + errors.push(createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name)); } switch (opt.type) { @@ -1160,7 +1163,13 @@ namespace ts { } } else { - errors.push(createCompilerDiagnostic(unknownOptionDiagnostic, s)); + const possibleOption = getSpellingSuggestion(s, optionDeclarations, opt => `--${opt.name}`); + if (possibleOption) { + errors.push(createCompilerDiagnostic(diagnostics.unknownDidYouMeanDiagnostic, s, possibleOption.name)); + } + else { + errors.push(createCompilerDiagnostic(diagnostics.unknownOptionDiagnostic, s)); + } } } else { @@ -1203,11 +1212,13 @@ namespace ts { } } + const compilerOptionsDefaultDiagnostics = { + unknownOptionDiagnostic: Diagnostics.Unknown_compiler_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: Diagnostics.Compiler_option_0_expects_an_argument + }; export function parseCommandLine(commandLine: readonly string[], readFile?: (path: string) => string | undefined): ParsedCommandLine { - return parseCommandLineWorker(getOptionNameMap, [ - Diagnostics.Unknown_compiler_option_0, - Diagnostics.Compiler_option_0_expects_an_argument - ], commandLine, readFile); + return parseCommandLineWorker(getOptionNameMap, compilerOptionsDefaultDiagnostics, commandLine, readFile); } /** @internal */ @@ -1239,10 +1250,11 @@ namespace ts { export function parseBuildCommand(args: readonly string[]): ParsedBuildCommand { let buildOptionNameMap: OptionNameMap | undefined; const returnBuildOptionNameMap = () => (buildOptionNameMap || (buildOptionNameMap = createOptionNameMap(buildOpts))); - const { options, fileNames: projects, errors } = parseCommandLineWorker(returnBuildOptionNameMap, [ - Diagnostics.Unknown_build_option_0, - Diagnostics.Build_option_0_requires_a_value_of_type_1 - ], args); + const { options, fileNames: projects, errors } = parseCommandLineWorker(returnBuildOptionNameMap, { + unknownOptionDiagnostic: Diagnostics.Unknown_build_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_build_option_0_Did_you_mean_1, + optionTypeMismatchDiagnostic: Diagnostics.Build_option_0_requires_a_value_of_type_1 + }, args); const buildOptions = options as BuildOptions; if (projects.length === 0) { @@ -1389,19 +1401,28 @@ namespace ts { name: "compilerOptions", type: "object", elementOptions: commandLineOptionsToMap(optionDeclarations), - extraKeyDiagnosticMessage: Diagnostics.Unknown_compiler_option_0 + extraKeyDiagnostics: { + unknownOptionDiagnostic: Diagnostics.Unknown_compiler_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1 + }, }, { name: "typingOptions", type: "object", elementOptions: commandLineOptionsToMap(typeAcquisitionDeclarations), - extraKeyDiagnosticMessage: Diagnostics.Unknown_type_acquisition_option_0 + extraKeyDiagnostics: { + unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1 + }, }, { name: "typeAcquisition", type: "object", elementOptions: commandLineOptionsToMap(typeAcquisitionDeclarations), - extraKeyDiagnosticMessage: Diagnostics.Unknown_type_acquisition_option_0 + extraKeyDiagnostics: { + unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1 + } }, { name: "extends", @@ -1507,7 +1528,7 @@ namespace ts { function convertObjectLiteralExpressionToJson( node: ObjectLiteralExpression, knownOptions: Map | undefined, - extraKeyDiagnosticMessage: DiagnosticMessage | undefined, + extraKeyDiagnostics: DidYouMeanOptionalDiagnostics | undefined, parentOption: string | undefined ): any { const result: any = returnValue ? {} : undefined; @@ -1527,8 +1548,19 @@ namespace ts { const textOfKey = getTextOfPropertyName(element.name); const keyText = textOfKey && unescapeLeadingUnderscores(textOfKey); const option = keyText && knownOptions ? knownOptions.get(keyText) : undefined; - if (keyText && extraKeyDiagnosticMessage && !option) { - errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnosticMessage, keyText)); + if (keyText && extraKeyDiagnostics && !option) { + if (knownOptions) { + const possibleOption = getSpellingSuggestion(keyText, arrayFrom(knownOptions.keys()), identity); + if (possibleOption) { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownDidYouMeanDiagnostic, keyText, possibleOption)); + } + else { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText)); + } + } + else { + errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText)); + } } const value = convertPropertyValueToJson(element.initializer, option); if (typeof keyText !== "undefined") { @@ -1630,9 +1662,9 @@ namespace ts { // vs what we set in the json // If need arises, we can modify this interface and callbacks as needed if (option) { - const { elementOptions, extraKeyDiagnosticMessage, name: optionName } = option; + const { elementOptions, extraKeyDiagnostics, name: optionName } = option; return convertObjectLiteralExpressionToJson(objectLiteralExpression, - elementOptions, extraKeyDiagnosticMessage, optionName); + elementOptions, extraKeyDiagnostics, optionName); } else { return convertObjectLiteralExpressionToJson( @@ -2468,7 +2500,7 @@ namespace ts { basePath: string, errors: Push, configFileName?: string): CompilerOptions { const options = getDefaultCompilerOptions(configFileName); - convertOptionsFromJson(optionDeclarations, jsonOptions, basePath, options, Diagnostics.Unknown_compiler_option_0, errors); + convertOptionsFromJson(optionDeclarations, jsonOptions, basePath, options, compilerOptionsDefaultDiagnostics, errors); if (configFileName) { options.configFilePath = normalizeSlashes(configFileName); } @@ -2484,13 +2516,19 @@ namespace ts { const options = getDefaultTypeAcquisition(configFileName); const typeAcquisition = convertEnableAutoDiscoveryToEnable(jsonOptions); - convertOptionsFromJson(typeAcquisitionDeclarations, typeAcquisition, basePath, options, Diagnostics.Unknown_type_acquisition_option_0, errors); + + const diagnostics = { + unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0, + unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1 , + }; + convertOptionsFromJson(typeAcquisitionDeclarations, typeAcquisition, basePath, options, diagnostics, errors); return options; } + function convertOptionsFromJson(optionDeclarations: readonly CommandLineOption[], jsonOptions: any, basePath: string, - defaultOptions: CompilerOptions | TypeAcquisition, diagnosticMessage: DiagnosticMessage, errors: Push) { + defaultOptions: CompilerOptions | TypeAcquisition, diagnostics: DidYouMeanOptionalDiagnostics, errors: Push) { if (!jsonOptions) { return; @@ -2504,7 +2542,13 @@ namespace ts { defaultOptions[opt.name] = convertJsonOption(opt, jsonOptions[id], basePath, errors); } else { - errors.push(createCompilerDiagnostic(diagnosticMessage, id)); + const possibleOption = getSpellingSuggestion(id, optionDeclarations, opt => opt.name); + if (possibleOption) { + errors.push(createCompilerDiagnostic(diagnostics.unknownDidYouMeanDiagnostic, id, possibleOption.name)); + } + else { + errors.push(createCompilerDiagnostic(diagnostics.unknownOptionDiagnostic, id)); + } } } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index cf2bdfb8ceca5..d93b725c01b19 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3177,6 +3177,10 @@ "category": "Error", "code": 5024 }, + "Unknown compiler option '{0}'. Did you mean '{1}'?": { + "category": "Error", + "code": 5025 + }, "Could not write file '{0}': {1}.": { "category": "Error", "code": 5033 @@ -3297,6 +3301,10 @@ "category": "Error", "code": 5076 }, + "Unknown build option '{0}'. Did you mean '{1}'?": { + "category": "Error", + "code": 5077 + }, "Generates a sourcemap for each corresponding '.d.ts' file.": { "category": "Message", @@ -4747,7 +4755,10 @@ "category": "Error", "code": 17017 }, - + "Unknown type acquisition option '{0}'. Did you mean '{1}'?": { + "category": "Error", + "code": 17018 + }, "Circularity detected while resolving configuration: {0}": { "category": "Error", "code": 18000 diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 3b387d6ec1e41..9aef31dda4ea3 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5199,11 +5199,17 @@ namespace ts { type: Map; // an object literal mapping named values to actual values } + /* @internal */ + export interface DidYouMeanOptionalDiagnostics { + unknownOptionDiagnostic: DiagnosticMessage, + unknownDidYouMeanDiagnostic: DiagnosticMessage, + } + /* @internal */ export interface TsConfigOnlyOption extends CommandLineOptionBase { type: "object"; elementOptions?: Map; - extraKeyDiagnosticMessage?: DiagnosticMessage; + extraKeyDiagnostics?: DidYouMeanOptionalDiagnostics; } /* @internal */ diff --git a/src/testRunner/unittests/config/commandLineParsing.ts b/src/testRunner/unittests/config/commandLineParsing.ts index 5792ee1a39c40..6ad786fbfc4fd 100644 --- a/src/testRunner/unittests/config/commandLineParsing.ts +++ b/src/testRunner/unittests/config/commandLineParsing.ts @@ -40,6 +40,33 @@ namespace ts { }); }); + it("Handles 'did you mean?' for misspelt flags", () => { + // --declarations --allowTS + assertParseResult(["--declarations", "--allowTS"], { + errors: [ + { + messageText:"Unknown compiler option '--declarations'. Did you mean 'declaration'?", + category: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, + code: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code, + file: undefined, + start: undefined, + length: undefined + }, + { + messageText: "Unknown compiler option '--allowTS'. Did you mean 'allowJs'?", + category: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category, + code: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code, + file: undefined, + start: undefined, + length: undefined + } + ], + fileNames: [], + options: {} + }); + }); + + it("Parse multiple options of library flags ", () => { // --lib es5,es2015.symbol.wellknown 0.ts assertParseResult(["--lib", "es5,es2015.symbol.wellknown", "0.ts"], @@ -556,4 +583,6 @@ namespace ts { verifyInvalidCombination("watch", "dry"); }); }); + + } diff --git a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts index 0c0b953209f40..e8ca5f15d69d9 100644 --- a/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts +++ b/src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts @@ -111,8 +111,8 @@ namespace ts { }, errors: [ { - category: Diagnostics.Unknown_type_acquisition_option_0.category, - code: Diagnostics.Unknown_type_acquisition_option_0.code, + category: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, + code: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, file: undefined, start: 0, length: 0, @@ -206,8 +206,8 @@ namespace ts { }, errors: [ { - category: Diagnostics.Unknown_type_acquisition_option_0.category, - code: Diagnostics.Unknown_type_acquisition_option_0.code, + category: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category, + code: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code, file: undefined, start: 0, length: 0, diff --git a/src/testRunner/unittests/tscWatch/helpers.ts b/src/testRunner/unittests/tscWatch/helpers.ts index 1ab49cec139a4..7f6a69aa48d7e 100644 --- a/src/testRunner/unittests/tscWatch/helpers.ts +++ b/src/testRunner/unittests/tscWatch/helpers.ts @@ -273,6 +273,11 @@ namespace ts.tscWatch { return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); } + export function getUnknownDidYouMeanCompilerOption(program: Program, configFile: File, option: string, didYouMean: string) { + const quotedOption = `"${option}"`; + return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean); + } + export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { const quotedModuleName = `"${moduleName}"`; return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName); diff --git a/src/testRunner/unittests/tscWatch/programUpdates.ts b/src/testRunner/unittests/tscWatch/programUpdates.ts index 0b9cc883b85d1..e6f09c2628af6 100644 --- a/src/testRunner/unittests/tscWatch/programUpdates.ts +++ b/src/testRunner/unittests/tscWatch/programUpdates.ts @@ -757,7 +757,7 @@ namespace ts.tscWatch { const watch = createWatchOfConfigFile(configFile.path, host); checkOutputErrorsInitial(host, [ getUnknownCompilerOption(watch(), configFile, "foo"), - getUnknownCompilerOption(watch(), configFile, "allowJS") + getUnknownDidYouMeanCompilerOption(watch(), configFile, "allowJS", "allowJs") ]); }); diff --git a/src/testRunner/unittests/tsserver/projectErrors.ts b/src/testRunner/unittests/tsserver/projectErrors.ts index 64c07c77010f2..41eea1de9e7d6 100644 --- a/src/testRunner/unittests/tsserver/projectErrors.ts +++ b/src/testRunner/unittests/tsserver/projectErrors.ts @@ -496,14 +496,14 @@ declare module '@custom/plugin' { }); describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => { - function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string): ConfigFileDiagnostic { - const d = Diagnostics.Unknown_compiler_option_0; + function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string, didYouMean?: string): ConfigFileDiagnostic { + const d = didYouMean ? Diagnostics.Unknown_compiler_option_0_Did_you_mean_1 : Diagnostics.Unknown_compiler_option_0; const start = configFile.content.indexOf(prop) - 1; // start at "prop" return { fileName: configFile.path, start, length: prop.length + 2, - messageText: formatStringFromArgs(d.message, [prop]), + messageText: formatStringFromArgs(d.message, didYouMean ? [prop, didYouMean] : [prop]), category: d.category, code: d.code, reportsUnnecessary: undefined @@ -543,7 +543,7 @@ declare module '@custom/plugin' { openFilesForSession([file], serverEventManager.session); serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [ getUnknownCompilerOptionDiagnostic(configFile, "foo"), - getUnknownCompilerOptionDiagnostic(configFile, "allowJS") + getUnknownCompilerOptionDiagnostic(configFile, "allowJS", "allowJs") ]); });