Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
fix(mocks): escape C# keyword identifiers at all emit sites (#5679)
Mocked types with members named after C# reserved keywords (e.g.
@Class, @event, @namespace) produced uncompilable code because
member names were emitted unescaped at declaration and call sites.

Centralised the escape rule in a new IdentifierEscaping.EscapeIdentifier
helper and applied it to every emit point in MockImplBuilder,
MockMembersBuilder, MockBridgeBuilder, MockEventsBuilder and
MockWrapperTypeBuilder. Stored model Name values stay UNESCAPED so
engine dispatch keys, logging and identity remain stable.

Restored the T18 KitchenSinkEdgeCases scenario and extended it with a
two-keyword-parameter method (@namespace(@new, @static)) to exercise
joint member-name + parameter-name escaping.
  • Loading branch information
thomhurst committed Apr 23, 2026
commit e883b9c1eaef587d70962ab9bb477905744e3d16
7 changes: 4 additions & 3 deletions TUnit.Mocks.SourceGenerator/Builders/MockBridgeBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq;
using TUnit.Mocks.SourceGenerator.Models;
using static TUnit.Mocks.SourceGenerator.IdentifierEscaping;

namespace TUnit.Mocks.SourceGenerator.Builders;

Expand Down Expand Up @@ -99,7 +100,7 @@ private static void GenerateStaticMethodDim(CodeWriter writer, MockMemberModel m

writer.AppendLineIfNotEmpty(method.ObsoleteAttribute);

using (writer.Block($"static {signatureReturnType} {method.ExplicitInterfaceName}.{method.Name}{typeParams}({paramList}){constraints}"))
using (writer.Block($"static {signatureReturnType} {method.ExplicitInterfaceName}.{EscapeIdentifier(method.Name)}{typeParams}({paramList}){constraints}"))
{
GenerateStaticEngineDispatchBody(writer, method, staticEngineTypeName);
}
Expand All @@ -108,7 +109,7 @@ private static void GenerateStaticMethodDim(CodeWriter writer, MockMemberModel m
private static void GenerateStaticPropertyDim(CodeWriter writer, MockMemberModel prop, string staticEngineTypeName)
{
writer.AppendLineIfNotEmpty(prop.ObsoleteAttribute);
writer.AppendLine($"static {prop.ReturnType} {prop.ExplicitInterfaceName}.{prop.Name}");
writer.AppendLine($"static {prop.ReturnType} {prop.ExplicitInterfaceName}.{EscapeIdentifier(prop.Name)}");
writer.OpenBrace();

if (prop.HasGetter)
Expand Down Expand Up @@ -146,7 +147,7 @@ private static void GenerateStaticPropertyDim(CodeWriter writer, MockMemberModel
private static void GenerateStaticEventDim(CodeWriter writer, MockEventModel evt)
{
writer.AppendLineIfNotEmpty(evt.ObsoleteAttribute);
writer.AppendLine($"static event {evt.EventHandlerType} {evt.ExplicitInterfaceName}.{evt.Name}");
writer.AppendLine($"static event {evt.EventHandlerType} {evt.ExplicitInterfaceName}.{EscapeIdentifier(evt.Name)}");
writer.OpenBrace();
writer.AppendLine("add { }");
writer.AppendLine("remove { }");
Expand Down
3 changes: 2 additions & 1 deletion TUnit.Mocks.SourceGenerator/Builders/MockEventsBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using TUnit.Mocks.SourceGenerator.Models;
using static TUnit.Mocks.SourceGenerator.IdentifierEscaping;

namespace TUnit.Mocks.SourceGenerator.Builders;

Expand Down Expand Up @@ -49,7 +50,7 @@ public static string Build(MockTypeModel model)
if (!first) writer.AppendLine();
first = false;

writer.AppendLine($"public global::TUnit.Mocks.EventSubscriptionAccessor {evt.Name}");
writer.AppendLine($"public global::TUnit.Mocks.EventSubscriptionAccessor {EscapeIdentifier(evt.Name)}");
writer.AppendLine($" => new(events.Engine, \"{evt.Name}\");");
}
}
Expand Down
63 changes: 32 additions & 31 deletions TUnit.Mocks.SourceGenerator/Builders/MockImplBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq;
using TUnit.Mocks.SourceGenerator.Models;
using static TUnit.Mocks.SourceGenerator.IdentifierEscaping;

namespace TUnit.Mocks.SourceGenerator.Builders;

Expand Down Expand Up @@ -192,7 +193,7 @@ private static void GenerateWrapMethod(CodeWriter writer, MockMemberModel method

// C# prohibits restating generic constraints on override methods (CS0460)
var accessModifier = method.IsProtected ? "protected" : "public";
using (writer.Block($"{accessModifier} override {signatureReturnType} {method.Name}{typeParams}({paramList})"))
using (writer.Block($"{accessModifier} override {signatureReturnType} {EscapeIdentifier(method.Name)}{typeParams}({paramList})"))
{
if (method.IsAbstractMember)
{
Expand Down Expand Up @@ -234,7 +235,7 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me
writer.AppendLine("return;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"_wrappedInstance.{method.Name}({argPassList});");
writer.AppendLine($"_wrappedInstance.{EscapeIdentifier(method.Name)}({argPassList});");
}
else if (method.IsVoid && method.IsAsync)
{
Expand All @@ -252,7 +253,7 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me
}
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});");
writer.AppendLine($"return _wrappedInstance.{EscapeIdentifier(method.Name)}({argPassList});");
}
else if (method.IsAsync)
{
Expand Down Expand Up @@ -280,7 +281,7 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me
}
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});");
writer.AppendLine($"return _wrappedInstance.{EscapeIdentifier(method.Name)}({argPassList});");
}
else if (method.IsRefStructReturn)
{
Expand All @@ -298,7 +299,7 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me
}
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});");
writer.AppendLine($"return _wrappedInstance.{EscapeIdentifier(method.Name)}({argPassList});");
}
else if (method.IsReturnTypeStaticAbstractInterface)
{
Expand All @@ -310,7 +311,7 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me
writer.AppendLine("return __result;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});");
writer.AppendLine($"return _wrappedInstance.{EscapeIdentifier(method.Name)}({argPassList});");
}
else
{
Expand All @@ -321,7 +322,7 @@ private static void GenerateWrapMethodBody(CodeWriter writer, MockMemberModel me
writer.AppendLine("return __result;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{method.Name}({argPassList});");
writer.AppendLine($"return _wrappedInstance.{EscapeIdentifier(method.Name)}({argPassList});");
}
}

Expand All @@ -330,7 +331,7 @@ private static void GenerateWrapProperty(CodeWriter writer, MockMemberModel prop
var accessModifier = prop.IsProtected ? "protected" : "public";
var autoMockFactory = GetAutoMockFactoryLambda(prop);
writer.AppendLineIfNotEmpty(prop.ObsoleteAttribute);
writer.AppendLine($"{accessModifier} override {prop.ReturnType} {prop.Name}");
writer.AppendLine($"{accessModifier} override {prop.ReturnType} {EscapeIdentifier(prop.Name)}");
writer.OpenBrace();

if (prop.HasGetter)
Expand All @@ -356,7 +357,7 @@ private static void GenerateWrapProperty(CodeWriter writer, MockMemberModel prop
writer.AppendLine("return default;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{prop.Name};");
writer.AppendLine($"return _wrappedInstance.{EscapeIdentifier(prop.Name)};");
writer.CloseBrace();
}
}
Expand All @@ -378,7 +379,7 @@ private static void GenerateWrapProperty(CodeWriter writer, MockMemberModel prop
writer.AppendLine($"return ({prop.ReturnType})__rawResult!;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{prop.Name};");
writer.AppendLine($"return _wrappedInstance.{EscapeIdentifier(prop.Name)};");
writer.CloseBrace();
}
else
Expand All @@ -391,7 +392,7 @@ private static void GenerateWrapProperty(CodeWriter writer, MockMemberModel prop
writer.AppendLine("return __result;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return _wrappedInstance.{prop.Name};");
writer.AppendLine($"return _wrappedInstance.{EscapeIdentifier(prop.Name)};");
writer.CloseBrace();
}
}
Expand All @@ -414,7 +415,7 @@ private static void GenerateWrapProperty(CodeWriter writer, MockMemberModel prop
writer.AppendLine($"if (!_engine.TryHandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", {setterArgs}))");
writer.AppendLine("{");
writer.IncreaseIndent();
writer.AppendLine($"_wrappedInstance.{prop.Name} = value;");
writer.AppendLine($"_wrappedInstance.{EscapeIdentifier(prop.Name)} = value;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.CloseBrace();
Expand Down Expand Up @@ -536,20 +537,20 @@ private static void GenerateInterfaceMethod(CodeWriter writer, MockMemberModel m
// Return type is compatible (e.g. IEnumerable.GetEnumerator → IEnumerable<T>.GetEnumerator)
// — delegate to the public method.
var argPassList = GetArgPassList(method);
writer.AppendLine($"{signatureReturnType} {method.ExplicitInterfaceName}.{method.Name}{typeParams}({paramList}){constraints} => {method.Name}({argPassList});");
writer.AppendLine($"{signatureReturnType} {method.ExplicitInterfaceName}.{EscapeIdentifier(method.Name)}{typeParams}({paramList}){constraints} => {EscapeIdentifier(method.Name)}({argPassList});");
}
else
{
// Return types are incompatible — dispatch through the engine with a dedicated member id.
using (writer.Block($"{signatureReturnType} {method.ExplicitInterfaceName}.{method.Name}{typeParams}({paramList}){constraints}"))
using (writer.Block($"{signatureReturnType} {method.ExplicitInterfaceName}.{EscapeIdentifier(method.Name)}{typeParams}({paramList}){constraints}"))
{
GenerateEngineDispatchBody(writer, method);
}
}
return;
}

using (writer.Block($"public {signatureReturnType} {method.Name}{typeParams}({paramList}){constraints}"))
using (writer.Block($"public {signatureReturnType} {EscapeIdentifier(method.Name)}{typeParams}({paramList}){constraints}"))
{
GenerateEngineDispatchBody(writer, method);
}
Expand All @@ -566,7 +567,7 @@ private static void GeneratePartialMethod(CodeWriter writer, MockMemberModel met

// C# prohibits restating generic constraints on override methods (CS0460)
var accessModifier = method.IsProtected ? "protected" : "public";
using (writer.Block($"{accessModifier} override {signatureReturnType} {method.Name}{typeParams}({paramList})"))
using (writer.Block($"{accessModifier} override {signatureReturnType} {EscapeIdentifier(method.Name)}{typeParams}({paramList})"))
{
if (method.IsAbstractMember)
{
Expand Down Expand Up @@ -608,7 +609,7 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel
writer.AppendLine("return;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"base.{method.Name}{GetTypeParameterList(method)}({argPassList});");
writer.AppendLine($"base.{EscapeIdentifier(method.Name)}{GetTypeParameterList(method)}({argPassList});");
}
else if (method.IsVoid && method.IsAsync)
{
Expand All @@ -628,7 +629,7 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel
}
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{method.Name}{GetTypeParameterList(method)}({argPassList});");
writer.AppendLine($"return base.{EscapeIdentifier(method.Name)}{GetTypeParameterList(method)}({argPassList});");
}
else if (method.IsAsync)
{
Expand Down Expand Up @@ -658,7 +659,7 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel
}
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{method.Name}{GetTypeParameterList(method)}({argPassList});");
writer.AppendLine($"return base.{EscapeIdentifier(method.Name)}{GetTypeParameterList(method)}({argPassList});");
}
else if (method.IsRefStructReturn)
{
Expand All @@ -677,7 +678,7 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel
}
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{method.Name}{GetTypeParameterList(method)}({argPassList});");
writer.AppendLine($"return base.{EscapeIdentifier(method.Name)}{GetTypeParameterList(method)}({argPassList});");
}
else if (method.IsReturnTypeStaticAbstractInterface)
{
Expand All @@ -689,7 +690,7 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel
writer.AppendLine("return __result;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{method.Name}{GetTypeParameterList(method)}({argPassList});");
writer.AppendLine($"return base.{EscapeIdentifier(method.Name)}{GetTypeParameterList(method)}({argPassList});");
}
else
{
Expand All @@ -701,7 +702,7 @@ private static void GeneratePartialMethodBody(CodeWriter writer, MockMemberModel
writer.AppendLine("return __result;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{method.Name}{GetTypeParameterList(method)}({argPassList});");
writer.AppendLine($"return base.{EscapeIdentifier(method.Name)}{GetTypeParameterList(method)}({argPassList});");
}
}

Expand Down Expand Up @@ -851,7 +852,7 @@ private static void GenerateInterfaceProperty(CodeWriter writer, MockMemberModel
{
// Explicit interface property with incompatible return type.
// Dispatches independently through the engine with a dedicated MemberId.
writer.AppendLine($"{prop.ReturnType} {prop.ExplicitInterfaceName}.{prop.Name}");
writer.AppendLine($"{prop.ReturnType} {prop.ExplicitInterfaceName}.{EscapeIdentifier(prop.Name)}");
writer.OpenBrace();
if (prop.HasGetter)
{
Expand All @@ -867,7 +868,7 @@ private static void GenerateInterfaceProperty(CodeWriter writer, MockMemberModel
return;
}

writer.AppendLine($"public {prop.ReturnType} {prop.Name}");
writer.AppendLine($"public {prop.ReturnType} {EscapeIdentifier(prop.Name)}");
writer.OpenBrace();

if (prop.HasGetter)
Expand Down Expand Up @@ -914,7 +915,7 @@ private static void GeneratePartialProperty(CodeWriter writer, MockMemberModel p
var accessModifier = prop.IsProtected ? "protected" : "public";
var autoMockFactory = GetAutoMockFactoryLambda(prop);
writer.AppendLineIfNotEmpty(prop.ObsoleteAttribute);
writer.AppendLine($"{accessModifier} override {prop.ReturnType} {prop.Name}");
writer.AppendLine($"{accessModifier} override {prop.ReturnType} {EscapeIdentifier(prop.Name)}");
writer.OpenBrace();

if (prop.HasGetter)
Expand All @@ -940,7 +941,7 @@ private static void GeneratePartialProperty(CodeWriter writer, MockMemberModel p
writer.AppendLine("return default;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{prop.Name};");
writer.AppendLine($"return base.{EscapeIdentifier(prop.Name)};");
writer.CloseBrace();
}
}
Expand All @@ -963,7 +964,7 @@ private static void GeneratePartialProperty(CodeWriter writer, MockMemberModel p
writer.AppendLine($"return ({prop.ReturnType})__rawResult!;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{prop.Name};");
writer.AppendLine($"return base.{EscapeIdentifier(prop.Name)};");
writer.CloseBrace();
}
else
Expand All @@ -977,7 +978,7 @@ private static void GeneratePartialProperty(CodeWriter writer, MockMemberModel p
writer.AppendLine("return __result;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.AppendLine($"return base.{prop.Name};");
writer.AppendLine($"return base.{EscapeIdentifier(prop.Name)};");
writer.CloseBrace();
}
}
Expand All @@ -1001,7 +1002,7 @@ private static void GeneratePartialProperty(CodeWriter writer, MockMemberModel p
writer.AppendLine($"if (!_engine.TryHandleCall({prop.SetterMemberId}, \"set_{prop.Name}\", {setterArgs}))");
writer.AppendLine("{");
writer.IncreaseIndent();
writer.AppendLine($"base.{prop.Name} = value;");
writer.AppendLine($"base.{EscapeIdentifier(prop.Name)} = value;");
writer.DecreaseIndent();
writer.AppendLine("}");
writer.CloseBrace();
Expand All @@ -1019,7 +1020,7 @@ private static void GenerateEvent(CodeWriter writer, MockEventModel evt)

// Event add/remove accessors
writer.AppendLineIfNotEmpty(evt.ObsoleteAttribute);
writer.AppendLine($"public event {evt.EventHandlerTypeNonNullable}? {evt.Name}");
writer.AppendLine($"public event {evt.EventHandlerTypeNonNullable}? {EscapeIdentifier(evt.Name)}");
writer.OpenBrace();
writer.AppendLine($"add {{ _backing_{evt.Name} += value; _engine.RecordEventSubscription(\"{evt.Name}\", true); }}");
writer.AppendLine($"remove {{ _backing_{evt.Name} -= value; _engine.RecordEventSubscription(\"{evt.Name}\", false); }}");
Expand Down Expand Up @@ -1053,7 +1054,7 @@ private static void GeneratePartialEvent(CodeWriter writer, MockEventModel evt)

// Event add/remove accessors with override
writer.AppendLineIfNotEmpty(evt.ObsoleteAttribute);
writer.AppendLine($"public override event {evt.EventHandlerTypeNonNullable}? {evt.Name}");
writer.AppendLine($"public override event {evt.EventHandlerTypeNonNullable}? {EscapeIdentifier(evt.Name)}");
writer.OpenBrace();
writer.AppendLine($"add {{ _backing_{evt.Name} += value; _engine.RecordEventSubscription(\"{evt.Name}\", true); }}");
writer.AppendLine($"remove {{ _backing_{evt.Name} -= value; _engine.RecordEventSubscription(\"{evt.Name}\", false); }}");
Expand Down
3 changes: 2 additions & 1 deletion TUnit.Mocks.SourceGenerator/Builders/MockMembersBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Immutable;
using System.Linq;
using TUnit.Mocks.SourceGenerator.Models;
using static TUnit.Mocks.SourceGenerator.IdentifierEscaping;

namespace TUnit.Mocks.SourceGenerator.Builders;

Expand Down Expand Up @@ -137,7 +138,7 @@ private static string GetWrapperName(string safeName, MockMemberModel method)
=> $"{safeName}_{method.Name}_M{method.MemberId}_MockCall";

private static string GetSafeMemberName(string name)
=> MockMemberNames.Contains(name) ? name + "_" : name;
=> EscapeIdentifier(MockMemberNames.Contains(name) ? name + "_" : name);

private static string GetCombinedTypeParameterList(MockTypeModel model, MockMemberModel method)
{
Expand Down
Loading