Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d88d0ce
Initial plan for issue
Copilot May 27, 2025
1443d5e
Optimize performance of CommandLineOptionsValidator
Copilot May 27, 2025
85ffb88
Remove PerformanceSensitiveAttribute usage
Copilot May 27, 2025
39d6db5
Fix code formatting in CommandLineOptionsValidator.cs
Copilot May 27, 2025
78e96f8
Add blank line after conditional block in CommandLineOptionsValidator.cs
Copilot May 27, 2025
975d2d6
Fix whitespace formatting in CommandLineOptionsValidator.cs
Copilot May 27, 2025
86c987c
Replace var with explicit type in foreach loop at line 114
Copilot Jun 1, 2025
2e2ee02
Replace var with explicit types and fix whitespace formatting
Copilot Jun 1, 2025
07a4700
Fix formatting: move string.Format to separate line with proper inden…
Copilot Jun 1, 2025
6c344de
Replace var with explicit types in ValidateOptionsAreNotDuplicated me…
Copilot Jun 1, 2025
3ba2fb0
Update CommandLineOptionsValidator.cs
Youssef1313 Jun 1, 2025
e1369e7
Update CommandLineOptionsValidator.cs
Youssef1313 Jun 1, 2025
780fd3b
Update CommandLineOptionsValidator.cs
Youssef1313 Jun 1, 2025
fe102f2
Update CommandLineOptionsValidator.cs
Youssef1313 Jun 1, 2025
cae2a1b
Update CommandLineOptionsValidator.cs
Youssef1313 Jun 2, 2025
97c8299
Update CommandLineOptionsValidator.cs
Youssef1313 Jun 2, 2025
63ab4f7
Add more tests
Evangelink Feb 22, 2026
3f0ce4e
Merge branch 'main' into copilot/fix-5651
Youssef1313 Mar 31, 2026
1cc37c3
Update CommandLineOptionsValidator.cs
Youssef1313 Apr 1, 2026
a470f0c
Update CommandLineOptionsValidator.cs
Youssef1313 Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,40 +127,66 @@
Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> extensionOptionsByProvider,
Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> systemOptionsByProvider)
{
IEnumerable<string> allExtensionOptions = extensionOptionsByProvider.Values.SelectMany(x => x).Select(x => x.Name).Distinct();
IEnumerable<string> allSystemOptions = systemOptionsByProvider.Values.SelectMany(x => x).Select(x => x.Name).Distinct();

IEnumerable<string> invalidReservedOptions = allSystemOptions.Intersect(allExtensionOptions);
if (invalidReservedOptions.Any())
// Create a HashSet of all system option names for faster lookup
var systemOptionNames = new HashSet<string>();
foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> provider in systemOptionsByProvider)
{
var stringBuilder = new StringBuilder();
foreach (string reservedOption in invalidReservedOptions)
foreach (CommandLineOption option in provider.Value)
{
IEnumerable<string> faultyProviderNames = extensionOptionsByProvider.Where(tuple => tuple.Value.Any(x => x.Name == reservedOption)).Select(tuple => tuple.Key.DisplayName);
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionIsReserved, reservedOption, string.Join("', '", faultyProviderNames)));
systemOptionNames.Add(option.Name);
}
}

return ValidationResult.Invalid(stringBuilder.ToTrimmedString());
StringBuilder? stringBuilder = null;
foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> provider in extensionOptionsByProvider)
{
foreach (CommandLineOption option in provider.Value)
{
if (systemOptionNames.Contains(option.Name))
{
stringBuilder ??= new StringBuilder();
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionIsReserved, option.Name, provider.Key.DisplayName));
}
}
}

Comment on lines +140 to 152
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

ValidateExtensionOptionsDoNotContainReservedOptions now emits one error line per (provider, option) and only includes the current provider’s DisplayName. Previously it emitted one line per reserved option and included all offending providers, so this is a behavior/error-message regression (and will get worse if the resource string is corrected to use the provider placeholder). Consider aggregating by option name (e.g., option -> list of providers) and formatting a single message per reserved option with the complete provider list.

Suggested change
StringBuilder? stringBuilder = null;
foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> provider in extensionOptionsByProvider)
{
foreach (CommandLineOption option in provider.Value)
{
if (systemOptionNames.Contains(option.Name))
{
stringBuilder ??= new StringBuilder();
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionIsReserved, option.Name, provider.Key.DisplayName));
}
}
}
// Aggregate reserved options by name and track all offending providers
Dictionary<string, List<ICommandLineOptionsProvider>> reservedOptionToProviders = new();
foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> kvp in extensionOptionsByProvider)
{
ICommandLineOptionsProvider provider = kvp.Key;
foreach (CommandLineOption option in kvp.Value)
{
if (systemOptionNames.Contains(option.Name))
{
if (!reservedOptionToProviders.TryGetValue(option.Name, out List<ICommandLineOptionsProvider>? providers))
{
providers = new List<ICommandLineOptionsProvider>();
reservedOptionToProviders[option.Name] = providers;
}
providers.Add(provider);
}
}
}
StringBuilder? stringBuilder = null;
foreach (KeyValuePair<string, List<ICommandLineOptionsProvider>> kvp in reservedOptionToProviders)
{
string reservedOption = kvp.Key;
stringBuilder ??= new StringBuilder();
IEnumerable<string> faultyProvidersDisplayNames = kvp.Value.Select(p => p.DisplayName);
stringBuilder.AppendLine(string.Format(
CultureInfo.InvariantCulture,
PlatformResources.CommandLineOptionIsReserved,
reservedOption,
string.Join("', '", faultyProvidersDisplayNames)));
}

Copilot uses AI. Check for mistakes.
return ValidationResult.Valid();
return stringBuilder?.Length > 0
? ValidationResult.Invalid(stringBuilder.ToTrimmedString())
: ValidationResult.Valid();
}

private static ValidationResult ValidateOptionsAreNotDuplicated(
Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> extensionOptionsByProvider)
{
IEnumerable<string> duplications = extensionOptionsByProvider.Values.SelectMany(x => x)
.Select(x => x.Name)
.GroupBy(x => x)
.Where(x => x.Skip(1).Any())
.Select(x => x.Key);
// Use a dictionary to track option names and their providers
var optionNameToProviders = new Dictionary<string, List<ICommandLineOptionsProvider>>();
foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> kvp in extensionOptionsByProvider)
{
ICommandLineOptionsProvider provider = kvp.Key;
foreach (CommandLineOption option in kvp.Value)
{
string name = option.Name;
if (!optionNameToProviders.TryGetValue(name, out List<ICommandLineOptionsProvider>? providers))
{
providers = new List<ICommandLineOptionsProvider>();

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Debug)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Debug)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Release)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Release)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build Linux Release)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Release)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Release)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Debug)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Debug)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx (Build MacOS Debug)

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)

Check failure on line 171 in src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs

View check run for this annotation

Azure Pipelines / microsoft.testfx

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs#L171

src/Platform/Microsoft.Testing.Platform/CommandLine/CommandLineOptionsValidator.cs(171,33): error IDE0028: (NETCORE_ENGINEERING_TELEMETRY=Build) Collection initialization can be simplified (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0028)
optionNameToProviders[name] = providers;
}
Comment thread
Youssef1313 marked this conversation as resolved.

providers.Add(provider);
}
}
Comment on lines +161 to +177
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

The new duplication tracking counts providers, not distinct providers. If a single provider declares the same option name multiple times, kvp.Value.Count > 1 will be true and the error message may list the same provider multiple times. To preserve the previous behavior (providers listed once), store providers in a HashSet<ICommandLineOptionsProvider> (or store display names in a HashSet<string>) per option name, or apply Distinct() before counting and formatting.

Copilot uses AI. Check for mistakes.

// Check for duplications
StringBuilder? stringBuilder = null;
foreach (string duplicatedOption in duplications)
foreach (KeyValuePair<string, List<ICommandLineOptionsProvider>> kvp in optionNameToProviders)
{
IEnumerable<string> faultyProvidersDisplayNames = extensionOptionsByProvider.Where(tuple => tuple.Value.Any(x => x.Name == duplicatedOption)).Select(tuple => tuple.Key.DisplayName);
stringBuilder ??= new();
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionIsDeclaredByMultipleProviders, duplicatedOption, string.Join("', '", faultyProvidersDisplayNames)));
if (kvp.Value.Count > 1)
{
string duplicatedOption = kvp.Key;
stringBuilder ??= new();
IEnumerable<string> faultyProvidersDisplayNames = kvp.Value.Select(p => p.DisplayName);
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionIsDeclaredByMultipleProviders, duplicatedOption, string.Join("', '", faultyProvidersDisplayNames)));
}
Comment on lines +161 to +189
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

ValidateOptionsAreNotDuplicated tracks providers in a List and counts duplicates based on total entries, which will incorrectly report “multiple extensions” if a single provider returns the same option name more than once (and can also print the same provider name multiple times). Track distinct providers per option (e.g., HashSet) and base the duplication decision / formatted provider list on the distinct-provider count.

Copilot uses AI. Check for mistakes.
}
Comment on lines +181 to 190
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

The new duplication tracking counts providers, not distinct providers. If a single provider declares the same option name multiple times, kvp.Value.Count > 1 will be true and the error message may list the same provider multiple times. To preserve the previous behavior (providers listed once), store providers in a HashSet<ICommandLineOptionsProvider> (or store display names in a HashSet<string>) per option name, or apply Distinct() before counting and formatting.

Copilot uses AI. Check for mistakes.

return stringBuilder?.Length > 0
Expand All @@ -173,10 +199,28 @@
Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> extensionOptionsByProvider,
Dictionary<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> systemOptionsByProvider)
{
// Create a HashSet of all valid option names for faster lookup
var validOptionNames = new HashSet<string>();
foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> provider in extensionOptionsByProvider)
{
foreach (CommandLineOption option in provider.Value)
{
validOptionNames.Add(option.Name);
}
}

foreach (KeyValuePair<ICommandLineOptionsProvider, IReadOnlyCollection<CommandLineOption>> provider in systemOptionsByProvider)
{
foreach (CommandLineOption option in provider.Value)
{
validOptionNames.Add(option.Name);
}
}

StringBuilder? stringBuilder = null;
foreach (CommandLineParseOption optionRecord in parseResult.Options)
{
if (!extensionOptionsByProvider.Union(systemOptionsByProvider).Any(tuple => tuple.Value.Any(x => x.Name == optionRecord.Name)))
if (!validOptionNames.Contains(optionRecord.Name))
{
stringBuilder ??= new();
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineUnknownOption, optionRecord.Name));
Expand All @@ -192,7 +236,7 @@
CommandLineParseResult parseResult,
Dictionary<string, (ICommandLineOptionsProvider Provider, CommandLineOption Option)> providerAndOptionByOptionName)
{
StringBuilder stringBuilder = new();
StringBuilder? stringBuilder = null;
foreach (IGrouping<string, CommandLineParseOption> groupedOptions in parseResult.Options.GroupBy(x => x.Name))
{
// getting the arguments count for an option.
Expand All @@ -207,19 +251,22 @@

if (arity > option.Arity.Max && option.Arity.Max == 0)
{
stringBuilder ??= new();
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionExpectsNoArguments, optionName, provider.DisplayName, provider.Uid));
}
else if (arity < option.Arity.Min)
{
stringBuilder ??= new();
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionExpectsAtLeastArguments, optionName, provider.DisplayName, provider.Uid, option.Arity.Min));
}
else if (arity > option.Arity.Max)
{
stringBuilder ??= new();
stringBuilder.AppendLine(string.Format(CultureInfo.InvariantCulture, PlatformResources.CommandLineOptionExpectsAtMostArguments, optionName, provider.DisplayName, provider.Uid, option.Arity.Max));
}
}

return stringBuilder.Length > 0
return stringBuilder?.Length > 0
? ValidationResult.Invalid(stringBuilder.ToTrimmedString())
: ValidationResult.Valid();
}
Expand Down Expand Up @@ -280,7 +327,23 @@
}

private static string ToTrimmedString(this StringBuilder stringBuilder)
#pragma warning disable RS0030 // Do not use banned APIs
=> stringBuilder.ToString().TrimEnd(Environment.NewLine.ToCharArray());
#pragma warning restore RS0030 // Do not use banned APIs
{
// Use a more efficient approach to trim without creating unnecessary intermediate strings
string result = stringBuilder.ToString();
int end = result.Length;

// Find the last non-whitespace char
while (end > 0)
{
char c = result[end - 1];
if (c is not ('\r' or '\n'))
Comment on lines +331 to +339
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

ToTrimmedString comments are misleading: it says “trim without creating unnecessary intermediate strings” and “last non-whitespace char”, but the implementation still allocates via ToString() and only trims '\r'/'\n'. Please adjust the comment to match the actual behavior (trimming trailing newlines only) to avoid future confusion.

Copilot uses AI. Check for mistakes.
{
break;
}
Comment thread
Youssef1313 marked this conversation as resolved.
Comment thread
Youssef1313 marked this conversation as resolved.

end--;
}

return end == result.Length ? result : result.Substring(0, end);
}
Comment on lines 329 to +348
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

Despite the comment, this still potentially allocates twice (ToString() + Substring()), so the allocation win vs TrimEnd(...) may be limited. If the goal is to reduce allocations, consider trimming by mutating the StringBuilder length (remove trailing \\r/\\n characters from the builder) before calling ToString(), which guarantees a single final string allocation. Also, the comment says 'non-whitespace' but the logic only trims CR/LF; update the comment to match behavior.

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,91 @@ public async Task ParseAndValidateAsync_UnknownOption_ReturnsFalse()
Assert.AreEqual("Unknown option '--x'", result.ErrorMessage);
}

[TestMethod]
public async Task ParseAndValidateAsync_MultipleUnknownOptions_ReportsAll()
{
// Arrange
string[] args = ["--x", "--y"];
CommandLineParseResult parseResult = CommandLineParser.Parse(args, new SystemEnvironment());

ICommandLineOptionsProvider[] extensionCommandLineProvider =
[
new ExtensionCommandLineProviderMockUnknownOption()
];

// Act
ValidationResult result = await CommandLineOptionsValidator.ValidateAsync(parseResult, _systemCommandLineOptionsProviders,
extensionCommandLineProvider, new Mock<ICommandLineOptions>().Object);

// Assert
Assert.IsFalse(result.IsValid);
Assert.Contains("Unknown option '--x'", result.ErrorMessage);
Assert.Contains("Unknown option '--y'", result.ErrorMessage);
}

[TestMethod]
public async Task ParseAndValidateAsync_MultipleReservedOptionsFromDifferentProviders_ReturnsFalse()
{
// Arrange
string[] args = [];
CommandLineParseResult parseResult = CommandLineParser.Parse(args, new SystemEnvironment());
ICommandLineOptionsProvider[] extensionCommandLineProvider =
[
new ExtensionCommandLineProviderMockReservedOptions(),
new ExtensionCommandLineProviderMockWithNamedOption("help", "Provider2")
];

// Act
ValidationResult result = await CommandLineOptionsValidator.ValidateAsync(parseResult, _systemCommandLineOptionsProviders,
extensionCommandLineProvider, new Mock<ICommandLineOptions>().Object);

// Assert
Assert.IsFalse(result.IsValid);
Assert.Contains("Option '--help' is reserved and cannot be used by providers: 'help'", result.ErrorMessage);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

This test name says it covers reserved options from different providers, but the assertion only checks for a single provider token ('help') and does not verify that both providers are reported. To make the test actually validate the scenario, assert that the error message includes each expected provider display name (e.g., Provider2 and the other mock’s display name) or assert the full expected line(s) depending on the intended aggregation format.

Suggested change
Assert.Contains("Option '--help' is reserved and cannot be used by providers: 'help'", result.ErrorMessage);
Assert.Contains("Option '--help' is reserved and cannot be used by providers:", result.ErrorMessage);
Assert.Contains("'help'", result.ErrorMessage);
Assert.Contains("'Provider2'", result.ErrorMessage);

Copilot uses AI. Check for mistakes.
}

[TestMethod]
public async Task ParseAndValidateAsync_DuplicateOptionWithDistinctProviderNames_ReportsAllProviders()
{
// Arrange
string[] args = [];
CommandLineParseResult parseResult = CommandLineParser.Parse(args, new SystemEnvironment());
ICommandLineOptionsProvider[] extensionCommandLineOptionsProviders =
[
new ExtensionCommandLineProviderMockWithNamedOption("userOption", "ProviderOne"),
new ExtensionCommandLineProviderMockWithNamedOption("userOption", "ProviderTwo")
];

// Act
ValidationResult result = await CommandLineOptionsValidator.ValidateAsync(parseResult, _systemCommandLineOptionsProviders,
extensionCommandLineOptionsProviders, new Mock<ICommandLineOptions>().Object);

// Assert
Assert.IsFalse(result.IsValid);
Assert.Contains("Option '--userOption' is declared by multiple extensions: 'ProviderOne', 'ProviderTwo'", result.ErrorMessage);
}

[TestMethod]
public async Task ParseAndValidateAsync_ValidOptionsWithManyProviders_ReturnsTrue()
{
// Arrange
string[] args = ["--option1", "--option2", "--option3"];
CommandLineParseResult parseResult = CommandLineParser.Parse(args, new SystemEnvironment());
ICommandLineOptionsProvider[] extensionCommandLineOptionsProviders =
[
new ExtensionCommandLineProviderMockWithNamedOption("option1", "Provider1"),
new ExtensionCommandLineProviderMockWithNamedOption("option2", "Provider2"),
new ExtensionCommandLineProviderMockWithNamedOption("option3", "Provider3")
];

// Act
ValidationResult result = await CommandLineOptionsValidator.ValidateAsync(parseResult, _systemCommandLineOptionsProviders,
extensionCommandLineOptionsProviders, new Mock<ICommandLineOptions>().Object);

// Assert
Assert.IsTrue(result.IsValid);
}

[TestMethod]
public async Task ParseAndValidateAsync_InvalidValidConfiguration_ReturnsFalse()
{
Expand Down Expand Up @@ -435,4 +520,38 @@ public IReadOnlyCollection<CommandLineOption> GetCommandLineOptions() =>

public Task<ValidationResult> ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) => ValidationResult.ValidTask;
}

private sealed class ExtensionCommandLineProviderMockWithNamedOption : ICommandLineOptionsProvider
{
private readonly string _option;

public ExtensionCommandLineProviderMockWithNamedOption(string optionName, string displayName)
{
_option = optionName;
DisplayName = displayName;
}

public string Uid { get; } = nameof(PlatformCommandLineProvider);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

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

For a test extension provider mock, hard-coding Uid to PlatformCommandLineProvider and describing it as 'Built-in' is misleading and can make diagnostics harder to interpret if Uid/description ever appear in validation output. Consider setting Uid to something unique to the mock (or derived from displayName) and updating the description to reflect that it’s a test/extension provider.

Copilot uses AI. Check for mistakes.

/// <inheritdoc />
public string Version { get; } = AppVersion.DefaultSemVer;

/// <inheritdoc />
public string DisplayName { get; }

/// <inheritdoc />
public string Description { get; } = "Built-in command line provider";

/// <inheritdoc />
public Task<bool> IsEnabledAsync() => Task.FromResult(true);

public IReadOnlyCollection<CommandLineOption> GetCommandLineOptions() =>
[
new(_option, "Show command line option.", ArgumentArity.ZeroOrOne, false)
];

public Task<ValidationResult> ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions) => ValidationResult.ValidTask;

public Task<ValidationResult> ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) => ValidationResult.ValidTask;
}
}
Loading