Skip to content

Commit f818268

Browse files
committed
Initial draft of forwarded explicit property attributes
1 parent e6257d8 commit f818268

File tree

4 files changed

+153
-2
lines changed

4 files changed

+153
-2
lines changed

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Collections.Immutable;
88
using System.Linq;
9+
using System.Threading;
910
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
1011
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
1112
using Microsoft.CodeAnalysis;
@@ -51,6 +52,49 @@ public static AttributeInfo From(AttributeData attributeData)
5152
namedArguments.ToImmutable());
5253
}
5354

55+
/// <summary>
56+
/// Creates a new <see cref="AttributeInfo"/> instance from a given syntax node.
57+
/// </summary>
58+
/// <param name="typeSymbol">The symbol for the attribute type.</param>
59+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
60+
/// <param name="arguments">The sequence of <see cref="AttributeArgumentSyntax"/> instances to process.</param>
61+
/// <param name="token">The cancellation token for the current operation.</param>
62+
/// <returns>A <see cref="AttributeInfo"/> instance representing the input attribute data.</returns>
63+
public static AttributeInfo From(INamedTypeSymbol typeSymbol, SemanticModel semanticModel, IEnumerable<AttributeArgumentSyntax> arguments, CancellationToken token)
64+
{
65+
string typeName = typeSymbol.GetFullyQualifiedName();
66+
67+
ImmutableArray<TypedConstantInfo>.Builder constructorArguments = ImmutableArray.CreateBuilder<TypedConstantInfo>();
68+
ImmutableArray<(string, TypedConstantInfo)>.Builder namedArguments = ImmutableArray.CreateBuilder<(string, TypedConstantInfo)>();
69+
70+
foreach (AttributeArgumentSyntax argument in arguments)
71+
{
72+
// The attribute expression has to have an available operation to extract information from
73+
if (semanticModel.GetOperation(argument.Expression, token) is not IOperation operation)
74+
{
75+
continue;
76+
}
77+
78+
TypedConstantInfo argumentInfo = TypedConstantInfo.From(operation);
79+
80+
// Try to get the identifier name if the current expression is a named argument expression. If it
81+
// isn't, then the expression is a normal attribute constructor argument, so no extra work is needed.
82+
if (argument.NameEquals is { Name.Identifier.ValueText: string argumentName })
83+
{
84+
namedArguments.Add((argumentName, argumentInfo));
85+
}
86+
else
87+
{
88+
constructorArguments.Add(argumentInfo);
89+
}
90+
}
91+
92+
return new(
93+
typeName,
94+
constructorArguments.ToImmutable(),
95+
namedArguments.ToImmutable());
96+
}
97+
5498
/// <summary>
5599
/// Gets an <see cref="AttributeSyntax"/> instance representing the current value.
56100
/// </summary>

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/TypedConstantInfo.Factory.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Immutable;
77
using System.Linq;
88
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.Operations;
910

1011
namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
1112

@@ -57,4 +58,61 @@ public static TypedConstantInfo From(TypedConstant arg)
5758
_ => throw new ArgumentException("Invalid typed constant type"),
5859
};
5960
}
61+
62+
/// <summary>
63+
/// Creates a new <see cref="TypedConstantInfo"/> instance from a given <see cref="IOperation"/> value.
64+
/// </summary>
65+
/// <param name="arg">The input <see cref="IOperation"/> value.</param>
66+
/// <returns>A <see cref="TypedConstantInfo"/> instance representing <paramref name="arg"/>.</returns>
67+
/// <exception cref="ArgumentException">Thrown if the input argument is not valid.</exception>
68+
public static TypedConstantInfo From(IOperation arg)
69+
{
70+
if (arg.ConstantValue.HasValue)
71+
{
72+
// Enum values are constant but need to be checked explicitly in this case
73+
if (arg.Type?.TypeKind is TypeKind.Enum)
74+
{
75+
return new Enum(arg.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), arg.ConstantValue.Value!);
76+
}
77+
78+
// Handle all other constant literals normally
79+
return arg.ConstantValue.Value switch
80+
{
81+
null => new Null(),
82+
string text => new Primitive.String(text),
83+
bool flag => new Primitive.Boolean(flag),
84+
byte b => new Primitive.Of<byte>(b),
85+
char c => new Primitive.Of<char>(c),
86+
double d => new Primitive.Of<double>(d),
87+
float f => new Primitive.Of<float>(f),
88+
int i => new Primitive.Of<int>(i),
89+
long l => new Primitive.Of<long>(l),
90+
sbyte sb => new Primitive.Of<sbyte>(sb),
91+
short sh => new Primitive.Of<short>(sh),
92+
uint ui => new Primitive.Of<uint>(ui),
93+
ulong ul => new Primitive.Of<ulong>(ul),
94+
ushort ush => new Primitive.Of<ushort>(ush),
95+
_ => throw new ArgumentException("Invalid primitive type")
96+
};
97+
}
98+
99+
if (arg is ITypeOfOperation typeOfOperation)
100+
{
101+
return new Type(typeOfOperation.TypeOperand.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
102+
}
103+
104+
if (arg is IArrayCreationOperation arrayCreationOperation)
105+
{
106+
string? elementTypeName = ((IArrayTypeSymbol?)arg.Type)?.ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
107+
108+
// If the element type is not available (since the attribute wasn't checked), just default to object
109+
elementTypeName ??= "object";
110+
111+
ImmutableArray<TypedConstantInfo> items = ImmutableArray<TypedConstantInfo>.Empty; // TODO
112+
113+
return new Array(elementTypeName, items);
114+
}
115+
116+
throw new ArgumentException("Invalid attribute argument value");
117+
}
60118
}

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.ComponentModel;
77
using System.Globalization;
88
using System.Linq;
9+
using System.Threading;
910
using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
1011
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
1112
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
@@ -28,10 +29,18 @@ internal static class Execute
2829
/// <summary>
2930
/// Processes a given field.
3031
/// </summary>
32+
/// <param name="fieldSyntax">The <see cref="FieldDeclarationSyntax"/> instance to process.</param>
3133
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
34+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
35+
/// <param name="token">The cancellation token for the current operation.</param>
3236
/// <param name="diagnostics">The resulting diagnostics from the processing operation.</param>
3337
/// <returns>The resulting <see cref="PropertyInfo"/> instance for <paramref name="fieldSymbol"/>, if successful.</returns>
34-
public static PropertyInfo? TryGetInfo(IFieldSymbol fieldSymbol, out ImmutableArray<Diagnostic> diagnostics)
38+
public static PropertyInfo? TryGetInfo(
39+
FieldDeclarationSyntax fieldSyntax,
40+
IFieldSymbol fieldSymbol,
41+
SemanticModel semanticModel,
42+
CancellationToken token,
43+
out ImmutableArray<Diagnostic> diagnostics)
3544
{
3645
ImmutableArray<Diagnostic>.Builder builder = ImmutableArray.CreateBuilder<Diagnostic>();
3746

@@ -168,6 +177,45 @@ internal static class Execute
168177
}
169178
}
170179

180+
// Gather explicit forwarded attributes info
181+
foreach (AttributeListSyntax attributeList in fieldSyntax.AttributeLists)
182+
{
183+
// Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a
184+
// CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor
185+
// that recognizes uses of this target specifically to support [ObservableProperty].
186+
if (attributeList.Target?.Identifier.Kind() is not SyntaxKind.PropertyKeyword)
187+
{
188+
continue;
189+
}
190+
191+
foreach (AttributeSyntax attribute in attributeList.Attributes)
192+
{
193+
// Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual.
194+
// To reconstruct all necessary attribute info to generate the serialized model, we use the following steps:
195+
// - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not
196+
// available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node.
197+
// If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute.
198+
// The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type.
199+
// - We then go over each attribute argument expression and get the operation for it. This will still be available even
200+
// though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all
201+
// constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T)
202+
// expressions, or arrays of either these two types, or of other arrays with the same rules, recursively.
203+
// - From the syntax, we can also determine the identifier names for named attribute arguments, if any.
204+
// There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the
205+
// generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the
206+
// lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway.
207+
SymbolInfo attributeSymbolInfo = semanticModel.GetSymbolInfo(attribute, token);
208+
209+
if ((attributeSymbolInfo.Symbol ?? attributeSymbolInfo.CandidateSymbols.SingleOrDefault()) is not ISymbol attributeSymbol ||
210+
(attributeSymbol as INamedTypeSymbol ?? attributeSymbol.ContainingType) is not INamedTypeSymbol attributeTypeSymbol)
211+
{
212+
continue;
213+
}
214+
215+
forwardedAttributes.Add(AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>(), token));
216+
}
217+
}
218+
171219
// Log the diagnostic for missing ObservableValidator, if needed
172220
if (hasAnyValidationAttributes &&
173221
!fieldSymbol.ContainingType.InheritsFromFullyQualifiedName("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator"))

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3737
return default;
3838
}
3939

40+
FieldDeclarationSyntax fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!;
4041
IFieldSymbol fieldSymbol = (IFieldSymbol)context.TargetSymbol;
4142

4243
// Produce the incremental models
4344
HierarchyInfo hierarchy = HierarchyInfo.From(fieldSymbol.ContainingType);
44-
PropertyInfo? propertyInfo = Execute.TryGetInfo(fieldSymbol, out ImmutableArray<Diagnostic> diagnostics);
45+
PropertyInfo? propertyInfo = Execute.TryGetInfo(fieldDeclaration, fieldSymbol, context.SemanticModel, token, out ImmutableArray<Diagnostic> diagnostics);
4546

4647
return (Hierarchy: hierarchy, new Result<PropertyInfo?>(propertyInfo, diagnostics));
4748
})

0 commit comments

Comments
 (0)