Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Benchmarks/Mockolate.Benchmarks/CompleteEventBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void Event_NSubstitute()
EventHandler handler = (_, _) => { };

mock.SomeEvent += handler;
mock.SomeEvent += Raise.EventWith(null, EventArgs.Empty);
mock.SomeEvent += Raise.EventWith(null!, EventArgs.Empty);

mock.Received(1).SomeEvent += Arg.Any<EventHandler>();
}
Expand Down
5 changes: 4 additions & 1 deletion Docs/pages/08-analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ implementing it, so the rule still fires.
The same applies per accessor. For a property whose accessors differ in accessibility, such as
`public abstract string Flavour { get; internal set; }`, an override further down discharges only the
inaccessible half. The mock then overrides the accessor it can see and leaves the other one to the
referenced assembly's implementation, so writes through the mock are not intercepted or recorded.
referenced assembly's implementation. Because writes never reach the mock, `Setup` and `Verify` expose
only the getter for such a property, so a write that could never be recorded is not offered for
configuration or verification. See
[properties with only one accessor](setup/properties#properties-with-only-one-accessor) for details.

## Mockolate0003

Expand Down
51 changes: 51 additions & 0 deletions Docs/pages/setup/01-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,57 @@ sut.Mock.Setup.TotalDispensed.OnGet
.Do(() => Console.WriteLine("Execute on all odd read interactions"));
```

## Properties with only one accessor

The setup and verify surfaces only offer the accessors the mock actually intercepts.
`Register()` and `SkippingBaseClass(…)` stay available on both, but the accessor-specific members do
not: a get-only property has no `OnSet`, and a set-only property has neither `OnGet` nor the
`Returns`/`Throws` read-sequence nor `InitializeWith`, since there is no getter to read the value
back.

```csharp
public interface IChocolateInventory
{
int RemainingBars { get; } // no setter
string LastCountedBy { set; } // no getter
}

IChocolateInventory sut = IChocolateInventory.CreateMock();

sut.Mock.Setup.RemainingBars.Returns(3);
sut.Mock.Setup.RemainingBars.Register();
sut.Mock.Setup.RemainingBars.OnSet… // does not compile
sut.Mock.Setup.RemainingBars.Returns(3).OnSet… // does not compile

sut.Mock.Setup.LastCountedBy.OnSet.Do(value => { });
sut.Mock.Setup.LastCountedBy.Register();
sut.Mock.Setup.LastCountedBy.Returns("Ada")… // does not compile
sut.Mock.Setup.LastCountedBy.InitializeWith("Ada")… // does not compile
sut.Mock.Setup.LastCountedBy.OnSet.Do(value => { }).OnGet… // does not compile
```

The verify facade likewise offers the intercepted accessor only:

```csharp
_ = sut.RemainingBars;
sut.LastCountedBy = "Ada";

await That(sut.Mock.Verify.RemainingBars.Got()).Once();
await That(sut.Mock.Verify.LastCountedBy.Set("Ada")).Once();

sut.Mock.Verify.RemainingBars.Set(3)… // does not compile
sut.Mock.Verify.LastCountedBy.Got()… // does not compile
```

This also applies when the property declares an accessor the mock cannot see, such as
`{ get; internal set; }` on a type from an assembly that does not grant `InternalsVisibleTo`. Writes
never reach the mock in that case, so configuring or verifying one could only ever report zero
interactions. See [Mockolate0002](../analyzers#mockolate0002) for when such a type is mockable at all.

Both facades are fully restricted: the fluent builders returned by `Returns`, `Throws`, `Do` and
`TransitionTo` stay on the narrowed surface, so no amount of chaining reaches the accessor the mock
does not intercept.

**Notes:**

- Use `.SkippingBaseClass(…)` to override the base class behavior for a specific property (only for class mocks).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Mockolate.SourceGenerators.Entities;

internal enum PropertyAccessors
{
Both,
GetOnly,
SetOnly,
}
74 changes: 64 additions & 10 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3142,6 +3142,54 @@ private static bool ParametersBlockAllValuesPromotion(EquatableArray<MethodParam
return false;
}

/// <summary>
/// Which accessors of <paramref name="property" /> the mock actually intercepts, and therefore
/// which setup/verify facade the generated surface exposes. The setup type, the verify type and the
/// verify constructor arguments all derive from this one classification so they cannot disagree.
/// </summary>
/// <remarks>
/// The classification follows the accessors the mock emits a body for, which is exactly what
/// <see cref="Property.Getter" />/<see cref="Property.Setter" /> being non-null means: the property
/// declares the accessor, and it is not an <c>internal</c>/<c>private protected</c> one hidden from
/// the mock's assembly. Other accessibilities are not considered here, so a
/// <c>{ get; private set; }</c> property still classifies as <see cref="PropertyAccessors.Both" />.
/// The <see cref="PropertyAccessors.Both" /> arm therefore also absorbs degenerate shapes that
/// cannot produce compiling output regardless of the facade chosen.
/// </remarks>
private static PropertyAccessors GetInterceptedAccessors(Property property)
=> (property.Getter, property.Setter) switch
{
(not null, null) => PropertyAccessors.GetOnly,
(null, not null) => PropertyAccessors.SetOnly,
_ => PropertyAccessors.Both,
};

/// <summary>
/// The setup facade emitted for <paramref name="property" />, narrowed to the accessors classified
/// by <see cref="GetInterceptedAccessors" />.
/// </summary>
private static string GetPropertySetupType(Property property)
=> GetInterceptedAccessors(property) switch
{
PropertyAccessors.GetOnly => $"global::Mockolate.Setup.IPropertyGetterOnlySetup<{property.Type.Fullname}>",
PropertyAccessors.SetOnly => $"global::Mockolate.Setup.IPropertySetterOnlySetup<{property.Type.Fullname}>",
_ => $"global::Mockolate.Setup.PropertySetup<{property.Type.Fullname}>",
};

/// <summary>
/// The verify facade emitted for <paramref name="property" />, narrowed to the accessors classified
/// by <see cref="GetInterceptedAccessors" />.
/// </summary>
private static string GetPropertyVerifyType(Property property, string verifyName)
=> GetInterceptedAccessors(property) switch
{
PropertyAccessors.GetOnly =>
$"global::Mockolate.Verify.VerificationPropertyGetterResult<{verifyName}>",
PropertyAccessors.SetOnly =>
$"global::Mockolate.Verify.VerificationPropertySetterResult<{verifyName}, {property.Type.Fullname}>",
_ => $"global::Mockolate.Verify.VerificationPropertyResult<{verifyName}, {property.Type.Fullname}>",
};

private static void DefineSetupInterface(StringBuilder sb, Class @class, MemberType memberType,
bool hasOverloadResolutionPriority)
{
Expand All @@ -3154,7 +3202,7 @@ private static void DefineSetupInterface(StringBuilder sb, Class @class, MemberT
{
sb.AppendXmlSummary(
$"Setup for the {property.Type.Fullname.EscapeForXmlDoc()} property <see cref=\"{property.ContainingType.EscapeForXmlDoc()}.{property.Name}\" />.");
sb.Append("\t\tglobal::Mockolate.Setup.PropertySetup<").Append(property.Type.Fullname).Append("> ")
sb.Append("\t\t").Append(GetPropertySetupType(property)).Append(' ')
.Append(property.Name).Append(" { get; }").AppendLine();
sb.AppendLine();
}
Expand Down Expand Up @@ -3591,8 +3639,8 @@ private static void ImplementSetupInterface(StringBuilder sb, Class @class, stri
sb.Append(
"\t\t[global::System.Diagnostics.DebuggerBrowsable(global::System.Diagnostics.DebuggerBrowsableState.Never)]")
.AppendLine();
sb.Append("\t\tglobal::Mockolate.Setup.PropertySetup<").Append(property.Type.Fullname)
.Append("> global::Mockolate.Mock.").Append(setupName).Append('.').Append(property.Name).AppendLine();
sb.Append("\t\t").Append(GetPropertySetupType(property))
.Append(" global::Mockolate.Mock.").Append(setupName).Append('.').Append(property.Name).AppendLine();
sb.Append("\t\t{").AppendLine();
sb.Append("\t\t\tget").AppendLine();
sb.Append("\t\t\t{").AppendLine();
Expand Down Expand Up @@ -5013,8 +5061,8 @@ private static void DefineVerifyInterface(StringBuilder sb, Class @class, string
{
sb.AppendXmlSummary(
$"Verify interactions with the {property.Type.Fullname.EscapeForXmlDoc()} property <see cref=\"{property.ContainingType.EscapeForXmlDoc()}.{property.Name}\" />.");
sb.Append("\t\tglobal::Mockolate.Verify.VerificationPropertyResult<").Append(verifyName).Append(", ")
.Append(property.Type.Fullname).Append("> ").Append(property.Name).Append(" { get; }").AppendLine();
sb.Append("\t\t").Append(GetPropertyVerifyType(property, verifyName)).Append(' ')
.Append(property.Name).Append(" { get; }").AppendLine();
sb.AppendLine();
}

Expand Down Expand Up @@ -5287,15 +5335,21 @@ private static void ImplementVerifyInterface(StringBuilder sb, Class @class, str
sb.Append(
"\t\t[global::System.Diagnostics.DebuggerBrowsable(global::System.Diagnostics.DebuggerBrowsableState.Never)]")
.AppendLine();
sb.Append("\t\tglobal::Mockolate.Verify.VerificationPropertyResult<").Append(verifyName).Append(", ")
.Append(property.Type.Fullname).Append("> ").Append(verifyName).Append('.').Append(property.Name)
string verifyType = GetPropertyVerifyType(property, verifyName);
// The narrow views take only the member id of the accessor they expose.
string verifyMemberIds = GetInterceptedAccessors(property) switch
{
PropertyAccessors.GetOnly => propertyGetMemberId,
PropertyAccessors.SetOnly => propertySetMemberId,
_ => $"{propertyGetMemberId}, {propertySetMemberId}",
};
sb.Append("\t\t").Append(verifyType).Append(' ').Append(verifyName).Append('.').Append(property.Name)
.AppendLine();
sb.Append("\t\t{").AppendLine();
sb.Append("\t\t\tget").AppendLine();
sb.Append("\t\t\t{").AppendLine();
sb.Append("\t\t\t\treturn new global::Mockolate.Verify.VerificationPropertyResult<").Append(verifyName)
.Append(", ").Append(property.Type.Fullname).Append(">(this, this.").Append(mockRegistryName)
.Append(", ").Append(propertyGetMemberId).Append(", ").Append(propertySetMemberId).Append(", ")
sb.Append("\t\t\t\treturn new ").Append(verifyType).Append("(this, this.").Append(mockRegistryName)
.Append(", ").Append(verifyMemberIds).Append(", ")
.Append(property.GetUniqueNameString()).Append(");").AppendLine();
sb.Append("\t\t\t}").AppendLine();
sb.Append("\t\t}").AppendLine();
Expand Down
Loading
Loading