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
5 changes: 3 additions & 2 deletions Docs/pages/08-analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ The same applies per accessor. For a property whose accessors differ in accessib
inaccessible half. The mock then overrides the accessor it can see and leaves the other one to the
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.
configuration or verification. The same narrowing applies to indexers. See
[properties with only one accessor](setup/properties#properties-with-only-one-accessor) and
[indexers with only one accessor](setup/indexers#indexers-with-only-one-accessor) for details.

## Mockolate0003

Expand Down
48 changes: 48 additions & 0 deletions Docs/pages/setup/03-indexers.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,54 @@ sut.Mock.Setup[It.IsAny<string>()].OnGet
.Do(() => Console.WriteLine("Execute on all odd read interactions"));
```

## Indexers with only one accessor

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

```csharp
public interface IChocolateStorage
{
int this[string shelf] { get; } // no setter
string this[int box] { set; } // no getter
}

IChocolateStorage sut = IChocolateStorage.CreateMock();

sut.Mock.Setup[It.IsAny<string>()].Returns(3);
sut.Mock.Setup[It.IsAny<string>()].OnSet… // does not compile
sut.Mock.Setup[It.IsAny<string>()].Returns(3).OnSet… // does not compile

sut.Mock.Setup[It.IsAny<int>()].OnSet.Do(value => { });
sut.Mock.Setup[It.IsAny<int>()].Returns("Ada")… // does not compile
sut.Mock.Setup[It.IsAny<int>()].InitializeWith("Ada")… // does not compile
sut.Mock.Setup[It.IsAny<int>()].OnSet.Do(value => { }).OnGet… // does not compile
```

The verify facade likewise offers the intercepted accessor only:

```csharp
_ = sut["top"];
sut[7] = "Ada";

await That(sut.Mock.Verify[It.Is("top")].Got()).Once();
await That(sut.Mock.Verify[It.Is(7)].Set("Ada")).Once();

sut.Mock.Verify[It.Is("top")].Set(3)… // does not compile
sut.Mock.Verify[It.Is(7)].Got()… // does not compile
```

This also applies when the indexer 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. Indexers with more than four keys keep the full surface.

**Notes:**

- All callbacks support more advanced features like conditional execution, frequency control, parallel execution, and
Expand Down
99 changes: 90 additions & 9 deletions Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3190,6 +3190,64 @@ private static string GetPropertyVerifyType(Property property, string verifyName
_ => $"global::Mockolate.Verify.VerificationPropertyResult<{verifyName}, {property.Type.Fullname}>",
};

/// <summary>
/// Which accessors of the <paramref name="indexer" /> the narrowed setup/verify facades follow. The
/// narrowed indexer types only exist for up to <see cref="MaxExplicitParameters" /> keys, so indexers
/// with more keys keep the full surface regardless of <see cref="GetInterceptedAccessors" />.
/// </summary>
private static PropertyAccessors GetIndexerInterceptedAccessors(Property indexer)
=> indexer.IndexerParameters!.Value.Count <= MaxExplicitParameters
? GetInterceptedAccessors(indexer)
: PropertyAccessors.Both;

/// <summary>
/// The open-generic setup facade emitted for the <paramref name="indexer" />, narrowed to the accessors
/// classified by <see cref="GetIndexerInterceptedAccessors" />. The caller appends the value and key type
/// arguments and the closing angle bracket.
/// </summary>
private static string GetIndexerSetupType(Property indexer)
=> GetIndexerInterceptedAccessors(indexer) switch
{
PropertyAccessors.GetOnly => "global::Mockolate.Setup.IIndexerGetterOnlySetup<",
PropertyAccessors.SetOnly => "global::Mockolate.Setup.IIndexerSetterOnlySetup<",
_ => "global::Mockolate.Setup.IndexerSetup<",
};

/// <summary>
/// Appends the verify facade emitted for the <paramref name="indexer" />, narrowed to the accessors
/// classified by <see cref="GetIndexerInterceptedAccessors" />. The narrowed results are keyed by the
/// indexer parameters (the setter result additionally by the value type), the full
/// <c>VerificationIndexerResult</c> only by the value type.
/// </summary>
private static void AppendIndexerVerifyType(StringBuilder sb, Property indexer, string verifyName)
{
switch (GetIndexerInterceptedAccessors(indexer))
{
case PropertyAccessors.GetOnly:
sb.Append("global::Mockolate.Verify.VerificationIndexerGetterResult<").Append(verifyName);
foreach (MethodParameter parameter in indexer.IndexerParameters!.Value)
{
sb.Append(", ").AppendTypeOrWrapper(parameter.Type);
}

sb.Append('>');
break;
case PropertyAccessors.SetOnly:
sb.Append("global::Mockolate.Verify.VerificationIndexerSetterResult<").Append(verifyName);
foreach (MethodParameter parameter in indexer.IndexerParameters!.Value)
{
sb.Append(", ").AppendTypeOrWrapper(parameter.Type);
}

sb.Append(", ").AppendTypeOrWrapper(indexer.Type).Append('>');
break;
default:
sb.Append("global::Mockolate.Verify.VerificationIndexerResult<").Append(verifyName).Append(", ")
.AppendTypeOrWrapper(indexer.Type).Append('>');
break;
}
}

private static void DefineSetupInterface(StringBuilder sb, Class @class, MemberType memberType,
bool hasOverloadResolutionPriority)
{
Expand Down Expand Up @@ -4222,7 +4280,7 @@ private static void AppendIndexerSetupDefinition(StringBuilder sb, Property inde
.Append(valueFlags?.Count(x => !x).ToString() ?? "int.MaxValue").Append(")]").AppendLine();
}

sb.Append("\t\tglobal::Mockolate.Setup.IndexerSetup<").AppendTypeOrWrapper(indexer.Type);
sb.Append("\t\t").Append(GetIndexerSetupType(indexer)).AppendTypeOrWrapper(indexer.Type);
foreach (MethodParameter parameter in indexer.IndexerParameters!)
{
sb.Append(", ").AppendTypeOrWrapper(parameter.Type);
Expand Down Expand Up @@ -4299,7 +4357,7 @@ private static void AppendIndexerSetupImplementation(StringBuilder sb, Property
sb.Append(
"\t\t[global::System.Diagnostics.DebuggerBrowsable(global::System.Diagnostics.DebuggerBrowsableState.Never)]")
.AppendLine();
sb.Append("\t\tglobal::Mockolate.Setup.IndexerSetup<").AppendTypeOrWrapper(indexer.Type);
sb.Append("\t\t").Append(GetIndexerSetupType(indexer)).AppendTypeOrWrapper(indexer.Type);
foreach (MethodParameter parameter in indexer.IndexerParameters!)
{
sb.Append(", ").AppendTypeOrWrapper(parameter.Type);
Expand Down Expand Up @@ -4675,8 +4733,9 @@ private static void AppendIndexerVerifyDefinition(StringBuilder sb, Property ind
.Append(valueFlags?.Count(x => !x).ToString() ?? "int.MaxValue").Append(")]").AppendLine();
}

sb.Append("\t\tglobal::Mockolate.Verify.VerificationIndexerResult<").Append(verifyName).Append(", ")
.AppendTypeOrWrapper(indexer.Type).Append("> this[");
sb.Append("\t\t");
AppendIndexerVerifyType(sb, indexer, verifyName);
sb.Append(" this[");
int i = 0;
foreach (MethodParameter parameter in indexer.IndexerParameters!.Value)
{
Expand Down Expand Up @@ -4731,8 +4790,9 @@ private static void AppendIndexerVerifyImplementation(StringBuilder sb, Property
sb.Append(
"\t\t[global::System.Diagnostics.DebuggerBrowsable(global::System.Diagnostics.DebuggerBrowsableState.Never)]")
.AppendLine();
sb.Append("\t\tglobal::Mockolate.Verify.VerificationIndexerResult<").Append(verifyName).Append(", ")
.AppendTypeOrWrapper(indexer.Type).Append("> ").Append(verifyName).Append(".this[");
sb.Append("\t\t");
AppendIndexerVerifyType(sb, indexer, verifyName);
sb.Append(' ').Append(verifyName).Append(".this[");
int i = 0;
foreach (MethodParameter parameter in indexer.IndexerParameters!.Value)
{
Expand Down Expand Up @@ -4767,14 +4827,35 @@ private static void AppendIndexerVerifyImplementation(StringBuilder sb, Property
bool useTypedVerify = indexer.IndexerParameters.Value.Count is >= 1 and <= 4;
if (useTypedVerify)
{
sb.Append("\t\t\t\treturn new global::Mockolate.Verify.VerificationIndexerResult<").Append(verifyName);
PropertyAccessors interceptedAccessors = GetIndexerInterceptedAccessors(indexer);
string typedVerifyType = interceptedAccessors switch
{
PropertyAccessors.GetOnly => "VerificationIndexerGetterResult",
PropertyAccessors.SetOnly => "VerificationIndexerSetterResult",
_ => "VerificationIndexerResult",
};
// The narrow views take only the member id of the accessor they expose.
string verifyMemberIds = interceptedAccessors switch
{
PropertyAccessors.GetOnly => indexerGetMemberId,
PropertyAccessors.SetOnly => indexerSetMemberId,
_ => $"{indexerGetMemberId}, {indexerSetMemberId}",
};
sb.Append("\t\t\t\treturn new global::Mockolate.Verify.").Append(typedVerifyType).Append('<')
.Append(verifyName);
foreach (MethodParameter parameter in indexer.IndexerParameters.Value)
{
sb.Append(", ").AppendTypeOrWrapper(parameter.Type);
}

sb.Append(", ").AppendTypeOrWrapper(indexer.Type).Append(">(this, this.").Append(mockRegistryName)
.Append(", ").Append(indexerGetMemberId).Append(", ").Append(indexerSetMemberId).Append(",").AppendLine();
// Only Set(...) needs the value type, so the getter-only result is keyed by the parameters alone.
if (interceptedAccessors != PropertyAccessors.GetOnly)
{
sb.Append(", ").AppendTypeOrWrapper(indexer.Type);
}

sb.Append(">(this, this.").Append(mockRegistryName)
.Append(", ").Append(verifyMemberIds).Append(",").AppendLine();

AppendIndexerVerifyTypedMatches(sb, indexer.IndexerParameters.Value, valueFlags);
}
Expand Down
Loading
Loading