Skip to content

fix: skip constructors with inaccessible parameter types - #846

Merged
vbreuss merged 5 commits into
mainfrom
fix/inaccessible-constructor-parameter-types
Aug 25, 2026
Merged

fix: skip constructors with inaccessible parameter types#846
vbreuss merged 5 commits into
mainfrom
fix/inaccessible-constructor-parameter-types

Conversation

@vbreuss

@vbreuss vbreuss commented Aug 25, 2026

Copy link
Copy Markdown
Member

A constructor parameter type is named verbatim in MockExtensionsForXXX, which does not derive from the mocked type. When the type is only reachable through inheritance (a protected nested type, or a protected internal one across assemblies), the generated code failed to compile with CS0122 there and CS0051 on the generated public constructor. Such a constructor cannot be driven from the outside at all, so it is now dropped entirely: classes with another accessible constructor still generate a mock, and classes left without one fall into the existing IsValidMockDeclaration gate.

The conservative accessibility walk that already guarded emitted attribute names is promoted to a shared Helpers.IsAccessibleFrom, extended to recurse through array element types and generic type arguments.

@vbreuss vbreuss self-assigned this Aug 25, 2026
Copilot AI lite review requested due to automatic review settings August 25, 2026 14:20
@vbreuss vbreuss added the bug Something isn't working label Aug 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a source-generator compilation failure when emitting mock constructors whose parameter types are not nameable from the generated public surfaces (e.g., MockExtensionsForX), especially for protected nested types and protected internal nested types across assembly boundaries without InternalsVisibleTo. It centralizes and extends the generator’s “can this type be named from here?” check, and uses it to drop unusable constructors rather than generating uncompilable code.

Changes:

  • Promote the generator’s conservative accessibility walk to Helpers.IsAccessibleFrom(...) and extend it to recurse through array element types and generic type arguments.
  • Filter generated mock constructors to exclude any constructor whose parameter types are not accessible from the generated assembly.
  • Add unit tests covering same-assembly and cross-assembly (InternalsVisibleTo / no InternalsVisibleTo) cases, including composed inaccessible types (arrays/generics).

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
Tests/Mockolate.SourceGenerators.Tests/MockTests.CrossAssemblyTests.cs Adds cross-assembly regression tests for protected internal nested constructor parameter types with and without InternalsVisibleTo.
Tests/Mockolate.SourceGenerators.Tests/MockGeneratorTests.cs Adds same-assembly regression tests ensuring inaccessible nested parameter types cause constructor omission (and mock omission when no constructors remain).
Source/Mockolate.SourceGenerators/Helpers.cs Introduces Helpers.IsAccessibleFrom and reuses it for attribute emission filtering; adds recursion for arrays/pointers/generic arguments.
Source/Mockolate.SourceGenerators/Entities/MockClass.cs Drops constructors whose parameter types are not accessible/nameable from the generated mock surfaces.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Test Results

    24 files  ± 0      24 suites  ±0   13m 54s ⏱️ - 6m 3s
 4 351 tests +26   4 349 ✅ +26  2 💤 ±0  0 ❌ ±0 
27 665 runs  +78  27 661 ✅ +78  4 💤 ±0  0 ❌ ±0 

Results for commit 1e47e6e. ± Comparison against base commit 4a94550.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

🚀 Benchmark Results

Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Event Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 323.4 ns 8.95 ns 8.37 ns 0.91 1.7 KB 1.00
Mockolate 355.7 ns 7.65 ns 7.16 ns 1.00 1.7 KB 1.00
Imposter 1,638.4 ns 30.20 ns 28.25 ns 4.61 8.8 KB 5.17
TUnitMocks 229.0 ns 1.78 ns 1.67 ns 0.64 1.34 KB 0.79
Moq 16,100.9 ns 36.95 ns 30.86 ns 45.28 12.51 KB 7.34
NSubstitute 6,195.2 ns 14.70 ns 12.27 ns 17.42 9.05 KB 5.31
FakeItEasy 217,477.7 ns 1,273.32 ns 1,191.06 ns 611.56 15.26 KB 8.96
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Method N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 371.4 ns 9.62 ns 9.00 ns 1.03 1.93 KB 1.00
Mockolate 1 360.7 ns 3.45 ns 3.06 ns 1.00 1.93 KB 1.00
Imposter 1 561.1 ns 4.02 ns 3.76 ns 1.56 4.04 KB 2.09
TUnitMocks 1 540.3 ns 6.19 ns 5.79 ns 1.50 2.02 KB 1.04
Moq 1 133,261.5 ns 702.27 ns 622.54 ns 369.46 14.59 KB 7.56
NSubstitute 1 5,938.4 ns 38.80 ns 36.29 ns 16.46 9.12 KB 4.72
FakeItEasy 1 5,728.2 ns 54.35 ns 50.84 ns 15.88 8.05 KB 4.17
baseline* 10 652.4 ns 6.27 ns 5.56 ns 1.02 2.14 KB 1.00
Mockolate 10 636.7 ns 4.49 ns 3.98 ns 1.00 2.14 KB 1.00
Imposter 10 1,101.8 ns 7.29 ns 6.82 ns 1.73 5.52 KB 2.58
TUnitMocks 10 1,490.3 ns 11.81 ns 11.05 ns 2.34 3.73 KB 1.74
Moq 10 136,960.2 ns 599.48 ns 500.60 ns 215.10 18.48 KB 8.63
NSubstitute 10 8,531.5 ns 154.34 ns 144.37 ns 13.40 12.07 KB 5.64
FakeItEasy 10 8,719.5 ns 115.02 ns 107.59 ns 13.69 15.42 KB 7.20
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.63GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Indexer N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 1,054.9 ns 10.57 ns 8.83 ns 1.05 3.77 KB 1.00
Mockolate 1 1,005.3 ns 25.93 ns 22.99 ns 1.00 3.77 KB 1.00
Imposter 1 998.0 ns 30.84 ns 28.85 ns 0.99 5.16 KB 1.37
Moq 1 218,520.9 ns 1,050.27 ns 931.03 ns 217.48 20.37 KB 5.41
NSubstitute 1 10,922.4 ns 85.55 ns 71.44 ns 10.87 12.78 KB 3.39
FakeItEasy 1 12,119.9 ns 128.95 ns 120.62 ns 12.06 13.63 KB 3.62
baseline* 10 2,801.1 ns 8.18 ns 7.25 ns 1.10 4.82 KB 1.00
Mockolate 10 2,551.1 ns 55.46 ns 51.88 ns 1.00 4.82 KB 1.00
Imposter 10 2,505.9 ns 49.02 ns 45.86 ns 0.98 7.97 KB 1.65
Moq 10 232,435.7 ns 966.80 ns 857.04 ns 91.15 29.89 KB 6.20
NSubstitute 10 25,425.3 ns 347.77 ns 325.30 ns 9.97 25.58 KB 5.31
FakeItEasy 10 25,246.9 ns 325.34 ns 304.33 ns 9.90 32.97 KB 6.84
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Callback Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 318.1 ns 5.52 ns 5.16 ns 0.99 1.57 KB 1.00
Mockolate 321.5 ns 7.01 ns 6.56 ns 1.00 1.57 KB 1.00
Imposter 413.6 ns 4.50 ns 3.99 ns 1.29 2.38 KB 1.52
TUnitMocks 556.7 ns 2.83 ns 2.21 ns 1.73 1.99 KB 1.27
Moq 72,590.1 ns 369.91 ns 308.90 ns 225.85 8.96 KB 5.71
NSubstitute 4,448.9 ns 43.16 ns 40.37 ns 13.84 7.71 KB 4.91
FakeItEasy 4,558.4 ns 69.55 ns 65.06 ns 14.18 6.81 KB 4.33
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V74 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

Property N Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 1 599.7 ns 13.76 ns 12.87 ns 1.11 2.41 KB 1.00
Mockolate 1 542.4 ns 20.43 ns 19.11 ns 1.00 2.41 KB 1.00
Imposter 1 472.1 ns 11.26 ns 10.54 ns 0.87 3.13 KB 1.29
TUnitMocks 1 462.1 ns 8.08 ns 7.56 ns 0.85 1.64 KB 0.68
Moq 1 10,434.8 ns 91.08 ns 85.20 ns 19.26 10.39 KB 4.30
NSubstitute 1 7,060.3 ns 91.43 ns 85.52 ns 13.03 11.45 KB 4.74
FakeItEasy 1 7,819.6 ns 93.62 ns 87.58 ns 14.43 11.24 KB 4.66
baseline* 10 1,139.3 ns 14.26 ns 12.65 ns 1.06 2.91 KB 1.00
Mockolate 10 1,078.0 ns 21.66 ns 20.26 ns 1.00 2.91 KB 1.00
Imposter 10 1,123.8 ns 17.95 ns 16.79 ns 1.04 4.67 KB 1.61
TUnitMocks 10 1,645.4 ns 21.11 ns 19.75 ns 1.53 3.94 KB 1.35
Moq 10 17,166.7 ns 129.88 ns 108.46 ns 15.93 18.28 KB 6.29
NSubstitute 10 16,696.9 ns 146.36 ns 136.90 ns 15.49 21.08 KB 7.25
FakeItEasy 10 18,464.8 ns 114.97 ns 107.54 ns 17.13 30.81 KB 10.60
Details

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 7763 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.400
[Host] : .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3

Job=InProcess Toolchain=InProcessEmitToolchain IterationCount=15
LaunchCount=1 WarmupCount=10

CreateMock Mean Error StdDev Ratio Allocated Alloc Ratio
baseline* 19.17 ns 0.269 ns 0.252 ns 0.97 160 B 1.00
Mockolate 19.76 ns 0.560 ns 0.524 ns 1.00 160 B 1.00
Imposter 337.79 ns 11.851 ns 11.085 ns 17.10 2248 B 14.05
TUnitMocks 39.03 ns 0.910 ns 0.851 ns 1.98 200 B 1.25
Moq 1,429.44 ns 10.254 ns 9.592 ns 72.38 2096 B 13.10
NSubstitute 1,960.87 ns 14.626 ns 12.965 ns 99.28 5048 B 31.55
FakeItEasy 1,762.52 ns 38.784 ns 34.381 ns 89.24 2763 B 17.27

baseline* rows show the corresponding Mockolate benchmark from the most recent successful main branch build with results, for regression comparison.

@vbreuss
vbreuss force-pushed the fix/inaccessible-constructor-parameter-types branch from 3d7dd36 to fdd6706 Compare August 25, 2026 15:00
Copilot AI review requested due to automatic review settings August 25, 2026 15:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

A constructor parameter type is named verbatim in `MockExtensionsForXXX`,
which does not derive from the mocked type. When the type is only reachable
through inheritance (a `protected` nested type, or a `protected internal` one
across assemblies), the generated code failed to compile with CS0122 there and
CS0051 on the generated `public` constructor. Such a constructor cannot be
driven from the outside at all, so it is now dropped entirely: classes with
another accessible constructor still generate a mock, and classes left without
one fall into the existing `IsValidMockDeclaration` gate.

The conservative accessibility walk that already guarded emitted attribute
names is promoted to a shared `Helpers.IsAccessibleFrom`, extended to recurse
through array element types and generic type arguments.
`IsAccessibleFrom` only inspected the type arguments of the innermost named
type, so `Wrapper<ProtectedNested>.Inner` slipped through and still produced
CS0051/CS0122. Both the declaration and the type arguments are now checked per
nesting level.

Apply the same rule to member signatures (return/member type, parameter types,
generic constraint types), which the mock restates verbatim on surfaces that do
not derive from the mocked type. A virtual member is dropped from the surface,
an abstract one makes the type unmockable, mirroring how an inaccessible member
is already handled. `MockabilityAnalyzer` reports Mockolate0002 for the same
condition so the user gets a diagnostic instead of a missing mock.
@vbreuss
vbreuss force-pushed the fix/inaccessible-constructor-parameter-types branch from fdd6706 to 65e0f69 Compare August 25, 2026 15:06
Copilot AI review requested due to automatic review settings August 25, 2026 15:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Collapse the `<remarks>` blocks into their summaries and drop the
restatements of C# accessibility rules, keeping only the CS-number
rationale.
`IsSlotReachable` judged only the base declaration's own accessibility, so
folding `HasAccessibleSignature` into `IsOverridableFromMock` made a concrete
class unmockable whenever a base slot it already overrides names a type the
mock cannot restate:

    public abstract class Base
    {
        protected abstract void Consume(Configuration configuration);
        protected class Configuration { }
    }
    public class Derived : Base
    {
        protected override void Consume(Configuration configuration) { }
    }

`Derived` has no obligation left, yet the generator emitted nothing while
`MockabilityAnalyzer` - which skips filled slots unconditionally - reported no
Mockolate0002, leaving only a bare CS0117 on `CreateMock()`. Teach the slot
check about the signature so both sides agree.

Also guard the analyzer's containing-assembly lookup, matching the
`HasInternalAccess` mirror in the generator.
Copilot AI review requested due to automatic review settings August 25, 2026 15:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 25, 2026 15:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

@sonarqubecloud

Copy link
Copy Markdown

@vbreuss
vbreuss merged commit c60eb7f into main Aug 25, 2026
18 checks passed
@vbreuss
vbreuss deleted the fix/inaccessible-constructor-parameter-types branch August 25, 2026 16:14
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants