Skip to content

fix: migrate PerfDiff to System.CommandLine 2.0.3#1043

Merged
rjmurillo merged 44 commits intomainfrom
fix/914-perfdiff-system-commandline
Mar 15, 2026
Merged

fix: migrate PerfDiff to System.CommandLine 2.0.3#1043
rjmurillo merged 44 commits intomainfrom
fix/914-perfdiff-system-commandline

Conversation

@rjmurillo-bot
Copy link
Copy Markdown
Collaborator

@rjmurillo-bot rjmurillo-bot commented Mar 7, 2026

Summary

  • Upgrade System.CommandLine from 2.0.0-beta1.21216.1 to 2.0.3
  • Remove System.CommandLine.Rendering package (removed in stable release)
  • Replace removed IConsole, ITerminal, CommandHandler.Create APIs with System.Console and SetAction
  • Framework-provided CancellationToken from SetAction replaces manual CancellationTokenSource + Console.CancelKeyPress hook

Changes

File Change
Directory.Packages.props Version bump, remove Rendering package
PerfDiff.csproj Remove Rendering PackageReference
DiffCommand.cs Remove Handler delegate, expose options as static properties
Program.cs Use SetAction + ParseResult.GetValue, remove IConsole parameter
SimpleConsoleLogger.cs Replace IConsole/ITerminal with System.Console
SimpleConsoleLoggerProvider.cs Remove IConsole dependency
SimpleConsoleLoggerFactoryExtensions.cs Remove IConsole parameter

Test plan

  • dotnet build Moq.Analyzers.sln passes with 0 errors, 0 warnings

Closes #914

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Updated System.CommandLine dependency to stable 2.0.3 (from beta).
    • Removed System.CommandLine.Rendering dependency.
    • Refactored internal command-line and logging infrastructure for improved maintainability.

@rjmurillo-bot rjmurillo-bot requested a review from rjmurillo as a code owner March 7, 2026 22:53
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Updates PerfDiff tool from System.CommandLine beta 2.0.0 to stable 2.0.3 by removing IConsole dependencies, eliminating System.CommandLine.Rendering package, refactoring command-line option handling into static Option properties, replacing console abstraction with direct Console usage, and restructuring logging initialization to use dependency injection instead of console-based setup.

Changes

Cohort / File(s) Summary
Package Configuration
Directory.Packages.props, src/tools/PerfDiff/PerfDiff.csproj
Updates System.CommandLine from beta 2.0.0-beta1.21216.1 to stable 2.0.3; removes System.CommandLine.Rendering beta dependency from both central package versions and project references.
Command-Line Options Refactoring
src/tools/PerfDiff/DiffCommand.cs
Replaces Handler delegate with static Option properties (BaselineOption, ResultsOption, VerbosityOption, FailOnRegressionOption); adds VerbosityMap frozen dictionary mapping strings to LogLevel; introduces CreateFilePathOption and CreateVerbosityOption helpers; updates CreateCommandLineOptions to use new option properties.
Logging Infrastructure Redesign
src/tools/PerfDiff/Logging/SimpleConsoleLogger.cs, src/tools/PerfDiff/Logging/SimpleConsoleLoggerProvider.cs, src/tools/PerfDiff/Logging/SimpleConsoleLoggerFactoryExtensions.cs
Removes IConsole abstraction in favor of direct Console usage; SimpleConsoleLogger constructor signature removes IConsole parameter; SimpleConsoleLoggerProvider caches single logger instance instead of creating per-request; removes SimpleConsoleLoggerFactoryExtensions file entirely; replaces immutable dictionaries with frozen dictionaries.
Core Application Logic
src/tools/PerfDiff/Program.cs, src/tools/PerfDiff/PerfDiff.cs
Changes command wiring from CommandHandler.Create to SetAction with inline parameter construction; converts RunAsync from public to internal and replaces IConsole parameter with CancellationToken; refactors SetupLogging to return ServiceProvider for DI-based logger retrieval; removes console-based cancellation handling; makes PerfDiff class and CompareAsync internal; removes failOnRegression parameter from CheckEltTraces; adjusts ETL result handling logic to use return values rather than direct comparison.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as System.CommandLine
    participant SetAction as SetAction Handler
    participant Program as Program.RunAsync
    participant DI as ServiceProvider
    participant Logger as Logger/LoggerFactory
    participant PerfDiff as PerfDiff.CompareAsync
    participant Console as System.Console

    User->>CLI: Execute with args
    CLI->>CLI: Parse args (DiffCommand options)
    CLI->>SetAction: Invoke with parsed options
    SetAction->>SetAction: Extract baseline, results,<br/>verbosity, failOnRegression
    SetAction->>Program: RunAsync(params, CancellationToken)
    
    Program->>Program: SetupLogging(verbosity)
    Program->>DI: Configure ServiceProvider<br/>(AddLogging)
    DI->>Logger: Create LoggerFactory
    Logger->>Logger: AddSimpleConsole<br/>(minimalLogLevel)
    DI-->>Program: Return ServiceProvider
    
    Program->>DI: GetService<ILogger>
    DI-->>Program: Return Logger instance
    Program->>PerfDiff: CompareAsync(baseline,<br/>results, failOnRegression,<br/>logger, token)
    PerfDiff->>Logger: Log warnings/traces
    Logger->>Console: Write with formatting
    Console-->>Logger: 
    PerfDiff-->>Program: Return exit code
    Program-->>CLI: Return int
    CLI-->>User: Exit with code
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • MattKotsenas
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: migrating PerfDiff to System.CommandLine 2.0.3, which is the primary objective of the PR.
Description check ✅ Passed The description provides a clear summary, lists key changes in a table format, includes test plan confirmation, and references the linked issue #914, meeting the template requirements.
Linked Issues check ✅ Passed Code changes successfully implement all primary requirements: System.CommandLine upgraded to 2.0.3, IConsole removed from all 5 affected files, System.CommandLine.Rendering dependency removed, and SetAction/CancellationToken integration completed [#914].
Out of Scope Changes check ✅ Passed All changes directly address issue #914 requirements. PerfDiff.cs visibility changes (public to internal) and signature modifications are scope-appropriate for the System.CommandLine 2.0.3 migration and decoupling from IConsole.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/914-perfdiff-system-commandline
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@deepsource-io
Copy link
Copy Markdown

deepsource-io bot commented Mar 7, 2026

DeepSource Code Review

We reviewed changes in dd99146...4cfa058 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
C# Mar 14, 2026 7:28p.m. Review ↗

@rjmurillo rjmurillo added dependencies Pull requests that update a dependency file releasable labels Mar 7, 2026
@github-actions github-actions bot added the bug label Mar 7, 2026
@codacy-production
Copy link
Copy Markdown

codacy-production bot commented Mar 7, 2026

Coverage summary from Codacy

See diff coverage on Codacy

Coverage variation Diff coverage
Report missing for dd991461 (target: 95.00%)
Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (dd99146) Report Missing Report Missing Report Missing
Head commit (4cfa058) 2554 2295 89.86%

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#1043) 0 0 ∅ (not applicable)

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

See your quality gate settings    Change summary preferences

Footnotes

  1. Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.

@coderabbitai coderabbitai bot requested a review from MattKotsenas March 8, 2026 00:04
@coderabbitai coderabbitai bot added the build label Mar 8, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Directory.Packages.props`:
- Around line 33-38: The Directory.Packages.props entry for Perfolizer is still
pinned to 0.6.1 while the PR migrates PerfDiff to 0.6.7; update the
PackageVersion element for Perfolizer to Version="0.6.7" so the central package
table matches the code changes (look for the PackageVersion Include="Perfolizer"
element and change its Version value), and run a restore/build to verify no
dependency mismatches.

In `@src/tools/PerfDiff/DiffCommand.cs`:
- Around line 20-27: The BaselineOption and ResultsOption in DiffCommand.cs are
currently optional and default to null, allowing empty strings to reach
RunAsync; make both options required so the parser rejects missing values early:
update the Option<string?> definitions for BaselineOption and ResultsOption (the
Option instances declared) to mark them as required (e.g., set IsRequired = true
or construct them with required behavior) and remove/avoid a default null
fallback so the CLI parser enforces presence before invoking the action.

In `@src/tools/PerfDiff/Program.cs`:
- Around line 27-30: The CheckEltTraces tuple conflates "comparison succeeded"
with "should fail the command", so change CheckEltTraces (and any overloads) to
return two distinct booleans (e.g., comparisonSucceeded / etlSucceeded and
shouldFailOrHasRegression) and update CompareAsync and PerfDiff.CompareAsync
callers to accept both values; wire the parsed failOnRegression flag from
RunAsync/Program.cs into CompareAsync so that CompareAsync only returns a
failing exit code when either comparisonSucceeded is false OR the new shouldFail
value is true (honoring failOnRegression), and ensure places that previously
treated the single boolean as both are updated to use the two separate booleans
(functions to edit: CheckEltTraces, CompareAsync, PerfDiff.CompareAsync and
RunAsync wiring of failOnRegression).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2f1cc5d2-747e-4c7a-90df-7040c6813430

📥 Commits

Reviewing files that changed from the base of the PR and between 375f77f and 58f613d.

📒 Files selected for processing (7)
  • Directory.Packages.props
  • src/tools/PerfDiff/DiffCommand.cs
  • src/tools/PerfDiff/Logging/SimpleConsoleLogger.cs
  • src/tools/PerfDiff/Logging/SimpleConsoleLoggerFactoryExtensions.cs
  • src/tools/PerfDiff/Logging/SimpleConsoleLoggerProvider.cs
  • src/tools/PerfDiff/PerfDiff.csproj
  • src/tools/PerfDiff/Program.cs
💤 Files with no reviewable changes (1)
  • src/tools/PerfDiff/PerfDiff.csproj

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tools/PerfDiff/Program.cs`:
- Around line 67-76: The SetupLogging method currently instantiates
LoggerFactory and registers it as a singleton; make it more idiomatic by
removing the explicit LoggerFactory creation and instead configure logging via
ServiceCollection.AddLogging(...) — call serviceCollection.AddLogging(builder =>
builder.SetMinimumLevel(minimalLogLevel).AddSimpleConsole(minimalLogLevel,
minimalErrorLevel)) (or the equivalent AddConsole provider your project uses) so
logging is configured through the DI builder and the ServiceProvider manages
disposal; update references to LoggerFactory only if other code depends on the
concrete instance.
- Around line 22-33: The SetAction delegate currently uses an async lambda that
assigns parseResult.Action!.ExitCode but must instead return the exit code
because rootCommand.SetAction expects Func<ParseResult, CancellationToken,
Task<int>>; update the lambda used in rootCommand.SetAction to return the int
from RunAsync(baseline, results, verbosity, failOnRegression, cancellationToken)
(i.e., replace the assignment to parseResult.Action!.ExitCode with "return
exitCode;") so the exit code propagates correctly from the lambda, keeping the
RunAsync call and variables (baseline, results, verbosity, failOnRegression)
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c50c625e-fe39-45e0-97b5-a229a50eb783

📥 Commits

Reviewing files that changed from the base of the PR and between 58f613d and 7e5555d.

📒 Files selected for processing (4)
  • Directory.Packages.props
  • src/tools/PerfDiff/DiffCommand.cs
  • src/tools/PerfDiff/PerfDiff.cs
  • src/tools/PerfDiff/Program.cs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/tools/PerfDiff/Program.cs (1)

66-74: 🧹 Nitpick | 🔵 Trivial

Consider using the DI builder pattern for cleaner logging setup.

The custom SimpleConsoleLoggerProvider already implements filtering via IsEnabled() in SimpleConsoleLogger, so Debug/Trace messages do reach the provider. However, the current pattern—manually creating a LoggerFactory, adding the provider, registering it in DI, and then calling AddLogging()—mixes manual factory management with DI conventions, which is unconventional and harder to reason about.

♻️ Suggested refactor
 static ServiceProvider SetupLogging(LogLevel minimalLogLevel, LogLevel minimalErrorLevel)
 {
     ServiceCollection serviceCollection = new ServiceCollection();
-    LoggerFactory loggerFactory = new LoggerFactory();
-    loggerFactory.AddSimpleConsole(minimalLogLevel, minimalErrorLevel);
-    serviceCollection.AddSingleton<ILoggerFactory>(loggerFactory);
-    serviceCollection.AddLogging();
+    serviceCollection.AddLogging(builder =>
+    {
+        builder.AddProvider(new SimpleConsoleLoggerProvider(minimalLogLevel, minimalErrorLevel));
+    });
 
     return serviceCollection.BuildServiceProvider();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/PerfDiff/Program.cs` around lines 66 - 74, Replace the manual
LoggerFactory creation with the DI logging builder pattern inside SetupLogging:
stop instantiating LoggerFactory and registering it, and instead call
ServiceCollection.AddLogging(Action<ILoggingBuilder>) to configure minimum level
and add the SimpleConsoleLoggerProvider (or call your existing AddSimpleConsole
extension) so the provider is registered through the ILoggingBuilder; reference
SetupLogging, ServiceCollection, AddLogging, SimpleConsoleLoggerProvider and
SimpleConsoleLogger.IsEnabled when making the change and keep returning
serviceCollection.BuildServiceProvider() after configuring logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/tools/PerfDiff/Program.cs`:
- Around line 66-74: Replace the manual LoggerFactory creation with the DI
logging builder pattern inside SetupLogging: stop instantiating LoggerFactory
and registering it, and instead call
ServiceCollection.AddLogging(Action<ILoggingBuilder>) to configure minimum level
and add the SimpleConsoleLoggerProvider (or call your existing AddSimpleConsole
extension) so the provider is registered through the ILoggingBuilder; reference
SetupLogging, ServiceCollection, AddLogging, SimpleConsoleLoggerProvider and
SimpleConsoleLogger.IsEnabled when making the change and keep returning
serviceCollection.BuildServiceProvider() after configuring logging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 520617cd-129e-4b78-b972-57865eae80b7

📥 Commits

Reviewing files that changed from the base of the PR and between 7e5555d and e4eeb11.

📒 Files selected for processing (1)
  • src/tools/PerfDiff/Program.cs

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 8, 2026
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 8, 2026

CLA assistant check
All committers have signed the CLA.

rjmurillo and others added 8 commits March 8, 2026 11:03
Replace removed IConsole, ITerminal, CommandHandler.Create, and
System.CommandLine.Rendering APIs with System.Console and SetAction.
The CancellationToken from SetAction replaces the manual
CancellationTokenSource + Console.CancelKeyPress hook.

Closes #914

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…allocation

Wrap LogToConsole call in try/finally to ensure Console.ResetColor() runs
even if an exception occurs.

Change LogLevelColorMap from expression-bodied property (=>) to static
readonly field (=) so the dictionary is allocated once, not on every access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Return ServiceProvider from SetupLogging so the caller can dispose it in
the existing finally block. Prevents resource leak.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… code

Use await using with ConfigureAwait(false) for async disposal instead of
manual Dispose() in a finally block. Remove dead currentDirectory code
that was never assigned. Register LoggerFactory as ILoggerFactory
singleton so the container manages its lifetime. Use GetRequiredService
for fail-fast behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The PackageVersion was pinned to 0.6.1 but the PR migrates to 0.6.7.
Aligns the central package version with the actual dependency used.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set IsRequired = true on both options so the CLI parser rejects
missing values. Change types from string? to string and remove
the null-to-empty fallback in Program.cs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CheckEltTraces previously conflated "comparison succeeded" with
"should fail based on regression". Separated concerns: CheckEltTraces
now returns pure comparison results without policy logic. CompareAsync
uses failOnRegression to decide the exit code. Also fixed typo
"is was" to "it was" in log message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The System.CommandLine 2.0.3 SetAction async overload expects
Func<ParseResult, CancellationToken, Task<int>>. Return the exit code
directly instead of assigning to parseResult.Action.ExitCode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@rjmurillo-bot rjmurillo-bot force-pushed the fix/914-perfdiff-system-commandline branch from c2b9e70 to 4893082 Compare March 8, 2026 18:04
rjmurillo and others added 4 commits March 8, 2026 11:07
- Pass description to RootCommand constructor instead of setting it after
- Remove redundant null assignment in TryGetETLPaths
- Remove unused `using System` directive from Program.cs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix build errors from breaking API changes in System.CommandLine 2.0.3:
IsRequired -> Required, LegalFilePathsOnly -> AcceptLegalFilePathsOnly,
FromAmong -> AcceptOnlyFromAmong, constructor signature changes.

Consolidate repeated CA1848/CA2254 pragma suppressions to method scope.
Remove redundant null assignment in TryGetETLPaths. Remove unused using.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The DI container now owns the LoggerFactory lifetime. Previously, the
manually created instance was registered as a singleton object, which
meant the container would not dispose it on teardown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…um level

Promote LogTrace to LogWarning on exit paths that return non-zero so
callers see why the tool failed without requiring diagnostic verbosity.

Add SetMinimumLevel to the logging factory so Debug/Trace messages
reach the provider when requested via --verbosity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodAnalyzerTests.cs (1)

220-261: ⚠️ Potential issue | 🟡 Minor

XML documentation contradicts the updated test behavior.

The XML doc comments (lines 221-223, 225-233) still describe this as a "known limitation" where generic .Callback<T>() type mismatches are "NOT currently validated." However, the test now expects diagnostic Moq1100 to be raised, indicating the analyzer does detect this mismatch.

The documentation should be updated to reflect that this validation is now implemented.

📝 Suggested documentation update
-    /// <summary>
-    /// Test to document the known limitation with generic callback validation.
-    /// This test documents that .Callback&lt;T&gt;() with wrong type parameters is NOT currently validated.
-    /// This is an accepted limitation as the explicit generic syntax is rarely used in practice.
-    /// </summary>
-    /// <remarks>
-    /// <para>
-    /// Analysis shows zero real-world usage of the explicit generic .Callback&lt;T&gt;() syntax in open-source projects.
-    /// The recommended approach is to use lambda parameter inference which provides full type validation:
-    /// <c>.Callback(param => { })</c> or <c>.Callback((string param) => { })</c>.
-    /// </para>
-    /// <para>
-    /// See docs/rules/Moq1100.md "Known Limitations" section for best practices.
-    /// </para>
-    /// </remarks>
+    /// <summary>
+    /// Verifies that generic <c>.Callback&lt;T&gt;()</c> with mismatched type parameters
+    /// triggers the Moq1100 diagnostic.
+    /// </summary>
+    /// <remarks>
+    /// When the generic type argument does not match the mocked method's parameter type
+    /// (e.g., <c>.Callback&lt;int&gt;</c> on a method expecting <see langword="string"/>),
+    /// the analyzer correctly reports a type mismatch.
+    /// </remarks>
     /// <param name="referenceAssemblyGroup">The Moq version reference assembly group.</param>
     /// <returns>A task representing the asynchronous unit test.</returns>

Based on learnings: The retrieved learning stating "Moq1100 currently has a gap where it does not validate generic .Callback<T>() type parameters" is now outdated—the analyzer has been fixed to detect this mismatch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodAnalyzerTests.cs`
around lines 220 - 261, The XML documentation for the test
GenericCallbackValidation_DetectsTypeMismatch is outdated: it states generic
.Callback<T>() type mismatches are "NOT currently validated" but the test
expects diagnostic Moq1100; update the summary/remarks to reflect that generic
.Callback<T>() mismatches are now validated by the analyzer and remove wording
about the known limitation, and adjust any guidance recommending lambda
inference (you can keep it as an alternative) so the doc comments around the
GenericCallbackValidation_DetectsTypeMismatch test and the remarks referencing
docs/rules/Moq1100.md accurately state the current behavior (Moq1100 is raised
when .Callback<T> uses the wrong type parameter).
src/tools/PerfDiff/PerfDiff.cs (2)

17-19: ⚠️ Potential issue | 🟠 Major

Re-check cancellation after the BenchmarkDotNet compare completes.

token is only observed before the first await. If Ctrl+C arrives while TryCompareBenchmarkDotNetResultsAsync is running, this method can still continue into the ETL path and even return a normal success/failure code instead of honoring cancellation. Add another ThrowIfCancellationRequested() after the await and before ETL processing, and pass the token into downstream compares if they can do long-running I/O.

🛑 Minimal fix
         (bool compareSucceeded, bool regressionDetected) = await BenchmarkDotNetDiffer.TryCompareBenchmarkDotNetResultsAsync(baselineFolder, resultsFolder, logger).ConfigureAwait(false);
+        token.ThrowIfCancellationRequested();
 ...
+        token.ThrowIfCancellationRequested();
         (bool etlCompareSucceeded, bool etlRegressionDetected) = CheckEltTraces(baselineFolder, resultsFolder);

Also applies to: 33-47

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/PerfDiff/PerfDiff.cs` around lines 17 - 19, After awaiting
BenchmarkDotNetDiffer.TryCompareBenchmarkDotNetResultsAsync, re-check
cancellation by calling token.ThrowIfCancellationRequested() immediately before
entering the ETL/processing path; also propagate the CancellationToken into any
downstream long-running compare/IO methods (e.g., pass token into
BenchmarkDotNetDiffer.TryCompareBenchmarkDotNetResultsAsync and any subsequent
compare/ETL helpers) so those operations can observe cancellation too. Ensure
you add the same post-await ThrowIfCancellationRequested() in the other affected
block (lines ~33-47) where a long-running await precedes ETL work.

74-87: ⚠️ Potential issue | 🟠 Major

Harden ETL discovery against false matches and duplicate traces.

ETLFileExtension = "etl.zip" also matches names like hotel.zip, and SingleOrDefault() will throw if a directory contains more than one matching trace. That turns a recoverable "ETL unavailable" case into an unhandled exception. Use .etl.zip as the suffix and return false when you see 0 or >1 matches instead of throwing.

🧭 Proposed fix
-    private static readonly string ETLFileExtension = "etl.zip";
+    private const string ETLFileExtension = ".etl.zip";
...
-            string[] files = Directory.GetFiles(path, $"*{ETLFileExtension}", SearchOption.AllDirectories);
-            etlPath = files.SingleOrDefault();
-            return etlPath is not null;
+            string? singleMatch = null;
+            foreach (string candidate in Directory.EnumerateFiles(path, $"*{ETLFileExtension}", SearchOption.AllDirectories))
+            {
+                if (singleMatch is not null)
+                {
+                    etlPath = null;
+                    return false;
+                }
+
+                singleMatch = candidate;
+            }
+
+            etlPath = singleMatch;
+            return etlPath is not null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/PerfDiff/PerfDiff.cs` around lines 74 - 87, Change ETLFileExtension
from "etl.zip" to ".etl.zip" to avoid partial matches like "hotel.zip", and in
TryGetETLPaths replace the SingleOrDefault usage with a safe length check: when
Directory.Exists(path) call Directory.GetFiles(path, $"*{ETLFileExtension}",
SearchOption.AllDirectories) and if files.Length == 1 set etlPath = files[0] and
return true; otherwise set etlPath = null and return false (so 0 or >1 matches
both return false). Also ensure the file-case branch still uses
path.EndsWith(ETLFileExtension, StringComparison.OrdinalIgnoreCase).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Directory.Packages.props`:
- Line 36: The PR updated the PackageVersion for System.CommandLine to 2.0.3 but
lacks machine-readable validation evidence; run dotnet build and dotnet test for
all affected projects after this .props change and capture/attach the full
machine-readable logs (e.g., use dotnet test --logger
"trx;LogFileName=tests.trx" and dotnet build /v:minimal or msbuild /nologo
/clp:PerformanceSummary) that show successful compilation and test results, then
add those artifacts to the PR to validate the System.CommandLine Version="2.0.3"
change.

In `@src/tools/PerfDiff/Program.cs`:
- Around line 21-49: Add integration tests that exercise the new CLI contract by
invoking the command path that uses rootCommand.SetAction and by calling
Program.RunAsync directly: cover parser errors (missing required options from
DiffCommand), cancellation via a CancellationToken, and all exit-code branches
including success, failOnRegression=true/false, and ETL/PerfDiff.CompareAsync
failure paths; ensure tests assert the returned int exit codes and that
logging/exception propagation behavior mirrors the SetAction/RunAsync changes so
parser, cancellation, and CompareAsync outcomes are pinned down.

---

Outside diff comments:
In `@src/tools/PerfDiff/PerfDiff.cs`:
- Around line 17-19: After awaiting
BenchmarkDotNetDiffer.TryCompareBenchmarkDotNetResultsAsync, re-check
cancellation by calling token.ThrowIfCancellationRequested() immediately before
entering the ETL/processing path; also propagate the CancellationToken into any
downstream long-running compare/IO methods (e.g., pass token into
BenchmarkDotNetDiffer.TryCompareBenchmarkDotNetResultsAsync and any subsequent
compare/ETL helpers) so those operations can observe cancellation too. Ensure
you add the same post-await ThrowIfCancellationRequested() in the other affected
block (lines ~33-47) where a long-running await precedes ETL work.
- Around line 74-87: Change ETLFileExtension from "etl.zip" to ".etl.zip" to
avoid partial matches like "hotel.zip", and in TryGetETLPaths replace the
SingleOrDefault usage with a safe length check: when Directory.Exists(path) call
Directory.GetFiles(path, $"*{ETLFileExtension}", SearchOption.AllDirectories)
and if files.Length == 1 set etlPath = files[0] and return true; otherwise set
etlPath = null and return false (so 0 or >1 matches both return false). Also
ensure the file-case branch still uses path.EndsWith(ETLFileExtension,
StringComparison.OrdinalIgnoreCase).

In
`@tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodAnalyzerTests.cs`:
- Around line 220-261: The XML documentation for the test
GenericCallbackValidation_DetectsTypeMismatch is outdated: it states generic
.Callback<T>() type mismatches are "NOT currently validated" but the test
expects diagnostic Moq1100; update the summary/remarks to reflect that generic
.Callback<T>() mismatches are now validated by the analyzer and remove wording
about the known limitation, and adjust any guidance recommending lambda
inference (you can keep it as an alternative) so the doc comments around the
GenericCallbackValidation_DetectsTypeMismatch test and the remarks referencing
docs/rules/Moq1100.md accurately state the current behavior (Moq1100 is raised
when .Callback<T> uses the wrong type parameter).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ea84db64-3942-4cb0-b892-a19433821934

📥 Commits

Reviewing files that changed from the base of the PR and between e4eeb11 and 10c6afb.

📒 Files selected for processing (9)
  • Directory.Packages.props
  • src/tools/PerfDiff/DiffCommand.cs
  • src/tools/PerfDiff/Logging/SimpleConsoleLogger.cs
  • src/tools/PerfDiff/Logging/SimpleConsoleLoggerFactoryExtensions.cs
  • src/tools/PerfDiff/Logging/SimpleConsoleLoggerProvider.cs
  • src/tools/PerfDiff/PerfDiff.cs
  • src/tools/PerfDiff/PerfDiff.csproj
  • src/tools/PerfDiff/Program.cs
  • tests/Moq.Analyzers.Test/CallbackSignatureShouldMatchMockedMethodAnalyzerTests.cs
💤 Files with no reviewable changes (2)
  • src/tools/PerfDiff/PerfDiff.csproj
  • src/tools/PerfDiff/Logging/SimpleConsoleLoggerFactoryExtensions.cs

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 9, 2026
rjmurillo and others added 2 commits March 9, 2026 08:37
Resolved conflict in CallbackSignatureShouldMatchMockedMethodAnalyzerTests.cs
by taking main's version (GenericCallbackWithWrongType_ProducesDiagnostic).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
rjmurillo and others added 2 commits March 9, 2026 09:33
ECS0200 is disabled in PerfDiff's .editorconfig, so const is the
correct choice for compile-time literals. Reverts the unnecessary
static readonly conversions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/tools/PerfDiff/PerfDiff.cs (1)

76-82: ⚠️ Potential issue | 🟡 Minor

SingleOrDefault throws when multiple ETL files exist.

If a folder contains more than one *.etl.zip file, SingleOrDefault() throws InvalidOperationException instead of returning null. This causes an unhandled crash rather than the expected (false, false) fallback.

Consider using FirstOrDefault() if any match suffices, or catch the exception / check files.Length to handle the ambiguous case gracefully.

🛠️ Proposed fix using length check
         if (Directory.Exists(path))
         {
             string[] files = Directory.GetFiles(path, $"*{ETLFileExtension}", SearchOption.AllDirectories);
-            etlPath = files.SingleOrDefault();
-            return etlPath is not null;
+            if (files.Length == 1)
+            {
+                etlPath = files[0];
+                return true;
+            }
+
+            etlPath = null;
+            return false;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/PerfDiff/PerfDiff.cs` around lines 76 - 82, The TryGetETLPaths
method uses files.SingleOrDefault() which will throw if more than one ETL file
exists; change the logic to handle multiple matches gracefully by either using
files.FirstOrDefault() (if any match is acceptable) or explicitly check
files.Length and treat multiple results as an ambiguous case (set etlPath = null
and return false or log/handle as appropriate) so the method never throws;
update the code in TryGetETLPaths to perform that safe selection/length check
and return false when ambiguous.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tools/PerfDiff/Program.cs`:
- Around line 51-61: In the FileNotFoundException catch block replace the
current call that only logs fex.Message with a call that passes the exception
object to the logger so the stack trace and FileName are preserved (i.e., change
the use of logger.LogError(fex.Message) in the catch (FileNotFoundException fex)
block to an overload that accepts the exception like logger.LogError(fex, "...")
and keep the existing pragma disables around the statement if needed); this
ensures FileNotFoundException fex, its stacktrace and FileName are recorded for
debugging.

---

Outside diff comments:
In `@src/tools/PerfDiff/PerfDiff.cs`:
- Around line 76-82: The TryGetETLPaths method uses files.SingleOrDefault()
which will throw if more than one ETL file exists; change the logic to handle
multiple matches gracefully by either using files.FirstOrDefault() (if any match
is acceptable) or explicitly check files.Length and treat multiple results as an
ambiguous case (set etlPath = null and return false or log/handle as
appropriate) so the method never throws; update the code in TryGetETLPaths to
perform that safe selection/length check and return false when ambiguous.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ccabf817-eaaf-4583-8398-33631ddfa5ae

📥 Commits

Reviewing files that changed from the base of the PR and between 10c6afb and 9fedf86.

📒 Files selected for processing (2)
  • src/tools/PerfDiff/PerfDiff.cs
  • src/tools/PerfDiff/Program.cs

Pass the exception object to LogError to preserve stack trace and
FileName property for debugging file-not-found issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 9, 2026
renovate bot and others added 2 commits March 9, 2026 19:03
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
|
[Meziantou.Analyzer](https://github.com/meziantou/Meziantou.Analyzer)
| `3.0.21` → `3.0.22` |
![age](https://developer.mend.io/api/mc/badges/age/nuget/Meziantou.Analyzer/3.0.22?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/nuget/Meziantou.Analyzer/3.0.21/3.0.22?slim=true)
|

---

### Release Notes

<details>
<summary>meziantou/Meziantou.Analyzer (Meziantou.Analyzer)</summary>

###
[`v3.0.22`](https://github.com/meziantou/Meziantou.Analyzer/releases/tag/3.0.22)

[Compare
Source](https://github.com/meziantou/Meziantou.Analyzer/compare/3.0.21...3.0.22)

NuGet package:
<https://www.nuget.org/packages/Meziantou.Analyzer/3.0.22>

##### What's Changed

- Add MA0188: Use System.TimeProvider instead of a custom time
abstraction by [@&#8203;Copilot](https://github.com/Copilot) in
[#&#8203;1055](https://github.com/meziantou/Meziantou.Analyzer/pull/1055)

**Full Changelog**:
<meziantou/Meziantou.Analyzer@3.0.21...3.0.22>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/rjmurillo/moq.analyzers).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My41OS4wIiwidXBkYXRlZEluVmVyIjoiNDMuNTkuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Owner

@rjmurillo rjmurillo left a comment

Choose a reason for hiding this comment

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

Review: Request Changes - Migration is solid, DeepSource failure + CancellationToken gap

What Works Well

  • Correct migration from System.CommandLine 2.0.0-beta1 to stable 2.0.3. SetAction with Func<ParseResult, CancellationToken, Task<int>> is the right API.
  • FrozenDictionary with StringComparer.OrdinalIgnoreCase for verbosity mapping is appropriate.
  • IsRequired = true on --baseline and --results options (fixed in follow-up commit).
  • Perfolizer correctly pinned to 0.6.1 to match BenchmarkDotNet 0.15.8 exact dependency [0.6.1].
  • Separation of CheckEltTraces from failOnRegression policy improves cohesion.
  • Visibility narrowing to internal is correct for a non-shipped tool.
  • All CodeRabbit review issues were addressed in follow-up commits.

Required Fixes

1. DeepSource CI failure (Blocking)
DeepSource C# analysis reports blocking issues. These must be investigated and resolved before merge. Check the run at the DeepSource dashboard for specific findings.

2. CancellationToken not threaded through operations (Medium)
CancellationToken is only checked once at entry (ThrowIfCancellationRequested). It is never passed to TryCompareBenchmarkDotNetResultsAsync or CheckEltTraces. Long-running operations remain non-cancellable. This is a pre-existing gap but worth addressing in this migration since the framework now provides the token natively.

Suggested Follow-ups

  • File an issue for CLI contract test coverage (parser errors, exit code branches).
  • The SetupLogging method with manual LoggerFactory creation is functional but unconventional. Not blocking.

rjmurillo and others added 3 commits March 14, 2026 11:18
Pass CancellationToken from CompareAsync through
BenchmarkDotNetDiffer, BenchmarkComparisonService, and
BenchmarkFileReader down to File.ReadAllTextAsync. Previously
the token was only checked once at entry via
ThrowIfCancellationRequested.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SingleOrDefault throws InvalidOperationException when multiple ETL
files exist in a directory. Use FirstOrDefault instead and log a
warning when multiple files are found.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix CS1573 and SA1611 warnings from missing XML documentation for
the cancellationToken parameters added in the previous commit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace .Any() with .Length for array emptiness checks. Use
Array.TrueForAll instead of negated LINQ .Any(). Replace double
dictionary lookup (ContainsKey + indexer) with single TryGetValue
loop. Fix string interpolation in log template to use structured
logging parameter. Remove stray blank line in csproj.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ontract

Add PerfDiff.Tests project with 20 tests covering:
- CancellationToken propagation through async call chain
- FirstOrDefault behavior with multiple ETL files
- DiffCommand CLI option parsing and validation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 14, 2026
Replace `await using ConfiguredAsyncDisposable asyncDisposal` with
explicit try/finally and DisposeAsync().ConfigureAwait(false) to
eliminate DeepSource CS-W1100 (unused variable) while preserving
async disposal with ConfigureAwait(false) for MA0004 compliance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
}
}

return matched.ToArray();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Function with many returns (count = 6): TryGetBdnResultsAsync [qlty:return-statements]

rjmurillo and others added 2 commits March 14, 2026 12:03
Resolve conflicts in BenchmarkFileReader.cs and Program.cs:
- Keep CancellationToken threading and Array.TrueForAll from #1043
- Add CancelledExitCode from main for distinct cancellation exit code
- Keep AddLogging builder pattern from #1043 (replaces IConsole)
- Keep try/finally serviceProvider disposal from static analysis fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@rjmurillo rjmurillo merged commit a2cbdcf into main Mar 15, 2026
39 of 42 checks passed
@rjmurillo rjmurillo deleted the fix/914-perfdiff-system-commandline branch March 15, 2026 01:23
@rjmurillo rjmurillo added this to the vNext milestone Mar 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug build dependencies Pull requests that update a dependency file releasable

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: rewrite PerfDiff for System.CommandLine 2.0.3 (IConsole removal)

3 participants