Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\FieldReferenceForObservablePropertyFieldAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Analyzers\UnsupportedCSharpLanguageVersionAnalyzer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\Suppressors\ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\DiagnosticDescriptors.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Diagnostics\SuppressionDescriptors.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\AttributeDataExtensions.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
using CommunityToolkit.Mvvm.SourceGenerators.Helpers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

Expand All @@ -17,10 +18,12 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
/// <summary>
/// A model representing an attribute declaration.
/// </summary>
/// <param name="AttributeTarget">Indicates the target of the attribute.</param>
/// <param name="TypeName">The type name of the attribute.</param>
/// <param name="ConstructorArgumentInfo">The <see cref="TypedConstantInfo"/> values for all constructor arguments for the attribute.</param>
/// <param name="NamedArgumentInfo">The <see cref="TypedConstantInfo"/> values for all named arguments for the attribute.</param>
internal sealed record AttributeInfo(
SyntaxKind AttributeTarget,
string TypeName,
EquatableArray<TypedConstantInfo> ConstructorArgumentInfo,
EquatableArray<(string Name, TypedConstantInfo Value)> NamedArgumentInfo)
Expand Down Expand Up @@ -50,6 +53,7 @@ public static AttributeInfo Create(AttributeData attributeData)
}

return new(
SyntaxKind.PropertyKeyword,
typeName,
constructorArguments.ToImmutable(),
namedArguments.ToImmutable());
Expand All @@ -61,13 +65,15 @@ public static AttributeInfo Create(AttributeData attributeData)
/// <param name="typeSymbol">The symbol for the attribute type.</param>
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
/// <param name="arguments">The sequence of <see cref="AttributeArgumentSyntax"/> instances to process.</param>
/// <param name="syntaxKind">The kind of target for the attribute.</param>
/// <param name="token">The cancellation token for the current operation.</param>
/// <param name="info">The resulting <see cref="AttributeInfo"/> instance, if available</param>
/// <returns>Whether a resulting <see cref="AttributeInfo"/> instance could be created.</returns>
public static bool TryCreate(
INamedTypeSymbol typeSymbol,
SemanticModel semanticModel,
IEnumerable<AttributeArgumentSyntax> arguments,
SyntaxKind syntaxKind,
CancellationToken token,
[NotNullWhen(true)] out AttributeInfo? info)
{
Expand Down Expand Up @@ -105,6 +111,7 @@ public static bool TryCreate(
}

info = new AttributeInfo(
syntaxKind,
typeName,
constructorArguments.ToImmutable(),
namedArguments.ToImmutable());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,10 @@ public static bool TryGetInfo(
// Gather explicit forwarded attributes info
foreach (AttributeListSyntax attributeList in fieldSyntax.AttributeLists)
{
// Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a
// CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor
// that recognizes uses of this target specifically to support [ObservableProperty].
if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword))
// Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will
// normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic
// suppressor that recognizes uses of this target specifically to support [ObservableProperty].
if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.GetKeyword or SyntaxKind.SetKeyword) targetIdentifier)
{
continue;
}
Expand Down Expand Up @@ -256,7 +256,7 @@ public static bool TryGetInfo(
IEnumerable<AttributeArgumentSyntax> attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty<AttributeArgumentSyntax>();

// Try to extract the forwarded attribute
if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo))
if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo))
{
builder.Add(
InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField,
Expand Down Expand Up @@ -1025,11 +1025,22 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
Argument(IdentifierName("value")))),
Block(setterStatements.AsEnumerable()));

// Prepare the forwarded attributes, if any
ImmutableArray<AttributeListSyntax> forwardedAttributes =
// Prepare the forwarded attributes, if any, for all targets
AttributeListSyntax[] forwardedPropertyAttributes =
propertyInfo.ForwardedAttributes
.Where(static a => a.AttributeTarget is SyntaxKind.PropertyKeyword)
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
.ToImmutableArray();
.ToArray();
AttributeListSyntax[] forwardedGetAccessorAttributes =
propertyInfo.ForwardedAttributes
.Where(static a => a.AttributeTarget is SyntaxKind.GetKeyword)
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
.ToArray();
AttributeListSyntax[] forwardedSetAccessorAttributes =
propertyInfo.ForwardedAttributes
.Where(static a => a.AttributeTarget is SyntaxKind.SetKeyword)
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
.ToArray();

// Prepare the setter for the generated property:
//
Expand Down Expand Up @@ -1065,6 +1076,9 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("The type of the current instance cannot be statically discovered.")))))));
}

// Also add any forwarded attributes
setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes);

// Construct the generated property as follows:
//
// /// <inheritdoc cref="<FIELD_NAME>"/>
Expand All @@ -1073,6 +1087,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
// <FORWARDED_ATTRIBUTES>
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
// {
// <FORWARDED_ATTRIBUTES>
// get => <FIELD_NAME>;
// <SET_ACCESSOR>
// }
Expand All @@ -1086,12 +1101,13 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString()))))))
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{getterFieldIdentifierName}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())),
AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))
.AddAttributeLists(forwardedAttributes.ToArray())
.AddAttributeLists(forwardedPropertyAttributes)
.AddModifiers(Token(SyntaxKind.PublicKeyword))
.AddAccessorListAccessors(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithExpressionBody(ArrowExpressionClause(getterFieldExpression))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
.AddAttributeLists(forwardedGetAccessorAttributes),
setAccessor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ internal static class SuppressionDescriptors
public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new(
id: "MVVMTKSPR0001",
suppressedDiagnosticId: "CS0657",
justification: "Fields using [ObservableProperty] can use [property:] attribute lists to forward attributes to the generated properties");
justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties");

/// <summary>
/// Gets a <see cref="SuppressionDescriptor"/> for a field using [ObservableProperty] with an attribute list targeting a get or set accessor.
/// </summary>
public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyFieldAccessors = new(
id: "MVVMTKSPR0001",
suppressedDiagnosticId: "CS0658",
justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties");

/// <summary>
/// Gets a <see cref="SuppressionDescriptor"/> for a method using [RelayCommand] with an attribute list targeting a field or property.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;

/// <summary>
/// <para>
/// A diagnostic suppressor to suppress CS0657 warnings for fields with [ObservableProperty] using a [property:] attribute list.
/// A diagnostic suppressor to suppress CS0657 warnings for fields with [ObservableProperty] using a [property:] attribute list (or [set:] or [get:]).
/// </para>
/// <para>
/// That is, this diagnostic suppressor will suppress the following diagnostic:
Expand All @@ -29,10 +29,10 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
/// </para>
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor
public sealed class ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor : DiagnosticSuppressor
{
/// <inheritdoc/>
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(PropertyAttributeListForObservablePropertyField);
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(PropertyAttributeListForObservablePropertyField, PropertyAttributeListForObservablePropertyFieldAccessors);

/// <inheritdoc/>
public override void ReportSuppressions(SuppressionAnalysisContext context)
Expand All @@ -43,7 +43,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)

// Check that the target is effectively [property:] over a field declaration with at least one variable, which is the only case we are interested in
if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: FieldDeclarationSyntax { Declaration.Variables.Count: > 0 } fieldDeclaration } attributeTarget &&
attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword))
(attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword) || attributeTarget.Identifier.IsKind(SyntaxKind.GetKeyword) || attributeTarget.Identifier.IsKind(SyntaxKind.SetKeyword)))
{
SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models;
/// <param name="AllowConcurrentExecutions">Whether or not concurrent executions have been enabled.</param>
/// <param name="FlowExceptionsToTaskScheduler">Whether or not exceptions should flow to the task scheduler.</param>
/// <param name="IncludeCancelCommand">Whether or not to also generate a cancel command.</param>
/// <param name="ForwardedFieldAttributes">The sequence of forwarded attributes for the generated field.</param>
/// <param name="ForwardedPropertyAttributes">The sequence of forwarded attributes for the generated property.</param>
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated members.</param>
internal sealed record CommandInfo(
string MethodName,
string FieldName,
Expand All @@ -39,5 +38,4 @@ internal sealed record CommandInfo(
bool AllowConcurrentExecutions,
bool FlowExceptionsToTaskScheduler,
bool IncludeCancelCommand,
EquatableArray<AttributeInfo> ForwardedFieldAttributes,
EquatableArray<AttributeInfo> ForwardedPropertyAttributes);
EquatableArray<AttributeInfo> ForwardedAttributes);
Loading