Skip to content

Commit 6bbd293

Browse files
committed
fix: keep a global's canonical token visible across case-mode changes
IsGlobalOptionAliasHidden checked membership in HiddenAliases using the currently active comparer, with no distinction between the canonical token and an alias. A global registered under case-sensitive parsing with canonical --tenant and a case-distinct hidden alias --TENANT (a legal registration at that point) had its canonical token incorrectly classified as hidden the moment parsing switched to case-insensitive, because "--tenant" then compares equal to the already-hidden "--TENANT" under the new comparer. GlobalOptionBuilder.HiddenAlias's own contract promises the canonical token stays visible; only Hidden() on the whole definition may retract it. The canonical-identity check must use Ordinal specifically: using the effective (now case-insensitive) comparer for it would also exempt the "--TENANT" alias string itself from its own HiddenAliases membership, undoing the fix for the entry the caller actually asked to hide.
1 parent 910cef0 commit 6bbd293

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

src/Repl.Core/ParsingOptions.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,20 @@ internal void SetGlobalOptionAliasHidden(string canonicalName, string alias, boo
257257
_customTokenOwnershipCache = null;
258258
}
259259

260+
// The canonical token's visibility is governed solely by the definition's own IsHidden, never
261+
// by HiddenAliases: a case-distinct hidden alias (registered while case-sensitive) can become
262+
// equivalent to the canonical token once parsing switches to case-insensitive, and without this
263+
// guard that would incorrectly hide the canonical spelling too.
264+
//
265+
// The canonical check is deliberately Ordinal, not the effective comparer: the effective
266+
// comparer is exactly what makes "--TENANT" equivalent to canonical "--tenant" once parsing
267+
// turns case-insensitive, and using it here would ALSO exempt that distinct alias string from
268+
// its own HiddenAliases membership — the opposite of what HiddenAlias("--TENANT") asked for.
269+
// Ordinal identifies only the canonical spelling itself; every other alias string still falls
270+
// through to the normal effective-comparer membership check below.
260271
internal bool IsGlobalOptionAliasHidden(GlobalOptionDefinition definition, string token) =>
261-
definition.HiddenAliases.Contains(token, ResolveOptionTokenComparer());
272+
!string.Equals(token, definition.CanonicalToken, StringComparison.Ordinal)
273+
&& definition.HiddenAliases.Contains(token, ResolveOptionTokenComparer());
262274

263275
private StringComparer ResolveOptionTokenComparer() =>
264276
OptionCaseSensitivity == ReplCaseSensitivity.CaseInsensitive

src/Repl.IntegrationTests/Given_HelpDiscovery.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,26 @@ public void When_GlobalAliasIsUnhiddenThroughEquivalentCasing_Then_OneRepresenta
279279
upper.Text.Should().Contain("north");
280280
}
281281

282+
[TestMethod]
283+
[Description("A global registered under case-sensitive parsing with canonical --tenant and a case-distinct hidden alias --TENANT must keep its canonical token visible after switching to case-insensitive parsing: the canonical token's visibility is governed by the definition's own IsHidden, never by HiddenAliases becoming case-equivalent to it.")]
284+
public void When_CaseDistinctHiddenAliasBecomesEquivalentToCanonicalAfterModeChange_Then_CanonicalStaysVisible()
285+
{
286+
var sut = ReplApp.Create();
287+
sut.Options(options =>
288+
options.Parsing.AddGlobalOption<string>("tenant", aliases: ["--TENANT"]));
289+
sut.Options(options => options.Parsing.GlobalOption("tenant").HiddenAlias("--TENANT"));
290+
sut.Options(options => options.Parsing.OptionCaseSensitivity = ReplCaseSensitivity.CaseInsensitive);
291+
sut.Map("show", static string (IGlobalOptionsAccessor globals) => globals.GetValue<string>("tenant") ?? "none");
292+
293+
var help = ConsoleCaptureHelper.Capture(() => sut.Run(["--help", "--no-logo"]));
294+
var canonical = ConsoleCaptureHelper.Capture(() => sut.Run(["--tenant", "acme", "show", "--no-logo"]));
295+
296+
help.Text.Should().Contain("--tenant");
297+
help.Text.Should().NotContain("--TENANT");
298+
canonical.ExitCode.Should().Be(0, canonical.Text);
299+
canonical.Text.Should().Contain("acme");
300+
}
301+
282302
[TestMethod]
283303
[Description("A hidden command option stays bindable when explicitly provided but is omitted from command help.")]
284304
public void When_CommandOptionIsHidden_Then_HelpOmitsItAndExplicitInvocationStillBinds()

0 commit comments

Comments
 (0)