Skip to content
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

fix: Don't bypass conflicts when using requires #4523

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

pierrechevalier83
Copy link

The intent for is_missing_required_ok is to allow a situation where
two args are required, but they're part of the same group, or one
overrides the other, which is equivalent, so having either of them is
good enough.

The way it was implemented, by reusing gather_conflicts introduced a
loophole where required could be bypassed when it shouldn't.

We first introduce a failing test that illustrates the problem, then we make
the test pass by fixing the implementation of is_missing_required_ok.

Fixes #4520

This test is semantically equivalent to the situation described in that
issue's description:
a first option conflicts with a second option.
a third option requires the second option.
Clap lets us provide the first and the third options only, which
shouldn't be allowed.

The test fails here, but is fixed by the next commit in this PR.
@pierrechevalier83
Copy link
Author

Note: the implementation is not the most elegant as it duplicates code and introduces gather_overrides within Conflicts, which feels out-of-place, but it does serve to highlight where the current code is incorrect.

Feedback welcome on better ways to express this, or a follow-up refactoring change if we want the fix first.

@pierrechevalier83 pierrechevalier83 changed the title fix: don't bypass conflicts when using requires fix: Don't bypass conflicts when using requires Nov 29, 2022
The intent for `is_missing_required_ok` is to allow a situation where
two args are required, but they're part of the same group, or one
overrides the other, which is equivalent, so having either of them is
good enough.

The way it was implemented, by reusing `gather_conflicts` introduced a
loophole where `required` could be bypassed when it shouldn't.

Fixes clap-rs#4520
Comment on lines +44 to +54
#[test]
fn option_required_and_conflicts_with_other_option() {
let result = Command::new("cli")
.arg(arg!(brick: -b "do something harmful"))
.arg(arg!(fix: -f "do something good").conflicts_with("brick"))
.arg(arg!(dry_run: -d "don't do it Louis").requires("fix"))
.try_get_matches_from(vec!["", "-b", "-d"]);
assert!(result.is_err());
let err = result.err().unwrap();
assert_eq!(err.kind(), ErrorKind::MissingRequiredArgument);
}
Copy link
Member

Choose a reason for hiding this comment

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

As I would prefer every commit to pass tests, please make this test pass with the bad behavior and then the followup commit would make it pass with the good behavior

@@ -381,8 +381,8 @@ impl<'cmd> Validator<'cmd> {
conflicts: &mut Conflicts,
) -> bool {
debug!("Validator::is_missing_required_ok: {}", a.get_id());
let conflicts = conflicts.gather_conflicts(self.cmd, matcher, a.get_id());
!conflicts.is_empty()
let overrides = conflicts.gather_overrides(self.cmd, matcher, a.get_id());
Copy link
Member

Choose a reason for hiding this comment

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

merge conflict with 85ecb3e and 4a34b9d

Comment on lines +534 to +535
fn gather_overrides(&mut self, cmd: &Command, matcher: &ArgMatcher, arg_id: &Id) -> Vec<Id> {
debug!("Conflicts::gather_overrides: arg={:?}", arg_id);
Copy link
Member

Choose a reason for hiding this comment

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

Could we have an intermediate commit that breaks out these functions, duplicating the other behavior, and then the commit after changes it to the new behavior? It'd make it a lot easier to see what the behavior difference is.

}

fn gather_direct_overrides(&mut self, cmd: &Command, arg_id: &Id) -> &[Id] {
self.potential.entry(arg_id.clone()).or_insert_with(|| {
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this using the same cache as gather_direct_conflicts which would cause the data to get mixed up?

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.

requires is bypassed when using conflicts_with a certain way
2 participants