From 65e4815701ecbb5139be7ba15a36d642a19fc286 Mon Sep 17 00:00:00 2001 From: Sayed Kaif Date: Mon, 20 Jul 2026 23:24:14 +0530 Subject: [PATCH] scope duplicate-flag detection to command boundaries --- args.hxx | 10 ++++- test/detect_duplicate_flags.cxx | 53 ++++++++++++++++++++++++ test/detect_duplicate_flags_noexcept.cxx | 47 +++++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/args.hxx b/args.hxx index 4bcbd67..38b2ef6 100644 --- a/args.hxx +++ b/args.hxx @@ -2016,7 +2016,15 @@ namespace args } else if(auto group = dynamic_cast(child)) { - group->DetectDuplicateFlags(usedShortFlags, usedLongFlags); + // A command opens its own flag namespace and runs its + // own duplicate detection as a separate root, so a flag + // reused either side of a command boundary is not a + // genuine duplicate. Only descend into plain groups + // here; IsGroup() is false for a Command. + if(group->IsGroup()) + { + group->DetectDuplicateFlags(usedShortFlags, usedLongFlags); + } } } } diff --git a/test/detect_duplicate_flags.cxx b/test/detect_duplicate_flags.cxx index e07f24b..f1a756b 100644 --- a/test/detect_duplicate_flags.cxx +++ b/test/detect_duplicate_flags.cxx @@ -70,6 +70,55 @@ void testDuplicateLongInTwoGroups() }); } +void testFlagReuseAcrossCommandBoundary() +{ + // A flag inside a command and a flag in an ancestor scope may share a + // name: they live in separate namespaces. Declaring the command flag + // first must not be reported as a duplicate. + args::ArgumentParser parser("This is a test program.", "This goes after the options."); + args::Group commands(parser, "commands"); + args::Command run(commands, "run", "run command"); + args::Flag run_message(run, "message", "message", {'m', "message"}); + test::require_nothrow([&]{ + args::Flag top_message(parser, "message", "top message", {'m', "message"}); + }); +} + +void testFlagReuseInNestedGroupAcrossCommandBoundary() +{ + args::ArgumentParser parser("This is a test program.", "This goes after the options."); + args::Group commands(parser, "commands"); + args::Command run(commands, "run", "run command"); + args::Group runopts(run, "run options"); + args::Flag run_message(runopts, "message", "message", {'m', "message"}); + test::require_nothrow([&]{ + args::Flag top_message(parser, "message", "top message", {'m', "message"}); + }); +} + +void testFlagReuseBetweenSiblingCommands() +{ + args::ArgumentParser parser("This is a test program.", "This goes after the options."); + args::Group commands(parser, "commands"); + args::Command commit(commands, "commit", "commit command"); + args::Flag commit_message(commit, "message", "message", {'m', "message"}); + args::Command tag(commands, "tag", "tag command"); + test::require_nothrow([&]{ + args::Flag tag_message(tag, "message", "message", {'m', "message"}); + }); +} + +void testDuplicateWithinCommandStillDetected() +{ + args::ArgumentParser parser("This is a test program.", "This goes after the options."); + args::Group commands(parser, "commands"); + args::Command run(commands, "run", "run command"); + args::Flag run_aone(run, "aone", "test flag", {'a', "aone"}); + test::require_throws_as([&]{ + args::Flag run_atwo(run, "atwo", "test flag", {'a', "atwo"}); + }); +} + int main() { testDuplicateShort(); @@ -78,5 +127,9 @@ int main() testDuplicateLongInGroup(); testDuplicateShortInTwoGroups(); testDuplicateLongInTwoGroups(); + testFlagReuseAcrossCommandBoundary(); + testFlagReuseInNestedGroupAcrossCommandBoundary(); + testFlagReuseBetweenSiblingCommands(); + testDuplicateWithinCommandStillDetected(); } \ No newline at end of file diff --git a/test/detect_duplicate_flags_noexcept.cxx b/test/detect_duplicate_flags_noexcept.cxx index d6aa061..36391d2 100644 --- a/test/detect_duplicate_flags_noexcept.cxx +++ b/test/detect_duplicate_flags_noexcept.cxx @@ -64,6 +64,49 @@ void testDuplicateLongInTwoGroups() test::require(parser.GetError() == args::Error::Usage); } +void testFlagReuseAcrossCommandBoundary() +{ + args::ArgumentParser parser("This is a test program.", "This goes after the options."); + args::Group commands(parser, "commands"); + args::Command run(commands, "run", "run command"); + args::Flag run_message(run, "message", "message", {'m', "message"}); + args::Flag top_message(parser, "message", "top message", {'m', "message"}); + test::require(parser.GetError() == args::Error::None); +} + +void testFlagReuseInNestedGroupAcrossCommandBoundary() +{ + args::ArgumentParser parser("This is a test program.", "This goes after the options."); + args::Group commands(parser, "commands"); + args::Command run(commands, "run", "run command"); + args::Group runopts(run, "run options"); + args::Flag run_message(runopts, "message", "message", {'m', "message"}); + args::Flag top_message(parser, "message", "top message", {'m', "message"}); + test::require(parser.GetError() == args::Error::None); +} + +void testFlagReuseBetweenSiblingCommands() +{ + args::ArgumentParser parser("This is a test program.", "This goes after the options."); + args::Group commands(parser, "commands"); + args::Command commit(commands, "commit", "commit command"); + args::Flag commit_message(commit, "message", "message", {'m', "message"}); + args::Command tag(commands, "tag", "tag command"); + args::Flag tag_message(tag, "message", "message", {'m', "message"}); + test::require(parser.GetError() == args::Error::None); +} + +void testDuplicateWithinCommandStillDetected() +{ + args::ArgumentParser parser("This is a test program.", "This goes after the options."); + args::Group commands(parser, "commands"); + args::Command run(commands, "run", "run command"); + args::Flag run_aone(run, "aone", "test flag", {'a', "aone"}); + args::Flag run_atwo(run, "atwo", "test flag", {'a', "atwo"}); + // The duplicate is recorded on the offending flag when it is added. + test::require(run_atwo.GetError() == args::Error::Usage); +} + int main() { testDuplicateShort(); @@ -72,4 +115,8 @@ int main() testDuplicateLongInGroup(); testDuplicateShortInTwoGroups(); testDuplicateLongInTwoGroups(); + testFlagReuseAcrossCommandBoundary(); + testFlagReuseInNestedGroupAcrossCommandBoundary(); + testFlagReuseBetweenSiblingCommands(); + testDuplicateWithinCommandStillDetected(); } \ No newline at end of file