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;
}