Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Generic Attributes #81

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ public sealed class Person
```

### Create a partial interface
And annotate this with `ProxyInterfaceGenerator.Proxy[...]` and with the Type which needs to be wrapped:

#### Annotate with `[ProxyInterfaceGenerator.Proxy(typeof(...)]`
And annotate this partial interface with `[ProxyInterfaceGenerator.Proxy(typeof(...))]` and with the Type which needs to be wrapped:

``` c#
[ProxyInterfaceGenerator.Proxy(typeof(Person))]
Expand All @@ -35,6 +37,16 @@ public partial interface IPerson
}
```

#### Annotate with `[ProxyInterfaceGenerator.Proxy<...>]`
Since version 0.5.0 it's also possible to use the generic version of the attribute:
``` c#
[ProxyInterfaceGenerator.Proxy<Person>()]
public partial interface IPerson
{
}
```


#### ProxyBaseClasses
In case also want to proxy the properties/methods/events from the base class(es), use this:

Expand Down
9 changes: 9 additions & 0 deletions src-examples/ProxyInterfaceConsumer/IPersonGeneric.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using ProxyInterfaceGenerator;

namespace ProxyInterfaceConsumer
{
[Proxy<Person2>()]
public partial interface IPersonGeneric
{
}
}
7 changes: 7 additions & 0 deletions src-examples/ProxyInterfaceConsumer/Person2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace ProxyInterfaceConsumer
{
public class Person2
{
public int Id { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Mapster" Version="7.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.10.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.10.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.12.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using ProxyInterfaceSourceGenerator.Extensions;
using ProxyInterfaceSourceGenerator.Models;

namespace ProxyInterfaceSourceGenerator.FileGenerators;
Expand All @@ -6,23 +7,11 @@ internal class ExtraFilesGenerator : IFileGenerator
{
private const string Name = "ProxyInterfaceGenerator.Extra.g.cs";

public FileData GenerateFile(bool supportsNullable)
public FileData GenerateFile(bool supportsNullable, bool supportsGenericAttributes)
{
var stringArray = supportsNullable ? "string[]?" : "string[]";

return new FileData($"{Name}", $@"//----------------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by https://github.com/StefH/ProxyInterfaceSourceGenerator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//----------------------------------------------------------------------------------------

using System;

namespace ProxyInterfaceGenerator
{{
var attribute = $@"
[AttributeUsage(AttributeTargets.Interface)]
internal sealed class ProxyAttribute : Attribute
{{
Expand Down Expand Up @@ -62,7 +51,65 @@ public ProxyAttribute(Type type, bool proxyBaseClasses, ProxyClassAccessibility
Accessibility = accessibility;
MembersToIgnore = membersToIgnore;
}}
}}
}}";

var genericAttribute = $@"
[AttributeUsage(AttributeTargets.Interface)]
internal sealed class ProxyAttribute<T> : Attribute where T : class
{{
public bool ProxyBaseClasses {{ get; }}
public ProxyClassAccessibility Accessibility {{ get; }}
public {stringArray} MembersToIgnore {{ get; }}

public ProxyAttribute() : this(false, ProxyClassAccessibility.Public)
{{
}}

public ProxyAttribute(bool proxyBaseClasses) : this(type, proxyBaseClasses, ProxyClassAccessibility.Public)
{{
}}

public ProxyAttribute(ProxyClassAccessibility accessibility) : this(type, false, accessibility)
{{
}}

public ProxyAttribute(ProxyClassAccessibility accessibility, {stringArray} membersToIgnore) : this(type, false, accessibility, membersToIgnore)
{{
}}

public ProxyAttribute(bool proxyBaseClasses, ProxyClassAccessibility accessibility) : this(type, proxyBaseClasses, accessibility, null)
{{
}}

public ProxyAttribute({stringArray} membersToIgnore) : this(type, false, ProxyClassAccessibility.Public, null)
{{
}}

public ProxyAttribute(bool proxyBaseClasses, ProxyClassAccessibility accessibility, {stringArray} membersToIgnore)
{{
Type = typeof(T);
ProxyBaseClasses = proxyBaseClasses;
Accessibility = accessibility;
MembersToIgnore = membersToIgnore;
}}
}}";

return new FileData($"{Name}", $@"//----------------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by https://github.com/StefH/ProxyInterfaceSourceGenerator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//----------------------------------------------------------------------------------------

using System;

namespace ProxyInterfaceGenerator
{{
{attribute}

{supportsGenericAttributes.IIf(genericAttribute)}

[Flags]
internal enum ProxyClassAccessibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ namespace ProxyInterfaceSourceGenerator.FileGenerators;

internal interface IFileGenerator
{
FileData GenerateFile(bool supportsNullable);
FileData GenerateFile(bool supportsNullable, bool supportsGenericAttributes);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ public void Execute(GeneratorExecutionContext context)
throw new NotSupportedException($"Only {nameof(ProxySyntaxReceiver)} is supported.");
}

// https://github.com/reactiveui/refit/blob/main/InterfaceStubGenerator.Core/InterfaceStubGenerator.cs
var supportsNullable = csharpParseOptions.LanguageVersion >= LanguageVersion.CSharp8;
var supportsGenericAttributes = csharpParseOptions.LanguageVersion >= LanguageVersion.CSharp11;

GenerateProxyAttribute(context, receiver, supportsNullable);
GenerateProxyAttribute(context, receiver, supportsNullable, supportsGenericAttributes);
GeneratePartialInterfaces(context, receiver, supportsNullable);
GenerateProxyClasses(context, receiver, supportsNullable);
}
Expand All @@ -56,15 +56,15 @@ public void Execute(GeneratorExecutionContext context)
}
}

private void GenerateProxyAttribute(GeneratorExecutionContext ctx, ProxySyntaxReceiver receiver, bool supportsNullable)
private void GenerateProxyAttribute(GeneratorExecutionContext ctx, ProxySyntaxReceiver receiver, bool supportsNullable, bool supportsGenericAttributes)
{
var context = new Context
{
GeneratorExecutionContext = ctx,
Candidates = receiver.CandidateInterfaces
};

var attributeData = _proxyAttributeGenerator.GenerateFile(supportsNullable);
var attributeData = _proxyAttributeGenerator.GenerateFile(supportsNullable, supportsGenericAttributes);
context.GeneratorExecutionContext.AddSource(attributeData.FileName, SourceText.From(attributeData.Text, Encoding.UTF8));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Version>0.4.0</Version>
<Version>0.5.0-preview-01</Version>
<TargetFramework>netstandard2.0</TargetFramework>
<ProjectGuid>{12344228-91F4-4502-9595-39584E5ABB34}</ProjectGuid>
<LangVersion>10</LangVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Authors>Stef Heyenrath</Authors>
<Description></Description>
Expand All @@ -26,7 +26,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Configurations>Debug;Release;DebugAttach</Configurations>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<EnforceExtendedAnalyzerRules>false</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
Expand All @@ -43,11 +43,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,52 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using ProxyInterfaceSourceGenerator.Extensions;
using ProxyInterfaceSourceGenerator.Types;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace ProxyInterfaceSourceGenerator.SyntaxReceiver;

internal static class AttributeArgumentListParser
{
public static ProxyInterfaceGeneratorAttributeArguments ParseAttributeArguments(AttributeArgumentListSyntax? argumentList, SemanticModel semanticModel)
private static readonly Regex ProxyAttributesRegex = new(@"^ProxyInterfaceGenerator\.Proxy|Proxy(?:<([^>]+)>)?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));

public static bool IsMatch(AttributeSyntax attributeSyntax)
{
if (argumentList is null || argumentList.Arguments.Count is < 1 or > 4)
return ProxyAttributesRegex.IsMatch(attributeSyntax.Name.ToString());
}

public static ProxyInterfaceGeneratorAttributeArguments Parse(AttributeSyntax? attributeSyntax, SemanticModel semanticModel)
{
if (attributeSyntax == null)
{
throw new ArgumentException("The ProxyAttribute requires 1, 2, 3 or 4 arguments.");
throw new ArgumentNullException(nameof(attributeSyntax));
}

ProxyInterfaceGeneratorAttributeArguments result;
if (TryParseAsType(argumentList.Arguments[0].Expression, semanticModel, out var fullyQualifiedDisplayString, out var metadataName))
int skip = 0;
ProxyInterfaceGeneratorAttributeArguments? result;
if (TryParseAsType(attributeSyntax.Name, semanticModel, out var infoGeneric))
{
result = new ProxyInterfaceGeneratorAttributeArguments(infoGeneric.Value.FullyQualifiedDisplayString, infoGeneric.Value.MetadataName);
}
else if (attributeSyntax.ArgumentList == null || attributeSyntax.ArgumentList.Arguments.Count is < 1 or > 4)
{
result = new ProxyInterfaceGeneratorAttributeArguments(fullyQualifiedDisplayString, metadataName);
throw new ArgumentException("The ProxyAttribute requires 1, 2, 3 or 4 arguments.");
}
else if (TryParseAsType(attributeSyntax.ArgumentList.Arguments[0].Expression, semanticModel, out var info))
{
skip = 1;
result = new ProxyInterfaceGeneratorAttributeArguments(info.Value.FullyQualifiedDisplayString, info.Value.MetadataName);
}
else
{
throw new ArgumentException("The first argument from the ProxyAttribute should be a Type.");
}

foreach (var argument in argumentList.Arguments.Skip(1))
var array = attributeSyntax.ArgumentList?.Arguments.ToArray() ?? [];

foreach (var argument in array.Skip(skip))
{
if (TryParseAsStringArray(argument.Expression, out var membersToIgnore))
{
Expand Down Expand Up @@ -83,22 +102,38 @@ private static bool TryParseAsBoolean(ExpressionSyntax expressionSyntax, out boo
return false;
}

private static bool TryParseAsType(ExpressionSyntax expressionSyntax, SemanticModel semanticModel, [NotNullWhen(true)] out string? fullyQualifiedDisplayString, [NotNullWhen(true)] out string? metadataName)
private static bool TryParseAsType(
CSharpSyntaxNode? syntaxNode,
SemanticModel semanticModel,
[NotNullWhen(true)] out (string FullyQualifiedDisplayString, string MetadataName, bool IsGeneric)? info
)
{
fullyQualifiedDisplayString = null;
metadataName = null;
info = null;

if (expressionSyntax is TypeOfExpressionSyntax typeOfExpressionSyntax)
bool isGeneric;
TypeSyntax typeSyntax;
switch (syntaxNode)
{
var typeInfo = semanticModel.GetTypeInfo(typeOfExpressionSyntax.Type);
var typeSymbol = typeInfo.Type!;
metadataName = typeSymbol.GetFullMetadataName();
fullyQualifiedDisplayString = typeSymbol.ToFullyQualifiedDisplayString();

return true;
case TypeOfExpressionSyntax typeOfExpressionSyntax:
typeSyntax = typeOfExpressionSyntax.Type;
isGeneric = false;
break;

case QualifiedNameSyntax { Right: GenericNameSyntax genericRightNameSyntax }:
typeSyntax = genericRightNameSyntax.TypeArgumentList.Arguments.First();
isGeneric = true;
break;

default:
return false;
}

return false;
var typeInfo = semanticModel.GetTypeInfo(typeSyntax);
var typeSymbol = typeInfo.Type!;

info = new(typeSymbol.ToFullyQualifiedDisplayString(), typeSymbol.GetFullMetadataName(), isGeneric);

return true;
}

private static bool TryParseAsEnum<TEnum>(ExpressionSyntax expressionSyntax, out TEnum value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ namespace ProxyInterfaceSourceGenerator.SyntaxReceiver;
internal class ProxySyntaxReceiver : ISyntaxContextReceiver
{
private const string GlobalPrefix = "global::";
private static readonly string[] GenerateProxyAttributes = { "ProxyInterfaceGenerator.Proxy", "Proxy" };
private static readonly string[] Modifiers = { "public", "partial" };
private static readonly string[] Modifiers = ["public", "partial"];
public IDictionary<InterfaceDeclarationSyntax, ProxyData> CandidateInterfaces { get; } = new Dictionary<InterfaceDeclarationSyntax, ProxyData>();

public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
Expand Down Expand Up @@ -39,7 +38,8 @@ private static bool TryGet(InterfaceDeclarationSyntax interfaceDeclarationSyntax
return false;
}

var attributeList = interfaceDeclarationSyntax.AttributeLists.FirstOrDefault(x => x.Attributes.Any(a => GenerateProxyAttributes.Contains(a.Name.ToString())));
var attributeList = interfaceDeclarationSyntax.AttributeLists
.FirstOrDefault(x => x.Attributes.Any(AttributeArgumentListParser.IsMatch));
if (attributeList is null)
{
// InterfaceDeclarationSyntax should have the correct attribute
Expand All @@ -58,11 +58,11 @@ private static bool TryGet(InterfaceDeclarationSyntax interfaceDeclarationSyntax
{
foreach (var @using in cc.Usings)
{
usings.Add(@using.Name.ToString());
usings.Add(@using.Name!.ToString());
}
}

var fluentBuilderAttributeArguments = AttributeArgumentListParser.ParseAttributeArguments(attributeList.Attributes.FirstOrDefault()?.ArgumentList, semanticModel);
var fluentBuilderAttributeArguments = AttributeArgumentListParser.Parse(attributeList.Attributes.FirstOrDefault(), semanticModel);

var metadataName = fluentBuilderAttributeArguments.MetadataName;
var globalNamespace = string.IsNullOrEmpty(ns) ? string.Empty : $"{GlobalPrefix}{ns}";
Expand Down
Loading