From dc648cb029f3ca67f3c6af41a2e65f5af2c9e8ea Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Sat, 7 Mar 2026 14:43:14 -0800 Subject: [PATCH 01/10] fix: improve CI/CD reliability and security hygiene (#990) - Add timeout-minutes to test (30), analyzer-load-test (20), perf (60) jobs in main.yml to prevent hung jobs from wasting runner minutes - Add /p:ContinuousIntegrationBuild=true to the build command in the setup-restore-build composite action for CI/local parity - Restrict linters.yml push trigger to main branch only, eliminating duplicate runs on feature branches already covered by PR triggers - Add permissions: pull-requests: read to semantic-pr-check.yml for least-privilege token scoping - Fix Windows-style path (.\) to Unix-style (./) in powershell.yml since the job runs on ubuntu-latest - Pin devskim.yml runner to ubuntu-24.04-arm, matching repo convention - Pin LangVersion to 13 in Compiler.props instead of default, ensuring reproducible builds regardless of installed SDK version - Delete stale pr-labeler-current-milestone.yml one-time backfill workflow that re-labels old PRs on a weekly cron Co-Authored-By: Claude Opus 4.6 --- .../actions/setup-restore-build/action.yml | 2 +- .github/workflows/devskim.yml | 2 +- .github/workflows/linters.yml | 1 + .github/workflows/main.yml | 3 + .github/workflows/powershell.yml | 2 +- .../pr-labeler-current-milestone.yml | 55 ------------------- .github/workflows/semantic-pr-check.yml | 3 + build/targets/compiler/Compiler.props | 3 +- 8 files changed, 12 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/pr-labeler-current-milestone.yml diff --git a/.github/actions/setup-restore-build/action.yml b/.github/actions/setup-restore-build/action.yml index 6ef2cde0c..393c4c1fd 100644 --- a/.github/actions/setup-restore-build/action.yml +++ b/.github/actions/setup-restore-build/action.yml @@ -29,4 +29,4 @@ runs: - name: Build shell: pwsh - run: dotnet build --no-restore --configuration Release /p:Deterministic=true /p:UseSharedCompilation=false /p:BuildInParallel=false /nodeReuse:false /bl:./artifacts/logs/release/build.release.binlog + run: dotnet build --no-restore --configuration Release /p:Deterministic=true /p:ContinuousIntegrationBuild=true /p:UseSharedCompilation=false /p:BuildInParallel=false /nodeReuse:false /bl:./artifacts/logs/release/build.release.binlog diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index e7e07db22..a139c6779 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -16,7 +16,7 @@ on: jobs: lint: name: DevSkim - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm permissions: actions: read contents: read diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index f577fd554..0d5bd325c 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -2,6 +2,7 @@ name: Lint on: push: + branches: [main] pull_request: permissions: {} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 57c504357..a9b540bd3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -175,6 +175,7 @@ jobs: # is generated with correct local paths. test: needs: [check-paths] + timeout-minutes: 30 strategy: fail-fast: false matrix: @@ -281,6 +282,7 @@ jobs: # 2. msbuild.exe - csc runs on .NET Framework (VS), tested on Windows x64 analyzer-load-test: needs: [check-paths, build] + timeout-minutes: 20 runs-on: ${{ matrix.runs-on }} strategy: fail-fast: false @@ -453,6 +455,7 @@ jobs: # Builds from source on Linux ARM to get consistent benchmark results. perf: needs: [check-paths, build, test] + timeout-minutes: 60 runs-on: ubuntu-24.04-arm env: diff --git a/.github/workflows/powershell.yml b/.github/workflows/powershell.yml index d95d645ba..e404ee5fd 100644 --- a/.github/workflows/powershell.yml +++ b/.github/workflows/powershell.yml @@ -36,7 +36,7 @@ jobs: with: # Check https://github.com/microsoft/action-psscriptanalyzer for more info about the options. # The below set up runs PSScriptAnalyzer to your entire repository and runs some basic security rules. - path: .\ + path: ./ recurse: true # Include your own basic security rules. Removing this option will run all the rules includeRule: '"PSAvoidGlobalAliases", "PSAvoidUsingConvertToSecureStringWithPlainText"' diff --git a/.github/workflows/pr-labeler-current-milestone.yml b/.github/workflows/pr-labeler-current-milestone.yml deleted file mode 100644 index f590abb5a..000000000 --- a/.github/workflows/pr-labeler-current-milestone.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: "Label Previous Pull Requests" -on: - workflow_dispatch: - schedule: - - cron: "0 1 * * 1" - -jobs: - labeler: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-24.04-arm - steps: - - # Label PRs - - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6 - with: - pr-number: | - 52 - 57 - 59 - 60 - 61 - 63 - 65 - 66 - 69 - 70 - 71 - 73 - 74 - 76 - 77 - 79 - 80 - 81 - 82 - 83 - 84 - 86 - 87 - 88 - 89 - 91 - 92 - 93 - 94 - 99 - 101 - 102 - 105 - 106 - 108 - 113 - 114 diff --git a/.github/workflows/semantic-pr-check.yml b/.github/workflows/semantic-pr-check.yml index 0d2939149..a8e4b6464 100644 --- a/.github/workflows/semantic-pr-check.yml +++ b/.github/workflows/semantic-pr-check.yml @@ -7,6 +7,9 @@ on: - edited - synchronize +permissions: + pull-requests: read + jobs: main: name: Validate PR title diff --git a/build/targets/compiler/Compiler.props b/build/targets/compiler/Compiler.props index c02cb4c1a..2c7b50003 100644 --- a/build/targets/compiler/Compiler.props +++ b/build/targets/compiler/Compiler.props @@ -1,7 +1,8 @@ enable - default + + 13 enable From 1e64e33c44c0fa6d077fac76d3c56ed1f03c3d4b Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Sat, 7 Mar 2026 14:57:22 -0800 Subject: [PATCH 02/10] fix: revert LangVersion to default to match .NET 10 SDK Pinning LangVersion to 13 caused 339 build errors because the .NET 10 SDK (10.0.103) ships with C# 14 as default. The Polyfill package generates C# 14 code, which fails to compile under C# 13. Reverting to "default" lets LangVersion track the SDK version in global.json. Co-Authored-By: Claude Opus 4.6 --- build/targets/compiler/Compiler.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/targets/compiler/Compiler.props b/build/targets/compiler/Compiler.props index 2c7b50003..e1b1dfdc4 100644 --- a/build/targets/compiler/Compiler.props +++ b/build/targets/compiler/Compiler.props @@ -1,8 +1,8 @@ enable - - 13 + + default enable From 3f638fceb9f7814c5425fb50d70413c21c76cbfc Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Sat, 7 Mar 2026 15:58:25 -0800 Subject: [PATCH 03/10] fix(ci): pin linters runner and scope semantic-pr permissions - Pin linters.yml runner to ubuntu-24.04-arm to match repo convention - Move pull-requests: read from top-level to job-level permissions in semantic-pr-check.yml, set top-level to empty object Co-Authored-By: Claude Opus 4.6 --- .github/workflows/linters.yml | 2 +- .github/workflows/semantic-pr-check.yml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 0d5bd325c..1da2cc8b3 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -10,7 +10,7 @@ permissions: {} jobs: build: name: Lint - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm permissions: contents: read diff --git a/.github/workflows/semantic-pr-check.yml b/.github/workflows/semantic-pr-check.yml index a8e4b6464..b3d49b258 100644 --- a/.github/workflows/semantic-pr-check.yml +++ b/.github/workflows/semantic-pr-check.yml @@ -7,13 +7,14 @@ on: - edited - synchronize -permissions: - pull-requests: read +permissions: {} jobs: main: name: Validate PR title runs-on: ubuntu-latest + permissions: + pull-requests: read steps: - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6 env: From 90146aeaff5e261e143f44f73456661851bb08ae Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Sun, 8 Mar 2026 11:18:45 -0700 Subject: [PATCH 04/10] fix: address review findings in CI/CD workflows Co-Authored-By: Claude Opus 4.6 --- .github/workflows/devskim.yml | 3 ++- .github/workflows/semantic-pr-check.yml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index a139c6779..471cb2380 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -16,7 +16,8 @@ on: jobs: lint: name: DevSkim - runs-on: ubuntu-24.04-arm + # DevSkim requires x64 runner + runs-on: ubuntu-24.04 permissions: actions: read contents: read diff --git a/.github/workflows/semantic-pr-check.yml b/.github/workflows/semantic-pr-check.yml index b3d49b258..bcaa401ba 100644 --- a/.github/workflows/semantic-pr-check.yml +++ b/.github/workflows/semantic-pr-check.yml @@ -12,7 +12,7 @@ permissions: {} jobs: main: name: Validate PR title - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm permissions: pull-requests: read steps: From a337a801d69b3b0e6e9a19958b6fd5dc16b2aee0 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Sun, 8 Mar 2026 12:35:28 -0700 Subject: [PATCH 05/10] fix(ci): use x86_64 runner for super-linter Docker image super-linter v8.5.0 only publishes amd64 Docker images. The ubuntu-24.04-arm runner cannot pull the image because no linux/arm64 manifest exists. Switch to ubuntu-24.04 (x86_64) for this job. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/linters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 1da2cc8b3..2f17e0e31 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -10,7 +10,7 @@ permissions: {} jobs: build: name: Lint - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-24.04 permissions: contents: read From 99c4ec7cba8d6b3d75184c1bb0d5b75ae26b3253 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Sun, 8 Mar 2026 14:55:12 -0700 Subject: [PATCH 06/10] fix(ci): revert linters runner to ubuntu-latest Super-linter does not support ARM64 natively (super-linter/super-linter#5070). The Docker image is x86_64 only. On ARM runners it fails or uses slow QEMU emulation. ubuntu-latest already resolves to Ubuntu 24.04 since January 2025, so there is no reproducibility concern. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/linters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 2f17e0e31..0d5bd325c 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -10,7 +10,7 @@ permissions: {} jobs: build: name: Lint - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest permissions: contents: read From 89beb09c9ccfee5ce17979be9a707243e4bdaf3e Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Sun, 8 Mar 2026 14:56:30 -0700 Subject: [PATCH 07/10] chore(ci): document ARM64 incompatibility for super-linter Co-Authored-By: Claude Opus 4.6 --- .github/workflows/linters.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 0d5bd325c..686723369 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -10,6 +10,8 @@ permissions: {} jobs: build: name: Lint + # super-linter requires x86_64; ARM64 is not supported upstream + # https://github.com/super-linter/super-linter/issues/5070 runs-on: ubuntu-latest permissions: From bf070f8282fed2a85ced0504d94aba80466c14d8 Mon Sep 17 00:00:00 2001 From: "rjmurillo[bot]" <250269933+rjmurillo-bot@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:11:45 -0700 Subject: [PATCH 08/10] refactor: eliminate DRY violations across analyzer pairs (#1044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Resolves #980. Extracts duplicated logic from three analyzer groups without changing behavior. - **Overridable member check**: `IsOverridableOrAllowedMockMember` extracted to `ISymbolExtensions`, replacing identical private methods (`IsPropertyOrMethod`, `IsOverridableOrTaskResultMember`, `IsAllowedMockMember`) in `SetupShouldBeUsedOnlyForOverridableMembersAnalyzer`, `SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer`, and `VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer` - **MockBehavior reporting**: Added `TryReportMockBehaviorDiagnostic` and `TryHandleMissingMockBehaviorParameter` overloads with `messageArgs` plus shared `GetMockedTypeName` to `MockBehaviorDiagnosticAnalyzerBase`, removing duplicate methods from `SetExplicitMockBehaviorAnalyzer` and `SetStrictMockBehaviorAnalyzer` - **Event argument validation**: Replaced `RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.ValidateArgumentTypesWithEventName` with existing `EventSyntaxExtensions.ValidateEventArgumentTypes` Net result: 122 lines added, 285 lines removed (163 lines reduced). ## Test plan - [x] All 2901 existing tests pass with zero failures - [x] Build succeeds with zero warnings - [x] No diagnostic IDs, messages, or categories changed - [x] No analyzer behavior changed, only structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) ## Summary by CodeRabbit * **Refactor** * Consolidated mock-behavior diagnostic reporting to a single path with support for formatted messages and overload-aware reporting. * Replaced many inlined helpers with shared reporting/handling flows for strict/explicit mock analyses. * Switched event-argument validation to context-based extension calls for consistency. * Centralized overridable/allowed-member checks into a single symbol extension used by setup, verify, and sequence analyzers. --------- Co-authored-by: Richard Murillo Co-authored-by: Claude Opus 4.6 --- .../MockBehaviorDiagnosticAnalyzerBase.cs | 143 +++++++++++++++++- ...umentsShouldMatchEventSignatureAnalyzer.cs | 39 +---- ...umentsShouldMatchEventSignatureAnalyzer.cs | 57 +------ .../SetExplicitMockBehaviorAnalyzer.cs | 55 +------ .../SetStrictMockBehaviorAnalyzer.cs | 108 ++++--------- ...BeUsedOnlyForOverridableMembersAnalyzer.cs | 74 +-------- ...BeUsedOnlyForOverridableMembersAnalyzer.cs | 47 +----- ...BeUsedOnlyForOverridableMembersAnalyzer.cs | 26 +--- src/Common/EventSyntaxExtensions.cs | 111 ++------------ src/Common/ISymbolExtensions.Moq.cs | 23 +++ src/Common/ISymbolExtensions.cs | 2 +- 11 files changed, 227 insertions(+), 458 deletions(-) diff --git a/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs b/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs index 52a6f8555..4bc4a337c 100644 --- a/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs +++ b/src/Analyzers/MockBehaviorDiagnosticAnalyzerBase.cs @@ -1,4 +1,4 @@ -using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Operations; namespace Moq.Analyzers; @@ -20,6 +20,147 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationStartAction(RegisterCompilationStartAction); } + /// + /// Extracts the mocked type name from the operation for use in diagnostic messages. + /// + /// The operation being analyzed. + /// The target method symbol. + /// The display name of the mocked type. + internal virtual string GetMockedTypeName(IOperation operation, IMethodSymbol target) + { + // For object creation (new Mock), get the type argument from the Mock type + if (operation is IObjectCreationOperation objectCreation + && objectCreation.Type is INamedTypeSymbol namedType + && namedType.TypeArguments.Length > 0) + { + return namedType.TypeArguments[0].ToDisplayString(); + } + + // For method invocation (Mock.Of), get the type argument from the method + if (operation is IInvocationOperation invocation && invocation.TargetMethod.TypeArguments.Length > 0) + { + return invocation.TargetMethod.TypeArguments[0].ToDisplayString(); + } + + // Try the containing type's type arguments (e.g. Mock.ctor) + if (target.ContainingType?.TypeArguments.Length > 0) + { + return target.ContainingType.TypeArguments[0].ToDisplayString(); + } + + return "T"; + } + + /// + /// Attempts to report a diagnostic for a MockBehavior parameter issue. + /// + /// The operation analysis context. + /// The method to check for MockBehavior parameter. + /// The known Moq symbols. + /// The diagnostic rule to report. + /// The type of edit for the code fix. + /// True if a diagnostic was reported; otherwise, false. + internal bool TryReportMockBehaviorDiagnostic( + OperationAnalysisContext context, + IMethodSymbol method, + MoqKnownSymbols knownSymbols, + DiagnosticDescriptor rule, + DiagnosticEditProperties.EditType editType) + { + if (!method.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) + { + return false; + } + + ImmutableDictionary properties = new DiagnosticEditProperties + { + TypeOfEdit = editType, + EditPosition = parameterMatch.Ordinal, + }.ToImmutableDictionary(); + + context.ReportDiagnostic(context.Operation.CreateDiagnostic(rule, properties)); + return true; + } + + /// + /// Attempts to report a diagnostic for a MockBehavior parameter issue, with the mocked type name. + /// + /// The operation analysis context. + /// The method to check for MockBehavior parameter. + /// The known Moq symbols. + /// The diagnostic rule to report. + /// The type of edit for the code fix. + /// The mocked type name to format into the diagnostic message. + /// True if a diagnostic was reported; otherwise, false. + internal bool TryReportMockBehaviorDiagnostic( + OperationAnalysisContext context, + IMethodSymbol method, + MoqKnownSymbols knownSymbols, + DiagnosticDescriptor rule, + DiagnosticEditProperties.EditType editType, + string mockedTypeName) + { + if (!method.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) + { + return false; + } + + ImmutableDictionary properties = new DiagnosticEditProperties + { + TypeOfEdit = editType, + EditPosition = parameterMatch.Ordinal, + }.ToImmutableDictionary(); + + context.ReportDiagnostic(context.Operation.CreateDiagnostic(rule, properties, mockedTypeName)); + return true; + } + + /// + /// Attempts to handle missing MockBehavior parameter by checking for overloads that accept it. + /// + /// The operation analysis context. + /// The MockBehavior parameter (should be null to trigger overload check). + /// The target method to check for overloads. + /// The known Moq symbols. + /// The diagnostic rule to report. + /// True if a diagnostic was reported; otherwise, false. + internal bool TryHandleMissingMockBehaviorParameter( + OperationAnalysisContext context, + IParameterSymbol? mockParameter, + IMethodSymbol target, + MoqKnownSymbols knownSymbols, + DiagnosticDescriptor rule) + { + // If the target method doesn't have a MockBehavior parameter, check if there's an overload that does + return mockParameter is null + && target.TryGetOverloadWithParameterOfType(knownSymbols.MockBehavior!, out IMethodSymbol? methodMatch, out _, cancellationToken: context.CancellationToken) + && TryReportMockBehaviorDiagnostic(context, methodMatch, knownSymbols, rule, DiagnosticEditProperties.EditType.Insert); + } + + /// + /// Attempts to handle missing MockBehavior parameter by checking for overloads that accept it, + /// with the mocked type name. + /// + /// The operation analysis context. + /// The MockBehavior parameter (should be null to trigger overload check). + /// The target method to check for overloads. + /// The known Moq symbols. + /// The diagnostic rule to report. + /// The mocked type name to format into the diagnostic message. + /// True if a diagnostic was reported; otherwise, false. + internal bool TryHandleMissingMockBehaviorParameter( + OperationAnalysisContext context, + IParameterSymbol? mockParameter, + IMethodSymbol target, + MoqKnownSymbols knownSymbols, + DiagnosticDescriptor rule, + string mockedTypeName) + { + return mockParameter is null + && target.TryGetOverloadWithParameterOfType(knownSymbols.MockBehavior!, out IMethodSymbol? methodMatch, out _, cancellationToken: context.CancellationToken) + && TryReportMockBehaviorDiagnostic(context, methodMatch, knownSymbols, rule, DiagnosticEditProperties.EditType.Insert, mockedTypeName); + } + private protected abstract void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols); private void RegisterCompilationStartAction(CompilationStartAnalysisContext context) diff --git a/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs b/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs index 9a70aaad8..8024008c9 100644 --- a/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs +++ b/src/Analyzers/RaiseEventArgumentsShouldMatchEventSignatureAnalyzer.cs @@ -82,7 +82,7 @@ private static void Analyze(SyntaxNodeAnalysisContext context, MoqKnownSymbols k context.SemanticModel.TryGetEventNameFromLambdaSelector(eventSelector, out eventName); } - ValidateArgumentTypesWithEventName(context, eventArguments, expectedParameterTypes, invocation, eventName ?? "event"); + context.ValidateEventArgumentTypes(eventArguments, expectedParameterTypes, invocation, Rule, eventName ?? "event"); } private static bool TryGetRaiseMethodArguments( @@ -105,43 +105,6 @@ private static bool TryGetRaiseMethodArguments( knownSymbols); } - private static void ValidateArgumentTypesWithEventName(SyntaxNodeAnalysisContext context, ArgumentSyntax[] eventArguments, ITypeSymbol[] expectedParameterTypes, InvocationExpressionSyntax invocation, string eventName) - { - if (eventArguments.Length != expectedParameterTypes.Length) - { - Location location; - if (eventArguments.Length < expectedParameterTypes.Length) - { - // Too few arguments: report on the invocation - location = invocation.GetLocation(); - } - else - { - // Too many arguments: report on the first extra argument - location = eventArguments[expectedParameterTypes.Length].GetLocation(); - } - - Diagnostic diagnostic = location.CreateDiagnostic(Rule, eventName); - context.ReportDiagnostic(diagnostic); - return; - } - - // Check each argument type matches the expected parameter type - for (int i = 0; i < eventArguments.Length; i++) - { - TypeInfo argumentTypeInfo = context.SemanticModel.GetTypeInfo(eventArguments[i].Expression, context.CancellationToken); - ITypeSymbol? argumentType = argumentTypeInfo.Type; - ITypeSymbol expectedType = expectedParameterTypes[i]; - - if (argumentType != null && !context.SemanticModel.HasConversion(argumentType, expectedType)) - { - // Report on the specific argument with the wrong type - Diagnostic diagnostic = eventArguments[i].GetLocation().CreateDiagnostic(Rule, eventName); - context.ReportDiagnostic(diagnostic); - } - } - } - private static bool IsRaiseMethodCall(SemanticModel semanticModel, InvocationExpressionSyntax invocation, MoqKnownSymbols knownSymbols) { SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(invocation); diff --git a/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs b/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs index 09d6ad2f0..738bbd8b0 100644 --- a/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs +++ b/src/Analyzers/RaisesEventArgumentsShouldMatchEventSignatureAnalyzer.cs @@ -77,10 +77,15 @@ private static void Analyze(SyntaxNodeAnalysisContext context, MoqKnownSymbols k return; } - // Extract event name from the lambda selector (first argument) - string eventName = TryGetEventNameFromLambdaSelector(invocation, context.SemanticModel) ?? "event"; + // Extract event name from the first argument (event selector lambda) + string? eventName = null; + if (invocation.ArgumentList.Arguments.Count > 0) + { + ExpressionSyntax eventSelector = invocation.ArgumentList.Arguments[0].Expression; + context.SemanticModel.TryGetEventNameFromLambdaSelector(eventSelector, out eventName); + } - EventSyntaxExtensions.ValidateEventArgumentTypes(context, eventArguments, expectedParameterTypes, invocation, Rule, eventName); + context.ValidateEventArgumentTypes(eventArguments, expectedParameterTypes, invocation, Rule, eventName ?? "event"); } private static bool TryGetRaisesMethodArguments(InvocationExpressionSyntax invocation, SemanticModel semanticModel, out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes) @@ -96,50 +101,4 @@ private static bool TryGetRaisesMethodArguments(InvocationExpressionSyntax invoc return (success, eventType); }); } - - /// - /// Extracts the event name from a lambda selector of the form: x => x.EventName += null. - /// - /// The method invocation containing the lambda selector. - /// The semantic model. - /// The event name if found; otherwise null. - private static string? TryGetEventNameFromLambdaSelector(InvocationExpressionSyntax invocation, SemanticModel semanticModel) - { - // Get the first argument which should be the lambda selector - SeparatedSyntaxList arguments = invocation.ArgumentList.Arguments; - if (arguments.Count < 1) - { - return null; - } - - ExpressionSyntax eventSelector = arguments[0].Expression; - - // The event selector should be a lambda like: p => p.EventName += null - if (eventSelector is not LambdaExpressionSyntax lambda) - { - return null; - } - - // The body should be an assignment expression with += operator - if (lambda.Body is not AssignmentExpressionSyntax assignment || - !assignment.OperatorToken.IsKind(SyntaxKind.PlusEqualsToken)) - { - return null; - } - - // The left side should be a member access to the event - if (assignment.Left is not MemberAccessExpressionSyntax memberAccess) - { - return null; - } - - // Get the symbol for the event - SymbolInfo symbolInfo = semanticModel.GetSymbolInfo(memberAccess); - if (symbolInfo.Symbol is IEventSymbol eventSymbol) - { - return eventSymbol.ToDisplayString(); - } - - return null; - } } diff --git a/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs b/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs index 6d5071479..06435c769 100644 --- a/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs +++ b/src/Analyzers/SetExplicitMockBehaviorAnalyzer.cs @@ -31,16 +31,14 @@ public class SetExplicitMockBehaviorAnalyzer : MockBehaviorDiagnosticAnalyzerBas private protected override void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols) { // Extract the type name for the diagnostic message - string typeName = GetMockedTypeName(context, target); + string typeName = GetMockedTypeName(context.Operation, target); // Check if the target method has a parameter of type MockBehavior IParameterSymbol? mockParameter = target.Parameters.DefaultIfNotSingle(parameter => parameter.Type.IsInstanceOf(knownSymbols.MockBehavior)); // If the target method doesn't have a MockBehavior parameter, check if there's an overload that does - if (mockParameter is null && target.TryGetOverloadWithParameterOfType(knownSymbols.MockBehavior!, out IMethodSymbol? methodMatch, out _, cancellationToken: context.CancellationToken)) + if (TryHandleMissingMockBehaviorParameter(context, mockParameter, target, knownSymbols, Rule, typeName)) { - // Using a method that doesn't accept a MockBehavior parameter, however there's an overload that does - ReportDiagnosticWithTypeName(context, methodMatch, typeName, knownSymbols, DiagnosticEditProperties.EditType.Insert); return; } @@ -49,7 +47,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM // Is the behavior set via a default value? if (mockArgument?.ArgumentKind == ArgumentKind.DefaultValue && mockArgument.Value.WalkDownConversion().ConstantValue.Value == knownSymbols.MockBehaviorDefault?.ConstantValue) { - ReportDiagnosticWithTypeName(context, target, typeName, knownSymbols, DiagnosticEditProperties.EditType.Insert); + TryReportMockBehaviorDiagnostic(context, target, knownSymbols, Rule, DiagnosticEditProperties.EditType.Insert, typeName); } // NOTE: This logic can't handle indirection (e.g. var x = MockBehavior.Default; new Mock(x);). We can't use the constant value either, @@ -58,52 +56,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM // The operation specifies a MockBehavior; is it MockBehavior.Default? if (mockArgument?.DescendantsAndSelf().OfType().Any(argument => argument.Member.IsInstanceOf(knownSymbols.MockBehaviorDefault)) == true) { - ReportDiagnosticWithTypeName(context, target, typeName, knownSymbols, DiagnosticEditProperties.EditType.Replace); + TryReportMockBehaviorDiagnostic(context, target, knownSymbols, Rule, DiagnosticEditProperties.EditType.Replace, typeName); } } - - private static string GetMockedTypeName(OperationAnalysisContext context, IMethodSymbol target) - { - // For object creation (new Mock), get the type argument from the Mock type - if (context.Operation is IObjectCreationOperation objectCreation && objectCreation.Type is INamedTypeSymbol namedType && namedType.TypeArguments.Length > 0) - { - return namedType.TypeArguments[0].ToDisplayString(); - } - - // For method invocation (MockRepository.Of), get the type argument from the method - if (context.Operation is IInvocationOperation invocation && invocation.TargetMethod.TypeArguments.Length > 0) - { - return invocation.TargetMethod.TypeArguments[0].ToDisplayString(); - } - - // If we can't determine the type, try to get it from the target method if it's generic - if (target.ContainingType?.TypeArguments.Length > 0) - { - return target.ContainingType.TypeArguments[0].ToDisplayString(); - } - - // Fallback to a generic name - return "T"; - } - - private void ReportDiagnosticWithTypeName( - OperationAnalysisContext context, - IMethodSymbol method, - string typeName, - MoqKnownSymbols knownSymbols, - DiagnosticEditProperties.EditType editType) - { - if (!method.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) - { - return; - } - - ImmutableDictionary properties = new DiagnosticEditProperties - { - TypeOfEdit = editType, - EditPosition = parameterMatch.Ordinal, - }.ToImmutableDictionary(); - - context.ReportDiagnostic(context.Operation.CreateDiagnostic(Rule, properties, typeName)); - } } diff --git a/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs b/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs index b5e6d26e1..af872c294 100644 --- a/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs +++ b/src/Analyzers/SetStrictMockBehaviorAnalyzer.cs @@ -26,6 +26,30 @@ public class SetStrictMockBehaviorAnalyzer : MockBehaviorDiagnosticAnalyzerBase /// public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(Rule); + /// + /// + /// The original strict analyzer resolved the mocked type name from 's + /// type arguments (not the invocation's) and fell back to "Unknown" instead of "T". + /// + internal override string GetMockedTypeName(IOperation operation, IMethodSymbol target) + { + // For object creation (new Mock), get the type argument from the Mock type + if (operation is IObjectCreationOperation objectCreation + && objectCreation.Type is INamedTypeSymbol namedType + && namedType.TypeArguments.Length > 0) + { + return namedType.TypeArguments[0].ToDisplayString(); + } + + // For any other case, use the target method's type arguments + if (target.TypeArguments.Length > 0) + { + return target.TypeArguments[0].ToDisplayString(); + } + + return "Unknown"; + } + /// [SuppressMessage("Design", "MA0051:Method is too long", Justification = "Should be fixed. Ignoring for now to avoid additional churn as part of larger refactor.")] private protected override void AnalyzeCore(OperationAnalysisContext context, IMethodSymbol target, ImmutableArray arguments, MoqKnownSymbols knownSymbols) @@ -37,7 +61,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM IParameterSymbol? mockParameter = target.Parameters.DefaultIfNotSingle(parameter => parameter.Type.IsInstanceOf(knownSymbols.MockBehavior)); // If the target method doesn't have a MockBehavior parameter, check if there's an overload that does - if (TryHandleMissingMockBehaviorParameter(context, mockParameter, target, knownSymbols, mockedTypeName)) + if (TryHandleMissingMockBehaviorParameter(context, mockParameter, target, knownSymbols, Rule, mockedTypeName)) { // Using a method that doesn't accept a MockBehavior parameter, however there's an overload that does return; @@ -47,7 +71,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM // Is the behavior set via a default value? if (mockArgument?.ArgumentKind == ArgumentKind.DefaultValue && mockArgument.Value.WalkDownConversion().ConstantValue.Value == knownSymbols.MockBehaviorDefault?.ConstantValue - && TryReportStrictMockBehaviorDiagnostic(context, target, knownSymbols, mockedTypeName, DiagnosticEditProperties.EditType.Insert)) + && TryReportMockBehaviorDiagnostic(context, target, knownSymbols, Rule, DiagnosticEditProperties.EditType.Insert, mockedTypeName)) { return; } @@ -58,85 +82,7 @@ private protected override void AnalyzeCore(OperationAnalysisContext context, IM if (mockArgument?.Value.WalkDownConversion().ConstantValue.Value != knownSymbols.MockBehaviorStrict?.ConstantValue && mockArgument?.DescendantsAndSelf().OfType().Any(argument => argument.Member.IsInstanceOf(knownSymbols.MockBehaviorStrict)) != true) { - TryReportStrictMockBehaviorDiagnostic(context, target, knownSymbols, mockedTypeName, DiagnosticEditProperties.EditType.Replace); + TryReportMockBehaviorDiagnostic(context, target, knownSymbols, Rule, DiagnosticEditProperties.EditType.Replace, mockedTypeName); } } - - /// - /// Extracts the mocked type name from the operation. - /// - /// The operation being analyzed. - /// The target method symbol. - /// The name of the mocked type, or "Unknown" if it cannot be determined. - private static string GetMockedTypeName(IOperation operation, IMethodSymbol target) - { - // For object creation like new Mock() - if (operation is IObjectCreationOperation objectCreation - && objectCreation.Type is INamedTypeSymbol namedType - && namedType.TypeArguments.Length > 0) - { - return namedType.TypeArguments[0].ToDisplayString(); - } - - // For method invocation like Mock.Of() - if (operation is IInvocationOperation && target.TypeArguments.Length > 0) - { - return target.TypeArguments[0].ToDisplayString(); - } - - return "Unknown"; - } - - /// - /// Attempts to report a strict mock behavior diagnostic with the mocked type name. - /// - /// The operation analysis context. - /// The method to check for MockBehavior parameter. - /// The known Moq symbols. - /// The name of the mocked type. - /// The type of edit for the code fix. - /// True if a diagnostic was reported; otherwise, false. - private bool TryReportStrictMockBehaviorDiagnostic( - OperationAnalysisContext context, - IMethodSymbol method, - MoqKnownSymbols knownSymbols, - string mockedTypeName, - DiagnosticEditProperties.EditType editType) - { - if (!method.TryGetParameterOfType(knownSymbols.MockBehavior!, out IParameterSymbol? parameterMatch, cancellationToken: context.CancellationToken)) - { - return false; - } - - ImmutableDictionary properties = new DiagnosticEditProperties - { - TypeOfEdit = editType, - EditPosition = parameterMatch.Ordinal, - }.ToImmutableDictionary(); - - context.ReportDiagnostic(context.Operation.CreateDiagnostic(Rule, properties, mockedTypeName)); - return true; - } - - /// - /// Attempts to handle missing MockBehavior parameter by checking for overloads that accept it. - /// - /// The operation analysis context. - /// The MockBehavior parameter (should be null to trigger overload check). - /// The target method to check for overloads. - /// The known Moq symbols. - /// The name of the mocked type. - /// True if a diagnostic was reported; otherwise, false. - private bool TryHandleMissingMockBehaviorParameter( - OperationAnalysisContext context, - IParameterSymbol? mockParameter, - IMethodSymbol target, - MoqKnownSymbols knownSymbols, - string mockedTypeName) - { - // If the target method doesn't have a MockBehavior parameter, check if there's an overload that does - return mockParameter is null - && target.TryGetOverloadWithParameterOfType(knownSymbols.MockBehavior!, out IMethodSymbol? methodMatch, out _, cancellationToken: context.CancellationToken) - && TryReportStrictMockBehaviorDiagnostic(context, methodMatch, knownSymbols, mockedTypeName, DiagnosticEditProperties.EditType.Insert); - } } diff --git a/src/Analyzers/SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Analyzers/SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index 085bd44e2..f7de28c56 100644 --- a/src/Analyzers/SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Analyzers/SetupSequenceShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.Operations; namespace Moq.Analyzers; @@ -48,7 +47,6 @@ private static void RegisterCompilationStartAction(CompilationStartAnalysisConte OperationKind.Invocation); } - [SuppressMessage("Design", "MA0051:Method is too long", Justification = "Should be fixed. Ignoring for now to avoid additional churn as part of larger refactor.")] private static void AnalyzeInvocation(OperationAnalysisContext context, MoqKnownSymbols knownSymbols) { if (context.Operation is not IInvocationOperation invocationOperation) @@ -56,86 +54,24 @@ private static void AnalyzeInvocation(OperationAnalysisContext context, MoqKnown return; } - IMethodSymbol targetMethod = invocationOperation.TargetMethod; - - // 1. Check if the invoked method is a Moq SetupSequence method - if (!targetMethod.IsMoqSetupSequenceMethod(knownSymbols)) + if (!invocationOperation.TargetMethod.IsMoqSetupSequenceMethod(knownSymbols)) { return; } - // 2. Attempt to locate the member reference from the SetupSequence expression argument. - // Typically, Moq SetupSequence calls have a single lambda argument like x => x.SomeMember. - // We'll extract that member reference or invocation to see whether it is overridable. ISymbol? mockedMemberSymbol = MoqVerificationHelpers.TryGetMockedMemberSymbol(invocationOperation); - if (mockedMemberSymbol == null) - { - return; - } - - // 3. Skip if the symbol is part of an interface, those are always "overridable". - if (mockedMemberSymbol.ContainingType?.TypeKind == TypeKind.Interface) + if (mockedMemberSymbol is null + || mockedMemberSymbol.ContainingType?.TypeKind == TypeKind.Interface + || mockedMemberSymbol.IsOverridableOrAllowedMockMember(knownSymbols)) { return; } - // 4. Check if symbol is a property or method, and if it is overridable or is returning a Task (which Moq allows). - if (IsOverridableOrTaskResultMember(mockedMemberSymbol, knownSymbols)) - { - return; - } - - // 5. If we reach here, the member is neither overridable nor allowed by Moq - // So we report the diagnostic. - // - // Try to get the specific member syntax for a more precise diagnostic location + // Use the specific member syntax for a more precise diagnostic location when available SyntaxNode? memberSyntax = MoqVerificationHelpers.TryGetMockedMemberSyntax(invocationOperation); Location diagnosticLocation = memberSyntax?.GetLocation() ?? invocationOperation.Syntax.GetLocation(); Diagnostic diagnostic = diagnosticLocation.CreateDiagnostic(Rule, mockedMemberSymbol.ToDisplayString()); context.ReportDiagnostic(diagnostic); } - - /// - /// Determines whether a member symbol is either overridable or represents a / Result property - /// that Moq allows to be setup even if the underlying property is not overridable. - /// - /// The mocked member symbol. - /// A instance for resolving well-known types. - /// - /// Returns when the member is overridable or is a / Result property; otherwise . - /// - private static bool IsOverridableOrTaskResultMember(ISymbol mockedMemberSymbol, MoqKnownSymbols knownSymbols) - { - switch (mockedMemberSymbol) - { - case IPropertySymbol propertySymbol: - // Check if the property is Task.Result and skip diagnostic if it is - if (propertySymbol.IsTaskOrValueResultProperty(knownSymbols)) - { - return true; - } - - if (propertySymbol.IsOverridable()) - { - return true; - } - - break; - - case IMethodSymbol methodSymbol: - if (methodSymbol.IsOverridable()) - { - return true; - } - - break; - - default: - // If it's not a property or method, it's not overridable - return false; - } - - return false; - } } diff --git a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index 9e33955df..8c75f4920 100644 --- a/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Analyzers/SetupShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -90,7 +90,7 @@ private static bool IsSetupOnNonOverridableMember( ISymbol? candidate = MoqVerificationHelpers.TryGetMockedMemberSymbol(invocationOperation); if (candidate is null || candidate.ContainingType?.TypeKind == TypeKind.Interface - || IsPropertyOrMethod(candidate, knownSymbols)) + || candidate.IsOverridableOrAllowedMockMember(knownSymbols)) { return false; } @@ -98,49 +98,4 @@ private static bool IsSetupOnNonOverridableMember( mockedMemberSymbol = candidate; return true; } - - /// - /// Determines whether a property or method is either - /// , , , or - /// - OR - - /// if the is overridable. - /// - /// The mocked member symbol. - /// A instance for resolving well-known types. - /// - /// Returns when the diagnostic should not be triggered; otherwise . - /// - private static bool IsPropertyOrMethod(ISymbol mockedMemberSymbol, MoqKnownSymbols knownSymbols) - { - switch (mockedMemberSymbol) - { - case IPropertySymbol propertySymbol: - // Check if the property is Task.Result and skip diagnostic if it is - if (propertySymbol.IsTaskOrValueResultProperty(knownSymbols)) - { - return true; - } - - if (propertySymbol.IsOverridable()) - { - return true; - } - - break; - - case IMethodSymbol methodSymbol: - if (methodSymbol.IsOverridable()) - { - return true; - } - - break; - - default: - // If it's not a property or method, it's not overridable - return false; - } - - return false; - } } diff --git a/src/Analyzers/VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs b/src/Analyzers/VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs index 2a4459589..5d42606a3 100644 --- a/src/Analyzers/VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs +++ b/src/Analyzers/VerifyShouldBeUsedOnlyForOverridableMembersAnalyzer.cs @@ -112,7 +112,7 @@ private static bool IsMemberAllowedForVerification(ISymbol mockedMemberSymbol, I return mockedMemberSymbol is IPropertySymbol propertySymbol && propertySymbol.IsOverridable(); } - return IsAllowedMockMember(mockedMemberSymbol, knownSymbols); + return mockedMemberSymbol.IsOverridableOrAllowedMockMember(knownSymbols); } private static void ReportDiagnostic(OperationAnalysisContext context, IInvocationOperation invocationOperation, ISymbol mockedMemberSymbol) @@ -120,28 +120,4 @@ private static void ReportDiagnostic(OperationAnalysisContext context, IInvocati Diagnostic diagnostic = invocationOperation.Syntax.CreateDiagnostic(Rule, mockedMemberSymbol.Name); context.ReportDiagnostic(diagnostic); } - - /// - /// Determines whether a member can be mocked. - /// - /// The mocked member symbol. - /// The known symbols. - /// - /// Returns when the diagnostic should not be triggered; otherwise . - /// - private static bool IsAllowedMockMember(ISymbol mockedMemberSymbol, MoqKnownSymbols knownSymbols) - { - switch (mockedMemberSymbol) - { - case IPropertySymbol propertySymbol: - return propertySymbol.IsOverridable() || propertySymbol.IsTaskOrValueResultProperty(knownSymbols); - - case IMethodSymbol methodSymbol: - return methodSymbol.IsOverridable(); - - default: - // If it's not a property or method, it can't be mocked. This includes fields and events. - return false; - } - } } diff --git a/src/Common/EventSyntaxExtensions.cs b/src/Common/EventSyntaxExtensions.cs index 5162b8ec5..d7d7df516 100644 --- a/src/Common/EventSyntaxExtensions.cs +++ b/src/Common/EventSyntaxExtensions.cs @@ -5,24 +5,6 @@ namespace Moq.Analyzers.Common; /// internal static class EventSyntaxExtensions { - /// - /// Validates that event arguments match the expected parameter types. - /// - /// The analysis context. - /// The event arguments to validate. - /// The expected parameter types. - /// The invocation expression for error reporting. - /// The diagnostic rule to report. - internal static void ValidateEventArgumentTypes( - SyntaxNodeAnalysisContext context, - ArgumentSyntax[] eventArguments, - ITypeSymbol[] expectedParameterTypes, - InvocationExpressionSyntax invocation, - DiagnosticDescriptor rule) - { - ValidateEventArgumentTypes(context, eventArguments, expectedParameterTypes, invocation, rule, null); - } - /// /// Validates that event arguments match the expected parameter types. /// @@ -33,12 +15,12 @@ internal static void ValidateEventArgumentTypes( /// The diagnostic rule to report. /// The event name to include in diagnostic messages. internal static void ValidateEventArgumentTypes( - SyntaxNodeAnalysisContext context, + this SyntaxNodeAnalysisContext context, ArgumentSyntax[] eventArguments, ITypeSymbol[] expectedParameterTypes, InvocationExpressionSyntax invocation, DiagnosticDescriptor rule, - string? eventName) + string? eventName = null) { if (eventArguments.Length != expectedParameterTypes.Length) { @@ -79,44 +61,24 @@ internal static void ValidateEventArgumentTypes( } } - /// - /// Gets the parameter types for a given event delegate type. - /// - /// The event delegate type. - /// An array of parameter types expected by the event delegate. - internal static ITypeSymbol[] GetEventParameterTypes(ITypeSymbol eventType) - { - return GetEventParameterTypesInternal(eventType, null); - } - /// /// Gets the parameter types for a given event delegate type. /// /// The event delegate type. /// Known symbols for type checking. /// An array of parameter types expected by the event delegate. - internal static ITypeSymbol[] GetEventParameterTypes(ITypeSymbol eventType, KnownSymbols knownSymbols) + internal static ITypeSymbol[] GetEventParameterTypes(ITypeSymbol eventType, KnownSymbols? knownSymbols = null) { - return GetEventParameterTypesInternal(eventType, knownSymbols); - } + if (eventType is not INamedTypeSymbol namedType) + { + return []; + } - /// - /// Extracts arguments from an event method invocation. - /// - /// The method invocation. - /// The semantic model. - /// The extracted event arguments. - /// The expected parameter types. - /// Function to extract event type from the event selector. - /// if arguments were successfully extracted; otherwise, . - internal static bool TryGetEventMethodArguments( - InvocationExpressionSyntax invocation, - SemanticModel semanticModel, - out ArgumentSyntax[] eventArguments, - out ITypeSymbol[] expectedParameterTypes, - Func eventTypeExtractor) - { - return TryGetEventMethodArgumentsInternal(invocation, semanticModel, out eventArguments, out expectedParameterTypes, eventTypeExtractor, null); + ITypeSymbol[]? parameterTypes = TryGetActionDelegateParameters(namedType, knownSymbols) ?? + TryGetEventHandlerDelegateParameters(namedType, knownSymbols) ?? + TryGetCustomDelegateParameters(namedType); + + return parameterTypes ?? []; } /// @@ -135,32 +97,18 @@ internal static bool TryGetEventMethodArguments( out ArgumentSyntax[] eventArguments, out ITypeSymbol[] expectedParameterTypes, Func eventTypeExtractor, - KnownSymbols knownSymbols) - { - return TryGetEventMethodArgumentsInternal(invocation, semanticModel, out eventArguments, out expectedParameterTypes, eventTypeExtractor, knownSymbols); - } - - private static bool TryGetEventMethodArgumentsInternal( - InvocationExpressionSyntax invocation, - SemanticModel semanticModel, - out ArgumentSyntax[] eventArguments, - out ITypeSymbol[] expectedParameterTypes, - Func eventTypeExtractor, - KnownSymbols? knownSymbols) + KnownSymbols? knownSymbols = null) { eventArguments = []; expectedParameterTypes = []; - // Get the arguments to the method SeparatedSyntaxList arguments = invocation.ArgumentList.Arguments; - // Method should have at least 1 argument (the event selector) if (arguments.Count < 1) { return false; } - // First argument should be a lambda that selects the event ExpressionSyntax eventSelector = arguments[0].Expression; (bool success, ITypeSymbol? eventType) = eventTypeExtractor(semanticModel, eventSelector); if (!success || eventType == null) @@ -168,10 +116,8 @@ private static bool TryGetEventMethodArgumentsInternal( return false; } - // Get expected parameter types from the event delegate - expectedParameterTypes = knownSymbols != null ? GetEventParameterTypes(eventType, knownSymbols) : GetEventParameterTypes(eventType); + expectedParameterTypes = GetEventParameterTypes(eventType, knownSymbols); - // The remaining arguments should match the event parameter types if (arguments.Count <= 1) { eventArguments = []; @@ -188,35 +134,6 @@ private static bool TryGetEventMethodArgumentsInternal( return true; } - /// - /// Gets the parameter types for a given event delegate type. - /// This method handles various delegate types including Action delegates, EventHandler delegates, - /// and custom delegates by analyzing their structure and extracting parameter information. - /// - /// The event delegate type to analyze. - /// Known symbols for enhanced type checking and recognition. - /// - /// An array of parameter types expected by the event delegate: - /// - For Action delegates: Returns all generic type arguments - /// - For EventHandler<T> delegates: Returns the single generic argument T - /// - For custom delegates: Returns parameters from the Invoke method - /// - For non-delegate types: Returns empty array. - /// - private static ITypeSymbol[] GetEventParameterTypesInternal(ITypeSymbol eventType, KnownSymbols? knownSymbols) - { - if (eventType is not INamedTypeSymbol namedType) - { - return []; - } - - // Try different delegate type handlers in order of specificity - ITypeSymbol[]? parameterTypes = TryGetActionDelegateParameters(namedType, knownSymbols) ?? - TryGetEventHandlerDelegateParameters(namedType, knownSymbols) ?? - TryGetCustomDelegateParameters(namedType); - - return parameterTypes ?? []; - } - /// /// Attempts to get parameter types from Action delegate types. /// diff --git a/src/Common/ISymbolExtensions.Moq.cs b/src/Common/ISymbolExtensions.Moq.cs index 788a36567..d61764e1e 100644 --- a/src/Common/ISymbolExtensions.Moq.cs +++ b/src/Common/ISymbolExtensions.Moq.cs @@ -145,6 +145,29 @@ internal static bool IsMoqCallbackMethod(this ISymbol symbol, MoqKnownSymbols kn symbol.IsInstanceOf(knownSymbols.ICallback2Callback); } + /// + /// Determines whether a member symbol is either overridable or represents a + /// / Result property + /// that Moq allows to be set up even if the underlying property is not overridable. + /// + /// The mocked member symbol. + /// A instance for resolving well-known types. + /// + /// when the member is overridable or is a Task/ValueTask Result property; + /// otherwise . + /// + internal static bool IsOverridableOrAllowedMockMember(this ISymbol mockedMemberSymbol, MoqKnownSymbols knownSymbols) + { + return mockedMemberSymbol switch + { + IPropertySymbol propertySymbol => + propertySymbol.IsOverridable() || propertySymbol.IsTaskOrValueResultProperty(knownSymbols), + IMethodSymbol methodSymbol => + methodSymbol.IsOverridable(), + _ => false, + }; + } + /// /// Determines whether a symbol is a Moq Raises method. /// diff --git a/src/Common/ISymbolExtensions.cs b/src/Common/ISymbolExtensions.cs index da0a5db72..5b681d1ef 100644 --- a/src/Common/ISymbolExtensions.cs +++ b/src/Common/ISymbolExtensions.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Moq.Analyzers.Common; From 6d8041be2e7e133b4a6345a2904e450213c6ab27 Mon Sep 17 00:00:00 2001 From: Richard Murillo Date: Sun, 8 Mar 2026 16:22:55 -0700 Subject: [PATCH 09/10] fix: update package snapshot files to include THIRD-PARTY-NOTICES.TXT Co-Authored-By: Claude Opus 4.6 --- .../PackageTests.Baseline#contents.verified.txt | 1 + .../PackageTests.Baseline_main#contents.verified.txt | 1 + .../PackageTests.Baseline_symbols#contents.verified.txt | 1 + 3 files changed, 3 insertions(+) diff --git a/tests/Moq.Analyzers.Test/PackageTests.Baseline#contents.verified.txt b/tests/Moq.Analyzers.Test/PackageTests.Baseline#contents.verified.txt index 128f1e37e..f9a8e7f2c 100644 --- a/tests/Moq.Analyzers.Test/PackageTests.Baseline#contents.verified.txt +++ b/tests/Moq.Analyzers.Test/PackageTests.Baseline#contents.verified.txt @@ -1,6 +1,7 @@ / |-- Moq.Analyzers.nuspec |-- README.md +|-- THIRD-PARTY-NOTICES.TXT |-- analyzers | |-- dotnet | | |-- cs diff --git a/tests/Moq.Analyzers.Test/PackageTests.Baseline_main#contents.verified.txt b/tests/Moq.Analyzers.Test/PackageTests.Baseline_main#contents.verified.txt index 128f1e37e..f9a8e7f2c 100644 --- a/tests/Moq.Analyzers.Test/PackageTests.Baseline_main#contents.verified.txt +++ b/tests/Moq.Analyzers.Test/PackageTests.Baseline_main#contents.verified.txt @@ -1,6 +1,7 @@ / |-- Moq.Analyzers.nuspec |-- README.md +|-- THIRD-PARTY-NOTICES.TXT |-- analyzers | |-- dotnet | | |-- cs diff --git a/tests/Moq.Analyzers.Test/PackageTests.Baseline_symbols#contents.verified.txt b/tests/Moq.Analyzers.Test/PackageTests.Baseline_symbols#contents.verified.txt index 128f1e37e..f9a8e7f2c 100644 --- a/tests/Moq.Analyzers.Test/PackageTests.Baseline_symbols#contents.verified.txt +++ b/tests/Moq.Analyzers.Test/PackageTests.Baseline_symbols#contents.verified.txt @@ -1,6 +1,7 @@ / |-- Moq.Analyzers.nuspec |-- README.md +|-- THIRD-PARTY-NOTICES.TXT |-- analyzers | |-- dotnet | | |-- cs From 47265f8dd571162a315d556106df8fc2e9612fb7 Mon Sep 17 00:00:00 2001 From: Richard Murillo <6811113+rjmurillo@users.noreply.github.com> Date: Sun, 8 Mar 2026 16:40:28 -0700 Subject: [PATCH 10/10] Update runner version to ubuntu-latest --- .github/workflows/devskim.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/devskim.yml b/.github/workflows/devskim.yml index 471cb2380..7d99773d4 100644 --- a/.github/workflows/devskim.yml +++ b/.github/workflows/devskim.yml @@ -17,7 +17,7 @@ jobs: lint: name: DevSkim # DevSkim requires x64 runner - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest permissions: actions: read contents: read