diff --git a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs index 076d5b0fd8..b7a73b4528 100644 --- a/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs +++ b/TUnit.Mocks.SourceGenerator.Tests/MockGeneratorTests.cs @@ -296,4 +296,28 @@ void M() return VerifyGeneratorOutput(source); } + + [Test] + public Task Interface_With_Keyword_Parameter_Names() + { + var source = """ + using TUnit.Mocks; + + public interface ITest + { + void Test(string @event); + string Get(int @class, string @return); + } + + public class TestUsage + { + void M() + { + var mock = Mock.Of(); + } + } + """; + + return VerifyGeneratorOutput(source); + } } diff --git a/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt new file mode 100644 index 0000000000..4c614d4cc2 --- /dev/null +++ b/TUnit.Mocks.SourceGenerator.Tests/Snapshots/Interface_With_Keyword_Parameter_Names.verified.txt @@ -0,0 +1,275 @@ +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + internal static class ITest_MockFactory + { + [global::System.Runtime.CompilerServices.ModuleInitializer] + internal static void Register() + { + global::TUnit.Mocks.Mock.RegisterFactory(Create); + } + + private static global::TUnit.Mocks.Mock Create(global::TUnit.Mocks.MockBehavior behavior) + { + var engine = new global::TUnit.Mocks.MockEngine(behavior); + var impl = new ITest_MockImpl(engine); + engine.Raisable = impl; + var mock = new global::TUnit.Mocks.Mock(impl, engine); + return mock; + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + internal sealed class ITest_MockImpl : global::ITest, global::TUnit.Mocks.IRaisable + { + private readonly global::TUnit.Mocks.MockEngine _engine; + + internal ITest_MockImpl(global::TUnit.Mocks.MockEngine engine) + { + _engine = engine; + } + + public void Test(string @event) + { + _engine.HandleCall(0, "Test", new object?[] { @event }); + } + + public string Get(int @class, string @return) + { + return _engine.HandleCallWithReturn(1, "Get", new object?[] { @class, @return }, ""); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public void RaiseEvent(string eventName, object? args) + { + throw new global::System.InvalidOperationException($"No event named '{eventName}' exists on this mock."); + } + } +} + + +// ===== FILE SEPARATOR ===== + +// +#nullable enable + +namespace TUnit.Mocks.Generated +{ + public static class ITest_MockMemberExtensions + { + public static ITest_Test_M0_MockCall Test(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg @event) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { @event.Matcher }; + return new ITest_Test_M0_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "Test", matchers); + } + + public static ITest_Test_M0_MockCall Test(this global::TUnit.Mocks.Mock mock, global::System.Func @event) + { + global::TUnit.Mocks.Arguments.Arg __fa_event = @event; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_event.Matcher }; + return new ITest_Test_M0_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 0, "Test", matchers); + } + + public static ITest_Get_M1_MockCall Get(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg @class, global::TUnit.Mocks.Arguments.Arg @return) + { + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { @class.Matcher, @return.Matcher }; + return new ITest_Get_M1_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Get", matchers); + } + + public static ITest_Get_M1_MockCall Get(this global::TUnit.Mocks.Mock mock, global::System.Func @class, global::TUnit.Mocks.Arguments.Arg @return) + { + global::TUnit.Mocks.Arguments.Arg __fa_class = @class; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_class.Matcher, @return.Matcher }; + return new ITest_Get_M1_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Get", matchers); + } + + public static ITest_Get_M1_MockCall Get(this global::TUnit.Mocks.Mock mock, global::TUnit.Mocks.Arguments.Arg @class, global::System.Func @return) + { + global::TUnit.Mocks.Arguments.Arg __fa_return = @return; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { @class.Matcher, __fa_return.Matcher }; + return new ITest_Get_M1_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Get", matchers); + } + + public static ITest_Get_M1_MockCall Get(this global::TUnit.Mocks.Mock mock, global::System.Func @class, global::System.Func @return) + { + global::TUnit.Mocks.Arguments.Arg __fa_class = @class; + global::TUnit.Mocks.Arguments.Arg __fa_return = @return; + var matchers = new global::TUnit.Mocks.Arguments.IArgumentMatcher[] { __fa_class.Matcher, __fa_return.Matcher }; + return new ITest_Get_M1_MockCall(global::TUnit.Mocks.Mock.GetEngine(mock), 1, "Get", matchers); + } + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class ITest_Test_M0_MockCall : global::TUnit.Mocks.Verification.ICallVerification + { + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private readonly global::System.Lazy _lazyBuilder; + + internal ITest_Test_M0_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + _lazyBuilder = new global::System.Lazy(() => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.VoidMethodSetupBuilder(setup); + } + ); + _ = _lazyBuilder.Value; + } + + private global::TUnit.Mocks.Setup.VoidMethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + + /// + public ITest_Test_M0_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public ITest_Test_M0_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public ITest_Test_M0_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public ITest_Test_M0_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public ITest_Test_M0_MockCall Throws(global::System.Func exceptionFactory) { EnsureSetup().Throws(exceptionFactory); return this; } + /// + public ITest_Test_M0_MockCall Raises(string eventName, object? args = null) { EnsureSetup().Raises(eventName, args); return this; } + /// + public ITest_Test_M0_MockCall SetsOutParameter(int paramIndex, object? value) { EnsureSetup().SetsOutParameter(paramIndex, value); return this; } + /// + public ITest_Test_M0_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public ITest_Test_M0_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Execute a typed callback using the actual method parameters. + public ITest_Test_M0_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(args => callback((string)args[0]!)); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public ITest_Test_M0_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((string)args[0]!)); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + } + + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public sealed class ITest_Get_M1_MockCall : global::TUnit.Mocks.Verification.ICallVerification + { + private readonly global::TUnit.Mocks.IMockEngineAccess _engine; + private readonly int _memberId; + private readonly string _memberName; + private readonly global::TUnit.Mocks.Arguments.IArgumentMatcher[] _matchers; + private readonly global::System.Lazy> _lazyBuilder; + + internal ITest_Get_M1_MockCall(global::TUnit.Mocks.IMockEngineAccess engine, int memberId, string memberName, global::TUnit.Mocks.Arguments.IArgumentMatcher[] matchers) + { + _engine = engine; + _memberId = memberId; + _memberName = memberName; + _matchers = matchers; + _lazyBuilder = new global::System.Lazy>(() => + { + var setup = new global::TUnit.Mocks.Setup.MethodSetup(_memberId, _matchers, _memberName); + _engine.AddSetup(setup); + return new global::TUnit.Mocks.Setup.MethodSetupBuilder(setup); + } + ); + } + + private global::TUnit.Mocks.Setup.MethodSetupBuilder EnsureSetup() => _lazyBuilder.Value; + + /// + public ITest_Get_M1_MockCall Returns(string value) { EnsureSetup().Returns(value); return this; } + /// + public ITest_Get_M1_MockCall Returns(global::System.Func factory) { EnsureSetup().Returns(factory); return this; } + /// + public ITest_Get_M1_MockCall ReturnsSequentially(params string[] values) { EnsureSetup().ReturnsSequentially(values); return this; } + /// + public ITest_Get_M1_MockCall Throws() where TException : global::System.Exception, new() { EnsureSetup().Throws(); return this; } + /// + public ITest_Get_M1_MockCall Throws(global::System.Exception exception) { EnsureSetup().Throws(exception); return this; } + /// + public ITest_Get_M1_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public ITest_Get_M1_MockCall Callback(global::System.Action callback) { EnsureSetup().Callback(callback); return this; } + /// + public ITest_Get_M1_MockCall Returns(global::System.Func factory) { EnsureSetup().Returns(factory); return this; } + /// + public ITest_Get_M1_MockCall Throws(global::System.Func exceptionFactory) { EnsureSetup().Throws(exceptionFactory); return this; } + /// + public ITest_Get_M1_MockCall Raises(string eventName, object? args = null) { EnsureSetup().Raises(eventName, args); return this; } + /// + public ITest_Get_M1_MockCall SetsOutParameter(int paramIndex, object? value) { EnsureSetup().SetsOutParameter(paramIndex, value); return this; } + /// + public ITest_Get_M1_MockCall TransitionsTo(string stateName) { EnsureSetup().TransitionsTo(stateName); return this; } + /// + public ITest_Get_M1_MockCall Then() { EnsureSetup().Then(); return this; } + + /// Configure a typed computed return value using the actual method parameters. + public ITest_Get_M1_MockCall Returns(global::System.Func factory) + { + EnsureSetup().Returns(args => factory((int)args[0]!, (string)args[1]!)); + return this; + } + + /// Execute a typed callback using the actual method parameters. + public ITest_Get_M1_MockCall Callback(global::System.Action callback) + { + EnsureSetup().Callback(args => callback((int)args[0]!, (string)args[1]!)); + return this; + } + + /// Configure a typed computed exception using the actual method parameters. + public ITest_Get_M1_MockCall Throws(global::System.Func exceptionFactory) + { + EnsureSetup().Throws(args => exceptionFactory((int)args[0]!, (string)args[1]!)); + return this; + } + + // ICallVerification + /// + public void WasCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(); + /// + public void WasCalled(global::TUnit.Mocks.Times times) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times); + /// + public void WasCalled(global::TUnit.Mocks.Times times, string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(times, message); + /// + public void WasCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasCalled(message); + /// + public void WasNeverCalled() => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(); + /// + public void WasNeverCalled(string? message) => _engine.CreateVerification(_memberId, _memberName, _matchers).WasNeverCalled(message); + } +} diff --git a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs index 7e352d1bf3..47cc1ddcd1 100644 --- a/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs +++ b/TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs @@ -500,7 +500,10 @@ private static void GenerateTypedOutRefMethods(CodeWriter writer, EquatableArray } private static string ToPascalCase(string name) - => string.IsNullOrEmpty(name) ? name : char.ToUpperInvariant(name[0]) + name[1..]; + { + if (name.StartsWith("@")) name = name[1..]; + return string.IsNullOrEmpty(name) ? name : char.ToUpperInvariant(name[0]) + name[1..]; + } private static string BuildCastArgs(List nonOutParams, List? allNonOutParams = null) { @@ -674,7 +677,8 @@ private static void EmitSingleFuncOverload(CodeWriter writer, MockMemberModel me foreach (var idx in funcIndices.OrderBy(i => i)) { var p = method.Parameters[idx]; - writer.AppendLine($"global::TUnit.Mocks.Arguments.Arg<{p.FullyQualifiedType}> __fa_{p.Name} = {p.Name};"); + var rawName = p.Name.StartsWith("@") ? p.Name[1..] : p.Name; + writer.AppendLine($"global::TUnit.Mocks.Arguments.Arg<{p.FullyQualifiedType}> __fa_{rawName} = {p.Name};"); } // Build matchers array @@ -685,7 +689,8 @@ private static void EmitSingleFuncOverload(CodeWriter writer, MockMemberModel me if (p.Direction == ParameterDirection.Out) continue; if (!includeRefStructArgs && p.IsRefStruct) continue; - matcherExprs.Add(funcIndices.Contains(i) ? $"__fa_{p.Name}.Matcher" : $"{p.Name}.Matcher"); + var rawName = p.Name.StartsWith("@") ? p.Name[1..] : p.Name; + matcherExprs.Add(funcIndices.Contains(i) ? $"__fa_{rawName}.Matcher" : $"{p.Name}.Matcher"); } if (matcherExprs.Count == 0) diff --git a/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs b/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs index 53e157ad2e..77438c1d2e 100644 --- a/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs +++ b/TUnit.Mocks.SourceGenerator/Discovery/MemberDiscovery.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using TUnit.Mocks.SourceGenerator.Extensions; using TUnit.Mocks.SourceGenerator.Models; using System.Collections.Generic; @@ -294,7 +295,7 @@ private static MockMemberModel CreateMethodModel(IMethodSymbol method, ref int m Parameters = new EquatableArray( method.Parameters.Select(p => new MockParameterModel { - Name = p.Name, + Name = EscapeIdentifier(p.Name), Type = p.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), FullyQualifiedType = p.Type.GetFullyQualifiedName(), Direction = p.GetParameterDirection(), @@ -392,7 +393,7 @@ public static EquatableArray DiscoverConstructors(INamedTy Parameters = new EquatableArray( ctor.Parameters.Select(p => new MockParameterModel { - Name = p.Name, + Name = EscapeIdentifier(p.Name), Type = p.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), FullyQualifiedType = p.Type.GetFullyQualifiedName(), Direction = p.GetParameterDirection(), @@ -429,7 +430,7 @@ private static MockMemberModel CreateIndexerModel(IPropertySymbol indexer, ref i Parameters = new EquatableArray( indexer.Parameters.Select(p => new MockParameterModel { - Name = p.Name, + Name = EscapeIdentifier(p.Name), Type = p.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), FullyQualifiedType = p.Type.GetFullyQualifiedName(), Direction = ParameterDirection.In @@ -468,8 +469,8 @@ private static MockEventModel CreateEventModel(IEventSymbol evt, string? explici { // EventHandler pattern: skip sender (first param), expose remaining as raise params var argsParams = invokeMethod.Parameters.Skip(1).ToArray(); - raiseParameters = string.Join(", ", argsParams.Select(p => $"{p.Type.GetFullyQualifiedName()} {p.Name}")); - invokeArgs = "this, " + string.Join(", ", argsParams.Select(p => p.Name)); + raiseParameters = string.Join(", ", argsParams.Select(p => $"{p.Type.GetFullyQualifiedName()} {EscapeIdentifier(p.Name)}")); + invokeArgs = "this, " + string.Join(", ", argsParams.Select(p => EscapeIdentifier(p.Name))); eventArgsType = argsParams.Length == 1 ? argsParams[0].Type.GetFullyQualifiedName() : raiseParameters; // fallback for multi-arg EventHandler subtypes @@ -478,8 +479,8 @@ private static MockEventModel CreateEventModel(IEventSymbol evt, string? explici else { // Custom delegate (Action, Func, user-defined): expose all params - raiseParameters = string.Join(", ", invokeMethod.Parameters.Select(p => $"{p.Type.GetFullyQualifiedName()} {p.Name}")); - invokeArgs = string.Join(", ", invokeMethod.Parameters.Select(p => p.Name)); + raiseParameters = string.Join(", ", invokeMethod.Parameters.Select(p => $"{p.Type.GetFullyQualifiedName()} {EscapeIdentifier(p.Name)}")); + invokeArgs = string.Join(", ", invokeMethod.Parameters.Select(p => EscapeIdentifier(p.Name))); eventArgsType = raiseParameters; raiseParams = invokeMethod.Parameters.ToArray(); } @@ -487,7 +488,7 @@ private static MockEventModel CreateEventModel(IEventSymbol evt, string? explici var raiseParameterList = new EquatableArray( raiseParams.Select(p => new MockParameterModel { - Name = p.Name, + Name = EscapeIdentifier(p.Name), FullyQualifiedType = p.Type.GetFullyQualifiedName(), Type = p.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), Direction = ParameterDirection.In @@ -549,6 +550,13 @@ private static string GetMethodKey(IMethodSymbol method) return value.ToString(); } + /// + /// Escapes a parameter name that is a C# reserved keyword by prepending '@'. + /// E.g., "event" → "@event", "class" → "@class", "return" → "@return". + /// + private static string EscapeIdentifier(string name) => + SyntaxFacts.GetKeywordKind(name) != SyntaxKind.None ? "@" + name : name; + /// /// For ReadOnlySpan<T> or Span<T> types, returns the fully qualified element type. /// Returns null for all other types.