Skip to content

introduce ParseResult.GetValue<T>(string name)#2083

Merged
adamsitnik merged 6 commits intodotnet:mainfrom
adamsitnik:getValueByName
Mar 15, 2023
Merged

introduce ParseResult.GetValue<T>(string name)#2083
adamsitnik merged 6 commits intodotnet:mainfrom
adamsitnik:getValueByName

Conversation

@adamsitnik
Copy link
Copy Markdown
Member

fixes #2052

@adamsitnik adamsitnik requested review from Keboo and jonsequitur March 8, 2023 22:04

if (!argument.HasDefaultValue && argument.Arity.MinimumNumberOfValues > 0)
{
argumentResult.AddError(LocalizationResources.RequiredArgumentMissing(argumentResult));
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fixes the issue I've mentioned in #2082 (comment) : when a required argument is missing, we report the error for the ArgumentResult, not for CommandResult

=> RootCommandResult.GetValue(option);

/// <summary>
/// Gets the parsed or default value for the specified symbol name, in the context of parsed command.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is important: the implementation provides only values for the parsed command, not for the entire symbol tree.

=> parseResult.GetValue<T>(argument.Name);

[Fact]
public void In_case_of_argument_name_conflict_the_value_which_belongs_to_the_last_parsed_command_is_returned()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few additional test cases come to mind:

  • What happens when an option and an argument name are the same...
    • at the same level of the tree?
    • at different levels of the tree?
  • What happens when symbols with the same name exist at different levels of the tree and the inner one is not provided? Does it fall back to the outer one's value?
    • Does this vary depending on whether the inner one has a default value defined?
  • How do customer parsers behave when...
    • ...parsing an inner symbol -x and looking up a value for -x when there's an outer symbol -x?
    • ...parsing an outer symbol -x and looking up a value for -x when there's an inner symbol -x?
    • ...parsing symbol -a and looking up a value for symbol -x...
      • ...which is above it in the tree
      • ...which is below it in the tree

I have opinions on what some of these behaviors should be but I'll wait so I don't anchor anyone.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • What happens when an option and an argument name are the same at the same level of the tree?

We should throw.

  • What happens when an option and an argument name are the same at different levels of the tree?

The new methods provides only values for the parsed command (parseResult.CommandResult), not for the entire symbol tree.

  • What happens when symbols with the same name exist at different levels of the tree and the inner one is not provided? Does it fall back to the outer one's value?

The outer value will never be returned, only inner if provided or has default.

How do customer parsers behave when...

Such scenario is not supported by design (nobody asked for it), the method is exposed only for ParseResult, not for SymbolResult. It simplifies the design a lot (the cache can be populated just once).


getRequired
.Should()
.Throw<InvalidOperationException>()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArgumentException might be more appropriate here, though I could make an argument for InvalidOperationException if the name exists on an unparsed branch of the larger grammar, e.g. under a subcommand that wasn't present in the current result.

getConflicted
.Should()
.Throw<NotSupportedException>()
.Where(ex => ex.Message == $"More than one symbol uses name \"{sameName}\" for command \"{command.Name}\".");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should be detecting this at parser configuration time, e.g. under ThrowIfInvalid or earlier.

Copy link
Copy Markdown
Member Author

@adamsitnik adamsitnik Mar 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name is immutable now, so we should be able to throw at the moment when given symbol is being added to the Command. We should discuss when is the right moment for throwing at our next design meeting.

parseResult.GetValue<int>(sameName).Should().Be(456);

parseResult = command.Parse($"outer 123");
parseResult.GetValue<int>(sameName).Should().Be(123);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not to reopen the old late binding can of worms, but are there tests for what happens in various cases if GetValue<T> is called for Option<U>?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, I've added the missing tests and fixed the discovered issue.

SymbolResultTree.TryGetValue(Command.Arguments[i], out SymbolResult? parsedResult);
cache.Add(Command.Arguments[i].Name, parsedResult);
}
Populate(cache, Command.Arguments);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm understanding this correctly, a call to Populate could be triggered by a custom parser before the complete command line has been parsed. Will symbols parsed afterward be findable by name?

Copy link
Copy Markdown
Member Author

@adamsitnik adamsitnik Mar 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A custom parser gets access to SymbolResult-derived type, it does not provide a reference to ParseResult. The CommandResult.GetValue<T>(string name) is internal, so it's also impossible to call outside of S.CL before the parsing has been finished. I can move this method to ParseResult itself to avoid confusion.

Edit: I've moved the implementation for ParseResult.

Copy link
Copy Markdown
Contributor

@jonsequitur jonsequitur Mar 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's also impossible to call outside of S.CL before the parsing has been finished

The specific call I was wondering about is calls to ArgumentResult from within a custom parse delegate. This happens before the ParseResult is ready.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With my latest changes it's impossible

* add test coverage for casting T to U, fix discovered issue
* throw ArgumentException instead InvalidOperationException when no symbol is found for given name
* move the method from CommandResult to ParseResult to make it clear that it's available only after parsing has finished
@adamsitnik adamsitnik merged commit 42c5853 into dotnet:main Mar 15, 2023
@adamsitnik adamsitnik deleted the getValueByName branch March 15, 2023 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce ParseResult.GetValue<T>(string name)

2 participants