Skip to content

Commit 857b4c3

Browse files
Refactor GetPossibleTokens to include parent recursive options (#2793)
* Refactor GetPossibleTokens method to improve clarity and functionality in ParseErrorAction * Add Tests
1 parent d943ad3 commit 857b4c3

2 files changed

Lines changed: 116 additions & 7 deletions

File tree

src/System.CommandLine.Tests/Invocation/TypoCorrectionTests.cs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,5 +188,106 @@ public async Task Suggestions_favor_matches_with_prefix()
188188

189189
output.ToString().Should().Contain($"'-all' was not matched. Did you mean one of the following?{NewLine}-call");
190190
}
191+
192+
[Fact]
193+
public async Task Recursive_options_from_parent_command_are_suggested_for_subcommand_token()
194+
{
195+
var subcommand = new Command("sub");
196+
var rootCommand = new RootCommand
197+
{
198+
subcommand,
199+
new Option<string>("--verbose") { Recursive = true }
200+
};
201+
202+
var output = new StringWriter();
203+
var result = rootCommand.Parse("sub --verbos");
204+
205+
await result.InvokeAsync(new() { Output = output }, CancellationToken.None);
206+
207+
output.ToString().Should().Contain($"'--verbos' was not matched. Did you mean one of the following?{NewLine}--verbose");
208+
}
209+
210+
[Fact]
211+
public async Task Non_recursive_options_from_parent_command_are_not_suggested_for_subcommand_token()
212+
{
213+
var subcommand = new Command("sub");
214+
var rootCommand = new RootCommand
215+
{
216+
subcommand,
217+
new Option<string>("--verbose") { Recursive = false }
218+
};
219+
220+
var output = new StringWriter();
221+
var result = rootCommand.Parse("sub --verbos");
222+
223+
await result.InvokeAsync(new() { Output = output }, CancellationToken.None);
224+
225+
output.ToString().Should().NotContain("--verbose");
226+
}
227+
228+
[Fact]
229+
public async Task Hidden_recursive_options_from_parent_command_are_not_suggested_for_subcommand_token()
230+
{
231+
var subcommand = new Command("sub");
232+
var rootCommand = new RootCommand
233+
{
234+
subcommand,
235+
new Option<string>("--verbose") { Recursive = true, Hidden = true }
236+
};
237+
238+
var output = new StringWriter();
239+
var result = rootCommand.Parse("sub --verbos");
240+
241+
await result.InvokeAsync(new() { Output = output }, CancellationToken.None);
242+
243+
output.ToString().Should().NotContain("--verbose");
244+
}
245+
246+
[Fact]
247+
public async Task Recursive_options_from_grandparent_command_are_suggested_for_deeply_nested_subcommand_token()
248+
{
249+
var grandchild = new Command("grandchild");
250+
var child = new Command("child") { grandchild };
251+
var rootCommand = new RootCommand
252+
{
253+
child,
254+
new Option<string>("--output") { Recursive = true }
255+
};
256+
257+
var output = new StringWriter();
258+
var result = rootCommand.Parse("child grandchild --outpu");
259+
260+
await result.InvokeAsync(new() { Output = output }, CancellationToken.None);
261+
262+
output.ToString().Should().Contain($"'--outpu' was not matched. Did you mean one of the following?{NewLine}--output");
263+
}
264+
265+
[Fact]
266+
public async Task Recursive_options_from_parent_and_own_options_are_both_suggested()
267+
{
268+
var subcommand = new Command("sub")
269+
{
270+
new Option<string>("--format")
271+
};
272+
var rootCommand = new RootCommand
273+
{
274+
subcommand,
275+
new Option<string>("--verbose") { Recursive = true }
276+
};
277+
278+
var output = new StringWriter();
279+
// "forma" is close to "--format" but not close to "--verbose"
280+
var result = rootCommand.Parse("sub forma");
281+
282+
if (result.Action is ParseErrorAction parseError)
283+
{
284+
parseError.ShowHelp = false;
285+
}
286+
287+
await result.InvokeAsync(new() { Output = output }, CancellationToken.None);
288+
289+
output.ToString().Should().Contain("--format");
290+
output.ToString().Should().NotContain("--verbose");
291+
}
191292
}
192293
}

src/System.CommandLine/Invocation/ParseErrorAction.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ private static void WriteTypoCorrectionSuggestions(ParseResult parseResult)
9494
var token = unmatchedTokens[i];
9595

9696
bool first = true;
97-
foreach (string suggestion in GetPossibleTokens(parseResult.CommandResult.Command, token))
97+
foreach (string suggestion in GetPossibleTokens(parseResult.CommandResult, token))
9898
{
9999
if (first)
100100
{
@@ -111,16 +111,23 @@ private static void WriteTypoCorrectionSuggestions(ParseResult parseResult)
111111
parseResult.InvocationConfiguration.Output.WriteLine();
112112
}
113113

114-
static IEnumerable<string> GetPossibleTokens(Command targetSymbol, string token)
114+
static IEnumerable<string> GetPossibleTokens(CommandResult commandResult, string token)
115115
{
116-
if (targetSymbol is { HasOptions: false, HasSubcommands: false })
116+
Command targetSymbol = commandResult.Command;
117+
118+
// Collect symbols from the current command's children (options + subcommands)
119+
// plus Recursive options from every ancestor command.
120+
IEnumerable<Symbol> candidates = targetSymbol.HasOptions || targetSymbol.HasSubcommands
121+
? targetSymbol.Children.Where(x => !x.Hidden && x is Option or Command)
122+
: Enumerable.Empty<Symbol>();
123+
124+
for (var parent = commandResult.Parent as CommandResult; parent is not null; parent = parent.Parent as CommandResult)
117125
{
118-
return Array.Empty<string>();
126+
candidates = candidates.Concat(
127+
parent.Command.Options.Where(o => o.Recursive && !o.Hidden));
119128
}
120129

121-
IEnumerable<string> possibleMatches = targetSymbol
122-
.Children
123-
.Where(x => !x.Hidden && x is Option or Command)
130+
IEnumerable<string> possibleMatches = candidates
124131
.Select(symbol =>
125132
{
126133
AliasSet? aliasSet = symbol is Option option ? option._aliases : ((Command)symbol)._aliases;
@@ -233,3 +240,4 @@ static int GetDistance(string first, string second)
233240

234241
private const int MaxLevenshteinDistance = 3;
235242
}
243+

0 commit comments

Comments
 (0)