fix(mocks): skip inaccessible internal accessors when mocking Azure.Response#5461
fix(mocks): skip inaccessible internal accessors when mocking Azure.Response#5461
Conversation
…esponse
Azure.Response.IsError is declared `public virtual bool IsError { get; internal set; }`.
The mock generator was emitting `override bool IsError { get; set; }` because it only
checked symbol presence (`property.SetMethod is not null`), not accessor-level
accessibility. External assemblies can't see the internal setter, producing CS0115:
'ResponseMockImpl.IsError.set': no suitable method found to override.
Adds an `IsAccessorAccessible` helper to `MemberDiscovery` and threads the compilation
assembly through `CreatePropertyModel`, `CreateIndexerModel`, and `MergePropertyAccessors`
so `HasGetter`/`HasSetter`/`SetterMemberId` reflect whether each accessor is actually
reachable from the user's assembly, across both class- and interface-discovery paths.
Fixes #5455
There was a problem hiding this comment.
Code Review
Good targeted fix for the setter accessibility issue. The root cause analysis is correct: was derived from symbol existence () rather than accessibility, which meant inaccessible internal accessors were being included in generated overrides.
What's working well
- Correct abstraction: cleanly separates the two concerns (exists + is accessible) and reuses the existing logic consistently.
- Consistent propagation: Threading
compilationAssemblythrough all four call sites (class walk, interface walk ×2, merge path) was the right approach. Previously the interface walk paths were missing this context entirely — a latent inconsistency that this PR resolves. - Counter hygiene:
setterIdis now only incremented whenhasSetteris true, which is correct. The getter ID counter always increments regardless ofhasGetter, matching the original behaviour — harmless but worth being aware of if counter-stability ever becomes important.
Potential follow-up: private accessor gap in IsMemberAccessible
IsMemberAccessible (line ~381) checks explicitly for Internal and ProtectedAndInternal but not for Private:
var accessibility = member.DeclaredAccessibility;
if (accessibility is Accessibility.Internal or Accessibility.ProtectedAndInternal)
{
if (!memberAssembly.GivesAccessTo(compilationAssembly))
return false;
}
// Private falls through to AreMemberSignatureTypesAccessible which only checks types, not the member itself
return AreMemberSignatureTypesAccessible(member, compilationAssembly);For a defined in an external assembly, IsAccessorAccessible on the setter would return true because Private isn't caught and the signature types are accessible. This would cause the same class of CS0115 error for properties with private set on external types. ProcessClassMembers already skips Private members (line 259), but that guards the property symbol itself, not its individual accessors.
Consider adding a Private check to IsMemberAccessible when memberAssembly != compilationAssembly:
if (accessibility is Accessibility.Private or Accessibility.Internal or Accessibility.ProtectedAndInternal)
{
if (!memberAssembly.GivesAccessTo(compilationAssembly))
return false;
}Note that Private members are never GivesAccessTo-eligible, so this would always return false for private accessors from external assemblies — which is the correct behaviour.
This isn't introduced by this PR (pre-existing gap) and private set on externally-defined virtual properties is much rarer than internal set, so a follow-up issue is reasonable rather than blocking this fix.
Test
The regression test is appropriate — compile-time failure is exactly the right thing to guard against here. One minor note: the test doesn't assert any behaviour (no call on the mock), but that's fine for a compilation regression test.
There was a problem hiding this comment.
Code Review
Good targeted fix for the internal setter accessibility issue. The root cause analysis is correct: HasSetter was derived from symbol existence (SetMethod is not null) rather than accessibility, which meant inaccessible internal accessors were being included in generated overrides.
What's working well
- Correct abstraction:
IsAccessorAccessiblecleanly separates the two concerns (exists + is accessible) and reuses the existingIsMemberAccessiblelogic consistently. - Consistent propagation: Threading
compilationAssemblythrough all four call sites (class walk, interface walk ×2, merge path) was the right approach. Previously the interface walk paths were missing this context entirely — a latent inconsistency this PR resolves. - Counter hygiene:
setterIdis now only incremented whenhasSetteris true. The getter ID counter always increments regardless ofhasGetter, matching original behaviour — harmless, but worth knowing if counter-stability ever matters.
Potential follow-up: private accessor gap in IsMemberAccessible
IsMemberAccessible (line ~381) checks explicitly for Internal and ProtectedAndInternal but not Private:
var accessibility = member.DeclaredAccessibility;
if (accessibility is Accessibility.Internal or Accessibility.ProtectedAndInternal)
{
if (!memberAssembly.GivesAccessTo(compilationAssembly))
return false;
}
// Private falls through to AreMemberSignatureTypesAccessible which checks types, not the member itself
return AreMemberSignatureTypesAccessible(member, compilationAssembly);For a public virtual bool Foo { get; private set; } defined in an external assembly, IsAccessorAccessible on the setter would return true because Private isn't caught and the signature types are accessible. This would cause the same class of CS0115 error for properties with private set on external types. ProcessClassMembers already skips Private members (line 259), but that guards the property symbol itself — not its individual accessors.
Consider adding Private to the accessibility guard in IsMemberAccessible when memberAssembly != compilationAssembly:
if (accessibility is Accessibility.Private or Accessibility.Internal or Accessibility.ProtectedAndInternal)
{
if (!memberAssembly.GivesAccessTo(compilationAssembly))
return false;
}Since Private members are never GivesAccessTo-eligible, this always returns false for private accessors from external assemblies — the correct behaviour.
This gap is pre-existing (not introduced here) and private set on externally-defined virtual properties is rarer than internal set, so a follow-up issue is fine rather than blocking this fix.
Test
The regression test is appropriate — a compile-time failure is exactly the right thing to guard against here. The test doesn't assert any runtime behaviour, but that's fine for a compilation regression.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| CodeStyle | 1 minor |
🟢 Metrics 0 complexity
Metric Results Complexity 0
TIP This summary will be updated as you push new changes. Give us feedback
Follow-up from review on #5461. The `IsMemberAccessible` / `IsMemberAccessibleFromExternal` helpers already blocked `Internal` and `ProtectedAndInternal`, but fell through on `Private`, relying on the fact that `ProcessClassMembers` and `DiscoverConstructors` pre-filter private whole-members. That pre-filter doesn't run on individual property accessors, so a `public virtual int Foo { get; private set; }` on an external type would re-introduce the same CS0115 pattern that #5455 fixed for `internal set`. Adds an explicit `Accessibility.Private` early-return in both helpers, and a snapshot regression test covering internal, private, and protected-internal setters on an external public virtual property.
There was a problem hiding this comment.
Follow-up Review
Previous feedback about the private accessor gap has been addressed in commit a56589d1. The PR is now in good shape.
Previous feedback — resolved ✓
The IsMemberAccessible function now has an explicit Private guard in both paths:
if (accessibility == Accessibility.Private)
return false;This prevents public virtual bool Foo { get; private set; } on external types from emitting an override setter — which was the same class of CS0115 error as the original internal set bug. The snapshot test was also expanded to cover all three cases (internal set, private set, protected internal set), so the regression guard is now comprehensive.
Minor observation on style
The Private check is added as a separate if statement before the combined Internal or ProtectedAndInternal check in both methods, rather than merged into the existing condition:
// Current
if (accessibility == Accessibility.Private)
return false;
if (accessibility is Accessibility.Internal or Accessibility.ProtectedAndInternal)
{ ... }
// Alternative (more compact)
if (accessibility is Accessibility.Private or Accessibility.Internal or Accessibility.ProtectedAndInternal)
{
if (!memberAssembly.GivesAccessTo(compilationAssembly))
return false;
}The alternative works because Private can never satisfy GivesAccessTo (private access is never cross-assembly). However, the current two-statement form is equally correct and arguably more explicit about intent — Private doesn't need the GivesAccessTo call at all, so separating it is defensible. Not a blocking concern.
Conclusion
The fix is correct and complete. Both internal and private inaccessible accessors on externally-defined virtual properties are now properly excluded from generated overrides. The snapshot suite confirms the generator output is correct for all three accessor combinations in the test.
- Drop the leaky `IsAccessorAccessible` cross-reference and example from the `IsMemberAccessible` Private guard — the one-line rationale suffices. - Drop ephemeral "reviewer-flagged" narration from the snapshot test comment. - Rename the test to `Partial_Mock_Omits_Inaccessible_Property_Setters` — shorter and more precise (it's specifically setters, not arbitrary accessors). - Note the `Reason` case explicitly as a control: its `protected internal` setter IS reachable via inheritance, so its setter must still be emitted.
There was a problem hiding this comment.
Code Review — Final Pass
Previous feedback has all been addressed. Here's a summary of the full review cycle:
Previous feedback — all resolved ✓
private accessor gap in IsMemberAccessible (raised in prior review): Added the Accessibility.Private early-return guard in both IsMemberAccessibleFromExternal and IsMemberAccessible. A public virtual bool Foo { get; private set; } on an external type now correctly produces a getter-only override.
Comment/test cleanup (latest commit 7c6f44ea): The leaky IsAccessorAccessible cross-reference was dropped from the Private guard comment, ephemeral "reviewer-flagged" narration was removed from the snapshot test, and the test was renamed to the more precise Partial_Mock_Omits_Inaccessible_Property_Setters. All clean.
What's good about this PR
- Correct root cause:
HasSetterwas derived fromSetMethod is not null(symbol existence) rather than accessibility. The newIsAccessorAccessiblehelper cleanly separates exists from is accessible from this assembly. - Complete propagation:
compilationAssemblyis now threaded through all four paths (class walk, interface walk ×2, merge path). The interface-walk paths were missing this context before — a latent inconsistency now resolved. - Counter hygiene:
setterIdis only incremented whenhasSetteris true. The getter ID always increments, matching original behaviour. - Comprehensive snapshot: The snapshot covers all three cases —
internal set(skipped),private set(skipped),protected internal set(included via inheritance). Good regression guard. - Real-world regression test:
Issue5455Tests.cstests against the actualAzure.Responsetype, which is the right level of confidence for a compile-time fix.
The fix is correct and complete. Approving.
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.29.0 to 1.30.0. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.30.0 <!-- Release notes generated using configuration in .github/release.yml at v1.30.0 --> ## What's Changed ### Other Changes * perf: eliminate locks from mock invocation and verification hot paths by @thomhurst in thomhurst/TUnit#5422 * feat: TUnit0074 analyzer for redundant hook attributes on overrides by @thomhurst in thomhurst/TUnit#5459 * fix(mocks): respect generic type argument accessibility (#5453) by @thomhurst in thomhurst/TUnit#5460 * fix(mocks): skip inaccessible internal accessors when mocking Azure.Response by @thomhurst in thomhurst/TUnit#5461 * fix: apply CultureAttribute and STAThreadExecutorAttribute to hooks (#5452) by @thomhurst in thomhurst/TUnit#5463 ### Dependencies * chore(deps): update tunit to 1.29.0 by @thomhurst in thomhurst/TUnit#5446 * chore(deps): update react to ^19.2.5 by @thomhurst in thomhurst/TUnit#5457 * chore(deps): update opentelemetry to 1.15.2 by @thomhurst in thomhurst/TUnit#5456 * chore(deps): update dependency qs to v6.15.1 by @thomhurst in thomhurst/TUnit#5458 **Full Changelog**: thomhurst/TUnit@v1.29.0...v1.30.0 Commits viewable in [compare view](thomhurst/TUnit@v1.29.0...v1.30.0). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Summary
Azure.Responsefailed withCS0115: 'ResponseMockImpl.IsError.set': no suitable method found to overridebecause the generator emitted anoverridesetter for the baseIsErrorproperty whose setter isinternal(invisible to external assemblies).MemberDiscoverynow checks accessor-level accessibility via a newIsAccessorAccessiblehelper, andHasGetter/HasSetter/SetterMemberIdon the property model are computed from that.IAssemblySymbol? compilationAssemblythroughCreatePropertyModel/CreateIndexerModel/MergePropertyAccessorsat every class- and interface-discovery call site so the behaviour is consistent (previously only the class-walk path had the context).Test plan
TUnit.Mocks.Tests/Issue5455Tests.cs::Mocking_Response_With_Internal_Setter_Compiles— fails to compile onmain, passes after the fixTUnit.Mocks.Testsfull suite: 773/773 pass (net10.0)TUnit.Mocks.SourceGenerator.Testssnapshot suite: 35/35 pass, no snapshot diffs