Skip to content

Commit

Permalink
Allow intercepting enums from other projects (#106)
Browse files Browse the repository at this point in the history
* Add interceptable attribute

* Add additional interceptors and unit test

* Add integration tests
  • Loading branch information
andrewlock authored Oct 17, 2024
1 parent c8d69fe commit 31e3977
Show file tree
Hide file tree
Showing 26 changed files with 1,597 additions and 184 deletions.
31 changes: 24 additions & 7 deletions NetEscapades.EnumGenerators.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "_build", "build\_build.cspr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEscapades.EnumGenerators.Benchmarks", "tests\NetEscapades.EnumGenerators.Benchmarks\NetEscapades.EnumGenerators.Benchmarks.csproj", "{C7CFC4AC-BC5B-4F07-BE93-4F245D5547D9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEscapades.EnumGenerators.Attributes", "src\NetEscapades.EnumGenerators.Attributes\NetEscapades.EnumGenerators.Attributes.csproj", "{8B6E63F3-EB6A-4A72-86B1-A49E53C62305}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEscapades.EnumGenerators.Nuget.Attributes.IntegrationTests", "tests\NetEscapades.EnumGenerators.Nuget.Attributes.IntegrationTests\NetEscapades.EnumGenerators.Nuget.Attributes.IntegrationTests.csproj", "{E7D66FE9-65BC-4DC8-AC86-5C5BE10D8E61}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{AC0DDDB0-385D-43B8-A770-C79EE2077D05}"
Expand All @@ -41,6 +39,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEscapades.EnumGenerators
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEscapades.EnumGenerators.Interceptors.IntegrationTests", "tests\NetEscapades.EnumGenerators.Interceptors.IntegrationTests\NetEscapades.EnumGenerators.Interceptors.IntegrationTests.csproj", "{E390E06A-27EE-49B3-9113-37812B92060A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEscapades.EnumGenerators.NetStandard.Interceptors.IntegrationTests", "tests\NetEscapades.EnumGenerators.NetStandard.Interceptors.IntegrationTests\NetEscapades.EnumGenerators.NetStandard.Interceptors.IntegrationTests.csproj", "{B7C9B52C-94C5-4729-9C5E-F49B3A112130}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEscapades.EnumGenerators.Attributes", "src\NetEscapades.EnumGenerators.Attributes\NetEscapades.EnumGenerators.Attributes.csproj", "{EA85D7C6-8691-47B9-92D1-DCDA98E16D02}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEscapades.EnumGenerators.Nuget.NetStandard", "tests\NetEscapades.EnumGenerators.Nuget.NetStandard\NetEscapades.EnumGenerators.Nuget.NetStandard.csproj", "{0011A3F4-BBFC-40CE-B8A3-A23FDAC60DE7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests", "tests\NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests\NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests.csproj", "{99AC1B59-04D1-4B19-957B-D7DE4D19A7CD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -70,10 +76,6 @@ Global
{C7CFC4AC-BC5B-4F07-BE93-4F245D5547D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C7CFC4AC-BC5B-4F07-BE93-4F245D5547D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C7CFC4AC-BC5B-4F07-BE93-4F245D5547D9}.Release|Any CPU.Build.0 = Release|Any CPU
{8B6E63F3-EB6A-4A72-86B1-A49E53C62305}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8B6E63F3-EB6A-4A72-86B1-A49E53C62305}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8B6E63F3-EB6A-4A72-86B1-A49E53C62305}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8B6E63F3-EB6A-4A72-86B1-A49E53C62305}.Release|Any CPU.Build.0 = Release|Any CPU
{E7D66FE9-65BC-4DC8-AC86-5C5BE10D8E61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7D66FE9-65BC-4DC8-AC86-5C5BE10D8E61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BDF8C04B-D0E3-4FF5-82C3-E8FDF3916C16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -106,14 +108,25 @@ Global
{E390E06A-27EE-49B3-9113-37812B92060A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E390E06A-27EE-49B3-9113-37812B92060A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E390E06A-27EE-49B3-9113-37812B92060A}.Release|Any CPU.Build.0 = Release|Any CPU
{B7C9B52C-94C5-4729-9C5E-F49B3A112130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B7C9B52C-94C5-4729-9C5E-F49B3A112130}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7C9B52C-94C5-4729-9C5E-F49B3A112130}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7C9B52C-94C5-4729-9C5E-F49B3A112130}.Release|Any CPU.Build.0 = Release|Any CPU
{EA85D7C6-8691-47B9-92D1-DCDA98E16D02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA85D7C6-8691-47B9-92D1-DCDA98E16D02}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA85D7C6-8691-47B9-92D1-DCDA98E16D02}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA85D7C6-8691-47B9-92D1-DCDA98E16D02}.Release|Any CPU.Build.0 = Release|Any CPU
{0011A3F4-BBFC-40CE-B8A3-A23FDAC60DE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0011A3F4-BBFC-40CE-B8A3-A23FDAC60DE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{99AC1B59-04D1-4B19-957B-D7DE4D19A7CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{99AC1B59-04D1-4B19-957B-D7DE4D19A7CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{FC6F4934-4512-45DB-ABDA-5FAD96742B73} = {78D56637-F921-437D-A9AC-FBEAD78344BE}
{443BC4C1-CF11-45E3-B772-72F886826C69} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
{F03831B0-630E-4B0E-BD25-AFE975B35E69} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
{328EFEB4-1D15-453E-9929-6FD0FACF3512} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
{C7CFC4AC-BC5B-4F07-BE93-4F245D5547D9} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
{8B6E63F3-EB6A-4A72-86B1-A49E53C62305} = {78D56637-F921-437D-A9AC-FBEAD78344BE}
{E7D66FE9-65BC-4DC8-AC86-5C5BE10D8E61} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
{BDF8C04B-D0E3-4FF5-82C3-E8FDF3916C16} = {AC0DDDB0-385D-43B8-A770-C79EE2077D05}
{40B2D8D1-7523-498D-9753-CECC3A44071A} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
Expand All @@ -123,5 +136,9 @@ Global
{3B52B2FC-007D-4CEE-9EF8-20E94F762850} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
{F8B8BAC8-5F2D-42DE-8234-16C8C1D11520} = {78D56637-F921-437D-A9AC-FBEAD78344BE}
{E390E06A-27EE-49B3-9113-37812B92060A} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
{B7C9B52C-94C5-4729-9C5E-F49B3A112130} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
{EA85D7C6-8691-47B9-92D1-DCDA98E16D02} = {78D56637-F921-437D-A9AC-FBEAD78344BE}
{0011A3F4-BBFC-40CE-B8A3-A23FDAC60DE7} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
{99AC1B59-04D1-4B19-957B-D7DE4D19A7CD} = {FF6F43E8-36EA-4BF0-9B2E-8ACC91C5F939}
EndGlobalSection
EndGlobal
1 change: 1 addition & 0 deletions build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class Build : NukeBuild
Solution.tests.NetEscapades_EnumGenerators_Nuget_IntegrationTests.Path,
Solution.tests.NetEscapades_EnumGenerators_Nuget_Attributes_IntegrationTests.Path,
Solution.tests.NetEscapades_EnumGenerators_Nuget_Interceptors_IntegrationTests.Path,
Solution.tests.NetEscapades_EnumGenerators_Nuget_NetStandard_Interceptors_IntegrationTests.Path,
};
if (!string.IsNullOrEmpty(PackagesDirectory))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace NetEscapades.EnumGenerators
{
/// <summary>
/// Add to an assembly to indicate that usages of the enum should
/// be automatically intercepted to use the extension methods
/// generated by <see cref="EnumExtensionsAttribute"/> in this project.
/// Note that the extension methods must be accessible from this project,
/// otherwise you will receive compilation errors
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Assembly, AllowMultiple = true)]
[System.Diagnostics.Conditional("NETESCAPADES_ENUMGENERATORS_USAGES")]
public class InterceptableAttribute<T> : System.Attribute
where T: System.Enum
{
/// <summary>
/// The namespace generated for the extension class.
/// If not provided, the namespace of the enum will be assumed.
/// </summary>
public string? ExtensionClassNamespace { get; set; }

/// <summary>
/// The name used for the extension class.
/// If not provided, the enum name with ""Extensions"" will be assumed.
/// For example for an Enum called StatusCodes, the assumed name
/// will be StatusCodesExtensions.
/// </summary>
public string? ExtensionClassName { get; set; }
}
}
36 changes: 29 additions & 7 deletions src/NetEscapades.EnumGenerators/EnumGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class EnumGenerator : IIncrementalGenerator
private const string DescriptionAttribute = "System.ComponentModel.DescriptionAttribute";
private const string EnumExtensionsAttribute = "NetEscapades.EnumGenerators.EnumExtensionsAttribute";
private const string ExternalEnumExtensionsAttribute = "NetEscapades.EnumGenerators.EnumExtensionsAttribute`1";
private const string InterceptableAttribute = "NetEscapades.EnumGenerators.InterceptableAttribute`1";
private const string FlagsAttribute = "System.FlagsAttribute";

public void Initialize(IncrementalGeneratorInitializationContext context)
Expand All @@ -25,7 +26,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
IncrementalValuesProvider<EnumToGenerate> enumsToGenerate = context.SyntaxProvider
.ForAttributeWithMetadataName(
EnumExtensionsAttribute,
predicate: (node, _) => node is EnumDeclarationSyntax,
predicate: static (node, _) => node is EnumDeclarationSyntax,
transform: GetTypeToGenerate)
.WithTrackingName(TrackingNames.InitialExtraction)
.Where(static m => m is not null)
Expand All @@ -36,12 +37,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.SyntaxProvider
.ForAttributeWithMetadataName(
ExternalEnumExtensionsAttribute,
predicate: (node, _) => node is CompilationUnitSyntax,
transform: GetExternalTypeToGenerate)
predicate: static (node, _) => node is CompilationUnitSyntax,
transform: static (context1, ct) => GetEnumToGenerateFromGenericAssemblyAttribute(context1, ct, "EnumExtensionsAttribute", "EnumExtensions"))
.Where(static m => m is not null)
.SelectMany(static (m, _) => m!.Value)
.WithTrackingName(TrackingNames.InitialExternalExtraction);

context.RegisterSourceOutput(enumsToGenerate,
static (spc, enumToGenerate) => Execute(in enumToGenerate, spc));

Expand All @@ -62,6 +63,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.WithTrackingName(TrackingNames.Settings);

#if INTERCEPTORS
// TODO: add an analyzer for these for old roslyn
var interceptableEnums = context
.SyntaxProvider
.ForAttributeWithMetadataName(
InterceptableAttribute,
predicate: (node, _) => node is CompilationUnitSyntax,
transform: (context1, ct) => GetEnumToGenerateFromGenericAssemblyAttribute(context1, ct, "InterceptableAttribute", "Interceptable"))
.Where(static m => m is not null)
.SelectMany(static (m, _) => m!.Value)
.WithTrackingName(TrackingNames.InitialExternalExtraction);

var interceptionEnabled = settings
.Select((x, _) => x.Left && x.Right);

Expand All @@ -87,11 +99,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Where(x => x is not null)
.WithTrackingName(TrackingNames.ExternalInterceptions);

var additionalInterceptions = interceptableEnums
.Combine(locations)
.Select(FilterInterceptorCandidates!)
.Where(x => x is not null)
.WithTrackingName(TrackingNames.AdditionalInterceptions);

context.RegisterImplementationSourceOutput(enumInterceptions,
static (spc, toIntercept) => ExecuteInterceptors(toIntercept, spc));

context.RegisterImplementationSourceOutput(externalInterceptions,
static (spc, toIntercept) => ExecuteInterceptors(toIntercept, spc));

context.RegisterImplementationSourceOutput(additionalInterceptions,
static (spc, toIntercept) => ExecuteInterceptors(toIntercept, spc));
#endif
context.RegisterImplementationSourceOutput(settings,
static (spc, args) =>
Expand Down Expand Up @@ -119,13 +140,14 @@ static void Execute(in EnumToGenerate enumToGenerate, SourceProductionContext co
context.AddSource(enumToGenerate.Name + "_EnumExtensions.g.cs", SourceText.From(result, Encoding.UTF8));
}

static EquatableArray<EnumToGenerate>? GetExternalTypeToGenerate(GeneratorAttributeSyntaxContext context, CancellationToken ct)
static EquatableArray<EnumToGenerate>? GetEnumToGenerateFromGenericAssemblyAttribute(
GeneratorAttributeSyntaxContext context, CancellationToken ct, string fullAttributeName, string shortAttributeName)
{
List<EnumToGenerate>? enums = null;
foreach (AttributeData attribute in context.Attributes)
{
if (!((attribute.AttributeClass?.Name == "EnumExtensionsAttribute" ||
attribute.AttributeClass?.Name == "EnumExtensions") &&
if (!((attribute.AttributeClass?.Name == fullAttributeName ||
attribute.AttributeClass?.Name == shortAttributeName) &&
attribute.AttributeClass.IsGenericType &&
attribute.AttributeClass.TypeArguments.Length == 1))
{
Expand Down
32 changes: 32 additions & 0 deletions src/NetEscapades.EnumGenerators/SourceGenerationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,38 @@ public class EnumExtensionsAttribute<T> : System.Attribute
/// </summary>
public bool IsInterceptable { get; set; } = true;
}
/// <summary>
/// Add to an assembly to indicate that usages of the enum should
/// be automatically intercepted to use the extension methods
/// generated by <see cref=""EnumExtensionsAttribute""/> in this project.
/// Note that the extension methods must be accessible from this project,
/// otherwise you will receive compilation errors
/// </summary>
[global::System.AttributeUsage(global::System.AttributeTargets.Assembly, AllowMultiple = true)]
[global::System.Diagnostics.Conditional(""NETESCAPADES_ENUMGENERATORS_USAGES"")]
#if NET5_0_OR_GREATER
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage(Justification = ""Generated by the NetEscapades.EnumGenerators source generator."")]
#else
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
#endif
public class InterceptableAttribute<T> : global::System.Attribute
where T: global::System.Enum
{
/// <summary>
/// The namespace generated for the extension class.
/// If not provided, the namespace of the enum will be assumed.
/// </summary>
public string? ExtensionClassNamespace { get; set; }
/// <summary>
/// The name used for the extension class.
/// If not provided, the enum name with """"Extensions"""" will be assumed.
/// For example for an Enum called StatusCodes, the assumed name
/// will be StatusCodesExtensions.
/// </summary>
public string? ExtensionClassName { get; set; }
}
}
#endif
";
Expand Down
2 changes: 2 additions & 0 deletions src/NetEscapades.EnumGenerators/TrackingNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ public class TrackingNames
{
public const string InitialExtraction = nameof(InitialExtraction);
public const string InitialExternalExtraction = nameof(InitialExternalExtraction);
public const string InitialInterceptable = nameof(InitialInterceptable);
public const string RemovingNulls = nameof(RemovingNulls);
public const string InterceptedLocations = nameof(InterceptedLocations);
public const string Settings = nameof(Settings);
public const string EnumInterceptions = nameof(EnumInterceptions);
public const string ExternalInterceptions = nameof(ExternalInterceptions);
public const string AdditionalInterceptions = nameof(AdditionalInterceptions);
}
23 changes: 23 additions & 0 deletions tests/NetEscapades.EnumGenerators.IntegrationTests/Enums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ public enum EnumInSystem
Second = 1,
Third = 2,
}

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

namespace Foo
Expand All @@ -33,6 +41,7 @@ public class Foo
public enum EnumInFoo
{
First = 0,
[Display(Name = "2nd")]
Second = 1,
Third = 2,
}
Expand All @@ -51,6 +60,8 @@ namespace NetEscapades.EnumGenerators.Nuget.Attributes.IntegrationTests
namespace NetEscapades.EnumGenerators.Nuget.IntegrationTests
#elif NUGET_INTERCEPTOR_TESTS
namespace NetEscapades.EnumGenerators.Nuget.Interceptors.IntegrationTests
#elif NUGET_NETSTANDARD_INTERCEPTOR_TESTS
namespace NetEscapades.EnumGenerators.Nuget.NetStandard.Interceptors.IntegrationTests
#else
#error Unknown integration tests
#endif
Expand Down Expand Up @@ -109,18 +120,30 @@ public enum LongEnum: long
[Flags]
public enum FlagsEnum
{
None = 0,
First = 1 << 0,
Second = 1 << 1,
Third = 1 << 2,
Fourth = 1 << 3,
ThirdAndFourth = Third | Fourth,
}

[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,
}

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

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

Third = 2,
}
}
Loading

0 comments on commit 31e3977

Please sign in to comment.