-
Notifications
You must be signed in to change notification settings - Fork 395
Name based lookup fixes #2255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Name based lookup fixes #2255
Conversation
7826a04
to
0f0311c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote that in the comments, but since it's important I am going to repeat it here: adding any method to SymbolResult
is tricky, as the type can be used before the parsing is finished: inside a validator and/or a custom parser. And the result is dependent on the order of provided args
. We can also fall into a trap of never-ending recursion (two custom parsers, each trying to get the other's value during parsing).
That is why I've added it only to ParseResult
, which makes it very predictable and performant. And I did that based on clear user requirements: "I want to get a value once parsing is finished".
I understand that the current implementation makes it possible to provide the values by name only for the parsed command, but I also did that on purpose as I wanted to avoid adding a complexity before anybody asks for it. If we have such a need and it makes sense, we can change the behavior to include parents in the search, but in my opinion we should avoid doing that proactively (I don't see any issues being referenced here), as again we might add complexity that will be expensive to maintain.
@@ -35,7 +35,7 @@ System.CommandLine | |||
public System.Void AcceptLegalFileNamesOnly() | |||
public System.Void AcceptLegalFilePathsOnly() | |||
public System.Void AcceptOnlyFromAmong(System.String[] values) | |||
public class CliCommand : CliSymbol, System.Collections.Generic.IEnumerable<CliSymbol>, System.Collections.IEnumerable | |||
public class CliCommand : CliSymbol, System.Collections.IEnumerable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, the fewer interfaces we implement, the better
BTW in the future we might want to use https://github.com/dotnet/csharplang/blob/main/proposals/collection-expressions.md#create-methods (sample usage: dotnet/runtime#88470)
public T GetValue<T>(CliArgument<T> argument) | ||
public T GetValue<T>(CliOption<T> option) | ||
public T GetValue<T>(System.String name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've mixed feelings about adding this method to SymbolResult
, as the type can be used before the parsing is finished. Example: inside a validator or a custom parser. Moreover, depending on the order of provided args, the behavior can be different.
Example:
--option1 foo --option2 bar
--option2 bar --option1 foo
If a custom parser provided for --option1
tries to get value of --option2
then in the first scenario it will get null/exception and value in the second.
It's not clear to me what we gain from exposing these methods, while it's clear that they are going to increase the complexity and cause trouble in the future. The PR is not linked to any issue, so I've no idea whether any customers asked for it or whether we believe that it might be useful for some scenarios in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That scenario is the reason this is a draft PR. I agree about the bug and I plan to address it before considering this PR ready.
SymbolResult.GetResult(string name)
is something that we've discussed adding a number of times in our syncs and for the sake of code simplicity and consistency, I believe this should be the core of the GetValue(string name)
implementation. The motivation is the same as for GetValue(string name)
. Without this API, the only way to call GetResult
in a custom parser is by having a reference to the symbol, which is awkward for the same reasons as it was for GetValue(CliSymbol symbol)
. The customer complaints about that awkwardness apply equally to GetResult
, though GetResult
is a less-used API. But in a custom parser, GetResult
is more important than GetValue
. (And note that I haven't added an equivalent method to ParseResult
for this reason.)
it's clear that they are going to increase the complexity and cause trouble in the future
I'm not sure this is the case. Aside from the name-based lookup itself, the binding code here all uses the pre-existing code path, which has been stable for some time.
@@ -275,4 +308,22 @@ public void Parse_errors_have_precedence_over_type_mismatch() | |||
.Throw<InvalidOperationException>() | |||
.Where(ex => ex.Message == LocalizationResources.RequiredOptionWasNotProvided("--required")); | |||
} | |||
|
|||
[Fact] | |||
public void Recursive_option_on_parent_command_can_be_looked_up_when_subcommand_is_specified() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the result of getting a value by name, if there are two commands in the same parse tree that define a symbol with the same name, but it's mandatory for one and optional for another?
CliRootCommand cmd = new()
{
new CliOption<string>("--opt")
{
Required = false
},
new CliCommand("subcommand")
{
new CliOption<string>("--opt")
{
Required = true
}
}
};
ParseResult result = cmd.Parse("--opt some subcommand");
result.GetValue<string>("--opt").Should.What(??)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be a parse error if there's no default value for the inner --opt
. But if the inner --opt
has a default value, then the name-based lookup would return its value, while the outer --opt
would be shadowed when using the name-based API.
This is one of the edge cases around name-based lookups that I don't think can ever be resolved and was why in its previous incarnation, the name-based lookups returned e.g. IEnumerable<SymbolResult>
, which was technically correct but hard to understand. The current compromise is that for the sake of the name-based lookup, the inner shadows the outer. And if you need an unambiguous way to check the values for both of them, you have to use the GetValue(CliSymbol symbol)
|
||
namespace System.CommandLine.Tests; | ||
|
||
public class CustomParsingTests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be easier to review this PR if you have separated the test refactor from the actual change. In case of CustomParsingTests
I simply trust that you just moved the logic to new type without any modifications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct.
The cleanups and this are in separate commits to make reviewing simpler. I moved this out to a separate class because not all of the custom parsing tests are argument tests, and because I'll be adding more tests here to cover using name-based lookups within custom parsers.
/// <summary> | ||
/// Gets or sets the <see cref="Type" /> that the option's parsed tokens will be converted to. | ||
/// </summary> | ||
public abstract Type ValueType { get; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there no valid scenarios for this API? Are you sure it should be removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If so, they're not represented by any tests. I think it was vestigial and only needed by reflection-based implementations that have been removed.
@KathleenDollard is this property something you've needed for source generator work?
public T GetValueOrDefault<T>() => | ||
ArgumentConversionResult.ConvertIfNeeded(typeof(T)) | ||
ArgumentConversionResult |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the method return T?
?
I've already had a couple of people ask why the name-based lookup wasn't working when the actual issue was that it didn't do parent lookups. I think that behavior was unintuitive, especially as it's probably the 80% case for recursive options. |
These changes add support for
SymbolResult.GetResult(string name)
to bring it into parity withGetValue
, which already had both symbol- and name-based lookups. This will allow custom parse delegates to look up other symbols without having to have a reference to the symbol object.This also fixes a bug where name-based lookups could not look up values for options or arguments on a parent command of the one invoked.
There are also some cleanups of dead code here, and
CliCommand
now implementsIEnumerable
rather thanIEnumerable<T>
. This was only ever done to support C# collection initializer syntax and this change will keep LINQ extension methods out of the IntelliSense list.