Skip to content

Commit 4da4670

Browse files
authored
Merge pull request #156 from BoBoBaSs84/refactor/generators/repetitive-attribute-named-argument-extraction-into-a-generic-helper
refactor(generators): refactor attribute extraction in generators
2 parents f97ccb8 + fe4e65d commit 4da4670

6 files changed

Lines changed: 116 additions & 352 deletions

File tree

src/BB84.SourceGenerators/DisposableGenerator.cs

Lines changed: 6 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ private void Execute(SourceProductionContext context, (ClassDeclarationSyntax Cl
3737
if (!GeneratorHelpers.TryCreateContext(input, out GeneratorContext ctx))
3838
return;
3939

40-
(bool generateFinalizer, bool generateAsync) = GetAttributeOptions(ctx.ClassDeclaration);
40+
bool generateFinalizer = GetGenerateFinalizer(ctx.ClassSymbol);
41+
bool generateAsync = GetGenerateAsync(ctx.ClassSymbol);
4142

4243
// Check if IAsyncDisposable is available in the target framework
4344
if (generateAsync)
@@ -97,55 +98,11 @@ private void Execute(SourceProductionContext context, (ClassDeclarationSyntax Cl
9798
context.AddSource(hintName, sb.ToString());
9899
}
99100

100-
private static (bool generateFinalizer, bool generateAsync) GetAttributeOptions(ClassDeclarationSyntax classDeclaration)
101-
{
102-
bool generateFinalizer = false;
103-
bool generateAsync = false;
101+
private static bool GetGenerateFinalizer(INamedTypeSymbol namedTypeSymbol)
102+
=> GeneratorHelpers.GetConstructorArgumentValue(namedTypeSymbol, AttributeNames.FullName, 0, false);
104103

105-
foreach (AttributeListSyntax attributeList in classDeclaration.AttributeLists)
106-
{
107-
foreach (AttributeSyntax attribute in attributeList.Attributes)
108-
{
109-
string name = attribute.Name.ToString();
110-
111-
if (name != AttributeNames.ShortName && name != AttributeNames.FullName)
112-
continue;
113-
114-
if (attribute.ArgumentList is null || attribute.ArgumentList.Arguments.Count == 0)
115-
return (generateFinalizer, generateAsync);
116-
117-
foreach (AttributeArgumentSyntax argument in attribute.ArgumentList.Arguments)
118-
{
119-
string? paramName = argument.NameColon?.Name.Identifier.Text;
120-
string argText = argument.Expression.ToString();
121-
122-
if (!bool.TryParse(argText, out bool value))
123-
continue;
124-
125-
switch (paramName)
126-
{
127-
case "generateFinalizer":
128-
generateFinalizer = value;
129-
break;
130-
case "async":
131-
generateAsync = value;
132-
break;
133-
default:
134-
int index = attribute.ArgumentList.Arguments.IndexOf(argument);
135-
if (index == 0)
136-
generateFinalizer = value;
137-
else if (index == 1)
138-
generateAsync = value;
139-
break;
140-
}
141-
}
142-
143-
return (generateFinalizer, generateAsync);
144-
}
145-
}
146-
147-
return (generateFinalizer, generateAsync);
148-
}
104+
private static bool GetGenerateAsync(INamedTypeSymbol namedTypeSymbol)
105+
=> GeneratorHelpers.GetConstructorArgumentValue(namedTypeSymbol, AttributeNames.FullName, 1, false);
149106

150107
private static ImmutableArray<(string FieldName, int Order, int SourceIndex)> GetDisposableFields(ClassDeclarationSyntax classDeclaration)
151108
{

src/BB84.SourceGenerators/Helpers/GeneratorHelpers.cs

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,6 @@ internal static string GetAccessibility(TypeDeclarationSyntax typeDeclaration)
8383
return outerTypes;
8484
}
8585

86-
/// <summary>
87-
/// Gets the set of excluded property names from an attribute's constructor arguments.
88-
/// </summary>
89-
/// <param name="classDeclaration">The class declaration syntax.</param>
90-
/// <param name="semanticModel">The semantic model.</param>
91-
/// <param name="attributeFullName">The full name of the attribute (e.g., "GenerateToStringAttribute").</param>
92-
/// <returns>A set of excluded property names.</returns>
93-
internal static HashSet<string> GetExcludedProperties(
94-
ClassDeclarationSyntax classDeclaration,
95-
SemanticModel semanticModel,
96-
string attributeFullName)
97-
=> GetExcludedProperties((TypeDeclarationSyntax)classDeclaration, semanticModel, attributeFullName);
98-
9986
/// <summary>
10087
/// Gets the set of excluded property names from an attribute's constructor arguments on a type declaration.
10188
/// </summary>
@@ -146,6 +133,89 @@ internal static string StripAttributeSuffix(string attributeName)
146133
? attributeName[..^AttributeKeyword.Length]
147134
: attributeName;
148135

136+
/// <summary>
137+
/// Gets the value of a named argument from an attribute on a type declaration.
138+
/// Searches the attribute lists for an attribute matching <paramref name="attributeShortName"/> or
139+
/// <paramref name="attributeFullName"/>, then looks for a named argument with the given <paramref name="propertyName"/>.
140+
/// </summary>
141+
/// <typeparam name="T">The expected type of the argument value.</typeparam>
142+
/// <param name="typeDeclaration">The type declaration syntax.</param>
143+
/// <param name="semanticModel">The semantic model.</param>
144+
/// <param name="attributeShortName">The short name of the attribute.</param>
145+
/// <param name="attributeFullName">The full name of the attribute.</param>
146+
/// <param name="propertyName">The name of the named argument to extract.</param>
147+
/// <param name="defaultValue">The default value to return if the argument is not found.</param>
148+
/// <returns>The extracted value, or <paramref name="defaultValue"/> if not found.</returns>
149+
internal static T GetNamedArgumentValue<T>(
150+
TypeDeclarationSyntax typeDeclaration,
151+
SemanticModel semanticModel,
152+
string attributeShortName,
153+
string attributeFullName,
154+
string propertyName,
155+
T defaultValue)
156+
{
157+
foreach (AttributeListSyntax attributeList in typeDeclaration.AttributeLists)
158+
{
159+
foreach (AttributeSyntax attribute in attributeList.Attributes)
160+
{
161+
string name = attribute.Name.ToString();
162+
163+
if (name != attributeShortName && name != attributeFullName)
164+
continue;
165+
166+
if (attribute.ArgumentList is null)
167+
return defaultValue;
168+
169+
foreach (AttributeArgumentSyntax arg in attribute.ArgumentList.Arguments)
170+
{
171+
if (arg.NameEquals?.Name.Identifier.Text != propertyName)
172+
continue;
173+
174+
Optional<object?> value = semanticModel.GetConstantValue(arg.Expression);
175+
176+
if (value.HasValue && value.Value is T typedValue)
177+
return typedValue;
178+
179+
break;
180+
}
181+
182+
return defaultValue;
183+
}
184+
}
185+
186+
return defaultValue;
187+
}
188+
189+
/// <summary>
190+
/// Gets the value of a positional constructor argument from an attribute on a type symbol.
191+
/// Searches the attributes of <paramref name="typeSymbol"/> for one whose class name matches
192+
/// <paramref name="attributeFullName"/>, then returns the constructor argument at <paramref name="index"/>.
193+
/// </summary>
194+
/// <typeparam name="T">The expected type of the constructor argument value.</typeparam>
195+
/// <param name="typeSymbol">The type symbol to inspect.</param>
196+
/// <param name="attributeFullName">The simple type name of the attribute (e.g., "GenerateIniFileAttribute").</param>
197+
/// <param name="index">The zero-based index of the constructor argument.</param>
198+
/// <param name="defaultValue">The default value to return if the argument is not found.</param>
199+
/// <returns>The extracted value, or <paramref name="defaultValue"/> if not found.</returns>
200+
internal static T GetConstructorArgumentValue<T>(
201+
INamedTypeSymbol typeSymbol,
202+
string attributeFullName,
203+
int index,
204+
T defaultValue)
205+
{
206+
foreach (AttributeData attributeData in typeSymbol.GetAttributes())
207+
{
208+
if (attributeData.AttributeClass?.Name != attributeFullName)
209+
continue;
210+
211+
return attributeData.ConstructorArguments.Length > index && attributeData.ConstructorArguments[index].Value is T typedValue
212+
? typedValue
213+
: defaultValue;
214+
}
215+
216+
return defaultValue;
217+
}
218+
149219
/// <summary>
150220
/// Returns the fully-qualified metadata name, the simple full name, and the short name
151221
/// (without the "Attribute" suffix) for the given attribute type.
@@ -185,7 +255,7 @@ internal static string EscapeVerbatimString(string value)
185255
/// <param name="context">The generator attribute syntax context.</param>
186256
/// <returns>A tuple of class declaration syntax and semantic model, or null if the target node is not a class declaration.</returns>
187257
internal static (ClassDeclarationSyntax ClassSyntax, SemanticModel SemanticModel)? TransformClassSyntax(GeneratorAttributeSyntaxContext context)
188-
=> context.TargetNode is not ClassDeclarationSyntax classSyntax ? null : ((ClassDeclarationSyntax ClassSyntax, SemanticModel SemanticModel)?)(classSyntax, context.SemanticModel);
258+
=> context.TargetNode is not ClassDeclarationSyntax classSyntax ? null : (classSyntax, context.SemanticModel);
189259

190260
/// <summary>
191261
/// Registers a class-based incremental source generator pipeline that filters for classes

src/BB84.SourceGenerators/IniFileGenerator.cs

Lines changed: 9 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ private void Execute(SourceProductionContext context, (ClassDeclarationSyntax Cl
3939
if (!GeneratorHelpers.TryCreateContext(input, out GeneratorContext ctx))
4040
return;
4141

42-
string stringComparison = GetStringComparison(ctx.ClassDeclaration, ctx.SemanticModel);
43-
string sectionDelimiter = GetSectionDelimiter(ctx.ClassDeclaration, ctx.SemanticModel);
44-
bool serializeComments = GetSerializeComments(ctx.ClassDeclaration);
42+
string stringComparison = GetStringComparison(ctx.ClassSymbol).ToString();
43+
string sectionDelimiter = GetSectionDelimiter(ctx.ClassSymbol);
44+
bool serializeComments = GetSerializeComments(ctx.ClassDeclaration, ctx.SemanticModel);
4545

4646
List<SectionInfo> sections = GetSections(ctx.ClassDeclaration, ctx.SemanticModel, sectionDelimiter, serializeComments);
4747

@@ -595,74 +595,14 @@ private static string GetToStringExpression(string expression, ValueInfo value)
595595
};
596596
}
597597

598-
private static string GetStringComparison(ClassDeclarationSyntax classDeclaration, SemanticModel semanticModel)
599-
{
600-
INamedTypeSymbol? classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
601-
602-
if (classSymbol is null)
603-
return nameof(StringComparison.OrdinalIgnoreCase);
604-
605-
AttributeData? attributeData = FindGeneratorAttribute(classSymbol);
606-
607-
return attributeData is not null
608-
&& attributeData.ConstructorArguments.Length > 0
609-
&& attributeData.ConstructorArguments[0].Value is int comparisonValue
610-
? comparisonValue switch
611-
{
612-
0 => nameof(StringComparison.CurrentCulture),
613-
1 => nameof(StringComparison.CurrentCultureIgnoreCase),
614-
2 => nameof(StringComparison.InvariantCulture),
615-
3 => nameof(StringComparison.InvariantCultureIgnoreCase),
616-
4 => nameof(StringComparison.Ordinal),
617-
5 => nameof(StringComparison.OrdinalIgnoreCase),
618-
_ => nameof(StringComparison.OrdinalIgnoreCase)
619-
}
620-
: nameof(StringComparison.OrdinalIgnoreCase);
621-
}
622-
623-
private static string GetSectionDelimiter(ClassDeclarationSyntax classDeclaration, SemanticModel semanticModel)
624-
{
625-
INamedTypeSymbol? classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
626-
if (classSymbol is null)
627-
return ".";
628-
629-
AttributeData? attributeData = FindGeneratorAttribute(classSymbol);
630-
631-
return attributeData is not null
632-
&& attributeData.ConstructorArguments.Length > 1
633-
&& attributeData.ConstructorArguments[1].Value is string delimiterValue
634-
? delimiterValue
635-
: ".";
636-
}
637-
638-
private static bool GetSerializeComments(ClassDeclarationSyntax classDeclaration)
639-
{
640-
foreach (AttributeListSyntax attributeList in classDeclaration.AttributeLists)
641-
{
642-
foreach (AttributeSyntax attribute in attributeList.Attributes)
643-
{
644-
string name = attribute.Name.ToString();
645-
646-
if (name != AttributeNames.ShortName && name != AttributeNames.FullName)
647-
continue;
598+
private static StringComparison GetStringComparison(INamedTypeSymbol classSymbol)
599+
=> (StringComparison)GeneratorHelpers.GetConstructorArgumentValue(classSymbol, AttributeNames.FullName, 0, (int)StringComparison.OrdinalIgnoreCase);
648600

649-
if (attribute.ArgumentList is null)
650-
continue;
601+
private static string GetSectionDelimiter(INamedTypeSymbol classSymbol)
602+
=> GeneratorHelpers.GetConstructorArgumentValue(classSymbol, AttributeNames.FullName, 1, ".");
651603

652-
foreach (AttributeArgumentSyntax argument in attribute.ArgumentList.Arguments)
653-
{
654-
if (argument.NameEquals?.Name.Identifier.Text == "SerializeComments")
655-
{
656-
string expressionText = argument.Expression.ToString();
657-
if (expressionText == "true")
658-
return true;
659-
}
660-
}
661-
}
662-
}
663-
664-
return false;
665-
}
604+
private static bool GetSerializeComments(ClassDeclarationSyntax classDeclaration, SemanticModel semanticModel)
605+
=> GeneratorHelpers.GetNamedArgumentValue(classDeclaration, semanticModel, AttributeNames.ShortName, AttributeNames.FullName, nameof(GenerateIniFileAttribute.SerializeComments), false);
666606

667607
private static string? GetXmlSummaryComment(ISymbol symbol)
668608
{
@@ -812,17 +752,6 @@ private static string GetNonNullableFullyQualifiedName(INamedTypeSymbol type)
812752
return typeName;
813753
}
814754

815-
private static AttributeData? FindGeneratorAttribute(INamedTypeSymbol classSymbol)
816-
{
817-
foreach (AttributeData attr in classSymbol.GetAttributes())
818-
{
819-
if (attr.AttributeClass?.ToDisplayString() == AttributeNames.MetadataName)
820-
return attr;
821-
}
822-
823-
return null;
824-
}
825-
826755
private sealed record SectionInfo(
827756
string PropertyName,
828757
string PropertyPath,

src/BB84.SourceGenerators/NotificationsGenerator.cs

Lines changed: 9 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ private void Execute(SourceProductionContext context, (ClassDeclarationSyntax Cl
3232
if (!GeneratorHelpers.TryCreateContext(input, out GeneratorContext ctx))
3333
return;
3434

35-
(bool generatePropertyChanged, bool generatePropertyChanging, bool generateHasChanged) = GetAttributeOptions(ctx.ClassDeclaration);
35+
bool generatePropertyChanged = GetPropertyChanged(ctx.ClassSymbol);
36+
bool generatePropertyChanging = GetPropertyChanging(ctx.ClassSymbol);
37+
bool generateHasChanged = GetHasChanged(ctx.ClassSymbol);
3638

3739
if (!generatePropertyChanged && !generatePropertyChanging)
3840
return;
@@ -69,62 +71,14 @@ private void Execute(SourceProductionContext context, (ClassDeclarationSyntax Cl
6971
context.AddSource(hintName, sb.ToString());
7072
}
7173

72-
private static (bool propertyChanged, bool propertyChanging, bool hasChanged) GetAttributeOptions(ClassDeclarationSyntax classDeclaration)
73-
{
74-
bool propertyChanged = true;
75-
bool propertyChanging = true;
76-
bool hasChanged = false;
77-
78-
foreach (AttributeListSyntax attributeList in classDeclaration.AttributeLists)
79-
{
80-
foreach (AttributeSyntax attribute in attributeList.Attributes)
81-
{
82-
string name = attribute.Name.ToString();
83-
84-
if (name != AttributeNames.ShortName && name != AttributeNames.FullName)
85-
continue;
86-
87-
if (attribute.ArgumentList is null || attribute.ArgumentList.Arguments.Count == 0)
88-
return (propertyChanged, propertyChanging, hasChanged);
74+
private static bool GetPropertyChanged(INamedTypeSymbol namedTypeSymbol)
75+
=> GeneratorHelpers.GetConstructorArgumentValue(namedTypeSymbol, AttributeNames.FullName, 0, true);
8976

90-
foreach (AttributeArgumentSyntax argument in attribute.ArgumentList.Arguments)
91-
{
92-
string? paramName = argument.NameColon?.Name.Identifier.Text;
93-
string argText = argument.Expression.ToString();
94-
95-
if (!bool.TryParse(argText, out bool value))
96-
continue;
97-
98-
switch (paramName)
99-
{
100-
case "propertyChanged":
101-
propertyChanged = value;
102-
break;
103-
case "propertyChanging":
104-
propertyChanging = value;
105-
break;
106-
case "hasChanged":
107-
hasChanged = value;
108-
break;
109-
default:
110-
// Positional: first=propertyChanged, second=propertyChanging, third=hasChanged
111-
int index = attribute.ArgumentList.Arguments.IndexOf(argument);
112-
if (index == 0)
113-
propertyChanged = value;
114-
else if (index == 1)
115-
propertyChanging = value;
116-
else if (index == 2)
117-
hasChanged = value;
118-
break;
119-
}
120-
}
77+
private static bool GetPropertyChanging(INamedTypeSymbol namedTypeSymbol)
78+
=> GeneratorHelpers.GetConstructorArgumentValue(namedTypeSymbol, AttributeNames.FullName, 1, true);
12179

122-
return (propertyChanged, propertyChanging, hasChanged);
123-
}
124-
}
125-
126-
return (propertyChanged, propertyChanging, hasChanged);
127-
}
80+
private static bool GetHasChanged(INamedTypeSymbol namedTypeSymbol)
81+
=> GeneratorHelpers.GetConstructorArgumentValue(namedTypeSymbol, AttributeNames.FullName, 2, false);
12882

12983
private static void AppendClassStart(SourceBuilder sb, string className, string accessibility, bool propertyChanged, bool propertyChanging, bool hasChanged)
13084
{

0 commit comments

Comments
 (0)