From 0b8ee5a0dd003f23135a152f9ba0f35506e2137c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 8 Mar 2023 17:35:38 +0100 Subject: [PATCH 1/6] Add SymbolInfoExtensions extensions, minor code tweaks --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 7 +--- .../Extensions/SymbolInfoExtensions.cs | 41 +++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 5e950360e..dd21c1870 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -54,6 +54,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index d84c3f96e..4cfbe41d0 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -214,12 +214,7 @@ public static bool TryGetInfo( // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. - SymbolInfo attributeSymbolInfo = semanticModel.GetSymbolInfo(attribute, token); - - // Check if the attribute type can be resolved, and emit a diagnostic if that is not the case. This happens if eg. the - // attribute type is spelled incorrectly, or if a user is missing the using directive for the attribute type being used. - if ((attributeSymbolInfo.Symbol ?? attributeSymbolInfo.CandidateSymbols.SingleOrDefault()) is not ISymbol attributeSymbol || - (attributeSymbol as INamedTypeSymbol ?? attributeSymbol.ContainingType) is not INamedTypeSymbol attributeTypeSymbol) + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) { builder.Add( InvalidPropertyTargetedAttributeOnObservablePropertyField, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs new file mode 100644 index 000000000..fc9be59f4 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SymbolInfoExtensions +{ + /// + /// Tries to get the resolved attribute type symbol from a given value. + /// + /// The value to check. + /// The resulting attribute type symbol, if correctly resolved. + /// Whether is resolved to a symbol. + /// + /// This can be used to ensure users haven't eg. spelled names incorrecty or missed a using directive. Normally, code would just + /// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore + /// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own. + /// + public static bool TryGetAttributeTypeSymbol(this SymbolInfo symbolInfo, [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol) + { + if ((symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.SingleOrDefault()) is not ISymbol attributeSymbol || + (attributeSymbol as INamedTypeSymbol ?? attributeSymbol.ContainingType) is not INamedTypeSymbol resultingSymbol) + { + typeSymbol = null; + + return false; + } + + typeSymbol = resultingSymbol; + + return true; + } +} From 1320475d0d53f2e13a745a0c0443b37e6585c565 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 8 Mar 2023 17:49:58 +0100 Subject: [PATCH 2/6] Add support for forwarded field/property command attributes --- .../AnalyzerReleases.Shipped.md | 8 ++ .../Diagnostics/DiagnosticDescriptors.cs | 16 +++ .../Input/Models/CommandInfo.cs | 7 +- .../Input/RelayCommandGenerator.Execute.cs | 107 +++++++++++++++++- .../Input/RelayCommandGenerator.cs | 8 +- 5 files changed, 142 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 957d6b750..75abaf30d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -49,3 +49,11 @@ MVVMTK0032 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenera MVVMTK0033 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0033 MVVMTK0034 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0034 MVVMTK0035 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0035 + +## Release 8.2 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MVVMTK0036 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0036 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 993f959c1..fc28be102 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -589,4 +589,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must correctly be resolved to valid types.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0035"); + + /// + /// Gets a indicating when a method with [RelayCommand] is using an invalid attribute targeting the field or property. + /// + /// Format: "The method {0} annotated with [RelayCommand] is using attribute "{1}" which was not recognized as a valid type (are you missing a using directive?)". + /// + /// + public static readonly DiagnosticDescriptor InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod = new DiagnosticDescriptor( + id: "MVVMTK0036", + title: "Invalid field targeted attribute type", + messageFormat: "The method {0} annotated with [RelayCommand] is using attribute \"{1}\" which was not recognized as a valid type (are you missing a using directive?)", + category: typeof(RelayCommandGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must correctly be resolved to valid types.", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0036"); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs index 0721cb966..16ca48c0a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; using CommunityToolkit.Mvvm.SourceGenerators.Helpers; namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models; @@ -22,6 +23,8 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models; /// Whether or not concurrent executions have been enabled. /// Whether or not exceptions should flow to the task scheduler. /// Whether or not to also generate a cancel command. +/// The sequence of forwarded attributes for the generated field. +/// The sequence of forwarded attributes for the generated property. internal sealed record CommandInfo( string MethodName, string FieldName, @@ -35,4 +38,6 @@ internal sealed record CommandInfo( CanExecuteExpressionType? CanExecuteExpressionType, bool AllowConcurrentExecutions, bool FlowExceptionsToTaskScheduler, - bool IncludeCancelCommand); + bool IncludeCancelCommand, + EquatableArray ForwardedFieldAttributes, + EquatableArray ForwardedPropertyAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs index 89b852953..3f9eb9c95 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs @@ -7,6 +7,8 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Threading; +using CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using CommunityToolkit.Mvvm.SourceGenerators.Helpers; using CommunityToolkit.Mvvm.SourceGenerators.Input.Models; @@ -32,12 +34,16 @@ internal static class Execute /// /// The input instance to process. /// The instance the method was annotated with. + /// The instance for the current run. + /// The cancellation token for the current operation. /// The resulting instance, if successfully generated. /// The resulting diagnostics from the processing operation. /// Whether a instance could be generated successfully. public static bool TryGetInfo( IMethodSymbol methodSymbol, AttributeData attributeData, + SemanticModel semanticModel, + CancellationToken token, [NotNullWhen(true)] out CommandInfo? commandInfo, out ImmutableArray diagnostics) { @@ -113,6 +119,15 @@ public static bool TryGetInfo( goto Failure; } + // Get all forwarded attributes (don't stop in case of errors, just ignore faulting attributes) + GatherForwardedAttributes( + methodSymbol, + semanticModel, + token, + in builder, + out ImmutableArray fieldAttributes, + out ImmutableArray propertyAttributes); + commandInfo = new CommandInfo( methodSymbol.Name, fieldName, @@ -126,7 +141,9 @@ public static bool TryGetInfo( canExecuteExpressionType, allowConcurrentExecutions, flowExceptionsToTaskScheduler, - generateCancelCommand); + generateCancelCommand, + fieldAttributes, + propertyAttributes); diagnostics = builder.ToImmutable(); @@ -160,10 +177,23 @@ public static ImmutableArray GetSyntax(CommandInfo comm ? commandInfo.DelegateType : $"{commandInfo.DelegateType}<{string.Join(", ", commandInfo.DelegateTypeArguments)}>"; + // Prepare the forwarded field attributes, if any + ImmutableArray forwardedFieldAttributes = + commandInfo.ForwardedFieldAttributes + .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) + .ToImmutableArray(); + + // Also prepare any forwarded property attributes + ImmutableArray forwardedPropertyAttributes = + commandInfo.ForwardedPropertyAttributes + .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) + .ToImmutableArray(); + // Construct the generated field as follows: // // /// The backing field for // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // // private ? ; FieldDeclarationSyntax fieldDeclaration = FieldDeclaration( @@ -176,7 +206,8 @@ public static ImmutableArray GetSyntax(CommandInfo comm .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))); + .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))) + .AddAttributeLists(forwardedFieldAttributes.ToArray()); // Prepares the argument to pass the underlying method to invoke using ImmutableArrayBuilder commandCreationArguments = ImmutableArrayBuilder.Rent(); @@ -265,6 +296,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm // /// Gets an and . // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // // public => ??= new (); PropertyDeclarationSyntax propertyDeclaration = PropertyDeclaration( @@ -282,6 +314,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) + .AddAttributeLists(forwardedPropertyAttributes.ToArray()) .WithExpressionBody( ArrowExpressionClause( AssignmentExpression( @@ -898,5 +931,75 @@ private static bool TryGetCanExecuteMemberFromGeneratedProperty( return false; } + + /// + /// Gathers all forwarded attributes for the generated field and property. + /// + /// The input instance to process. + /// The instance for the current run. + /// The cancellation token for the current operation. + /// The current collection of gathered diagnostics. + /// The resulting field attributes to forward. + /// The resulting property attributes to forward. + private static void GatherForwardedAttributes( + IMethodSymbol methodSymbol, + SemanticModel semanticModel, + CancellationToken token, + in ImmutableArrayBuilder diagnostics, + out ImmutableArray fieldAttributes, + out ImmutableArray propertyAttributes) + { + using ImmutableArrayBuilder fieldAttributesInfo = ImmutableArrayBuilder.Rent(); + using ImmutableArrayBuilder propertyAttributesInfo = ImmutableArrayBuilder.Rent(); + + foreach (SyntaxReference syntaxReference in methodSymbol.DeclaringSyntaxReferences) + { + // Try to get the target method declaration syntax node + if (syntaxReference.GetSyntax(token) is not MethodDeclarationSyntax methodDeclaration) + { + continue; + } + + // Gather explicit forwarded attributes info + foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists) + { + // Same as in the [ObservableProperty] generator, except we're also looking for fields here + if (attributeList.Target?.Identifier.Kind() is not (SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword)) + { + continue; + } + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + // Get the symbol info for the attribute (once again just like in the [ObservableProperty] generator) + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) + { + diagnostics.Add( + InvalidFieldOrPropertyTargetedAttributeOnRelayCommandMethod, + attribute, + methodSymbol, + attribute.Name); + + continue; + } + + AttributeInfo attributeInfo = AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty(), token); + + // Add the new attribute info to the right builder + if (attributeList.Target?.Identifier.Kind() is SyntaxKind.FieldKeyword) + { + fieldAttributesInfo.Add(attributeInfo); + } + else + { + propertyAttributesInfo.Add(attributeInfo); + } + } + } + } + + fieldAttributes = fieldAttributesInfo.ToImmutable(); + propertyAttributes = propertyAttributesInfo.ToImmutable(); + } } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.cs index 46b64a56c..e18f83b9c 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.cs @@ -40,7 +40,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // Get the hierarchy info for the target symbol, and try to gather the command info HierarchyInfo? hierarchy = HierarchyInfo.From(methodSymbol.ContainingType); - _ = Execute.TryGetInfo(methodSymbol, context.Attributes[0], out CommandInfo? commandInfo, out ImmutableArray diagnostics); + _ = Execute.TryGetInfo( + methodSymbol, + context.Attributes[0], + context.SemanticModel, + token, + out CommandInfo? commandInfo, + out ImmutableArray diagnostics); return (Hierarchy: hierarchy, new Result(commandInfo, diagnostics)); }) From 41100e26324d735b0664759bbc7481dfd5a617e5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 8 Mar 2023 18:01:43 +0100 Subject: [PATCH 3/6] Add SyntaxTokenExtensions, minor code refactoring --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 2 +- .../Extensions/SyntaxTokenExtensions.cs | 24 +++++++++++++++++++ .../Input/RelayCommandGenerator.Execute.cs | 4 ++-- 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenExtensions.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index dd21c1870..244c59521 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -59,6 +59,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 4cfbe41d0..c6d35b1d8 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -193,7 +193,7 @@ public static bool TryGetInfo( // 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.Kind() is not SyntaxKind.PropertyKeyword) + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) { continue; } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenExtensions.cs new file mode 100644 index 000000000..70f725e19 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxTokenExtensions +{ + /// + /// Deconstructs a into its value. + /// + /// The input value. + /// The resulting value for . + public static void Deconstruct(this SyntaxToken syntaxToken, out SyntaxKind syntaxKind) + { + syntaxKind = syntaxToken.Kind(); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs index 3f9eb9c95..33b2a752e 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs @@ -964,7 +964,7 @@ private static void GatherForwardedAttributes( foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists) { // Same as in the [ObservableProperty] generator, except we're also looking for fields here - if (attributeList.Target?.Identifier.Kind() is not (SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword)) + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword)) { continue; } @@ -986,7 +986,7 @@ private static void GatherForwardedAttributes( AttributeInfo attributeInfo = AttributeInfo.From(attributeTypeSymbol, semanticModel, attribute.ArgumentList?.Arguments ?? Enumerable.Empty(), token); // Add the new attribute info to the right builder - if (attributeList.Target?.Identifier.Kind() is SyntaxKind.FieldKeyword) + if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword)) { fieldAttributesInfo.Add(attributeInfo); } From 476a163433d5cd39ab377bda30cf79fa0881070a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 8 Mar 2023 18:11:49 +0100 Subject: [PATCH 4/6] Add RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../Diagnostics/SuppressionDescriptors.cs | 10 ++- ...eWithPropertyTargetDiagnosticSuppressor.cs | 6 +- ...eldOrPropertyTargetDiagnosticSuppressor.cs | 65 +++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 244c59521..b019f6d49 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -45,6 +45,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs index ad0000582..4a6c803a9 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -12,10 +12,18 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics; internal static class SuppressionDescriptors { /// - /// Gets a for a field using [ObservableProperty] with on attribute list targeting a property. + /// Gets a for a field using [ObservableProperty] with an attribute list targeting a property. /// 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"); + + /// + /// Gets a for a method using [RelayCommand] with an attribute list targeting a field or property. + /// + public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new( + id: "MVVMTKSPR0002", + suppressedDiagnosticId: "CS0657", + justification: "Methods using [RelayCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties"); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs index 5192e7412..f27f3969a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; -using System.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -19,7 +19,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// /// That is, this diagnostic suppressor will suppress the following diagnostic: /// -/// public class MyViewModel : ObservableObject +/// public partial class MyViewModel : ObservableObject /// { /// [ObservableProperty] /// [property: JsonPropertyName("Name")] @@ -53,7 +53,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) // Check if the field is using [ObservableProperty], in which case we should suppress the warning if (declaredSymbol is IFieldSymbol fieldSymbol && semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol observablePropertySymbol && - fieldSymbol.GetAttributes().Select(attribute => attribute.AttributeClass).Contains(observablePropertySymbol, SymbolEqualityComparer.Default)) + fieldSymbol.HasAttributeWithType(observablePropertySymbol)) { context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyField, diagnostic)); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs new file mode 100644 index 000000000..152d2f479 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.SuppressionDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// +/// A diagnostic suppressor to suppress CS0657 warnings for methods with [RelayCommand] using a [field:] or [property:] attribute list. +/// +/// +/// That is, this diagnostic suppressor will suppress the following diagnostic: +/// +/// public partial class MyViewModel : ObservableObject +/// { +/// [RelayCommand] +/// [field: JsonIgnore] +/// [property: SomeOtherAttribute] +/// private void DoSomething() +/// { +/// } +/// } +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class RelayCommandAttributeWithFieldOrPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor +{ + /// + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(FieldOrPropertyAttributeListForRelayCommandMethod); + + /// + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (Diagnostic diagnostic in context.ReportedDiagnostics) + { + SyntaxNode? syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + + // Check that the target is effectively [field:] or [property:] over a method declaration, which is the case we're looking for + if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: MethodDeclarationSyntax methodDeclaration, Identifier: SyntaxToken(SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) }) + { + SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); + + // Get the method symbol from the first variable declaration + ISymbol? declaredSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration, context.CancellationToken); + + // Check if the method is using [RelayCommand], in which case we should suppress the warning + if (declaredSymbol is IMethodSymbol methodSymbol && + semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") is INamedTypeSymbol relayCommandSymbol && + methodSymbol.HasAttributeWithType(relayCommandSymbol)) + { + context.ReportSuppression(Suppression.Create(FieldOrPropertyAttributeListForRelayCommandMethod, diagnostic)); + } + } + } + } +} From fd7fa695385d183a5fea70b2281cbceb54b1a47b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 8 Mar 2023 18:59:53 +0100 Subject: [PATCH 5/6] Add unit tests for forwarded [RelayCommand] attributes --- .../Test_SourceGeneratorsCodegen.cs | 75 ++++++++ .../Test_SourceGeneratorsDiagnostics.cs | 53 ++++++ .../Test_ObservablePropertyAttribute.cs | 8 +- .../Test_RelayCommandAttribute.cs | 164 ++++++++++++++++++ 4 files changed, 296 insertions(+), 4 deletions(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index 88567b938..fa76a7126 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -193,6 +193,81 @@ public object? A VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } + [TestMethod] + public void RelayCommandMethodWithForwardedAttributesWithNumberLiterals_PreservesType() + { + string source = """ + using CommunityToolkit.Mvvm.Input; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel + { + const double MyDouble = 3.14; + const float MyFloat = 3.14f; + + [RelayCommand] + [field: DefaultValue(0.0)] + [field: DefaultValue(1.24)] + [field: DefaultValue(0.0f)] + [field: DefaultValue(0.0f)] + [field: DefaultValue(MyDouble)] + [field: DefaultValue(MyFloat)] + [property: DefaultValue(0.0)] + [property: DefaultValue(1.24)] + [property: DefaultValue(0.0f)] + [property: DefaultValue(0.0f)] + [property: DefaultValue(MyDouble)] + [property: DefaultValue(MyFloat)] + private void Test() + { + } + } + + public class DefaultValueAttribute : Attribute + { + public DefaultValueAttribute(object value) + { + } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// The backing field for . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")] + [global::MyApp.DefaultValueAttribute(0D)] + [global::MyApp.DefaultValueAttribute(1.24D)] + [global::MyApp.DefaultValueAttribute(0F)] + [global::MyApp.DefaultValueAttribute(0F)] + [global::MyApp.DefaultValueAttribute(3.14D)] + [global::MyApp.DefaultValueAttribute(3.14F)] + private global::CommunityToolkit.Mvvm.Input.RelayCommand? testCommand; + /// Gets an instance wrapping . + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::MyApp.DefaultValueAttribute(0D)] + [global::MyApp.DefaultValueAttribute(1.24D)] + [global::MyApp.DefaultValueAttribute(0F)] + [global::MyApp.DefaultValueAttribute(0F)] + [global::MyApp.DefaultValueAttribute(3.14D)] + [global::MyApp.DefaultValueAttribute(3.14F)] + public global::CommunityToolkit.Mvvm.Input.IRelayCommand TestCommand => testCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Test)); + } + } + """; + + VerifyGenerateSources(source, new[] { new RelayCommandGenerator() }, ("MyApp.MyViewModel.Test.g.cs", result)); + } + [TestMethod] public void ObservablePropertyWithinGenericAndNestedTypes() { diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index fcb059fb5..414aa6566 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1672,6 +1672,59 @@ public partial class MyViewModel : ObservableObject VerifyGeneratedDiagnostics(source, "MVVMTK0035"); } + [TestMethod] + public void InvalidPropertyTargetedAttributeOnRelayCommandMethod_MissingUsingDirective() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.Input; + + namespace MyApp + { + public partial class MyViewModel + { + [RelayCommand] + [property: MyTest] + private void Test() + { + } + } + } + + namespace MyAttributes + { + [AttributeUsage(AttributeTargets.Property)] + public class MyTestAttribute : Attribute + { + } + } + """; + + VerifyGeneratedDiagnostics(source, "MVVMTK0036"); + } + + [TestMethod] + public void InvalidPropertyTargetedAttributeOnRelayCommandMethod_TypoInAttributeName() + { + string source = """ + using CommunityToolkit.Mvvm.Input; + + namespace MyApp + { + public partial class MyViewModel + { + [RelayCommand] + [property: Fbuifbweif] + private void Test() + { + } + } + } + """; + + VerifyGeneratedDiagnostics(source, "MVVMTK0036"); + } + /// /// Verifies the diagnostic errors for a given analyzer, and that all available source generators can run successfully with the input source (including subsequent compilation). /// diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index e9d5606d2..e485793df 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs @@ -1611,13 +1611,13 @@ public partial class MyViewModelWithExplicitPropertyAttributes : ObservableValid private int someComplexRandomAttribute; } - [AttributeUsage(AttributeTargets.Property)] - private sealed class TestAttribute : Attribute + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class TestAttribute : Attribute { } - [AttributeUsage(AttributeTargets.Property)] - private sealed class PropertyInfoAttribute : Attribute + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class PropertyInfoAttribute : Attribute { public PropertyInfoAttribute(object? o, Type t, bool flag, double d, string[] names, object[] objects) { diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs index 681f2fe08..3114cf1e5 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_RelayCommandAttribute.cs @@ -4,10 +4,13 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using System.Xml.Serialization; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -565,6 +568,97 @@ public void Test_RelayCommandAttribute_CanExecuteWithNullabilityAnnotations() Assert.IsTrue(model.DoSomething3Command.CanExecute((0, "Hello"))); } + [TestMethod] + public void Test_RelayCommandAttribute_WithExplicitAttributesForFieldAndProperty() + { + FieldInfo fooField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("fooCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; + + Assert.IsNotNull(fooField.GetCustomAttribute()); + Assert.IsNotNull(fooField.GetCustomAttribute()); + Assert.AreEqual(fooField.GetCustomAttribute()!.Length, 1); + Assert.IsNotNull(fooField.GetCustomAttribute()); + Assert.AreEqual(fooField.GetCustomAttribute()!.Length, 100); + + PropertyInfo fooProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooCommand")!; + + Assert.IsNotNull(fooProperty.GetCustomAttribute()); + Assert.IsNotNull(fooProperty.GetCustomAttribute()); + Assert.AreEqual(fooProperty.GetCustomAttribute()!.Length, 1); + Assert.IsNotNull(fooProperty.GetCustomAttribute()); + Assert.AreEqual(fooProperty.GetCustomAttribute()!.Length, 100); + + PropertyInfo barProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("BarCommand")!; + + Assert.IsNotNull(barProperty.GetCustomAttribute()); + Assert.AreEqual(barProperty.GetCustomAttribute()!.Name, "bar"); + Assert.IsNotNull(barProperty.GetCustomAttribute()); + + PropertyInfo bazProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("BazCommand")!; + + Assert.IsNotNull(bazProperty.GetCustomAttribute()); + + static void ValidateTestAttribute(TestValidationAttribute testAttribute) + { + Assert.IsNotNull(testAttribute); + Assert.IsNull(testAttribute.O); + Assert.AreEqual(testAttribute.T, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes)); + Assert.AreEqual(testAttribute.Flag, true); + Assert.AreEqual(testAttribute.D, 6.28); + CollectionAssert.AreEqual(testAttribute.Names, new[] { "Bob", "Ross" }); + + object[]? nestedArray = (object[]?)testAttribute.NestedArray; + + Assert.IsNotNull(nestedArray); + Assert.AreEqual(nestedArray!.Length, 3); + Assert.AreEqual(nestedArray[0], 1); + Assert.AreEqual(nestedArray[1], "Hello"); + Assert.IsTrue(nestedArray[2] is int[]); + CollectionAssert.AreEqual((int[])nestedArray[2], new[] { 2, 3, 4 }); + + Assert.AreEqual(testAttribute.Animal, Test_ObservablePropertyAttribute.Animal.Llama); + } + + FieldInfo fooBarField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("fooBarCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; + + ValidateTestAttribute(fooBarField.GetCustomAttribute()!); + + PropertyInfo fooBarProperty = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("FooBarCommand")!; + + ValidateTestAttribute(fooBarProperty.GetCustomAttribute()!); + + FieldInfo barBazField = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetField("barBazCommand", BindingFlags.Instance | BindingFlags.NonPublic)!; + + Assert.IsNotNull(barBazField.GetCustomAttribute()); + + PropertyInfo barBazCommand = typeof(MyViewModelWithExplicitFieldAndPropertyAttributes).GetProperty("BarBazCommand")!; + + Assert.IsNotNull(barBazCommand.GetCustomAttribute()); + + Test_ObservablePropertyAttribute.PropertyInfoAttribute testAttribute2 = barBazCommand.GetCustomAttribute()!; + + Assert.IsNotNull(testAttribute2); + Assert.IsNull(testAttribute2.O); + Assert.AreEqual(testAttribute2.T, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes)); + Assert.AreEqual(testAttribute2.Flag, true); + Assert.AreEqual(testAttribute2.D, 6.28); + Assert.IsNotNull(testAttribute2.Objects); + Assert.IsTrue(testAttribute2.Objects is object[]); + Assert.AreEqual(((object[])testAttribute2.Objects).Length, 1); + Assert.AreEqual(((object[])testAttribute2.Objects)[0], "Test"); + CollectionAssert.AreEqual(testAttribute2.Names, new[] { "Bob", "Ross" }); + + object[]? nestedArray2 = (object[]?)testAttribute2.NestedArray; + + Assert.IsNotNull(nestedArray2); + Assert.AreEqual(nestedArray2!.Length, 4); + Assert.AreEqual(nestedArray2[0], 1); + Assert.AreEqual(nestedArray2[1], "Hello"); + Assert.AreEqual(nestedArray2[2], 42); + Assert.IsNull(nestedArray2[3]); + + Assert.AreEqual(testAttribute2.Animal, (Test_ObservablePropertyAttribute.Animal)67); + } + #region Region public class Region { @@ -1038,4 +1132,74 @@ private void DoSomething3((int A, string? B) parameter) { } } + + public partial class MyViewModelWithExplicitFieldAndPropertyAttributes + { + [RelayCommand] + [field: Required] + [field: MinLength(1)] + [field: MaxLength(100)] + [property: Required] + [property: MinLength(1)] + [property: MaxLength(100)] + private void Foo() + { + } + + [RelayCommand] + [property: JsonPropertyName("bar")] + [property: XmlIgnore] + private void Bar() + { + } + + [RelayCommand] + [property: Test_ObservablePropertyAttribute.Test] + private async Task BazAsync() + { + await Task.Yield(); + } + + [RelayCommand] + [field: TestValidation(null, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Test_ObservablePropertyAttribute.Animal.Llama)] + [property: TestValidation(null, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Test_ObservablePropertyAttribute.Animal.Llama)] + private void FooBar() + { + } + + [RelayCommand] + [field: Test_ObservablePropertyAttribute.Test] + [property: Test_ObservablePropertyAttribute.Test] + [property: Test_ObservablePropertyAttribute.PropertyInfo(null, typeof(MyViewModelWithExplicitFieldAndPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, new object[] { "Test" }, NestedArray = new object[] { 1, "Hello", 42, null }, Animal = (Test_ObservablePropertyAttribute.Animal)67)] + private void BarBaz() + { + } + } + + // Copy of the attribute from Test_ObservablePropertyAttribute, to test nested types + private sealed class TestValidationAttribute : ValidationAttribute + { + public TestValidationAttribute(object? o, Type t, bool flag, double d, string[] names) + { + O = o; + T = t; + Flag = flag; + D = d; + Names = names; + } + + public object? O { get; } + + public Type T { get; } + + public bool Flag { get; } + + public double D { get; } + + public string[] Names { get; } + + public object? NestedArray { get; set; } + + public Test_ObservablePropertyAttribute.Animal Animal { get; set; } + } } From 13ce99005a05487e593f72bb122713318a567944 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 8 Mar 2023 20:18:25 +0100 Subject: [PATCH 6/6] Remove SingleOrDefault() use in SymbolInfo extension --- .../Extensions/SymbolInfoExtensions.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs index fc9be59f4..c87f9a8fb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.CodeAnalysis; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -26,8 +25,16 @@ internal static class SymbolInfoExtensions /// public static bool TryGetAttributeTypeSymbol(this SymbolInfo symbolInfo, [NotNullWhen(true)] out INamedTypeSymbol? typeSymbol) { - if ((symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.SingleOrDefault()) is not ISymbol attributeSymbol || - (attributeSymbol as INamedTypeSymbol ?? attributeSymbol.ContainingType) is not INamedTypeSymbol resultingSymbol) + ISymbol? attributeSymbol = symbolInfo.Symbol; + + // If no symbol is selected and there is a single candidate symbol, use that + if (attributeSymbol is null && symbolInfo.CandidateSymbols is [ISymbol candidateSymbol]) + { + attributeSymbol = candidateSymbol; + } + + // Extract the symbol from either the current one or the containing type + if ((attributeSymbol as INamedTypeSymbol ?? attributeSymbol?.ContainingType) is not INamedTypeSymbol resultingSymbol) { typeSymbol = null;