Skip to content

Commit ef0cca7

Browse files
author
Orta
authored
Adds 'did you mean' to the CLI args parser (microsoft#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?'
1 parent f5bdd4d commit ef0cca7

File tree

8 files changed

+131
-36
lines changed

8 files changed

+131
-36
lines changed

src/compiler/commandLineParser.ts

Lines changed: 69 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,12 +1087,15 @@ namespace ts {
10871087
[option: string]: CompilerOptionsValue | undefined;
10881088
}
10891089

1090-
/** Tuple with error messages for 'unknown compiler option', 'option requires type' */
1091-
type ParseCommandLineWorkerDiagnostics = [DiagnosticMessage, DiagnosticMessage];
1090+
interface ParseCommandLineWorkerDiagnostics {
1091+
unknownOptionDiagnostic: DiagnosticMessage,
1092+
unknownDidYouMeanDiagnostic: DiagnosticMessage,
1093+
optionTypeMismatchDiagnostic: DiagnosticMessage
1094+
}
10921095

10931096
function parseCommandLineWorker(
10941097
getOptionNameMap: () => OptionNameMap,
1095-
[unknownOptionDiagnostic, optionTypeMismatchDiagnostic]: ParseCommandLineWorkerDiagnostics,
1098+
diagnostics: ParseCommandLineWorkerDiagnostics,
10961099
commandLine: readonly string[],
10971100
readFile?: (path: string) => string | undefined) {
10981101
const options = {} as OptionsBase;
@@ -1123,7 +1126,7 @@ namespace ts {
11231126
else {
11241127
// Check to see if no argument was provided (e.g. "--locale" is the last command-line argument).
11251128
if (!args[i] && opt.type !== "boolean") {
1126-
errors.push(createCompilerDiagnostic(optionTypeMismatchDiagnostic, opt.name));
1129+
errors.push(createCompilerDiagnostic(diagnostics.optionTypeMismatchDiagnostic, opt.name));
11271130
}
11281131

11291132
switch (opt.type) {
@@ -1160,7 +1163,13 @@ namespace ts {
11601163
}
11611164
}
11621165
else {
1163-
errors.push(createCompilerDiagnostic(unknownOptionDiagnostic, s));
1166+
const possibleOption = getSpellingSuggestion(s, optionDeclarations, opt => `--${opt.name}`);
1167+
if (possibleOption) {
1168+
errors.push(createCompilerDiagnostic(diagnostics.unknownDidYouMeanDiagnostic, s, possibleOption.name));
1169+
}
1170+
else {
1171+
errors.push(createCompilerDiagnostic(diagnostics.unknownOptionDiagnostic, s));
1172+
}
11641173
}
11651174
}
11661175
else {
@@ -1203,11 +1212,13 @@ namespace ts {
12031212
}
12041213
}
12051214

1215+
const compilerOptionsDefaultDiagnostics = {
1216+
unknownOptionDiagnostic: Diagnostics.Unknown_compiler_option_0,
1217+
unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1,
1218+
optionTypeMismatchDiagnostic: Diagnostics.Compiler_option_0_expects_an_argument
1219+
};
12061220
export function parseCommandLine(commandLine: readonly string[], readFile?: (path: string) => string | undefined): ParsedCommandLine {
1207-
return parseCommandLineWorker(getOptionNameMap, [
1208-
Diagnostics.Unknown_compiler_option_0,
1209-
Diagnostics.Compiler_option_0_expects_an_argument
1210-
], commandLine, readFile);
1221+
return parseCommandLineWorker(getOptionNameMap, compilerOptionsDefaultDiagnostics, commandLine, readFile);
12111222
}
12121223

12131224
/** @internal */
@@ -1239,10 +1250,11 @@ namespace ts {
12391250
export function parseBuildCommand(args: readonly string[]): ParsedBuildCommand {
12401251
let buildOptionNameMap: OptionNameMap | undefined;
12411252
const returnBuildOptionNameMap = () => (buildOptionNameMap || (buildOptionNameMap = createOptionNameMap(buildOpts)));
1242-
const { options, fileNames: projects, errors } = parseCommandLineWorker(returnBuildOptionNameMap, [
1243-
Diagnostics.Unknown_build_option_0,
1244-
Diagnostics.Build_option_0_requires_a_value_of_type_1
1245-
], args);
1253+
const { options, fileNames: projects, errors } = parseCommandLineWorker(returnBuildOptionNameMap, {
1254+
unknownOptionDiagnostic: Diagnostics.Unknown_build_option_0,
1255+
unknownDidYouMeanDiagnostic: Diagnostics.Unknown_build_option_0_Did_you_mean_1,
1256+
optionTypeMismatchDiagnostic: Diagnostics.Build_option_0_requires_a_value_of_type_1
1257+
}, args);
12461258
const buildOptions = options as BuildOptions;
12471259

12481260
if (projects.length === 0) {
@@ -1389,19 +1401,28 @@ namespace ts {
13891401
name: "compilerOptions",
13901402
type: "object",
13911403
elementOptions: commandLineOptionsToMap(optionDeclarations),
1392-
extraKeyDiagnosticMessage: Diagnostics.Unknown_compiler_option_0
1404+
extraKeyDiagnostics: {
1405+
unknownOptionDiagnostic: Diagnostics.Unknown_compiler_option_0,
1406+
unknownDidYouMeanDiagnostic: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1
1407+
},
13931408
},
13941409
{
13951410
name: "typingOptions",
13961411
type: "object",
13971412
elementOptions: commandLineOptionsToMap(typeAcquisitionDeclarations),
1398-
extraKeyDiagnosticMessage: Diagnostics.Unknown_type_acquisition_option_0
1413+
extraKeyDiagnostics: {
1414+
unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0,
1415+
unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1
1416+
},
13991417
},
14001418
{
14011419
name: "typeAcquisition",
14021420
type: "object",
14031421
elementOptions: commandLineOptionsToMap(typeAcquisitionDeclarations),
1404-
extraKeyDiagnosticMessage: Diagnostics.Unknown_type_acquisition_option_0
1422+
extraKeyDiagnostics: {
1423+
unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0,
1424+
unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1
1425+
}
14051426
},
14061427
{
14071428
name: "extends",
@@ -1507,7 +1528,7 @@ namespace ts {
15071528
function convertObjectLiteralExpressionToJson(
15081529
node: ObjectLiteralExpression,
15091530
knownOptions: Map<CommandLineOption> | undefined,
1510-
extraKeyDiagnosticMessage: DiagnosticMessage | undefined,
1531+
extraKeyDiagnostics: DidYouMeanOptionalDiagnostics | undefined,
15111532
parentOption: string | undefined
15121533
): any {
15131534
const result: any = returnValue ? {} : undefined;
@@ -1527,8 +1548,19 @@ namespace ts {
15271548
const textOfKey = getTextOfPropertyName(element.name);
15281549
const keyText = textOfKey && unescapeLeadingUnderscores(textOfKey);
15291550
const option = keyText && knownOptions ? knownOptions.get(keyText) : undefined;
1530-
if (keyText && extraKeyDiagnosticMessage && !option) {
1531-
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnosticMessage, keyText));
1551+
if (keyText && extraKeyDiagnostics && !option) {
1552+
if (knownOptions) {
1553+
const possibleOption = getSpellingSuggestion(keyText, arrayFrom(knownOptions.keys()), identity);
1554+
if (possibleOption) {
1555+
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownDidYouMeanDiagnostic, keyText, possibleOption));
1556+
}
1557+
else {
1558+
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText));
1559+
}
1560+
}
1561+
else {
1562+
errors.push(createDiagnosticForNodeInSourceFile(sourceFile, element.name, extraKeyDiagnostics.unknownOptionDiagnostic, keyText));
1563+
}
15321564
}
15331565
const value = convertPropertyValueToJson(element.initializer, option);
15341566
if (typeof keyText !== "undefined") {
@@ -1630,9 +1662,9 @@ namespace ts {
16301662
// vs what we set in the json
16311663
// If need arises, we can modify this interface and callbacks as needed
16321664
if (option) {
1633-
const { elementOptions, extraKeyDiagnosticMessage, name: optionName } = <TsConfigOnlyOption>option;
1665+
const { elementOptions, extraKeyDiagnostics, name: optionName } = <TsConfigOnlyOption>option;
16341666
return convertObjectLiteralExpressionToJson(objectLiteralExpression,
1635-
elementOptions, extraKeyDiagnosticMessage, optionName);
1667+
elementOptions, extraKeyDiagnostics, optionName);
16361668
}
16371669
else {
16381670
return convertObjectLiteralExpressionToJson(
@@ -2468,7 +2500,7 @@ namespace ts {
24682500
basePath: string, errors: Push<Diagnostic>, configFileName?: string): CompilerOptions {
24692501

24702502
const options = getDefaultCompilerOptions(configFileName);
2471-
convertOptionsFromJson(optionDeclarations, jsonOptions, basePath, options, Diagnostics.Unknown_compiler_option_0, errors);
2503+
convertOptionsFromJson(optionDeclarations, jsonOptions, basePath, options, compilerOptionsDefaultDiagnostics, errors);
24722504
if (configFileName) {
24732505
options.configFilePath = normalizeSlashes(configFileName);
24742506
}
@@ -2484,13 +2516,19 @@ namespace ts {
24842516

24852517
const options = getDefaultTypeAcquisition(configFileName);
24862518
const typeAcquisition = convertEnableAutoDiscoveryToEnable(jsonOptions);
2487-
convertOptionsFromJson(typeAcquisitionDeclarations, typeAcquisition, basePath, options, Diagnostics.Unknown_type_acquisition_option_0, errors);
2519+
2520+
const diagnostics = {
2521+
unknownOptionDiagnostic: Diagnostics.Unknown_type_acquisition_option_0,
2522+
unknownDidYouMeanDiagnostic: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1 ,
2523+
};
2524+
convertOptionsFromJson(typeAcquisitionDeclarations, typeAcquisition, basePath, options, diagnostics, errors);
24882525

24892526
return options;
24902527
}
24912528

2529+
24922530
function convertOptionsFromJson(optionDeclarations: readonly CommandLineOption[], jsonOptions: any, basePath: string,
2493-
defaultOptions: CompilerOptions | TypeAcquisition, diagnosticMessage: DiagnosticMessage, errors: Push<Diagnostic>) {
2531+
defaultOptions: CompilerOptions | TypeAcquisition, diagnostics: DidYouMeanOptionalDiagnostics, errors: Push<Diagnostic>) {
24942532

24952533
if (!jsonOptions) {
24962534
return;
@@ -2504,7 +2542,13 @@ namespace ts {
25042542
defaultOptions[opt.name] = convertJsonOption(opt, jsonOptions[id], basePath, errors);
25052543
}
25062544
else {
2507-
errors.push(createCompilerDiagnostic(diagnosticMessage, id));
2545+
const possibleOption = getSpellingSuggestion(id, <CommandLineOption[]>optionDeclarations, opt => opt.name);
2546+
if (possibleOption) {
2547+
errors.push(createCompilerDiagnostic(diagnostics.unknownDidYouMeanDiagnostic, id, possibleOption.name));
2548+
}
2549+
else {
2550+
errors.push(createCompilerDiagnostic(diagnostics.unknownOptionDiagnostic, id));
2551+
}
25082552
}
25092553
}
25102554
}

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3177,6 +3177,10 @@
31773177
"category": "Error",
31783178
"code": 5024
31793179
},
3180+
"Unknown compiler option '{0}'. Did you mean '{1}'?": {
3181+
"category": "Error",
3182+
"code": 5025
3183+
},
31803184
"Could not write file '{0}': {1}.": {
31813185
"category": "Error",
31823186
"code": 5033
@@ -3297,6 +3301,10 @@
32973301
"category": "Error",
32983302
"code": 5076
32993303
},
3304+
"Unknown build option '{0}'. Did you mean '{1}'?": {
3305+
"category": "Error",
3306+
"code": 5077
3307+
},
33003308

33013309
"Generates a sourcemap for each corresponding '.d.ts' file.": {
33023310
"category": "Message",
@@ -4747,7 +4755,10 @@
47474755
"category": "Error",
47484756
"code": 17017
47494757
},
4750-
4758+
"Unknown type acquisition option '{0}'. Did you mean '{1}'?": {
4759+
"category": "Error",
4760+
"code": 17018
4761+
},
47514762
"Circularity detected while resolving configuration: {0}": {
47524763
"category": "Error",
47534764
"code": 18000

src/compiler/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5199,11 +5199,17 @@ namespace ts {
51995199
type: Map<number | string>; // an object literal mapping named values to actual values
52005200
}
52015201

5202+
/* @internal */
5203+
export interface DidYouMeanOptionalDiagnostics {
5204+
unknownOptionDiagnostic: DiagnosticMessage,
5205+
unknownDidYouMeanDiagnostic: DiagnosticMessage,
5206+
}
5207+
52025208
/* @internal */
52035209
export interface TsConfigOnlyOption extends CommandLineOptionBase {
52045210
type: "object";
52055211
elementOptions?: Map<CommandLineOption>;
5206-
extraKeyDiagnosticMessage?: DiagnosticMessage;
5212+
extraKeyDiagnostics?: DidYouMeanOptionalDiagnostics;
52075213
}
52085214

52095215
/* @internal */

src/testRunner/unittests/config/commandLineParsing.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,33 @@ namespace ts {
4040
});
4141
});
4242

43+
it("Handles 'did you mean?' for misspelt flags", () => {
44+
// --declarations --allowTS
45+
assertParseResult(["--declarations", "--allowTS"], {
46+
errors: [
47+
{
48+
messageText:"Unknown compiler option '--declarations'. Did you mean 'declaration'?",
49+
category: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category,
50+
code: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code,
51+
file: undefined,
52+
start: undefined,
53+
length: undefined
54+
},
55+
{
56+
messageText: "Unknown compiler option '--allowTS'. Did you mean 'allowJs'?",
57+
category: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.category,
58+
code: Diagnostics.Unknown_compiler_option_0_Did_you_mean_1.code,
59+
file: undefined,
60+
start: undefined,
61+
length: undefined
62+
}
63+
],
64+
fileNames: [],
65+
options: {}
66+
});
67+
});
68+
69+
4370
it("Parse multiple options of library flags ", () => {
4471
// --lib es5,es2015.symbol.wellknown 0.ts
4572
assertParseResult(["--lib", "es5,es2015.symbol.wellknown", "0.ts"],
@@ -556,4 +583,6 @@ namespace ts {
556583
verifyInvalidCombination("watch", "dry");
557584
});
558585
});
586+
587+
559588
}

src/testRunner/unittests/config/convertTypeAcquisitionFromJson.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ namespace ts {
111111
},
112112
errors: [
113113
{
114-
category: Diagnostics.Unknown_type_acquisition_option_0.category,
115-
code: Diagnostics.Unknown_type_acquisition_option_0.code,
114+
category: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category,
115+
code: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code,
116116
file: undefined,
117117
start: 0,
118118
length: 0,
@@ -206,8 +206,8 @@ namespace ts {
206206
},
207207
errors: [
208208
{
209-
category: Diagnostics.Unknown_type_acquisition_option_0.category,
210-
code: Diagnostics.Unknown_type_acquisition_option_0.code,
209+
category: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.category,
210+
code: Diagnostics.Unknown_type_acquisition_option_0_Did_you_mean_1.code,
211211
file: undefined,
212212
start: 0,
213213
length: 0,

src/testRunner/unittests/tscWatch/helpers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,11 @@ namespace ts.tscWatch {
273273
return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option);
274274
}
275275

276+
export function getUnknownDidYouMeanCompilerOption(program: Program, configFile: File, option: string, didYouMean: string) {
277+
const quotedOption = `"${option}"`;
278+
return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean);
279+
}
280+
276281
export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) {
277282
const quotedModuleName = `"${moduleName}"`;
278283
return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0, moduleName);

src/testRunner/unittests/tscWatch/programUpdates.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ namespace ts.tscWatch {
757757
const watch = createWatchOfConfigFile(configFile.path, host);
758758
checkOutputErrorsInitial(host, [
759759
getUnknownCompilerOption(watch(), configFile, "foo"),
760-
getUnknownCompilerOption(watch(), configFile, "allowJS")
760+
getUnknownDidYouMeanCompilerOption(watch(), configFile, "allowJS", "allowJs")
761761
]);
762762
});
763763

src/testRunner/unittests/tsserver/projectErrors.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -496,14 +496,14 @@ declare module '@custom/plugin' {
496496
});
497497

498498
describe("unittests:: tsserver:: Project Errors for Configure file diagnostics events", () => {
499-
function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string): ConfigFileDiagnostic {
500-
const d = Diagnostics.Unknown_compiler_option_0;
499+
function getUnknownCompilerOptionDiagnostic(configFile: File, prop: string, didYouMean?: string): ConfigFileDiagnostic {
500+
const d = didYouMean ? Diagnostics.Unknown_compiler_option_0_Did_you_mean_1 : Diagnostics.Unknown_compiler_option_0;
501501
const start = configFile.content.indexOf(prop) - 1; // start at "prop"
502502
return {
503503
fileName: configFile.path,
504504
start,
505505
length: prop.length + 2,
506-
messageText: formatStringFromArgs(d.message, [prop]),
506+
messageText: formatStringFromArgs(d.message, didYouMean ? [prop, didYouMean] : [prop]),
507507
category: d.category,
508508
code: d.code,
509509
reportsUnnecessary: undefined
@@ -543,7 +543,7 @@ declare module '@custom/plugin' {
543543
openFilesForSession([file], serverEventManager.session);
544544
serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path, [
545545
getUnknownCompilerOptionDiagnostic(configFile, "foo"),
546-
getUnknownCompilerOptionDiagnostic(configFile, "allowJS")
546+
getUnknownCompilerOptionDiagnostic(configFile, "allowJS", "allowJs")
547547
]);
548548
});
549549

0 commit comments

Comments
 (0)