Skip to content

Commit 8efe4cf

Browse files
committed
refactor(mocks): structurally enforce static-abstract guard via gated helper
Both DiscoverMembers and DiscoverMembersFromMultipleTypes used to hoist ShouldCollectStaticAbstractFromInterfaces(typeSymbol) into a local boolean and then check it inline at each static-member branch. This relied on documentation/discipline -- a future third loop could silently skip the gate and reintroduce the #5677 regression. Funnel both call sites through TryCollectStaticAbstractFromInterface, which derives the flag from the typeSymbol it was handed. The only way to collect a static-abstract interface member is now through this helper, so the bypass is structurally impossible.
1 parent 46f5894 commit 8efe4cf

1 file changed

Lines changed: 31 additions & 18 deletions

File tree

TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@ public static (EquatableArray<MockMemberModel> Methods, EquatableArray<MockMembe
4444
seenMethods, seenFullMethods, seenProperties, seenEvents, ref memberIdCounter);
4545
}
4646

47-
var collectStaticAbstractFromInterfaces = ShouldCollectStaticAbstractFromInterfaces(typeSymbol);
48-
4947
foreach (var iface in interfaces)
5048
{
5149
string? explicitInterfaceName = null;
@@ -55,10 +53,7 @@ public static (EquatableArray<MockMemberModel> Methods, EquatableArray<MockMembe
5553
{
5654
if (member.IsStatic)
5755
{
58-
if (member.IsAbstract && collectStaticAbstractFromInterfaces)
59-
{
60-
CollectStaticAbstractMember(member, interfaceFqn, methods, properties, events, seenMethods, seenProperties, seenEvents, ref memberIdCounter);
61-
}
56+
TryCollectStaticAbstractFromInterface(member, typeSymbol, interfaceFqn, methods, properties, events, seenMethods, seenProperties, seenEvents, ref memberIdCounter);
6257
continue;
6358
}
6459

@@ -193,8 +188,6 @@ public static (EquatableArray<MockMemberModel> Methods, EquatableArray<MockMembe
193188
? new[] { typeSymbol }.Concat(typeSymbol.AllInterfaces)
194189
: typeSymbol.AllInterfaces.AsEnumerable();
195190

196-
var collectStaticAbstractFromInterfaces = ShouldCollectStaticAbstractFromInterfaces(typeSymbol);
197-
198191
foreach (var iface in interfaces)
199192
{
200193
var interfaceFqn = iface.GetFullyQualifiedName();
@@ -203,10 +196,7 @@ public static (EquatableArray<MockMemberModel> Methods, EquatableArray<MockMembe
203196
{
204197
if (member.IsStatic)
205198
{
206-
if (member.IsAbstract && collectStaticAbstractFromInterfaces)
207-
{
208-
CollectStaticAbstractMember(member, interfaceFqn, methods, properties, events, seenMethods, seenProperties, seenEvents, ref memberIdCounter);
209-
}
199+
TryCollectStaticAbstractFromInterface(member, typeSymbol, interfaceFqn, methods, properties, events, seenMethods, seenProperties, seenEvents, ref memberIdCounter);
210200
continue;
211201
}
212202

@@ -1075,16 +1065,39 @@ private static string EscapeIdentifier(string name) =>
10751065
}
10761066

10771067
/// <summary>
1078-
/// Single source of truth for whether a member-discovery loop should collect static-abstract
1079-
/// interface members for the given target. Class targets already provide the concrete static
1080-
/// impl that satisfies any static-abstract interface members; emitting a bridge interface for
1081-
/// them would produce CS0527 (class in interface list) and CS0540 (explicit interface impl on
1082-
/// a type that doesn't list the interface). Any new collection loop over interface members
1083-
/// MUST gate static-abstract collection through this helper.
1068+
/// Internal predicate consumed only by <see cref="TryCollectStaticAbstractFromInterface"/>.
1069+
/// Class targets already provide the concrete static impl that satisfies any static-abstract
1070+
/// interface members; emitting a bridge interface for them would produce CS0527 (class in
1071+
/// interface list) and CS0540 (explicit interface impl on a type that doesn't list the
1072+
/// interface). Centralised here so no caller can bypass the gate by accident.
10841073
/// </summary>
10851074
private static bool ShouldCollectStaticAbstractFromInterfaces(ITypeSymbol typeSymbol)
10861075
=> typeSymbol.TypeKind != TypeKind.Class;
10871076

1077+
/// <summary>
1078+
/// Gated entry point used by every interface-member discovery loop for static members.
1079+
/// Derives the static-abstract collection flag from <paramref name="typeSymbol"/> internally
1080+
/// so that adding a future loop cannot silently re-introduce the #5677 regression — the
1081+
/// only way to collect a static-abstract member is through this helper.
1082+
/// </summary>
1083+
private static void TryCollectStaticAbstractFromInterface(
1084+
ISymbol member,
1085+
ITypeSymbol typeSymbol,
1086+
string interfaceFqn,
1087+
List<MockMemberModel> methods,
1088+
List<MockMemberModel> properties,
1089+
List<MockEventModel> events,
1090+
Dictionary<string, (int Index, ITypeSymbol? ReturnType)> seenMethods,
1091+
Dictionary<string, int?> seenProperties,
1092+
HashSet<string> seenEvents,
1093+
ref int memberIdCounter)
1094+
{
1095+
if (!member.IsAbstract) return;
1096+
if (!ShouldCollectStaticAbstractFromInterfaces(typeSymbol)) return;
1097+
1098+
CollectStaticAbstractMember(member, interfaceFqn, methods, properties, events, seenMethods, seenProperties, seenEvents, ref memberIdCounter);
1099+
}
1100+
10881101
/// <summary>
10891102
/// Returns true when the given type symbol is an interface that contains static abstract members
10901103
/// (directly or via inherited interfaces) without a most specific implementation.

0 commit comments

Comments
 (0)