Skip to content

Preserve override accessibility in generated mocks#5984

Merged
thomhurst merged 1 commit into
mainfrom
fix/mock-access-modifiers
May 21, 2026
Merged

Preserve override accessibility in generated mocks#5984
thomhurst merged 1 commit into
mainfrom
fix/mock-access-modifiers

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Fixes #5975.

This change preserves the effective accessibility of overridden members when generating partial and wrap mocks, so protected internal, internal, and private protected members emit valid overrides under the right assembly-access conditions.

Validation:

  • dotnet build TUnit.Mocks.SourceGenerator.Tests/TUnit.Mocks.SourceGenerator.Tests.csproj -v:minimal
  • dotnet test TUnit.Mocks.SourceGenerator.Tests/TUnit.Mocks.SourceGenerator.Tests.csproj --no-build --framework net10.0

@codacy-production
Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 9 complexity

Metric Results
Complexity 9

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@thomhurst thomhurst enabled auto-merge (squash) May 21, 2026 14:38
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Code Review: Preserve override accessibility in generated mocks

The core fix is correct and well-motivated. Replacing the binary IsProtected flag with OverrideAccessModifier (a string carrying the full resolved modifier) is the right abstraction, and the GetOverrideAccessModifier / HasInternalAccess logic in MemberDiscovery.cs handles all five accessibility levels correctly, including the ProtectedOrInternal"protected" downgrade when InternalsVisibleTo isn't present.

Issue: AssertContains has a substring-match false positive risk

The new helper tests use text.Contains(expected, StringComparison.Ordinal), but "internal override string InternalMethod()" is a substring of "protected internal override string InternalMethod()". This means the test

AssertContains(generated, "internal override string InternalMethod()");

would silently pass even if the generator incorrectly emitted protected internal override string InternalMethod() for a plain-internal member. The same risk applies to any assertion pair where the expected token is a suffix of a longer modifier (e.g. internal vs protected internal).

Suggested fix: Anchor assertions to a word boundary or leading whitespace, e.g.:

private static void AssertContainsLine(string text, string expected)
{
    // Wrap in newlines so the modifier is not found inside a longer token.
    if (!text.Contains($"\n    {expected}", StringComparison.Ordinal)
        && !text.Contains($"\r\n    {expected}", StringComparison.Ordinal))
    {
        throw new InvalidOperationException($"Expected generated output to contain: {expected}");
    }
}

Alternatively, use the existing VerifyGeneratorOutput snapshot approach (consistent with the surrounding test suite) so the entire generated file is validated—this would make cross-modifier confusion impossible to miss.

Minor: AssertContains / AssertDoesNotContain throw InvalidOperationException

Throwing InvalidOperationException from test helpers produces confusing stack traces and doesn't participate in test-framework output formatting. Prefer Assert.Fail(message) (or whatever the TUnit equivalent is) so failures are reported consistently alongside other test assertion failures.

Observation: CreateEventModel calls in the interface-member paths don't pass compilationAssembly (lines 152, 280)

These two call sites omit compilationAssembly, so the events always get the default OverrideAccessModifier = "public". Interface events are always public in C#, so this is harmless today, but it's inconsistent with the surrounding property/method call sites that do pass compilationAssembly. Worth making it uniform to avoid a latent bug if the code ever handles non-public default interface members.

Positive notes

  • The GetOverrideAccessModifier switch is clean and exhaustive; the _ => "public" fallback is safe because the accessibility filter upstream already excludes non-overridable members.
  • AccessorPrefix is a nice small helper that cleanly handles the "same modifier as property → no prefix" case.
  • HasInternalAccess correctly uses GivesAccessTo rather than a string comparison, so it handles InternalsVisibleTo transparently.
  • The snapshot fix for Partial_Mock_Omits_Inaccessible_Property_Setters (emitting protected set instead of bare set) is a real correctness improvement, not just cosmetic.
  • Test coverage across same-assembly, external-no-IVT, and external-with-IVT is comprehensive.

@thomhurst thomhurst merged commit 6f4513f into main May 21, 2026
14 checks passed
@thomhurst thomhurst deleted the fix/mock-access-modifiers branch May 21, 2026 14:59
@claude claude Bot mentioned this pull request May 21, 2026
1 task
This was referenced May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Mocking Protected Internal Virtual with Internals Visible To Not Working

1 participant