Skip to content

Commit c9014f3

Browse files
authored
Add support for Generic Attributes (#81)
* . * . * p * ... * f * ok * re * syntaxNode * . * ...
1 parent eb5491a commit c9014f3

16 files changed

+302
-70
lines changed

ProxyInterfaceSourceGenerator Solution.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ EndProject
88
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2CE637DC-E8F5-4603-8B57-E51A32F631F1}"
99
ProjectSection(SolutionItems) = preProject
1010
.editorconfig = .editorconfig
11-
.github\workflows\CreateRelease.yml = .github\workflows\CreateRelease.yml
1211
.github\workflows\BuildAndTest.yml = .github\workflows\BuildAndTest.yml
12+
.github\workflows\CreateRelease.yml = .github\workflows\CreateRelease.yml
1313
Generate-ReleaseNotes.bat = Generate-ReleaseNotes.bat
1414
PackageReadme.md = PackageReadme.md
1515
PackageReleaseNotes.template = PackageReleaseNotes.template

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ public sealed class Person
2626
```
2727

2828
### Create a partial interface
29-
And annotate this with `ProxyInterfaceGenerator.Proxy[...]` and with the Type which needs to be wrapped:
29+
30+
#### Annotate with `[ProxyInterfaceGenerator.Proxy(typeof(...)]`
31+
And annotate this partial interface with `[ProxyInterfaceGenerator.Proxy(typeof(...))]` and with the Type which needs to be wrapped:
3032

3133
``` c#
3234
[ProxyInterfaceGenerator.Proxy(typeof(Person))]
@@ -35,6 +37,16 @@ public partial interface IPerson
3537
}
3638
```
3739

40+
#### Annotate with `[ProxyInterfaceGenerator.Proxy<...>]`
41+
Since version 0.5.0 it's also possible to use the generic version of the attribute:
42+
``` c#
43+
[ProxyInterfaceGenerator.Proxy<Person>()]
44+
public partial interface IPerson
45+
{
46+
}
47+
```
48+
49+
3850
#### ProxyBaseClasses
3951
In case also want to proxy the properties/methods/events from the base class(es), use this:
4052

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using ProxyInterfaceGenerator;
2+
3+
namespace ProxyInterfaceConsumer
4+
{
5+
[Proxy<Person2>()]
6+
public partial interface IPersonGeneric
7+
{
8+
}
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace ProxyInterfaceConsumer
2+
{
3+
public class Person2
4+
{
5+
public int Id { get; set; }
6+
}
7+
}

src-examples/ProxyInterfaceConsumer/ProxyInterfaceConsumer.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414
</ItemGroup>
1515

1616
<ItemGroup>
17-
<PackageReference Include="Mapster" Version="7.3.0" />
18-
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
17+
<PackageReference Include="Mapster" Version="7.4.0" />
18+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
1919
<PrivateAssets>all</PrivateAssets>
2020
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
2121
</PackageReference>
22-
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.10.0" />
23-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.10.0" />
22+
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.12.0" />
23+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
2424
<PackageReference Include="System.Net.Http" Version="4.3.4" />
2525
</ItemGroup>
2626

src/ProxyInterfaceSourceGenerator/FileGenerators/ExtraFilesGenerator.cs

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,11 @@ internal class ExtraFilesGenerator : IFileGenerator
77
{
88
private const string Name = "ProxyInterfaceGenerator.Extra.g.cs";
99

10-
public FileData GenerateFile(bool supportsNullable)
10+
public FileData GenerateFile(bool supportsNullable, bool supportsGenericAttributes)
1111
{
1212
var stringArray = supportsNullable ? "string[]?" : "string[]";
1313

14-
return new FileData($"{Name}", $@"//----------------------------------------------------------------------------------------
15-
// <auto-generated>
16-
// This code was generated by https://github.com/StefH/ProxyInterfaceSourceGenerator.
17-
//
18-
// Changes to this file may cause incorrect behavior and will be lost if
19-
// the code is regenerated.
20-
// </auto-generated>
21-
//----------------------------------------------------------------------------------------
22-
23-
{supportsNullable.IIf("#nullable enable")}
24-
using System;
25-
26-
namespace ProxyInterfaceGenerator
27-
{{
14+
var attribute = $@"
2815
[AttributeUsage(AttributeTargets.Interface)]
2916
internal sealed class ProxyAttribute : Attribute
3017
{{
@@ -64,7 +51,66 @@ public ProxyAttribute(Type type, bool proxyBaseClasses, ProxyClassAccessibility
6451
Accessibility = accessibility;
6552
MembersToIgnore = membersToIgnore;
6653
}}
67-
}}
54+
}}";
55+
56+
var genericAttribute = $@"
57+
[AttributeUsage(AttributeTargets.Interface)]
58+
internal sealed class ProxyAttribute<T> : Attribute where T : class
59+
{{
60+
public bool ProxyBaseClasses {{ get; }}
61+
public ProxyClassAccessibility Accessibility {{ get; }}
62+
public {stringArray} MembersToIgnore {{ get; }}
63+
64+
public ProxyAttribute() : this(false, ProxyClassAccessibility.Public)
65+
{{
66+
}}
67+
68+
public ProxyAttribute(bool proxyBaseClasses) : this(type, proxyBaseClasses, ProxyClassAccessibility.Public)
69+
{{
70+
}}
71+
72+
public ProxyAttribute(ProxyClassAccessibility accessibility) : this(type, false, accessibility)
73+
{{
74+
}}
75+
76+
public ProxyAttribute(ProxyClassAccessibility accessibility, {stringArray} membersToIgnore) : this(type, false, accessibility, membersToIgnore)
77+
{{
78+
}}
79+
80+
public ProxyAttribute(bool proxyBaseClasses, ProxyClassAccessibility accessibility) : this(type, proxyBaseClasses, accessibility, null)
81+
{{
82+
}}
83+
84+
public ProxyAttribute({stringArray} membersToIgnore) : this(type, false, ProxyClassAccessibility.Public, null)
85+
{{
86+
}}
87+
88+
public ProxyAttribute(bool proxyBaseClasses, ProxyClassAccessibility accessibility, {stringArray} membersToIgnore)
89+
{{
90+
Type = typeof(T);
91+
ProxyBaseClasses = proxyBaseClasses;
92+
Accessibility = accessibility;
93+
MembersToIgnore = membersToIgnore;
94+
}}
95+
}}";
96+
97+
return new FileData($"{Name}", $@"//----------------------------------------------------------------------------------------
98+
// <auto-generated>
99+
// This code was generated by https://github.com/StefH/ProxyInterfaceSourceGenerator.
100+
//
101+
// Changes to this file may cause incorrect behavior and will be lost if
102+
// the code is regenerated.
103+
// </auto-generated>
104+
//----------------------------------------------------------------------------------------
105+
106+
{supportsNullable.IIf("#nullable enable")}
107+
using System;
108+
109+
namespace ProxyInterfaceGenerator
110+
{{
111+
{attribute}
112+
113+
{supportsGenericAttributes.IIf(genericAttribute)}
68114
69115
[Flags]
70116
internal enum ProxyClassAccessibility

src/ProxyInterfaceSourceGenerator/FileGenerators/IFileGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ namespace ProxyInterfaceSourceGenerator.FileGenerators;
44

55
internal interface IFileGenerator
66
{
7-
FileData GenerateFile(bool supportsNullable);
7+
FileData GenerateFile(bool supportsNullable, bool supportsGenericAttributes);
88
}

src/ProxyInterfaceSourceGenerator/ProxyInterfaceCodeGenerator.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ public void Execute(GeneratorExecutionContext context)
4343
throw new NotSupportedException($"Only {nameof(ProxySyntaxReceiver)} is supported.");
4444
}
4545

46-
// https://github.com/reactiveui/refit/blob/main/InterfaceStubGenerator.Core/InterfaceStubGenerator.cs
4746
var supportsNullable = csharpParseOptions.LanguageVersion >= LanguageVersion.CSharp8;
47+
var supportsGenericAttributes = csharpParseOptions.LanguageVersion >= LanguageVersion.CSharp11;
4848

49-
GenerateProxyAttribute(context, receiver, supportsNullable);
49+
GenerateProxyAttribute(context, receiver, supportsNullable, supportsGenericAttributes);
5050
GeneratePartialInterfaces(context, receiver, supportsNullable);
5151
GenerateProxyClasses(context, receiver, supportsNullable);
5252
}
@@ -56,15 +56,15 @@ public void Execute(GeneratorExecutionContext context)
5656
}
5757
}
5858

59-
private void GenerateProxyAttribute(GeneratorExecutionContext ctx, ProxySyntaxReceiver receiver, bool supportsNullable)
59+
private void GenerateProxyAttribute(GeneratorExecutionContext ctx, ProxySyntaxReceiver receiver, bool supportsNullable, bool supportsGenericAttributes)
6060
{
6161
var context = new Context
6262
{
6363
GeneratorExecutionContext = ctx,
6464
Candidates = receiver.CandidateInterfaces
6565
};
6666

67-
var attributeData = _proxyAttributeGenerator.GenerateFile(supportsNullable);
67+
var attributeData = _proxyAttributeGenerator.GenerateFile(supportsNullable, supportsGenericAttributes);
6868
context.GeneratorExecutionContext.AddSource(attributeData.Filename, SourceText.From(attributeData.Text, Encoding.UTF8));
6969
}
7070

src/ProxyInterfaceSourceGenerator/ProxyInterfaceSourceGenerator.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<Version>0.5.0</Version>
55
<TargetFramework>netstandard2.0</TargetFramework>
66
<ProjectGuid>{12344228-91F4-4502-9595-39584E5ABB34}</ProjectGuid>
7-
<LangVersion>10</LangVersion>
7+
<LangVersion>latest</LangVersion>
88
<Nullable>enable</Nullable>
99
<Authors>Stef Heyenrath</Authors>
1010
<Description></Description>
@@ -26,7 +26,7 @@
2626
<ImplicitUsings>enable</ImplicitUsings>
2727
<Configurations>Debug;Release;DebugAttach</Configurations>
2828
<IsRoslynComponent>true</IsRoslynComponent>
29-
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
29+
<EnforceExtendedAnalyzerRules>false</EnforceExtendedAnalyzerRules>
3030
</PropertyGroup>
3131

3232
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
@@ -43,11 +43,11 @@
4343
<PrivateAssets>all</PrivateAssets>
4444
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4545
</PackageReference>
46-
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
46+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0">
4747
<PrivateAssets>all</PrivateAssets>
4848
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
4949
</PackageReference>
50-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
50+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
5151
<PackageReference Include="Nullable" Version="1.3.1">
5252
<PrivateAssets>all</PrivateAssets>
5353
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

src/ProxyInterfaceSourceGenerator/SyntaxReceiver/AttributeArgumentListParser.cs

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,52 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Text.RegularExpressions;
13
using Microsoft.CodeAnalysis;
24
using Microsoft.CodeAnalysis.CSharp;
35
using Microsoft.CodeAnalysis.CSharp.Syntax;
46
using ProxyInterfaceSourceGenerator.Extensions;
57
using ProxyInterfaceSourceGenerator.Types;
6-
using System.Diagnostics.CodeAnalysis;
7-
using System.Reflection;
88

99
namespace ProxyInterfaceSourceGenerator.SyntaxReceiver;
1010

1111
internal static class AttributeArgumentListParser
1212
{
13-
public static ProxyInterfaceGeneratorAttributeArguments ParseAttributeArguments(AttributeArgumentListSyntax? argumentList, SemanticModel semanticModel)
13+
private static readonly Regex ProxyAttributesRegex = new(@"^ProxyInterfaceGenerator\.Proxy|Proxy(?:<([^>]+)>)?$", RegexOptions.Compiled, TimeSpan.FromSeconds(1));
14+
15+
public static bool IsMatch(AttributeSyntax attributeSyntax)
1416
{
15-
if (argumentList is null || argumentList.Arguments.Count is < 1 or > 4)
17+
return ProxyAttributesRegex.IsMatch(attributeSyntax.Name.ToString());
18+
}
19+
20+
public static ProxyInterfaceGeneratorAttributeArguments Parse(AttributeSyntax? attributeSyntax, SemanticModel semanticModel)
21+
{
22+
if (attributeSyntax == null)
1623
{
17-
throw new ArgumentException("The ProxyAttribute requires 1, 2, 3 or 4 arguments.");
24+
throw new ArgumentNullException(nameof(attributeSyntax));
1825
}
1926

20-
ProxyInterfaceGeneratorAttributeArguments result;
21-
if (TryParseAsType(argumentList.Arguments[0].Expression, semanticModel, out var fullyQualifiedDisplayString, out var metadataName))
27+
int skip = 0;
28+
ProxyInterfaceGeneratorAttributeArguments? result;
29+
if (TryParseAsType(attributeSyntax.Name, semanticModel, out var infoGeneric))
30+
{
31+
result = new ProxyInterfaceGeneratorAttributeArguments(infoGeneric.Value.FullyQualifiedDisplayString, infoGeneric.Value.MetadataName);
32+
}
33+
else if (attributeSyntax.ArgumentList == null || attributeSyntax.ArgumentList.Arguments.Count is < 1 or > 4)
2234
{
23-
result = new ProxyInterfaceGeneratorAttributeArguments(fullyQualifiedDisplayString, metadataName);
35+
throw new ArgumentException("The ProxyAttribute requires 1, 2, 3 or 4 arguments.");
36+
}
37+
else if (TryParseAsType(attributeSyntax.ArgumentList.Arguments[0].Expression, semanticModel, out var info))
38+
{
39+
skip = 1;
40+
result = new ProxyInterfaceGeneratorAttributeArguments(info.Value.FullyQualifiedDisplayString, info.Value.MetadataName);
2441
}
2542
else
2643
{
2744
throw new ArgumentException("The first argument from the ProxyAttribute should be a Type.");
2845
}
2946

30-
foreach (var argument in argumentList.Arguments.Skip(1))
47+
var array = attributeSyntax.ArgumentList?.Arguments.ToArray() ?? [];
48+
49+
foreach (var argument in array.Skip(skip))
3150
{
3251
if (TryParseAsStringArray(argument.Expression, out var membersToIgnore))
3352
{
@@ -83,22 +102,38 @@ private static bool TryParseAsBoolean(ExpressionSyntax expressionSyntax, out boo
83102
return false;
84103
}
85104

86-
private static bool TryParseAsType(ExpressionSyntax expressionSyntax, SemanticModel semanticModel, [NotNullWhen(true)] out string? fullyQualifiedDisplayString, [NotNullWhen(true)] out string? metadataName)
105+
private static bool TryParseAsType(
106+
CSharpSyntaxNode? syntaxNode,
107+
SemanticModel semanticModel,
108+
[NotNullWhen(true)] out (string FullyQualifiedDisplayString, string MetadataName, bool IsGeneric)? info
109+
)
87110
{
88-
fullyQualifiedDisplayString = null;
89-
metadataName = null;
111+
info = null;
90112

91-
if (expressionSyntax is TypeOfExpressionSyntax typeOfExpressionSyntax)
113+
bool isGeneric;
114+
TypeSyntax typeSyntax;
115+
switch (syntaxNode)
92116
{
93-
var typeInfo = semanticModel.GetTypeInfo(typeOfExpressionSyntax.Type);
94-
var typeSymbol = typeInfo.Type!;
95-
metadataName = typeSymbol.GetFullMetadataName();
96-
fullyQualifiedDisplayString = typeSymbol.ToFullyQualifiedDisplayString();
97-
98-
return true;
117+
case TypeOfExpressionSyntax typeOfExpressionSyntax:
118+
typeSyntax = typeOfExpressionSyntax.Type;
119+
isGeneric = false;
120+
break;
121+
122+
case GenericNameSyntax genericRightNameSyntax:
123+
typeSyntax = genericRightNameSyntax.TypeArgumentList.Arguments.First();
124+
isGeneric = true;
125+
break;
126+
127+
default:
128+
return false;
99129
}
100130

101-
return false;
131+
var typeInfo = semanticModel.GetTypeInfo(typeSyntax);
132+
var typeSymbol = typeInfo.Type!;
133+
134+
info = new(typeSymbol.ToFullyQualifiedDisplayString(), typeSymbol.GetFullMetadataName(), isGeneric);
135+
136+
return true;
102137
}
103138

104139
private static bool TryParseAsEnum<TEnum>(ExpressionSyntax expressionSyntax, out TEnum value)

0 commit comments

Comments
 (0)