Skip to content

Commit

Permalink
More interceptor tests (#104)
Browse files Browse the repository at this point in the history
* Move interceptor tests to separate test class

* Add interception of HasFlag

* Add global prefix

* Add more interceptor nuget tests
  • Loading branch information
andrewlock authored Oct 14, 2024
1 parent 121ec9f commit 6179b2d
Show file tree
Hide file tree
Showing 14 changed files with 1,248 additions and 362 deletions.
12 changes: 9 additions & 3 deletions src/NetEscapades.EnumGenerators/EnumGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,19 +309,25 @@ static void Execute(in EnumToGenerate enumToGenerate, SourceProductionContext co

#if INTERCEPTORS
private static bool InterceptorPredicate(SyntaxNode node, CancellationToken ct) =>
node is InvocationExpressionSyntax {Expression: MemberAccessExpressionSyntax {Name.Identifier.ValueText: "ToString"}};
node is InvocationExpressionSyntax {Expression: MemberAccessExpressionSyntax {Name.Identifier.ValueText: "ToString" or "HasFlag"}};

private static CandidateInvocation? InterceptorParser(GeneratorSyntaxContext ctx, CancellationToken ct)
{
if (ctx.Node is InvocationExpressionSyntax {Expression: MemberAccessExpressionSyntax {Name: { } nameSyntax}} invocation
&& ctx.SemanticModel.GetOperation(ctx.Node, ct) is IInvocationOperation targetOperation
&& targetOperation.TargetMethod is {Name : "ToString", ContainingType: {Name: "Enum", ContainingNamespace: {Name: "System", ContainingNamespace.IsGlobalNamespace: true}}}
&& targetOperation.TargetMethod is {Name : "ToString" or "HasFlag", ContainingType: {Name: "Enum", ContainingNamespace: {Name: "System", ContainingNamespace.IsGlobalNamespace: true}}}
&& targetOperation.Instance?.Type is { } type
#pragma warning disable RSEXPERIMENTAL002 // / Experimental interceptable location API
&& ctx.SemanticModel.GetInterceptableLocation(invocation) is { } location)
#pragma warning restore RSEXPERIMENTAL002
{
return new CandidateInvocation(location, type.ToString());
var targetToIntercept = targetOperation.TargetMethod.Name switch
{
"ToString" => InterceptorTarget.ToString,
"HasFlag" => InterceptorTarget.HasFlag,
_ => default, // can't be reached
};
return new CandidateInvocation(location, type.ToString(), targetToIntercept);
}

return null;
Expand Down
10 changes: 8 additions & 2 deletions src/NetEscapades.EnumGenerators/EnumToGenerate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public EnumToGenerate(

#if INTERCEPTORS
#pragma warning disable RSEXPERIMENTAL002 // / Experimental interceptable location API
public record CandidateInvocation(InterceptableLocation Location, string EnumName);
public record CandidateInvocation(InterceptableLocation Location, string EnumName, InterceptorTarget Target);
#pragma warning restore RSEXPERIMENTAL002

public record MethodToIntercept(
Expand All @@ -55,5 +55,11 @@ public MethodToIntercept(EquatableArray<CandidateInvocation> invocations, EnumTo
: this(invocations, enumToGenerate.Name, enumToGenerate.FullyQualifiedName, enumToGenerate.Namespace)
{
}
};
}

public enum InterceptorTarget
{
ToString,
HasFlag,
}
#endif
50 changes: 44 additions & 6 deletions src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -870,25 +870,63 @@ public InterceptsLocationAttribute(int version, string data)
}
namespace NetEscapades.EnumGenerators
{
static class EnumInterceptors
static file class EnumInterceptors
{
""");

bool toStringIntercepted = false;
foreach (var location in toIntercept.Invocations)
{
// locations are 0 based but we need 1 based
sb.AppendLine($""" [global::System.Runtime.CompilerServices.InterceptsLocation({location!.Location.Version}, "{location!.Location.Data}")] // {location.Location.GetDisplayLocation()}""");
if(location!.Target == InterceptorTarget.ToString)
{
toStringIntercepted = true;
sb.AppendLine(GetInterceptorAttr(location));
}
}

if(toStringIntercepted)
{
sb.AppendLine(
$$"""
public static string {{toIntercept.ExtensionTypeName}}ToString(this global::System.Enum value)
=> global::{{toIntercept.EnumNamespace}}.{{toIntercept.ExtensionTypeName}}.ToStringFast((global::{{toIntercept.FullyQualifiedName}})value);
""");
}

bool hasFlagIntercepted = false;
foreach (var location in toIntercept.Invocations)
{
if(location!.Target == InterceptorTarget.HasFlag)
{
hasFlagIntercepted = true;
sb.AppendLine(GetInterceptorAttr(location));
}
}

if(hasFlagIntercepted)
{
sb.AppendLine(
$$"""
public static bool {{toIntercept.ExtensionTypeName}}HasFlag(this global::System.Enum value, global::System.Enum flag)
=> global::{{toIntercept.EnumNamespace}}.{{toIntercept.ExtensionTypeName}}.HasFlagFast((global::{{toIntercept.FullyQualifiedName}})value, (global::{{toIntercept.FullyQualifiedName}})flag);
""");
}

sb.AppendLine(
$$"""
public static string {{toIntercept.ExtensionTypeName}}ToString(this global::System.Enum value)
=> {{toIntercept.EnumNamespace}}.{{toIntercept.ExtensionTypeName}}.ToStringFast(({{toIntercept.FullyQualifiedName}})value);
}
}
""");
return sb.ToString();

static string GetInterceptorAttr(CandidateInvocation location)
{
// locations are 0 based but we need 1 based
return $""" [global::System.Runtime.CompilerServices.InterceptsLocation({location.Location.Version}, "{location.Location.Data}")] // {location.Location.GetDisplayLocation()}""";
}
}
#endif
}
Original file line number Diff line number Diff line change
@@ -1,63 +1,198 @@
using Foo;
using System;
using System.ComponentModel.DataAnnotations;
using System.IO;
using NetEscapades.EnumGenerators;
using Xunit;

[assembly:NetEscapades.EnumGenerators.EnumExtensions<DateTimeKind>()]
[assembly:NetEscapades.EnumGenerators.EnumExtensions<System.IO.FileShare>()]

namespace Foo
{
// causes Error CS0426 : The type name 'TestEnum' does not exist in the type 'Foo'.
// workaround is to use global prefix
public class Foo { }

[EnumExtensions]
public enum EnumInFoo
{
First = 0,
[Display(Name = "2nd")]
Second = 1,
Third = 2,
}
}

#if INTERCEPTORS && NUGET_INTERCEPTOR_TESTS
namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests;
namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests
#elif INTERCEPTORS && INTERCEPTOR_TESTS
namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests;
namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests
#elif NUGET_INTERCEPTOR_TESTS
namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests.Roslyn4_4;
namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests.Roslyn4_4
#elif INTERCEPTOR_TESTS
namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests.Roslyn4_4;
namespace NetEscapades.EnumGenerators.Interceptors.IntegrationTests.Roslyn4_4
#else
#error Unknown project combination
#endif

[EnumExtensions]
public enum EnumWithDisplayNameInNamespace
{
First = 0,
[EnumExtensions]
public enum EnumWithDisplayNameInNamespace
{
First = 0,

[Display(Name = "2nd")]
Second = 1,
[Display(Name = "2nd")] Second = 1,

Third = 2,
}
Third = 2,
}

public class InterceptorTests
{
[EnumExtensions(ExtensionClassName="SomeExtension", ExtensionClassNamespace = "SomethingElse")]
public enum EnumWithExtensionInOtherNamespace
{
First = 0,

[Display(Name = "2nd")] Second = 1,

Third = 2,
}

[EnumExtensions]
[Flags]
public enum FlagsEnum
{
None = 0,
First = 1,
Second = 2,
}

[EnumExtensions]
public enum StringTesting
{
[System.ComponentModel.Description("Quotes \"")]
Quotes,

[System.ComponentModel.Description(@"Literal Quotes """)]
LiteralQuotes,

[System.ComponentModel.Description("Backslash \\")]
Backslash,

[System.ComponentModel.Description(@"LiteralBackslash \")]
BackslashLiteral,
}

public class InterceptorTests
{
#if INTERCEPTORS
[Fact]
[Fact]
#else
[Fact(Skip = "Interceptors are supported in this SDK")]
[Fact(Skip = "Interceptors are supported in this SDK")]
#endif
public void CallingToStringIsIntercepted()
{
var result1 = EnumWithDisplayNameInNamespace.Second.ToString();
var result2 = EnumWithDisplayNameInNamespace.Second.ToStringFast();
Assert.Equal(result1, result2);
public void CallingToStringIsIntercepted()
{
var result1 = EnumWithDisplayNameInNamespace.Second.ToString();
var result2 = EnumWithDisplayNameInNamespace.Second.ToStringFast();
Assert.Equal(result1, result2);

AssertValue(EnumWithDisplayNameInNamespace.First);
AssertValue(EnumWithDisplayNameInNamespace.Second);
AssertValue(EnumWithDisplayNameInNamespace.Third);

AssertValue(EnumWithDisplayNameInNamespace.First);
AssertValue(EnumWithDisplayNameInNamespace.Second);
AssertValue(EnumWithDisplayNameInNamespace.Third);
void AssertValue(EnumWithDisplayNameInNamespace value)
{
// These return different values when interception is not enabled
var toString = value.ToString();
var fast = value.ToStringFast();
Assert.Equal(fast, toString);
}
}

void AssertValue(EnumWithDisplayNameInNamespace value)
#if INTERCEPTORS
[Fact]
#else
[Fact(Skip = "Interceptors are supported in this SDK")]
#endif
public void CallingToStringIsIntercepted_StringTesting()
{
// These return different values when interception is not enabled
var toString = value.ToString();
var fast = value.ToStringFast();
Assert.Equal(fast, toString);
var result1 = StringTesting.Backslash.ToString();
var result2 = StringTesting.Backslash.ToStringFast();
Assert.Equal(result1, result2);
}
}

#if INTERCEPTORS
[Fact]
#else
[Fact(Skip = "Interceptors are supported in this SDK")]
#endif
public void CallingToStringIsIntercepted_EnumInFoo()
{
var result1 = EnumInFoo.Second.ToString();
var result2 = EnumInFoo.Second.ToStringFast();
Assert.Equal(result1, result2);
}

#if INTERCEPTORS
[Fact]
#else
[Fact(Skip = "Interceptors are supported in this SDK")]
#endif
public void CallingToStringIsIntercepted_EnumWithExtensionInOtherNamespace()
{
var result1 = EnumWithExtensionInOtherNamespace.Second.ToString();
var result2 = SomethingElse.SomeExtension.ToStringFast(EnumWithExtensionInOtherNamespace.Second);
Assert.Equal(result1, result2);
}

[Fact]
public void CallingToStringIsIntercepted_ExternalEnum()
{
// This doesn't _actually_ test interception, because can't
// differentiate with built-in version, it's only really verifying the generated code compiles
var result1 = DateTimeKind.Local.ToString();
var result2 = DateTimeKind.Local.ToStringFast();
Assert.Equal(result1, result2);
}

[Fact]
public void CallingHasFlagIsIntercepted()
{
// This doesn't _actually_ test interception, because can't
// differentiate with built-in version, it's only really verifying the generated code compiles
var value1 = FlagsEnum.First;
var result2 = FlagsEnum.Second.HasFlag(value1);
Assert.False(result2);
Assert.True(value1.HasFlag(FlagsEnum.None));

var combined = FlagsEnum.First | FlagsEnum.Second;
Assert.True(combined.HasFlag(FlagsEnum.First));
Assert.False(FlagsEnum.First.HasFlag(combined));
}

[Fact]
public void CallingHasFlagIsIntercepted_ExternalEnumFlags()
{
// This doesn't _actually_ test interception, because can't
// differentiate with built-in version, it's only really verifying the generated code compiles
var value1 = FileShare.Read;
var result2 = FileShare.Write.HasFlag(value1);
Assert.False(result2);
Assert.True(value1.HasFlag(FileShare.None));

var combined = FileShare.Read | FileShare.Write;
Assert.True(combined.HasFlag(FileShare.Read));
Assert.False(FileShare.Read.HasFlag(combined));
}

#if INTERCEPTORS
[Fact(Skip = "Interceptors are supported in this SDK")]
#else
[Fact]
#endif
public void CallingToStringIsNotIntercepted()
{
var result1 = EnumWithDisplayNameInNamespace.Second.ToString();
var result2 = EnumWithDisplayNameInNamespace.Second.ToStringFast();
Assert.NotEqual(result1, result2);
public void CallingToStringIsNotIntercepted()
{
var result1 = EnumWithDisplayNameInNamespace.Second.ToString();
var result2 = EnumWithDisplayNameInNamespace.Second.ToStringFast();
Assert.NotEqual(result1, result2);
}
}
}
Loading

0 comments on commit 6179b2d

Please sign in to comment.