Skip to content

Commit b962728

Browse files
committed
Added better parsing/output for generics, nullable generics, ref/out parameters, parameter attributes
1 parent 7820d0e commit b962728

File tree

30 files changed

+598
-67
lines changed

30 files changed

+598
-67
lines changed

src/PublicInterfaceGenerator.Attributes/GenerateInterfaceAttribute.cs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,26 @@ public class GenerateInterfaceAttribute : Attribute
2525
public string? Interfaces { get; set; }
2626

2727
/// <summary>
28-
/// Set this to specify the generates interface inherits from System.IDisposable.
29-
/// This will be appended to the list of interfaces.
30-
/// If you are also specifying interfaces with the ""{AttributeProperty_Interfaces}"" property, either set this to false and include ""System.IDisposable"" in the ""{AttributeProperty_Interfaces}"" property string, or set this to true and don't include ""System.IDisposable"" in the ""{AttributeProperty_Interfaces}"" property string.
28+
/// Set this to specify the generated interface inherits from System.IDisposable.
29+
/// This will be appended to the list of interfaces the generated interface inherits from.
30+
/// This is in addition to the <see cref="Interfaces"/> property.
31+
/// If you are also specifying interfaces with the <see cref="Interfaces"/> property,
32+
/// either set this to false and include "System.IDisposable" in the <see cref="Interfaces"/> property string,
33+
/// or set this to true and don't include "System.IDisposable" in the <see cref="Interfaces"/> string.
34+
/// Failure to do this will result in System.IDisposable being appended to the generated interface twice.
3135
/// </summary>
3236
public bool IsIDisposable { get; set; }
3337

38+
/// <summary>
39+
/// Set this to specify the generated interface inherits from <see cref="System.IAsyncDisposable"/>.
40+
/// This is in addition to the <see cref="Interfaces"/> property.
41+
/// If you are also specifying interfaces with the <see cref="Interfaces"/> property,
42+
/// either set this to false and include "System.IAsyncDisposable" in the <see cref="Interfaces"/> property string,
43+
/// or set this to true and don't include "System.IAsyncDisposable" in the <see cref="Interfaces"/> string.
44+
/// Failure to do this will result in System.IAsyncDisposable being appended to the generated interface twice.
45+
/// </summary>
46+
public bool IsIAsyncDisposable { get; set; }
47+
3448
public static class Constants
3549
{
3650
public const string GenerateInterfaceAttributeName = nameof(GenerateInterfaceAttribute);
@@ -43,5 +57,6 @@ public static class Constants
4357
public const string AttributeProperty_NamespaceName = nameof(Namespace);
4458
public const string AttributeProperty_Interfaces = nameof(Interfaces);
4559
public const string AttributeProperty_IsIDisposable = nameof(IsIDisposable);
60+
public const string AttributeProperty_IsIAsyncDisposable = nameof(IsIAsyncDisposable);
4661
}
4762
}

src/PublicInterfaceGenerator/GeneratorParsers/ClassParser.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public static class ClassParser
2626
string? namespaceName = null;
2727
string? interfacesNames = null;
2828
bool inheritsFromIDisposable = false;
29+
bool inheritsFromIAsyncDisposable = false;
2930

3031
foreach (AttributeData attributeData in symbol.GetAttributes())
3132
{
@@ -62,18 +63,40 @@ public static class ClassParser
6263
inheritsFromIDisposable = parsedIsIDisposable;
6364
}
6465
}
66+
else if (namedArgument.Key == GenerateInterfaceAttribute.Constants.AttributeProperty_IsIAsyncDisposable
67+
&& namedArgument.Value.Value?.ToString() is { } isIAsyncDisposable)
68+
{
69+
if (bool.TryParse(isIAsyncDisposable, out bool parsedIsIAsyncDisposable))
70+
{
71+
inheritsFromIAsyncDisposable = parsedIsIAsyncDisposable;
72+
}
73+
}
6574
}
6675
}
6776

68-
return TryExtractSymbols(symbol, interfaceName, namespaceName, interfacesNames, inheritsFromIDisposable);
77+
return TryExtractSymbols(
78+
symbol,
79+
interfaceName,
80+
namespaceName,
81+
interfacesNames,
82+
inheritsFromIDisposable,
83+
inheritsFromIAsyncDisposable);
6984
}
7085

71-
private static InterfaceToGenerateInfo? TryExtractSymbols(INamedTypeSymbol symbol, string? customInterfaceName, string? namespaceName, string? interfacesNames, bool inheritsFromIDisposable)
86+
private static InterfaceToGenerateInfo? TryExtractSymbols(
87+
INamedTypeSymbol symbol,
88+
string? customInterfaceName,
89+
string? namespaceName,
90+
string? interfacesNames,
91+
bool inheritsFromIDisposable,
92+
bool inheritsFromIAsyncDisposable)
7293
{
7394
var interfaceName = customInterfaceName ?? $"I{symbol.Name}";
7495
var nameSpace = namespaceName ?? (symbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : symbol.ContainingNamespace.ToString());
7596
var extraInterfaces = interfacesNames?.Trim() ?? string.Empty;
7697

98+
var comments = CommentsBlockParser.ParseCommentsBlock(symbol);
99+
77100
var interfaceGenericParameters = GenericsParser.ParseGenericParameters(symbol.TypeParameters);
78101

79102
var methodsBuilder = ImmutableArray.CreateBuilder<InterfaceToGenerateInfo.Method>();
@@ -85,7 +108,7 @@ public static class ClassParser
85108
{
86109
if (member is IMethodSymbol memberSymbol)
87110
{
88-
var method = MethodParser.ExtractMethod(memberSymbol, extraInterfaces, inheritsFromIDisposable);
111+
var method = MethodParser.ExtractMethod(memberSymbol, extraInterfaces, inheritsFromIDisposable, inheritsFromIAsyncDisposable);
89112
if (method is object)
90113
{
91114
methodsBuilder.Add(method);
@@ -115,7 +138,9 @@ public static class ClassParser
115138
FullNamespace: nameSpace,
116139
Interfaces: extraInterfaces,
117140
InheritsFromIDisposable: inheritsFromIDisposable,
141+
InheritsFromIAsyncDisposable: inheritsFromIAsyncDisposable,
118142
GenericParameters: interfaceGenericParameters,
143+
Comments: comments,
119144
Methods: methodsBuilder.ToImmutableArray(),
120145
Properties: propertiesBuilder.ToImmutableArray(),
121146
Events: eventsBuilder.ToImmutableArray());

src/PublicInterfaceGenerator/GeneratorParsers/MethodParser.cs

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Collections.Immutable;
4-
using System.Text;
5-
using System.Xml.Linq;
64

75
using Microsoft.CodeAnalysis;
8-
using Microsoft.CodeAnalysis.Text;
96

107
using ProgrammerAl.SourceGenerators.PublicInterfaceGenerator.Attributes;
118

@@ -16,9 +13,10 @@ public static class MethodParser
1613
public static InterfaceToGenerateInfo.Method? ExtractMethod(
1714
IMethodSymbol symbol,
1815
string extraClassInterfaces,
19-
bool inheritsFromIDisposable)
16+
bool inheritsFromIDisposable,
17+
bool inheritsFromIAsyncDisposable)
2018
{
21-
if (!IsSymbolValid(symbol, extraClassInterfaces, inheritsFromIDisposable))
19+
if (!IsSymbolValid(symbol, extraClassInterfaces, inheritsFromIDisposable, inheritsFromIAsyncDisposable))
2220
{
2321
return null;
2422
}
@@ -40,20 +38,41 @@ public static class MethodParser
4038

4139
var argumentBuilder = ImmutableArray.CreateBuilder<InterfaceToGenerateInfo.MethodArgument>();
4240

43-
foreach (var methodParameter in symbol.Parameters)
41+
foreach (var parameter in symbol.Parameters)
4442
{
45-
var argName = methodParameter.Name;
46-
var dataType = methodParameter.Type.ToDisplayString();
47-
var nullableAnnotation = methodParameter.NullableAnnotation;
48-
var interfaceArgument = new InterfaceToGenerateInfo.MethodArgument(argName, dataType, nullableAnnotation);
43+
var argName = parameter.ToDisplayString();
44+
var nullableAnnotation = parameter.NullableAnnotation;
45+
var attributeStrings = parameter.GetAttributes().Select(x => x.ToString()).ToImmutableArray();
46+
string defaultValue = "";
47+
48+
if (parameter.HasExplicitDefaultValue)
49+
{
50+
if (parameter.ExplicitDefaultValue is null)
51+
{
52+
defaultValue = "null";
53+
}
54+
else if (parameter.ExplicitDefaultValue is string stringValue)
55+
{
56+
defaultValue = $"\"{stringValue}\"";
57+
}
58+
else
59+
{
60+
defaultValue = parameter.ExplicitDefaultValue.ToString();
61+
}
62+
}
63+
64+
var interfaceArgument = new InterfaceToGenerateInfo.MethodArgument(argName, nullableAnnotation, attributeStrings, defaultValue);
4965
argumentBuilder.Add(interfaceArgument);
5066
}
5167

5268
return new InterfaceToGenerateInfo.Method(methodName, returnType, argumentBuilder.ToImmutableArray(), genericParameters, methodComments);
5369
}
5470

55-
56-
private static bool IsSymbolValid(IMethodSymbol symbol, string extraClassInterfaces, bool inheritsFromIDisposable)
71+
private static bool IsSymbolValid(
72+
IMethodSymbol symbol,
73+
string extraClassInterfaces,
74+
bool inheritsFromIDisposable,
75+
bool inheritsFromIAsyncDisposable)
5776
{
5877
if (string.Equals(".ctor", symbol.Name, StringComparison.Ordinal))
5978
{
@@ -102,6 +121,13 @@ private static bool IsSymbolValid(IMethodSymbol symbol, string extraClassInterfa
102121
// Note: This is different from the method check above, because the concrete class won't have IDisposable in the definition list, it's on the interface
103122
return false;
104123
}
124+
else if (IsIAsyncDisposeMethodAndImplementsIAsyncDisposable(symbol, extraClassInterfaces, inheritsFromIAsyncDisposable))
125+
{
126+
//If the code uses an attribute to set that the class implements IAsyncDisposable
127+
// and this is the DisposeAsync() method, don't include it in the interface
128+
// Note: This is different from the method check above, because the concrete class won't have IAsyncDisposable in the definition list, it's on the interface
129+
return false;
130+
}
105131

106132
return true;
107133
}
@@ -134,6 +160,34 @@ private static bool IsDisposeMethodAndImplementsIDisposable(IMethodSymbol symbol
134160
return true;
135161
}
136162

163+
private static bool IsIAsyncDisposeMethodAndImplementsIAsyncDisposable(IMethodSymbol symbol, string extraClassInterfaces, bool inheritsFromIAsyncDisposable)
164+
{
165+
if (!symbol.Name.Equals("DisposeAsync"))
166+
{
167+
return false;
168+
}
169+
170+
if (symbol.ReturnType.Name != "ValueTask")
171+
{
172+
return false;
173+
}
174+
175+
if (inheritsFromIAsyncDisposable)
176+
{
177+
return true;
178+
}
179+
180+
var interfaces = extraClassInterfaces.
181+
Split([','], StringSplitOptions.RemoveEmptyEntries)
182+
.Select(x => x.Trim());
183+
if (interfaces.Any(x => string.Equals(x, "System.IDisposable") || string.Equals(x, "IDisposable")))
184+
{
185+
return true;
186+
}
187+
188+
return true;
189+
}
190+
137191
private static bool IsMethodFromInheritedInterface(IMethodSymbol symbol)
138192
{
139193
var interfaces = symbol.ContainingType.AllInterfaces;

src/PublicInterfaceGenerator/InterfaceToGenerateInfo.cs

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public record InterfaceToGenerateInfo(
1414
string FullNamespace,
1515
string Interfaces,
1616
bool InheritsFromIDisposable,
17+
bool InheritsFromIAsyncDisposable,
18+
string Comments,
1719
ImmutableArray<GenericParameter> GenericParameters,
1820
ImmutableArray<Method> Methods,
1921
ImmutableArray<Property> Properties,
@@ -48,17 +50,36 @@ public string GenerateInterfaceDefinitionString()
4850
_ = builder.Append('>');
4951
}
5052

51-
if (!string.IsNullOrWhiteSpace(Interfaces))
53+
var hasStartedInterfaceList = false;
54+
var hasInterfacesList = !string.IsNullOrWhiteSpace(Interfaces);
55+
56+
if (hasInterfacesList)
5257
{
53-
_ = builder.Append($" : {Interfaces}");
58+
AddInterfacesToLine(Interfaces, builder, hasStartedInterfaceList);
59+
hasStartedInterfaceList = true;
60+
5461
if (InheritsFromIDisposable)
5562
{
56-
_ = builder.Append(", System.IDisposable");
63+
AddInterfacesToLine("System.IDisposable", builder, hasStartedInterfaceList);
64+
}
65+
66+
if (InheritsFromIAsyncDisposable)
67+
{
68+
AddInterfacesToLine("System.IAsyncDisposable", builder, hasStartedInterfaceList);
5769
}
5870
}
59-
else if (InheritsFromIDisposable)
71+
else
6072
{
61-
_ = builder.Append(" : System.IDisposable");
73+
if (InheritsFromIDisposable)
74+
{
75+
AddInterfacesToLine("System.IDisposable", builder, hasStartedInterfaceList);
76+
hasStartedInterfaceList = true;
77+
}
78+
79+
if (InheritsFromIAsyncDisposable)
80+
{
81+
AddInterfacesToLine("System.IAsyncDisposable", builder, hasStartedInterfaceList);
82+
}
6283
}
6384

6485
foreach (var genericParam in GenericParameters)
@@ -68,7 +89,20 @@ public string GenerateInterfaceDefinitionString()
6889
_ = builder.Append($" where {genericParam.Name} : {genericParam.ConstraintTypes}");
6990
}
7091
}
71-
return builder.ToString();
92+
93+
return InterfaceToGenerateInfo.CombineLineWithComments(Comments, builder);
94+
}
95+
96+
private void AddInterfacesToLine(string interfaces, StringBuilder builder, bool hasStartedInterfaceList)
97+
{
98+
if (hasStartedInterfaceList)
99+
{
100+
_ = builder.Append($", {interfaces}");
101+
}
102+
else
103+
{
104+
_ = builder.Append($" : {interfaces}");
105+
}
72106
}
73107

74108
public record GenericParameter(int Ordinal, string Name, NullableAnnotation NullableAnnotation, string ConstraintTypes);
@@ -85,6 +119,11 @@ internal static string CombineLineWithComments(string comments, string definitio
85119
}
86120
}
87121

122+
internal static string CombineLineWithComments(string comments, StringBuilder builder)
123+
{
124+
return CombineLineWithComments(comments, builder.ToString());
125+
}
126+
88127
public record Method(string Name, string ReturnType, ImmutableArray<MethodArgument> Arguments, ImmutableArray<GenericParameter> GenericParameters, string Comments)
89128
{
90129
public string ToMethodString()
@@ -128,16 +167,33 @@ public string ToMethodString()
128167

129168
_ = builder.Append(';');
130169

131-
var definitionLine = builder.ToString();
132-
return InterfaceToGenerateInfo.CombineLineWithComments(Comments, definitionLine);
170+
return InterfaceToGenerateInfo.CombineLineWithComments(Comments, builder);
133171
}
134172
}
135173

136-
public record MethodArgument(string Name, string DataType, NullableAnnotation NullableAnnotation)
174+
public record MethodArgument(string Name, NullableAnnotation NullableAnnotation, ImmutableArray<string> AttributeStrings, string DefaultValue)
137175
{
138176
public string ToArgumentString()
139177
{
140-
return $"{DataType} {Name}";
178+
string parameterString;
179+
if (AttributeStrings.Any())
180+
{
181+
var attributeStringBlocks = AttributeStrings.Select(x => $"[{x}]");
182+
183+
var attributes = string.Join("", attributeStringBlocks);
184+
parameterString = $"{attributes} {Name}";
185+
}
186+
else
187+
{
188+
parameterString = Name;
189+
}
190+
191+
if (!string.IsNullOrWhiteSpace(DefaultValue))
192+
{
193+
parameterString += $" = {DefaultValue}";
194+
}
195+
196+
return parameterString;
141197
}
142198
}
143199

src/PublicInterfaceGenerator/SourceGenerationHelper.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,35 @@ private static void AppendCodeLines(StringBuilder builder, string code)
5656

5757
private static bool ShouldEnableNullableReferences(in InterfaceToGenerateInfo interfaceInfo)
5858
{
59-
return interfaceInfo.Methods.Any(m => m.Arguments.Any(a => a.NullableAnnotation == NullableAnnotation.Annotated)
60-
|| m.ReturnType.EndsWith("?"))
61-
|| interfaceInfo.Properties.Any(x => x.ReturnType.EndsWith("?"))
62-
|| interfaceInfo.Events.Any(x => x.EventDataType.EndsWith("?"));
59+
foreach (var method in interfaceInfo.Methods)
60+
{
61+
//If the method returns a nullable value
62+
// or a non-nullable value but it's a generic type and the generic value can be null
63+
// Ex: Task<string?>
64+
if (method.ReturnType.Contains("?"))
65+
{
66+
return true;
67+
}
68+
69+
foreach (var arg in method.Arguments)
70+
{
71+
if (arg.NullableAnnotation == NullableAnnotation.Annotated)
72+
{
73+
return true;
74+
}
75+
}
76+
}
77+
78+
if (interfaceInfo.Properties.Any(x => x.ReturnType.EndsWith("?")))
79+
{
80+
return true;
81+
}
82+
83+
if (interfaceInfo.Events.Any(x => x.EventDataType.EndsWith("?")))
84+
{
85+
return true;
86+
}
87+
88+
return false;
6389
}
6490
}

0 commit comments

Comments
 (0)