From 28128a8bfe8e6d4938696596fdad1fedc06e0115 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 10 Jul 2023 16:50:04 +0200 Subject: [PATCH 1/2] Add AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer --- .../AnalyzerReleases.Shipped.md | 8 ++ ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...etedObservablePropertyAttributeAnalyzer.cs | 74 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 16 ++++ .../Extensions/AttributeDataExtensions.cs | 15 ++++ .../Extensions/ISymbolExtensions.cs | 20 ++++- 6 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index ef01360ba..53e58bfea 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -67,3 +67,11 @@ Rule ID | Category | Severity | Notes MVVMTK0037 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0037 MVVMTK0038 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0038 MVVMTK0039 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0039 + +## Release 8.2.2 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0040 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index edcdffa58..5bc1b1792 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -41,6 +41,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..8740fe206 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,74 @@ +// 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.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error when an auto-property is using [field: ObservableProperty]. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(AutoPropertyBackingFieldObservableProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Get the property symbol and the type symbol for the containing type + if (context.Symbol is not IPropertySymbol { ContainingType: INamedTypeSymbol typeSymbol } propertySymbol) + { + return; + } + + foreach (ISymbol memberSymbol in typeSymbol.GetMembers()) + { + // We're only looking for fields with an associated property + if (memberSymbol is not IFieldSymbol { AssociatedSymbol: IPropertySymbol associatedPropertySymbol }) + { + continue; + } + + // Check that this field is in fact the backing field for the target auto-property + if (!SymbolEqualityComparer.Default.Equals(associatedPropertySymbol, propertySymbol)) + { + continue; + } + + // If the field isn't using [ObservableProperty], this analyzer isn't applicable + if (!memberSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? attributeData)) + { + return; + } + + // Report the diagnostic on the attribute location + context.ReportDiagnostic(Diagnostic.Create( + AutoPropertyBackingFieldObservableProperty, + attributeData.GetLocation(), + typeSymbol, + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 1fef89e1d..431e5da40 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -658,4 +658,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "All asynchronous methods annotated with [RelayCommand] should return a Task type, to benefit from the additional support provided by AsyncRelayCommand and AsyncRelayCommand.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0039"); + + /// + /// Gets a indicating when [ObservableProperty] is used on a generated field of an auto-property. + /// + /// Format: "The backing field for property {0}.{1} cannot be annotated with [ObservableProperty] (the attribute can only be used directly on fields, and the generator will then handle generating the corresponding property)". + /// + /// + public static readonly DiagnosticDescriptor AutoPropertyBackingFieldObservableProperty = new DiagnosticDescriptor( + id: "MVVMTK0040", + title: "[ObservableProperty] on auto-property backing field", + messageFormat: "The backing field for property {0}.{1} cannot be annotated with [ObservableProperty] (the attribute can only be used directly on fields, and the generator will then handle generating the corresponding property)", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The backing fields of auto-properties cannot be annotated with [ObservableProperty] (the attribute can only be used directly on fields, and the generator will then handle generating the corresponding property).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0040"); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs index 3956c54a1..14f7498af 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -38,6 +38,21 @@ properties.Value.Value is T argumentValue && return false; } + /// + /// Tries to get the location of the input instance. + /// + /// The input instance to get the location for. + /// The resulting location for , if a syntax reference is available. + public static Location? GetLocation(this AttributeData attributeData) + { + if (attributeData.ApplicationSyntaxReference is { } syntaxReference) + { + return syntaxReference.SyntaxTree.GetLocation(syntaxReference.Span); + } + + return null; + } + /// /// Gets a given named argument value from an instance, or a fallback value. /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs index a0fa4d315..a825454fb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs @@ -2,9 +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. -#if !ROSLYN_4_3_1_OR_GREATER using System.Diagnostics.CodeAnalysis; -#endif using Microsoft.CodeAnalysis; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -65,21 +63,37 @@ public static bool HasAttributeWithFullyQualifiedMetadataName(this ISymbol symbo } /// - /// Checks whether or not a given symbol has an attribute with the specified fully qualified metadata name. + /// Checks whether or not a given symbol has an attribute with the specified type. /// /// The input instance to check. /// The instance for the attribute type to look for. /// Whether or not has an attribute with the specified type. public static bool HasAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol) + { + return TryGetAttributeWithType(symbol, typeSymbol, out _); + } + + /// + /// Tries to get an attribute with the specified type. + /// + /// The input instance to check. + /// The instance for the attribute type to look for. + /// The resulting attribute, if it was found. + /// Whether or not has an attribute with the specified type. + public static bool TryGetAttributeWithType(this ISymbol symbol, ITypeSymbol typeSymbol, [NotNullWhen(true)] out AttributeData? attributeData) { foreach (AttributeData attribute in symbol.GetAttributes()) { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, typeSymbol)) { + attributeData = attribute; + return true; } } + attributeData = null; + return false; } From 4c8f27b23586076ec3f81dac4ccb676e30c5a963 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 10 Jul 2023 17:03:09 +0200 Subject: [PATCH 2/2] Add unit tests for new diagnostic analyzer --- .../Test_SourceGeneratorsDiagnostics.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 26e29cdae..fc767e712 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1820,6 +1820,66 @@ public partial class MyViewModel await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); } + [TestMethod] + public async Task FieldTargetedObservablePropertyAttribute_InstanceAutoProperty() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [field: {|MVVMTK0040:ObservableProperty|}] + public string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task FieldTargetedObservablePropertyAttribute_StaticAutoProperty() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [field: {|MVVMTK0040:ObservableProperty|}] + public static string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task FieldTargetedObservablePropertyAttribute_RecordPrimaryConstructorParameter() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial record SampleViewModel([field: {|MVVMTK0040:ObservableProperty|}] string Name); + } + + namespace System.Runtime.CompilerServices + { + internal static class IsExternalInit + { + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); + } + /// /// Verifies the diagnostic errors for a given analyzer, and that all available source generators can run successfully with the input source (including subsequent compilation). ///