Adds 'did you mean' to the CLI args parser (#35063)

* Adds did you mean to the CLI args parser

* Adds test coverage for the did you mean on CLI args

* Adds did you mean to convertOptionsFromJson

* Ensure tsconfig compiler flags also get 'did you mean?'
This commit is contained in:
Orta 2019-11-13 20:16:48 -05:00 committed by GitHub
parent f5bdd4daca
commit ef0cca7d12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 131 additions and 36 deletions

View File

@ -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<CommandLineOption> | 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 } = <TsConfigOnlyOption>option;
const { elementOptions, extraKeyDiagnostics, name: optionName } = <TsConfigOnlyOption>option;
return convertObjectLiteralExpressionToJson(objectLiteralExpression,
elementOptions, extraKeyDiagnosticMessage, optionName);
elementOptions, extraKeyDiagnostics, optionName);
}
else {
return convertObjectLiteralExpressionToJson(
@ -2468,7 +2500,7 @@ namespace ts {
basePath: string, errors: Push<Diagnostic>, 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<Diagnostic>) {
defaultOptions: CompilerOptions | TypeAcquisition, diagnostics: DidYouMeanOptionalDiagnostics, errors: Push<Diagnostic>) {
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, <CommandLineOption[]>optionDeclarations, opt => opt.name);
if (possibleOption) {
errors.push(createCompilerDiagnostic(diagnostics.unknownDidYouMeanDiagnostic, id, possibleOption.name));
}
else {
errors.push(createCompilerDiagnostic(diagnostics.unknownOptionDiagnostic, id));
}
}
}
}

View File

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

View File

@ -5199,11 +5199,17 @@ namespace ts {
type: Map<number | string>; // 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<CommandLineOption>;
extraKeyDiagnosticMessage?: DiagnosticMessage;
extraKeyDiagnostics?: DidYouMeanOptionalDiagnostics;
}
/* @internal */

View File

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

View File

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

View File

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

View File

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

View File

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