diff --git a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
index 56ced6565a..723f1a08b6 100644
--- a/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
+++ b/src/TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
@@ -1208,7 +1208,8 @@ private static void GenerateEvent(CodeWriter writer, MockEventModel evt)
? ""
: string.Join(", ", evt.RaiseParameterList.Select(p => $"{p.FullyQualifiedType} {p.Name}"));
var invokeArgs = string.IsNullOrEmpty(evt.InvokeArgs) ? "" : evt.InvokeArgs;
- using (writer.Block($"internal void Raise_{evt.Name}({raiseParams})"))
+ var raiseAccessModifier = evt.IsSignatureAccessibleFromAssembly ? "internal" : "private";
+ using (writer.Block($"{raiseAccessModifier} void Raise_{evt.Name}({raiseParams})"))
{
if (string.IsNullOrEmpty(invokeArgs))
{
@@ -1242,7 +1243,8 @@ private static void GeneratePartialEvent(CodeWriter writer, MockEventModel evt)
? ""
: string.Join(", ", evt.RaiseParameterList.Select(p => $"{p.FullyQualifiedType} {p.Name}"));
var invokeArgs = string.IsNullOrEmpty(evt.InvokeArgs) ? "" : evt.InvokeArgs;
- using (writer.Block($"internal void Raise_{evt.Name}({raiseParams})"))
+ var raiseAccessModifier = evt.IsSignatureAccessibleFromAssembly ? "internal" : "private";
+ using (writer.Block($"{raiseAccessModifier} void Raise_{evt.Name}({raiseParams})"))
{
if (string.IsNullOrEmpty(invokeArgs))
{
@@ -1589,9 +1591,13 @@ private static void EmitOutRefParamAssignments(CodeWriter writer, MockMemberMode
/// struct out/ref params. Generic mock types and generic methods are excluded — their
/// param types may reference type parameters that aren't fully bound at delegate-decl
/// time and would require an allows ref struct constraint (C# 13, net9.0+ runtime).
+ /// Methods absent from the setup surface cannot register a setter and must not reference its
+ /// otherwise-unemitted delegate from the implementation.
///
internal static bool SupportsClosedRefStructSetter(MockTypeModel model, MockMemberModel method)
- => !method.IsGenericMethod && model.TypeParameters.Length == 0;
+ => method.IsSignatureAccessibleFromAssembly
+ && !method.IsGenericMethod
+ && model.TypeParameters.Length == 0;
internal static string EmitArgsArrayVariable(CodeWriter writer, MockMemberModel method)
{
diff --git a/src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs
index 270aef4509..468773c05c 100644
--- a/src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs
+++ b/src/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs
@@ -71,7 +71,7 @@ public static string Build(MockTypeModel model)
// name appends the interface; for everything else it equals GetSafeName(FullyQualifiedName).
var safeName = MockImplBuilder.GetCompositeSafeName(model);
var mockableType = MockImplBuilder.GetMockableTypeName(model);
- var instanceEvents = model.Events.Where(e => !e.IsStaticAbstract).ToArray();
+ var instanceEvents = model.Events.Where(IsConfigurableEvent).ToArray();
var hasEvents = instanceEvents.Length > 0;
var mockNamespace = MockImplBuilder.GetMockNamespace(model);
@@ -104,7 +104,7 @@ public static string Build(MockTypeModel model)
// setup/verify. Static abstract methods legitimately use ExplicitInterfaceName
// for bridge interface generation and still need setup extensions.
var instanceMethods = new EquatableArray(
- model.Methods.Where(m => m.ExplicitInterfaceName is null || m.IsStaticAbstract).ToImmutableArray());
+ model.Methods.Where(IsConfigurableMethod).ToImmutableArray());
var methodsWithDisambiguation = ApplyOutDisambiguation(instanceMethods);
// Methods
@@ -118,7 +118,9 @@ public static string Build(MockTypeModel model)
// Properties -- extension properties via C# 14 extension blocks
// (skip ref struct properties — can't use PropertyMockCall)
var memberProps = model.Properties
- .Where(p => p.IsConfigurableSurfaceProperty && (p.ExplicitInterfaceName is null || p.IsStaticAbstract))
+ .Where(p => p.IsSignatureAccessibleFromAssembly
+ && p.IsConfigurableSurfaceProperty
+ && (p.ExplicitInterfaceName is null || p.IsStaticAbstract))
.ToList();
if (memberProps.Count > 0)
{
@@ -132,6 +134,7 @@ public static string Build(MockTypeModel model)
// Each indexer overload (different parameter signature) gets its own pair.
var indexers = model.Properties
.Where(p => p.IsIndexer
+ && p.IsSignatureAccessibleFromAssembly
&& !p.IsStaticAbstract
&& !p.IsRefStructReturn
&& !p.IsReturnTypeStaticAbstractInterface)
@@ -178,7 +181,7 @@ public static string Build(MockTypeModel model)
// The out/ref setter delegates stay beside the mocked type: the generated impl references
// them through GetGlobalMockNamespacePrefix, and they are named from the type's short name.
- EmitOutRefSetterDelegateNamespace(writer, model, hasEvents, mockNamespace);
+ EmitOutRefSetterDelegateNamespace(writer, model, mockNamespace);
return writer.ToString();
}
@@ -186,13 +189,21 @@ public static string Build(MockTypeModel model)
/// Methods that get a typed call wrapper — the shared filter for both emission passes.
private static IEnumerable WrappedMethods(MockTypeModel model, bool hasEvents)
=> model.Methods.Where(m =>
- (m.ExplicitInterfaceName is null || m.IsStaticAbstract)
+ IsConfigurableMethod(m)
&& ShouldGenerateTypedWrapper(m, model, hasEvents));
- private static void EmitOutRefSetterDelegateNamespace(CodeWriter writer, MockTypeModel model, bool hasEvents, string mockNamespace)
+ private static bool IsConfigurableMethod(MockMemberModel method)
+ => method.IsSignatureAccessibleFromAssembly
+ && (method.ExplicitInterfaceName is null || method.IsStaticAbstract);
+
+ private static bool IsConfigurableEvent(MockEventModel evt)
+ => evt.IsSignatureAccessibleFromAssembly && !evt.IsStaticAbstract;
+
+ private static void EmitOutRefSetterDelegateNamespace(CodeWriter writer, MockTypeModel model, string mockNamespace)
{
- var methodsNeedingDelegates = WrappedMethods(model, hasEvents)
- .Where(m => MockImplBuilder.SupportsClosedRefStructSetter(model, m)
+ var methodsNeedingDelegates = model.Methods
+ .Where(m => !m.IsStaticAbstract
+ && MockImplBuilder.SupportsClosedRefStructSetter(model, m)
&& m.Parameters.Any(p => p.Direction is ParameterDirection.Out or ParameterDirection.Ref
&& p.IsNonSpanRefStruct))
.ToList();
@@ -1079,7 +1090,7 @@ private static (bool UseTypedWrapper, string ReturnType, string SetupReturnType)
? method.UnwrappedReturnType
: method.ReturnType;
- var hasEvents = model.Events.Any(e => !e.IsStaticAbstract);
+ var hasEvents = model.Events.Any(IsConfigurableEvent);
var useTypedWrapper = ShouldGenerateTypedWrapper(method, model, hasEvents);
string returnType;
@@ -1223,9 +1234,8 @@ private static void EmitAnyArgsOverload(CodeWriter writer, MockMemberModel metho
// Name uniqueness: same set of methods that drive extension-method emission.
int sameNameCount = 0;
- foreach (var m in model.Methods)
+ foreach (var m in model.Methods.Where(IsConfigurableMethod))
{
- if (m.ExplicitInterfaceName is not null && !m.IsStaticAbstract) continue;
if (m.Name == method.Name) sameNameCount++;
}
if (sameNameCount > 1) return;
@@ -1607,7 +1617,7 @@ private static void GenerateRaiseExtensionMethods(CodeWriter writer, MockTypeMod
var typeParams = MockImplBuilder.GetTypeParameterList(model);
var constraints = MockImplBuilder.GetConstraintClauses(model);
bool first = true;
- foreach (var evt in model.Events.Where(e => !e.IsStaticAbstract))
+ foreach (var evt in model.Events.Where(IsConfigurableEvent))
{
if (!first) writer.AppendLine();
first = false;
@@ -1765,10 +1775,9 @@ private static void EmitParamsAnyArgOverload(CodeWriter writer, MockMemberModel
// Two same-name params methods that differ only in element type (e.g. M(params int[]) and
// M(params string[])) would both produce this AnyArg-slotted signature — skip on collision.
- foreach (var m in model.Methods)
+ foreach (var m in model.Methods.Where(IsConfigurableMethod))
{
if (m.MemberId == method.MemberId || m.Name != method.Name) continue;
- if (m.ExplicitInterfaceName is not null && !m.IsStaticAbstract) continue;
if (m.TypeParameters.Length != method.TypeParameters.Length) continue;
var mLast = m.Parameters.Length > 0 ? m.Parameters[m.Parameters.Length - 1] : null;
if (mLast is null || mLast.ParamsElementType is null || mLast.Direction != ParameterDirection.In) continue;
diff --git a/src/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs b/src/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs
index 5d73b08900..f9c8af504d 100644
--- a/src/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs
+++ b/src/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs
@@ -364,7 +364,7 @@ private static void CollectMembers(
}
var explicitName = RequiresExplicitImpl(primaryClassSymbol, evt) ? interfaceFqn : null;
- state.Events.Add(Tag(CreateEventModel(evt, explicitName, interfaceFqn), ownerTypeIndex));
+ state.Events.Add(Tag(CreateEventModel(evt, explicitName, interfaceFqn, compilation: compilation), ownerTypeIndex));
break;
}
}
@@ -511,7 +511,7 @@ private static void ProcessClassMembers(
if (evt.IsAbstract || evt.IsVirtual || evt.IsOverride)
{
if (!seenEvents.Add(key)) continue;
- events.Add(CreateEventModel(evt, null, compilationAssembly: compilationAssembly));
+ events.Add(CreateEventModel(evt, null, compilationAssembly: compilationAssembly, compilation: compilation));
}
else
{
@@ -748,12 +748,21 @@ private static MockMemberModel CreateMethodModel(IMethodSymbol method, ref int m
OverrideAccessModifier = GetOverrideAccessModifier(method, compilationAssembly),
IsRefStructReturn = returnType.IsRefLikeType,
AutoMockFactoryMethod = autoMockFactoryMethod,
+ IsSignatureAccessibleFromAssembly = IsMethodSignatureAccessibleFromAssembly(method, compilation),
IsReturnTypeStaticAbstractInterface = returnTypeHasStaticAbstract,
SpanReturnElementType = returnType.IsRefLikeType ? GetSpanElementType(returnType) : null,
ObsoleteAttribute = GetObsoleteAttributeSyntax(method)
};
}
+ private static bool IsMethodSignatureAccessibleFromAssembly(IMethodSymbol method, Compilation compilation)
+ => TypeAccessibility.IsAccessibleFromAssembly(method.ReturnType, compilation)
+ && method.Parameters.All(parameter =>
+ TypeAccessibility.IsAccessibleFromAssembly(parameter.Type, compilation))
+ && method.TypeParameters.All(typeParameter =>
+ typeParameter.ConstraintTypes.All(constraint =>
+ TypeAccessibility.IsAccessibleFromAssembly(constraint, compilation)));
+
///
/// When a property with the same name appears from multiple interfaces, merge getter/setter
/// accessors so the generated class satisfies all interfaces.
@@ -819,6 +828,7 @@ private static MockMemberModel CreatePropertyModel(IPropertySymbol property, ref
SetterAccessModifier = GetAccessorAccessModifier(property.SetMethod, overrideAccessModifier, compilationAssembly),
IsRefStructReturn = property.Type.IsRefLikeType,
AutoMockFactoryMethod = GetAutoMockFactoryMethod(property.Type, compilation),
+ IsSignatureAccessibleFromAssembly = IsPropertySignatureAccessibleFromAssembly(property, compilation),
IsReturnTypeStaticAbstractInterface = IsInterfaceWithStaticAbstractMembers(property.Type),
SpanReturnElementType = property.Type.IsRefLikeType ? GetSpanElementType(property.Type) : null,
ObsoleteAttribute = propertyObsolete,
@@ -827,6 +837,11 @@ private static MockMemberModel CreatePropertyModel(IPropertySymbol property, ref
};
}
+ private static bool IsPropertySignatureAccessibleFromAssembly(IPropertySymbol property, Compilation compilation)
+ => TypeAccessibility.IsAccessibleFromAssembly(property.Type, compilation)
+ && property.Parameters.All(parameter =>
+ TypeAccessibility.IsAccessibleFromAssembly(parameter.Type, compilation));
+
/// Returns the [Obsolete] attribute for a single accessor, but only when the
/// containing property is NOT itself marked obsolete. When the property is marked, the
/// property-level emission already covers the accessor and emitting both would duplicate.
@@ -944,6 +959,7 @@ private static MockMemberModel CreateIndexerModel(IPropertySymbol indexer, ref i
SetterAccessModifier = GetAccessorAccessModifier(indexer.SetMethod, overrideAccessModifier, compilationAssembly),
IsRefStructReturn = indexer.Type.IsRefLikeType,
AutoMockFactoryMethod = GetAutoMockFactoryMethod(indexer.Type, compilation),
+ IsSignatureAccessibleFromAssembly = IsPropertySignatureAccessibleFromAssembly(indexer, compilation),
IsReturnTypeStaticAbstractInterface = IsInterfaceWithStaticAbstractMembers(indexer.Type),
SpanReturnElementType = indexer.Type.IsRefLikeType ? GetSpanElementType(indexer.Type) : null,
ObsoleteAttribute = indexerObsolete,
@@ -991,7 +1007,7 @@ private static MockMemberModel CreateIndexerModel(IPropertySymbol indexer, ref i
return $"{globalPrefix}{baseName}MockFactory.CreateAutoMock<{typeArguments}>";
}
- private static MockEventModel CreateEventModel(IEventSymbol evt, string? explicitInterfaceName, string? declaringInterfaceName = null, IAssemblySymbol? compilationAssembly = null)
+ private static MockEventModel CreateEventModel(IEventSymbol evt, string? explicitInterfaceName, string? declaringInterfaceName = null, IAssemblySymbol? compilationAssembly = null, Compilation compilation = null!)
{
var eventHandlerType = evt.Type.GetFullyQualifiedNameWithNullability();
@@ -1052,6 +1068,7 @@ private static MockEventModel CreateEventModel(IEventSymbol evt, string? explici
ExplicitInterfaceName = explicitInterfaceName,
DeclaringInterfaceName = declaringInterfaceName,
OverrideAccessModifier = GetOverrideAccessModifier(evt, compilationAssembly),
+ IsSignatureAccessibleFromAssembly = TypeAccessibility.IsAccessibleFromAssembly(evt.Type, compilation),
RaiseParameterList = raiseParameterList,
ObsoleteAttribute = GetObsoleteAttributeSyntax(evt)
};
@@ -1335,7 +1352,7 @@ private static void CollectStaticAbstractMember(
var key = $"E:{evt.Name}";
if (!seenEvents.Add(key)) break;
- var model = CreateEventModel(evt, interfaceFqn) with
+ var model = CreateEventModel(evt, interfaceFqn, compilation: compilation) with
{
IsStaticAbstract = true
};
diff --git a/src/TUnit.Mocks.SourceGenerator/Discovery/TypeAccessibility.cs b/src/TUnit.Mocks.SourceGenerator/Discovery/TypeAccessibility.cs
index f99dd3bc88..08d0191c4e 100644
--- a/src/TUnit.Mocks.SourceGenerator/Discovery/TypeAccessibility.cs
+++ b/src/TUnit.Mocks.SourceGenerator/Discovery/TypeAccessibility.cs
@@ -13,6 +13,9 @@ public static bool IsAccessibleFromAssembly(ITypeSymbol type, Compilation compil
{
switch (type)
{
+ case IErrorTypeSymbol:
+ return false;
+
case ITypeParameterSymbol:
return true;
diff --git a/src/TUnit.Mocks.SourceGenerator/Models/MockEventModel.cs b/src/TUnit.Mocks.SourceGenerator/Models/MockEventModel.cs
index f0152e71e3..0c723641e6 100644
--- a/src/TUnit.Mocks.SourceGenerator/Models/MockEventModel.cs
+++ b/src/TUnit.Mocks.SourceGenerator/Models/MockEventModel.cs
@@ -47,6 +47,12 @@ internal sealed record MockEventModel : IEquatable
public string OverrideAccessModifier { get; init; } = "public";
public bool IsStaticAbstract { get; init; }
+ ///
+ /// Whether non-derived generated code in the consumer assembly can name the event handler
+ /// type. Inaccessible events remain implemented but get no typed raise surface.
+ ///
+ public bool IsSignatureAccessibleFromAssembly { get; init; } = true;
+
///
/// Which type in a multi-type mock owns this event: 0 = the primary type,
/// n = 1-based index into .
@@ -75,6 +81,7 @@ public bool Equals(MockEventModel? other)
&& AdditionalExplicitInterfaceNames.Equals(other.AdditionalExplicitInterfaceNames)
&& OverrideAccessModifier == other.OverrideAccessModifier
&& IsStaticAbstract == other.IsStaticAbstract
+ && IsSignatureAccessibleFromAssembly == other.IsSignatureAccessibleFromAssembly
&& OwnerTypeIndex == other.OwnerTypeIndex
&& RaiseParameterList == other.RaiseParameterList
&& ObsoleteAttribute == other.ObsoleteAttribute;
@@ -92,6 +99,7 @@ public override int GetHashCode()
hash = hash * 31 + (DeclaringInterfaceName?.GetHashCode() ?? 0);
hash = hash * 31 + AdditionalExplicitInterfaceNames.GetHashCode();
hash = hash * 31 + OverrideAccessModifier.GetHashCode();
+ hash = hash * 31 + IsSignatureAccessibleFromAssembly.GetHashCode();
hash = hash * 31 + ObsoleteAttribute.GetHashCode();
hash = hash * 31 + OwnerTypeIndex;
return hash;
diff --git a/src/TUnit.Mocks.SourceGenerator/Models/MockMemberModel.cs b/src/TUnit.Mocks.SourceGenerator/Models/MockMemberModel.cs
index b486b8e2e5..7cb334648b 100644
--- a/src/TUnit.Mocks.SourceGenerator/Models/MockMemberModel.cs
+++ b/src/TUnit.Mocks.SourceGenerator/Models/MockMemberModel.cs
@@ -72,6 +72,13 @@ internal sealed record MockMemberModel : IEquatable
public bool IsStaticAbstract { get; init; }
public string? AutoMockFactoryMethod { get; init; }
+ ///
+ /// Whether non-derived generated code in the consumer assembly can name every type in this
+ /// member's signature. Inaccessible members still need an override when abstract, but cannot
+ /// have setup or verification extensions generated for them.
+ ///
+ public bool IsSignatureAccessibleFromAssembly { get; init; } = true;
+
///
/// Which type in a multi-type mock owns this member: 0 = the primary type,
/// n = 1-based index into .
@@ -173,6 +180,7 @@ public bool Equals(MockMemberModel? other)
&& IsRefStructReturn == other.IsRefStructReturn
&& IsStaticAbstract == other.IsStaticAbstract
&& AutoMockFactoryMethod == other.AutoMockFactoryMethod
+ && IsSignatureAccessibleFromAssembly == other.IsSignatureAccessibleFromAssembly
&& OwnerTypeIndex == other.OwnerTypeIndex
&& IsReturnTypeStaticAbstractInterface == other.IsReturnTypeStaticAbstractInterface
&& SpanReturnElementType == other.SpanReturnElementType
@@ -195,6 +203,7 @@ public override int GetHashCode()
hash = hash * 31 + GetterAccessModifier.GetHashCode();
hash = hash * 31 + SetterAccessModifier.GetHashCode();
hash = hash * 31 + (AutoMockFactoryMethod?.GetHashCode() ?? 0);
+ hash = hash * 31 + IsSignatureAccessibleFromAssembly.GetHashCode();
hash = hash * 31 + OwnerTypeIndex;
hash = hash * 31 + IsReturnTypeStaticAbstractInterface.GetHashCode();
hash = hash * 31 + (ExplicitInterfaceName?.GetHashCode() ?? 0);
diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6634Tests.cs b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6634Tests.cs
index c6bf70de5e..c6bafeeb8a 100644
--- a/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6634Tests.cs
+++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Issue6634Tests.cs
@@ -4,12 +4,29 @@ namespace TUnit.Mocks.SourceGenerator.Tests;
///
/// Regression: https://github.com/thomhurst/TUnit/issues/6634
-/// Constructor discovery runs in the generated subclass context, where protected nested types are
-/// accessible. Constructor models also feed a non-derived factory and static extension methods,
-/// whose signatures cannot name those types unless the consumer assembly has internal access.
+/// Generated subclasses can name protected nested types in constructor and member signatures.
+/// Their non-derived factories and setup extensions cannot, so those public surfaces must omit
+/// inaccessible signatures without dropping overrides required to instantiate abstract clients.
///
public class Issue6634Tests : SnapshotTestBase
{
+ private const string GrpcStyleExternalLibrary = """
+ namespace ExternalLib;
+
+ public abstract class GrpcClient
+ {
+ protected GrpcClient() { }
+
+ protected abstract GrpcClient NewInstance(ClientBaseConfiguration configuration);
+
+ protected internal class ClientBaseConfiguration { }
+
+ public GrpcClient Clone() => NewInstance(new ClientBaseConfiguration());
+
+ public abstract string Call();
+ }
+ """;
+
[Test]
public async Task Grpc_Style_Constructor_With_Protected_Internal_State_Is_Omitted()
{
@@ -32,6 +49,293 @@ protected internal class ClientBaseConfiguration { }
await Assert.That(generated).DoesNotContain("ClientBaseConfiguration");
}
+ [Test]
+ public async Task Grpc_Style_Abstract_Method_With_Protected_Internal_State_Is_Mockable()
+ {
+ var reference = CreateExternalAssemblyReference(GrpcStyleExternalLibrary);
+ var source = GrpcMockSource();
+ var generated = string.Join(Environment.NewLine, RunGenerator(source, [reference]));
+
+ await AssertNoAccessibilityErrors(source, reference);
+ await Assert.That(generated).Contains(
+ "protected override global::ExternalLib.GrpcClient NewInstance(global::ExternalLib.GrpcClient.ClientBaseConfiguration configuration)");
+ await Assert.That(generated).Contains("public override string Call()");
+ }
+
+ [Test]
+ public Task Grpc_Style_Abstract_Method_Generation_Snapshot()
+ {
+ var reference = CreateExternalAssemblyReference(GrpcStyleExternalLibrary);
+
+ return VerifyGeneratorOutput(GrpcMockSource(), [reference]);
+ }
+
+ [Test]
+ public async Task Composite_Inaccessible_Method_Parameter_Is_Mockable()
+ {
+ var reference = CreateExternalAssemblyReference("""
+ namespace ExternalLib;
+
+ public abstract class CompositeClient
+ {
+ protected CompositeClient() { }
+
+ protected abstract CompositeClient NewInstance(
+ System.Collections.Generic.IReadOnlyDictionary states);
+
+ protected internal class State { }
+ }
+ """);
+ var source = MockSource("ExternalLib.CompositeClient");
+
+ await AssertNoAccessibilityErrors(source, reference);
+ }
+
+ [Test]
+ public async Task Inaccessible_Method_Return_Type_Is_Mockable()
+ {
+ var reference = CreateExternalAssemblyReference("""
+ namespace ExternalLib;
+
+ public abstract class StateClient
+ {
+ protected StateClient() { }
+
+ protected abstract State GetState();
+
+ protected internal class State { }
+ }
+ """);
+ var source = MockSource("ExternalLib.StateClient");
+
+ await AssertNoAccessibilityErrors(source, reference);
+ }
+
+ [Test]
+ public async Task Protected_Method_Parameter_Is_Omitted_Even_In_Same_Assembly()
+ {
+ var source = """
+ using TUnit.Mocks;
+
+ public abstract class LocalClient
+ {
+ protected LocalClient() { }
+
+ protected abstract LocalClient NewInstance(State state);
+
+ protected class State { }
+ }
+
+ public class Test
+ {
+ public void Run() => Mock.Of();
+ }
+ """;
+
+ await AssertNoAccessibilityErrors(source);
+ }
+
+ [Test]
+ public async Task Inaccessible_Generic_Constraint_Is_Omitted_From_Member_Surface()
+ {
+ var reference = CreateExternalAssemblyReference("""
+ namespace ExternalLib;
+
+ public abstract class GenericClient
+ {
+ protected GenericClient() { }
+
+ protected abstract void Handle(T value) where T : State;
+
+ protected internal class State { }
+
+ public abstract void Ping(T value);
+ }
+ """);
+ var source = MockSource("ExternalLib.GenericClient");
+ var sources = RunGenerator(source, [reference]);
+ var memberSurface = GetMemberSurface(sources);
+
+ await AssertNoAccessibilityErrors(source, reference);
+ await Assert.That(string.Join(Environment.NewLine, sources)).Contains("protected override void Handle(T value)");
+ await Assert.That(memberSurface).DoesNotContain("Handle");
+ await Assert.That(memberSurface).Contains("Ping");
+ }
+
+ [Test]
+ public async Task Inaccessible_Overload_Does_Not_Suppress_AnyArgs_Helper()
+ {
+ var reference = CreateExternalAssemblyReference("""
+ namespace ExternalLib;
+
+ public abstract class OverloadClient
+ {
+ protected OverloadClient() { }
+
+ public abstract string Search(string query, int count);
+
+ protected abstract string Search(State state, int count);
+
+ protected internal class State { }
+ }
+ """);
+ var source = MockSource("ExternalLib.OverloadClient");
+ var memberSurface = GetMemberSurface(RunGenerator(source, [reference]));
+
+ await AssertNoAccessibilityErrors(source, reference);
+ await Assert.That(memberSurface).Contains(
+ "Search(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.AnyArgs _)");
+ }
+
+ [Test]
+ public async Task Inaccessible_Params_Overload_Does_Not_Suppress_AnyArg_Helper()
+ {
+ var reference = CreateExternalAssemblyReference("""
+ namespace ExternalLib;
+
+ public abstract class ParamsClient
+ {
+ protected ParamsClient() { }
+
+ public abstract int Pack(params int[] values);
+
+ protected abstract int Pack(params State[] values);
+
+ protected internal class State { }
+ }
+ """);
+ var source = MockSource("ExternalLib.ParamsClient");
+ var memberSurface = GetMemberSurface(RunGenerator(source, [reference]));
+
+ await AssertNoAccessibilityErrors(source, reference);
+ await Assert.That(memberSurface).Contains("global::TUnit.Mocks.Arguments.AnyArg values");
+ }
+
+ [Test]
+ public async Task Inaccessible_Property_Type_Is_Omitted_From_Member_Surface()
+ {
+ var reference = CreateExternalAssemblyReference("""
+ namespace ExternalLib;
+
+ public abstract class PropertyClient
+ {
+ protected PropertyClient() { }
+
+ protected abstract State Hidden { get; }
+
+ public abstract string Visible { get; }
+
+ protected internal class State { }
+ }
+ """);
+ var source = MockSource("ExternalLib.PropertyClient");
+ var memberSurface = GetMemberSurface(RunGenerator(source, [reference]));
+
+ await AssertNoAccessibilityErrors(source, reference);
+ await Assert.That(memberSurface).DoesNotContain("Hidden");
+ await Assert.That(memberSurface).Contains("Visible");
+ }
+
+ [Test]
+ public async Task Inaccessible_Indexer_Types_Are_Omitted_From_Member_Surface()
+ {
+ var reference = CreateExternalAssemblyReference("""
+ namespace ExternalLib;
+
+ public abstract class IndexerClient
+ {
+ protected IndexerClient() { }
+
+ protected abstract State this[State state] { get; }
+
+ public abstract string this[int index] { get; }
+
+ protected internal class State { }
+ }
+ """);
+ var source = MockSource("ExternalLib.IndexerClient");
+ var memberSurface = GetMemberSurface(RunGenerator(source, [reference]));
+
+ await AssertNoAccessibilityErrors(source, reference);
+ await Assert.That(memberSurface).DoesNotContain("IndexerClient.State");
+ await Assert.That(memberSurface).Contains("global::TUnit.Mocks.Arguments.Arg index");
+ }
+
+ [Test]
+ public async Task Inaccessible_Event_Type_Has_No_Typed_Raise_Surface()
+ {
+ var reference = CreateExternalAssemblyReference("""
+ namespace ExternalLib;
+
+ public abstract class EventClient
+ {
+ protected EventClient() { }
+
+ protected abstract event System.EventHandler? Hidden;
+
+ public abstract event System.EventHandler? Visible;
+
+ protected internal class State { }
+ }
+ """);
+ var source = MockSource("ExternalLib.EventClient");
+ var memberSurface = GetMemberSurface(RunGenerator(source, [reference]));
+
+ await AssertNoAccessibilityErrors(source, reference);
+ await Assert.That(memberSurface).DoesNotContain("RaiseHidden");
+ await Assert.That(memberSurface).Contains("RaiseVisible");
+ }
+
+ [Test]
+ public async Task Inaccessible_Method_Emits_No_Dangling_RefStruct_Setter_Delegate()
+ {
+ var reference = CreateExternalAssemblyReference("""
+ namespace ExternalLib;
+
+ public ref struct Buffer { }
+
+ public abstract class ReaderClient
+ {
+ protected ReaderClient() { }
+
+ protected abstract void Read(State state, ref Buffer buffer);
+
+ protected internal class State { }
+ }
+ """);
+ var source = MockSource("ExternalLib.ReaderClient");
+ var generated = string.Join(Environment.NewLine, RunGenerator(source, [reference]));
+
+ await AssertNoCompilerErrors(source, reference, "CS0051", "CS0122", "CS0246");
+ await Assert.That(generated).DoesNotContain("ReaderClient_Read_M0_Buffer_RefSetter");
+ }
+
+ [Test]
+ public async Task Unresolved_Signature_Type_Does_Not_Abort_Generation_Or_Leak_Into_Surface()
+ {
+ var source = """
+ using TUnit.Mocks;
+
+ public abstract class ErrorClient
+ {
+ public abstract MissingType Transform(MissingType value);
+ }
+
+ public class Test
+ {
+ public void Run() => Mock.Of();
+ }
+ """;
+
+ var (sources, diagnostics) = RunGeneratorForDiagnostics(source);
+ var generatorErrors = diagnostics.Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToList();
+ var memberSurface = GetMemberSurface(sources);
+
+ await Assert.That(generatorErrors).IsEmpty();
+ await Assert.That(string.Join(Environment.NewLine, sources)).Contains("Transform");
+ await Assert.That(memberSurface).DoesNotContain("Transform");
+ }
+
[Test]
public async Task Inaccessible_Types_Are_Found_Inside_Arrays_And_Generic_Arguments()
{
@@ -243,19 +547,28 @@ private static async Task GenerateFromExternalLibrary(string externalLib
return string.Join(Environment.NewLine, RunGenerator(source, [reference]));
}
- private static async Task AssertNoAccessibilityErrors(
+ private static Task AssertNoAccessibilityErrors(
string source,
MetadataReference? reference = null)
+ => AssertNoCompilerErrors(source, reference, "CS0051", "CS0122");
+
+ private static async Task AssertNoCompilerErrors(
+ string source,
+ MetadataReference? reference,
+ params string[] diagnosticIds)
{
MetadataReference[]? references = reference is null ? null : [reference];
var errors = GetGeneratedCompilationErrors(source, references)
- .Where(diagnostic => diagnostic.Id is "CS0051" or "CS0122")
+ .Where(diagnostic => diagnosticIds.Contains(diagnostic.Id, StringComparer.Ordinal))
.Select(diagnostic => diagnostic.ToString())
.ToList();
await Assert.That(errors).IsEmpty();
}
+ private static string GetMemberSurface(string[] sources)
+ => sources.Single(source => source.Contains("_MockMemberExtensions", StringComparison.Ordinal));
+
private static string MockSource(string typeName) => $$"""
using TUnit.Mocks;
@@ -264,4 +577,18 @@ public class Test
public void Run() => Mock.Of<{{typeName}}>();
}
""";
+
+ private static string GrpcMockSource() => """
+ using TUnit.Mocks;
+
+ public class Test
+ {
+ public void Run()
+ {
+ var mock = Mock.Of();
+ mock.Call().Returns("mocked");
+ _ = mock.Object.Clone();
+ }
+ }
+ """;
}
diff --git a/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Grpc_Style_Abstract_Method_Generation_Snapshot.verified.txt b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Grpc_Style_Abstract_Method_Generation_Snapshot.verified.txt
new file mode 100644
index 0000000000..7c907ec037
--- /dev/null
+++ b/tests/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Grpc_Style_Abstract_Method_Generation_Snapshot.verified.txt
@@ -0,0 +1,151 @@
+//
+#pragma warning disable
+#nullable enable
+
+namespace ExternalLib
+{
+ file sealed class GrpcClientMockImpl : global::ExternalLib.GrpcClient, global::TUnit.Mocks.IRaisable, global::TUnit.Mocks.IMockObject
+ {
+ private readonly global::TUnit.Mocks.MockEngine _engine;
+
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ global::TUnit.Mocks.IMock? global::TUnit.Mocks.IMockObject.MockWrapper { get; set; }
+
+ [global::System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
+ internal GrpcClientMockImpl(global::TUnit.Mocks.MockEngine engine) : base()
+ {
+ _engine = engine;
+ }
+
+ protected override global::ExternalLib.GrpcClient NewInstance(global::ExternalLib.GrpcClient.ClientBaseConfiguration configuration)
+ {
+ return _engine.HandleCallWithReturn(0, "NewInstance", configuration, default!);
+ }
+
+ public override string Call()
+ {
+ return _engine.HandleCallWithReturn(1, "Call", global::System.Array.Empty