diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems
index 40339da45..0b0abb069 100644
--- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems
+++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems
@@ -12,6 +12,7 @@
+
diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs
index a8067ace3..ba0c3bec6 100644
--- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs
+++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs
@@ -100,7 +100,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration &&
identifierName == fieldName)
{
- // Register the code fix to update the class declaration to inherit from ObservableObject instead
+ // Register the code fix to convert the field declaration to a partial property
context.RegisterCodeFix(
CodeAction.Create(
title: "Use a partial property",
diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs
new file mode 100644
index 000000000..9b4d7e63d
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs
@@ -0,0 +1,239 @@
+// 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.
+
+#if ROSLYN_4_12_0_OR_GREATER
+
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Editing;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.Simplification;
+using Microsoft.CodeAnalysis.Text;
+using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
+using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
+
+namespace CommunityToolkit.Mvvm.CodeFixers;
+
+///
+/// A code fixer that converts semi-auto properties to partial properties using [ObservableProperty].
+///
+[ExportCodeFixProvider(LanguageNames.CSharp)]
+[Shared]
+public sealed class UsePartialPropertyForSemiAutoPropertyCodeFixer : CodeFixProvider
+{
+ ///
+ public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoPropertyId);
+
+ ///
+ public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider()
+ {
+ return new FixAllProvider();
+ }
+
+ ///
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ Diagnostic diagnostic = context.Diagnostics[0];
+ TextSpan diagnosticSpan = context.Span;
+
+ // This code fixer needs the semantic model, so check that first
+ if (!context.Document.SupportsSemanticModel)
+ {
+ return;
+ }
+
+ SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+
+ // Get the property declaration from the target diagnostic
+ if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration)
+ {
+ // Get the semantic model, as we need to resolve symbols
+ SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!;
+
+ // Make sure we can resolve the [ObservableProperty] attribute (as we want to add it in the fixed code)
+ if (semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
+ {
+ return;
+ }
+
+ // Register the code fix to update the semi-auto property to a partial property
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Use a partial property",
+ createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration, observablePropertySymbol),
+ equivalenceKey: "Use a partial property"),
+ diagnostic);
+ }
+ }
+
+ ///
+ /// Applies the code fix to a target identifier and returns an updated document.
+ ///
+ /// The original document being fixed.
+ /// The original tree root belonging to the current document.
+ /// The for the property being updated.
+ /// The for [ObservableProperty].
+ /// An updated document with the applied code fix, and being replaced with a partial property.
+ private static async Task ConvertToPartialProperty(
+ Document document,
+ SyntaxNode root,
+ PropertyDeclarationSyntax propertyDeclaration,
+ INamedTypeSymbol observablePropertySymbol)
+ {
+ await Task.CompletedTask;
+
+ SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document);
+
+ // Create the attribute syntax for the new [ObservableProperty] attribute. Also
+ // annotate it to automatically add using directives to the document, if needed.
+ SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(observablePropertySymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation);
+ AttributeListSyntax observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax);
+
+ // Create an editor to perform all mutations
+ SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services);
+
+ ConvertToPartialProperty(
+ propertyDeclaration,
+ observablePropertyAttributeList,
+ syntaxEditor);
+
+ // Create the new document with the single change
+ return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot());
+ }
+
+ ///
+ /// Applies the code fix to a target identifier and returns an updated document.
+ ///
+ /// The for the property being updated.
+ /// The with the attribute to add.
+ /// The instance to use.
+ /// An updated document with the applied code fix, and being replaced with a partial property.
+ private static void ConvertToPartialProperty(
+ PropertyDeclarationSyntax propertyDeclaration,
+ AttributeListSyntax observablePropertyAttributeList,
+ SyntaxEditor syntaxEditor)
+ {
+ // Start setting up the updated attribute lists
+ SyntaxList attributeLists = propertyDeclaration.AttributeLists;
+
+ if (attributeLists is [AttributeListSyntax firstAttributeListSyntax, ..])
+ {
+ // Remove the trivia from the original first attribute
+ attributeLists = attributeLists.Replace(
+ nodeInList: firstAttributeListSyntax,
+ newNode: firstAttributeListSyntax.WithoutTrivia());
+
+ // If the property has at least an attribute list, move the trivia from it to the new attribute
+ observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax);
+
+ // Insert the new attribute
+ attributeLists = attributeLists.Insert(0, observablePropertyAttributeList);
+ }
+ else
+ {
+ // Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list
+ observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(propertyDeclaration);
+
+ // Save the new attribute list
+ attributeLists = attributeLists.Add(observablePropertyAttributeList);
+ }
+
+ // Get a new property that is partial and with semicolon token accessors
+ PropertyDeclarationSyntax updatedPropertyDeclaration =
+ propertyDeclaration
+ .AddModifiers(Token(SyntaxKind.PartialKeyword))
+ .WithoutLeadingTrivia()
+ .WithAttributeLists(attributeLists)
+ .WithAdditionalAnnotations(Formatter.Annotation)
+ .WithAccessorList(AccessorList(List(
+ [
+ // Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only
+ propertyDeclaration.AccessorList!.Accessors[0]
+ .WithBody(null)
+ .WithExpressionBody(null)
+ .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
+ .WithAdditionalAnnotations(Formatter.Annotation),
+ propertyDeclaration.AccessorList!.Accessors[1]
+ .WithBody(null)
+ .WithExpressionBody(null)
+ .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
+ .WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia())
+ .WithAdditionalAnnotations(Formatter.Annotation)
+ ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia()));
+
+ syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration);
+
+ // Find the parent type for the property
+ TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!;
+
+ // Make sure it's partial (we create the updated node in the function to preserve the updated property declaration).
+ // If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property.
+ if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword))
+ {
+ syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true)));
+ }
+ }
+
+ ///
+ /// A custom with the logic from .
+ ///
+ private sealed class FixAllProvider : DocumentBasedFixAllProvider
+ {
+ ///
+ protected override async Task FixAllAsync(FixAllContext fixAllContext, Document document, ImmutableArray diagnostics)
+ {
+ // Get the semantic model, as we need to resolve symbols
+ if (await document.GetSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SemanticModel semanticModel)
+ {
+ return document;
+ }
+
+ // Make sure we can resolve the [ObservableProperty] attribute here as well
+ if (semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol)
+ {
+ return document;
+ }
+
+ // Get the document root (this should always succeed)
+ if (await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SyntaxNode root)
+ {
+ return document;
+ }
+
+ SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document);
+
+ // Create the attribute syntax for the new [ObservableProperty] attribute here too
+ SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(observablePropertySymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation);
+ AttributeListSyntax observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax);
+
+ // Create an editor to perform all mutations (across all edits in the file)
+ SyntaxEditor syntaxEditor = new(root, fixAllContext.Solution.Services);
+
+ foreach (Diagnostic diagnostic in diagnostics)
+ {
+ // Get the current property declaration for the diagnostic
+ if (root.FindNode(diagnostic.Location.SourceSpan) is not PropertyDeclarationSyntax propertyDeclaration)
+ {
+ continue;
+ }
+
+ ConvertToPartialProperty(
+ propertyDeclaration,
+ observablePropertyAttributeList,
+ syntaxEditor);
+ }
+
+ return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot());
+ }
+ }
+}
+
+#endif
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
index f11f523ea..486e14158 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
@@ -97,3 +97,4 @@ MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator
MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053
MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054
MVVMTK0055 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0055
+MVVMTK0056 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0056
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
index 3c3b58a31..f875a91df 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
@@ -39,6 +39,7 @@
+
@@ -88,6 +89,7 @@
+
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs
new file mode 100644
index 000000000..a36aad071
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs
@@ -0,0 +1,346 @@
+// 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.
+
+#if ROSLYN_4_12_0_OR_GREATER
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using CommunityToolkit.Mvvm.SourceGenerators.Extensions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+using Microsoft.CodeAnalysis.PooledObjects;
+using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators;
+
+///
+/// A diagnostic analyzer that generates a suggestion whenever [ObservableProperty] is used on a semi-auto property when a partial property could be used instead.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class UseObservablePropertyOnSemiAutoPropertyAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ /// The number of pooled flags per stack (ie. how many properties we expect on average per type).
+ ///
+ private const int NumberOfPooledFlagsPerStack = 20;
+
+ ///
+ /// Shared pool for instances.
+ ///
+ [SuppressMessage("MicrosoftCodeAnalysisPerformance", "RS1008", Justification = "This is a pool of (empty) dictionaries, it is not actually storing compilation data.")]
+ private static readonly ObjectPool> PropertyMapPool = new(static () => new Dictionary(SymbolEqualityComparer.Default));
+
+ ///
+ /// Shared pool for -s of flags, one per type being processed.
+ ///
+ private static readonly ObjectPool> PropertyFlagsStackPool = new(CreatePropertyFlagsStack);
+
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoProperty);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(static context =>
+ {
+ // Using [ObservableProperty] on partial properties is only supported when using C# preview.
+ // As such, if that is not the case, return immediately, as no diagnostic should be produced.
+ if (!context.Compilation.IsLanguageVersionPreview())
+ {
+ return;
+ }
+
+ // Get the symbol for [ObservableProperty] and ObservableObject
+ if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol ||
+ context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject") is not INamedTypeSymbol observableObjectSymbol)
+ {
+ return;
+ }
+
+ // Get the symbol for the SetProperty method as well
+ if (!TryGetSetPropertyMethodSymbol(observableObjectSymbol, out IMethodSymbol? setPropertySymbol))
+ {
+ return;
+ }
+
+ context.RegisterSymbolStartAction(context =>
+ {
+ // We only care about types that could derive from ObservableObject
+ if (context.Symbol is not INamedTypeSymbol { IsStatic: false, IsReferenceType: true, BaseType.SpecialType: not SpecialType.System_Object } typeSymbol)
+ {
+ return;
+ }
+
+ // If the type does not derive from ObservableObject, ignore it
+ if (!typeSymbol.InheritsFromType(observableObjectSymbol))
+ {
+ return;
+ }
+
+ Dictionary propertyMap = PropertyMapPool.Allocate();
+ Stack propertyFlagsStack = PropertyFlagsStackPool.Allocate();
+
+ // Crawl all members to discover properties that might be of interest
+ foreach (ISymbol memberSymbol in typeSymbol.GetMembers())
+ {
+ // We're only looking for properties that might be valid candidates for conversion
+ if (memberSymbol is not IPropertySymbol
+ {
+ IsStatic: false,
+ IsPartialDefinition: false,
+ PartialDefinitionPart: null,
+ PartialImplementationPart: null,
+ ReturnsByRef: false,
+ ReturnsByRefReadonly: false,
+ Type.IsRefLikeType: false,
+ GetMethod: not null,
+ SetMethod.IsInitOnly: false
+ } propertySymbol)
+ {
+ continue;
+ }
+
+ // We can safely ignore properties that already have [ObservableProperty]
+ if (typeSymbol.HasAttributeWithType(observablePropertySymbol))
+ {
+ continue;
+ }
+
+ // Take an array from the stack or create a new one otherwise
+ bool[] flags = propertyFlagsStack.Count > 0
+ ? propertyFlagsStack.Pop()
+ : new bool[2];
+
+ // Track the property for later
+ propertyMap.Add(propertySymbol, flags);
+ }
+
+ // We want to process both accessors, where we specifically need both the syntax
+ // and their semantic model to verify what they're doing. We can use a code callback.
+ context.RegisterOperationBlockAction(context =>
+ {
+ // Make sure the current symbol is a property accessor
+ if (context.OwningSymbol is not IMethodSymbol { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet, AssociatedSymbol: IPropertySymbol propertySymbol })
+ {
+ return;
+ }
+
+ // If so, check that we are actually processing one of the properties we care about
+ if (!propertyMap.TryGetValue(propertySymbol, out bool[]? validFlags))
+ {
+ return;
+ }
+
+ // Handle the 'get' logic
+ if (SymbolEqualityComparer.Default.Equals(propertySymbol.GetMethod, context.OwningSymbol))
+ {
+ // We expect a top-level block operation, that immediately returns an expression
+ if (context.OperationBlocks is not [IBlockOperation { Operations: [IReturnOperation returnOperation] }])
+ {
+ return;
+ }
+
+ // Next, we expect the return to produce a field reference
+ if (returnOperation is not { ReturnedValue: IFieldReferenceOperation fieldReferenceOperation })
+ {
+ return;
+ }
+
+ // The field has to be implicitly declared and not constant (and not static)
+ if (fieldReferenceOperation.Field is not { IsImplicitlyDeclared: true, IsStatic: false } fieldSymbol)
+ {
+ return;
+ }
+
+ // Validate tha the field is indeed 'field' (it will be associated with the property)
+ if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.AssociatedSymbol, propertySymbol))
+ {
+ return;
+ }
+
+ // The 'get' accessor is valid
+ validFlags[0] = true;
+ }
+ else if (SymbolEqualityComparer.Default.Equals(propertySymbol.SetMethod, context.OwningSymbol))
+ {
+ // We expect a top-level block operation, that immediately performs an invocation
+ if (context.OperationBlocks is not [IBlockOperation { Operations: [IExpressionStatementOperation { Operation: IInvocationOperation invocationOperation }] }])
+ {
+ return;
+ }
+
+ // Brief filtering of the target method, also get the original definition
+ if (invocationOperation.TargetMethod is not { Name: "SetProperty", IsGenericMethod: true, IsStatic: false } methodSymbol)
+ {
+ return;
+ }
+
+ // First, check that we're calling 'ObservableObject.SetProperty'
+ if (!SymbolEqualityComparer.Default.Equals(methodSymbol.ConstructedFrom, setPropertySymbol))
+ {
+ return;
+ }
+
+ // We matched the method, now let's validate the arguments
+ if (invocationOperation.Arguments is not [{ } locationArgument, { } valueArgument, { } propertyNameArgument])
+ {
+ return;
+ }
+
+ // The field has to be implicitly declared and not constant (and not static)
+ if (locationArgument.Value is not IFieldReferenceOperation { Field: { IsImplicitlyDeclared: true, IsStatic: false } fieldSymbol })
+ {
+ return;
+ }
+
+ // Validate tha the field is indeed 'field' (it will be associated with the property)
+ if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.AssociatedSymbol, propertySymbol))
+ {
+ return;
+ }
+
+ // The value is just the 'value' keyword
+ if (valueArgument.Value is not IParameterReferenceOperation { Syntax: IdentifierNameSyntax { Identifier.Text: "value" } })
+ {
+ return;
+ }
+
+ // The property name should be the default value
+ if (propertyNameArgument is not { IsImplicit: true, ArgumentKind: ArgumentKind.DefaultValue })
+ {
+ return;
+ }
+
+ // The 'set' accessor is valid
+ validFlags[1] = true;
+ }
+ });
+
+ // We also need to track getters which have no body, and we need syntax for that
+ context.RegisterSyntaxNodeAction(context =>
+ {
+ // Let's just make sure we do have a property symbol
+ if (context.ContainingSymbol is not IPropertySymbol { GetMethod: not null } propertySymbol)
+ {
+ return;
+ }
+
+ // Lookup the property to get its flags
+ if (!propertyMap.TryGetValue(propertySymbol, out bool[]? validFlags))
+ {
+ return;
+ }
+
+ // We expect two accessors, skip if otherwise (the setter will be validated by the other callback)
+ if (context.Node is not PropertyDeclarationSyntax { AccessorList.Accessors: [{ } firstAccessor, { } secondAccessor] })
+ {
+ return;
+ }
+
+ // Check that either of them is a semicolon token 'get;' accessor (it can be in either position)
+ if (firstAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && firstAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken) ||
+ secondAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && secondAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken))
+ {
+ validFlags[0] = true;
+ }
+ }, SyntaxKind.PropertyDeclaration);
+
+ // Finally, we can consume this information when we finish processing the symbol
+ context.RegisterSymbolEndAction(context =>
+ {
+ // Emit a diagnostic for each property that was a valid match
+ foreach (KeyValuePair pair in propertyMap)
+ {
+ if (pair.Value is [true, true])
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ UseObservablePropertyOnSemiAutoProperty,
+ pair.Key.Locations.FirstOrDefault(),
+ pair.Key.ContainingType,
+ pair.Key.Name));
+ }
+ }
+
+ // Before clearing the dictionary, move back all values to the stack
+ foreach (bool[] propertyFlags in propertyMap.Values)
+ {
+ // Make sure the array is cleared before returning it
+ propertyFlags.AsSpan().Clear();
+
+ propertyFlagsStack.Push(propertyFlags);
+ }
+
+ // We are now done processing the symbol, we can return the dictionary.
+ // Note that we must clear it before doing so to avoid leaks and issues.
+ propertyMap.Clear();
+
+ PropertyMapPool.Free(propertyMap);
+
+ // Also do the same for the stack, except we don't need to clean it (since it roots no compilation objects)
+ PropertyFlagsStackPool.Free(propertyFlagsStack);
+ });
+ }, SymbolKind.NamedType);
+ });
+ }
+
+ ///
+ /// Tries to get the symbol for the target SetProperty method this analyzer looks for.
+ ///
+ /// The symbol for ObservableObject.
+ /// The resulting method symbol, if found (this should always be the case).
+ /// Whether could be resolved correctly.
+ private static bool TryGetSetPropertyMethodSymbol(INamedTypeSymbol observableObjectSymbol, [NotNullWhen(true)] out IMethodSymbol? setPropertySymbol)
+ {
+ foreach (ISymbol symbol in observableObjectSymbol.GetMembers("SetProperty"))
+ {
+ // We're guaranteed to only match methods here
+ IMethodSymbol methodSymbol = (IMethodSymbol)symbol;
+
+ // Match the exact signature we need (there's several overloads)
+ if (methodSymbol.Parameters is not
+ [
+ { Kind: SymbolKind.TypeParameter, RefKind: RefKind.Ref },
+ { Kind: SymbolKind.TypeParameter, RefKind: RefKind.None },
+ { Type.SpecialType: SpecialType.System_String }
+ ])
+ {
+ setPropertySymbol = methodSymbol;
+
+ return true;
+ }
+ }
+
+ setPropertySymbol = null;
+
+ return false;
+ }
+
+ ///
+ /// Produces a new instance to pool.
+ ///
+ /// The resulting instance to use.
+ private static Stack CreatePropertyFlagsStack()
+ {
+ static IEnumerable EnumerateFlags()
+ {
+ for (int i = 0; i < NumberOfPooledFlagsPerStack; i++)
+ {
+ yield return new bool[2];
+ }
+ }
+
+ return new(EnumerateFlags());
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
index 5196d5d89..b62fcb7ae 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs
@@ -44,6 +44,11 @@ internal static class DiagnosticDescriptors
///
public const string WinRTObservablePropertyOnFieldsIsNotAotCompatibleId = "MVVMTK0045";
+ ///
+ /// The diagnostic id for .
+ ///
+ public const string UseObservablePropertyOnSemiAutoPropertyId = "MVVMTK0056";
+
///
/// Gets a indicating when a duplicate declaration of would happen.
///
@@ -923,4 +928,20 @@ internal static class DiagnosticDescriptors
isEnabledByDefault: true,
description: "A property using [ObservableProperty] returns a pointer-like value ([ObservableProperty] must be used on properties of a non pointer-like type).",
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0055");
+
+ ///
+ /// Gets a for when a semi-auto property can be converted to use [ObservableProperty] instead.
+ ///
+ /// Format: "The semi-auto property {0}.{1} can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)".
+ ///
+ ///
+ public static readonly DiagnosticDescriptor UseObservablePropertyOnSemiAutoProperty = new DiagnosticDescriptor(
+ id: UseObservablePropertyOnSemiAutoPropertyId,
+ title: "Prefer using [ObservableProperty] over semi-auto properties",
+ messageFormat: """The semi-auto property {0}.{1} can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)""",
+ category: typeof(ObservablePropertyGenerator).FullName,
+ defaultSeverity: DiagnosticSeverity.Info,
+ isEnabledByDefault: true,
+ description: "Semi-auto properties should be converted to partial properties using [ObservableProperty] when possible, which is recommended (doing so makes the code less verbose and results in more optimized code).",
+ helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0056");
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs
new file mode 100644
index 000000000..18145afa0
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs
@@ -0,0 +1,164 @@
+// 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.
+
+// Ported and adapted from https://github.com/dotnet/roslyn
+
+using System;
+using System.Diagnostics;
+using System.Threading;
+
+#pragma warning disable RS1035, IDE0290
+
+namespace Microsoft.CodeAnalysis.PooledObjects;
+
+///
+/// Generic implementation of object pooling pattern with predefined pool size limit. The main
+/// purpose is that limited number of frequently used objects can be kept in the pool for
+/// further recycling.
+///
+/// Notes:
+/// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there
+/// is no space in the pool, extra returned objects will be dropped.
+///
+/// 2) it is implied that if object was obtained from a pool, the caller will return it back in
+/// a relatively short time. Keeping checked out objects for long durations is ok, but
+/// reduces usefulness of pooling. Just new up your own.
+///
+/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice.
+/// Rationale:
+/// If there is no intent for reusing the object, do not use pool - just use "new".
+///
+internal sealed class ObjectPool
+ where T : class
+{
+ // Storage for the pool objects. The first item is stored in a dedicated field because we
+ // expect to be able to satisfy most requests from it.
+ private T? firstItem;
+ private readonly Element[] items;
+
+ // The factory is stored for the lifetime of the pool. We will call this only when pool needs to
+ // expand. compared to "new T()", Func gives more flexibility to implementers and faster
+ // than "new T()".
+ private readonly Func factory;
+
+ ///
+ /// Creates a new instance with a given factory.
+ ///
+ /// The factory to use to produce new objects.
+ public ObjectPool(Func factory)
+ : this(factory, Environment.ProcessorCount * 2)
+ {
+ }
+
+ ///
+ /// Creates a new instance with a given factory.
+ ///
+ /// The factory to use to produce new objects.
+ /// The size of the pool.
+ public ObjectPool(Func factory, int size)
+ {
+ this.factory = factory;
+ this.items = new Element[size - 1];
+ }
+
+ ///
+ /// Produces an instance.
+ ///
+ /// The instance to return to the pool later.
+ ///
+ /// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
+ /// Note that Free will try to store recycled objects close to the start thus statistically
+ /// reducing how far we will typically search.
+ ///
+ public T Allocate()
+ {
+ // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements.
+ // Note that the initial read is optimistically not synchronized. That is intentional.
+ // We will interlock only when we have a candidate. in a worst case we may miss some
+ // recently returned objects. Not a big deal.
+ T? instance = this.firstItem;
+ if (instance == null || instance != Interlocked.CompareExchange(ref this.firstItem, null, instance))
+ {
+ instance = AllocateSlow();
+ }
+
+ return instance;
+ }
+
+ ///
+ /// Slow path to produce a new instance.
+ ///
+ /// The instance to return to the pool later.
+ private T AllocateSlow()
+ {
+ Element[] items = this.items;
+
+ for (int i = 0; i < items.Length; i++)
+ {
+ T? instance = items[i].Value;
+
+ if (instance is not null)
+ {
+ if (instance == Interlocked.CompareExchange(ref items[i].Value, null, instance))
+ {
+ return instance;
+ }
+ }
+ }
+
+ return this.factory();
+ }
+
+ ///
+ /// Returns objects to the pool.
+ ///
+ /// The object to return to the pool.
+ ///
+ /// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
+ /// Note that Free will try to store recycled objects close to the start thus statistically
+ /// reducing how far we will typically search in Allocate.
+ ///
+ public void Free(T obj)
+ {
+ if (this.firstItem is null)
+ {
+ this.firstItem = obj;
+ }
+ else
+ {
+ FreeSlow(obj);
+ }
+ }
+
+ ///
+ /// Slow path to return an object to the pool.
+ ///
+ /// The object to return to the pool.
+ private void FreeSlow(T obj)
+ {
+ Element[] items = this.items;
+
+ for (int i = 0; i < items.Length; i++)
+ {
+ if (items[i].Value == null)
+ {
+ items[i].Value = obj;
+
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Wrapper to avoid array covariance.
+ ///
+ [DebuggerDisplay("{Value,nq}")]
+ private struct Element
+ {
+ ///
+ /// The value for the current element.
+ ///
+ public T? Value;
+ }
+}
\ No newline at end of file
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs
index 07ac71412..f735ebc37 100644
--- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs
@@ -1243,4 +1243,210 @@ public unsafe partial class SampleViewModel : ObservableObject
await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
}
+
+ [TestMethod]
+ public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_NormalProperty_DoesNotWarn()
+ {
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public string Name { get; set; }
+ }
+ """;
+
+ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
+ }
+
+ [TestMethod]
+ public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_SimilarProperty_NotObservableObject_DoesNotWarn()
+ {
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : MyBaseViewModel
+ {
+ public string Name
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+
+ public abstract class MyBaseViewModel
+ {
+ protected void SetProperty(ref T location, T value, string propertyName = null)
+ {
+ }
+ }
+ """;
+
+ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
+ }
+
+ [TestMethod]
+ public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_NoGetter_DoesNotWarn()
+ {
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public string Name
+ {
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
+ }
+
+ [TestMethod]
+ public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_NoSetter_DoesNotWarn()
+ {
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public string Name
+ {
+ get => field;
+ }
+ }
+ """;
+
+ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
+ }
+
+ [TestMethod]
+ public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_OtherLocation_DoesNotWarn()
+ {
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public string Name
+ {
+ get => field;
+ set
+ {
+ string test = field;
+
+ SetProperty(ref test, value);
+ }
+ }
+ }
+ """;
+
+ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
+ }
+
+ [TestMethod]
+ public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_OtherValue_DoesNotWarn()
+ {
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public string Name
+ {
+ get => field;
+ set
+ {
+ string test = "Bob";
+
+ SetProperty(ref field, test);
+ }
+ }
+ }
+ """;
+
+ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
+ }
+
+ [TestMethod]
+ public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_Warns()
+ {
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public string {|MVVMTK0056:Name|}
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
+ }
+
+ [TestMethod]
+ public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_WithModifiers_Warns()
+ {
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public new string {|MVVMTK0056:Name|}
+ {
+ get => field;
+ private set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
+ }
+
+ [TestMethod]
+ public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_WithBlocks_Warns()
+ {
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public new string {|MVVMTK0056:Name|}
+ {
+ get
+ {
+ return field;
+ }
+ private set
+ {
+ SetProperty(ref field, value);
+ }
+ }
+ }
+ """;
+
+ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview);
+ }
}
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs
new file mode 100644
index 000000000..e9cccaa91
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs
@@ -0,0 +1,889 @@
+// 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.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using CSharpCodeFixTest = CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers.CSharpCodeFixWithLanguageVersionTest<
+ CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnSemiAutoPropertyAnalyzer,
+ CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForSemiAutoPropertyCodeFixer,
+ Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
+using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier<
+ CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnSemiAutoPropertyAnalyzer,
+ CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForSemiAutoPropertyCodeFixer,
+ Microsoft.CodeAnalysis.Testing.DefaultVerifier>;
+
+namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests;
+
+[TestClass]
+public class Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer
+{
+ [TestMethod]
+ public async Task SimpleProperty()
+ {
+ string original = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public class SampleViewModel : ObservableObject
+ {
+ public string Name
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ public partial string Name { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 23).WithArguments("MyApp.SampleViewModel", "Name"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_WithSemicolonTokenGetAccessor()
+ {
+ string original = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public class SampleViewModel : ObservableObject
+ {
+ public string Name
+ {
+ get;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ public partial string Name { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 23).WithArguments("MyApp.SampleViewModel", "Name"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_WithMissingUsingDirective()
+ {
+ string original = """
+ namespace MyApp;
+
+ public class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
+ {
+ public string Name
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
+ {
+ [ObservableProperty]
+ public partial string Name { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(5,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 19, 5, 23).WithArguments("MyApp.SampleViewModel", "Name"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_WithLeadingTrivia()
+ {
+ string original = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public class SampleViewModel : ObservableObject
+ {
+ ///
+ /// This is a property.
+ ///
+ public string Name
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ ///
+ /// This is a property.
+ ///
+ [ObservableProperty]
+ public partial string Name { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(10,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(10, 19, 10, 23).WithArguments("MyApp.SampleViewModel", "Name"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 31).WithArguments("MyApp.SampleViewModel.Name"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_WithLeadingTrivia_AndAttributes()
+ {
+ string original = """
+ using System;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public class SampleViewModel : ObservableObject
+ {
+ ///
+ /// This is a property.
+ ///
+ [Test("Targeting property")]
+ [field: Test("Targeting field")]
+ public string Name
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+
+ public class TestAttribute(string text) : Attribute;
+ """;
+
+ string @fixed = """
+ using System;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ ///
+ /// This is a property.
+ ///
+ [ObservableProperty]
+ [Test("Targeting property")]
+ [field: Test("Targeting field")]
+ public partial string Name { get; set; }
+ }
+
+ public class TestAttribute(string text) : Attribute;
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(13,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(13, 19, 13, 23).WithArguments("MyApp.SampleViewModel", "Name"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 31).WithArguments("MyApp.SampleViewModel.Name"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_Multiple()
+ {
+ string original = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public class SampleViewModel : ObservableObject
+ {
+ public string FirstName
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ public string LastName
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ public partial string FirstName { get; set; }
+
+ [ObservableProperty]
+ public partial string LastName { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 28).WithArguments("MyApp.SampleViewModel", "FirstName"),
+
+ // /0/Test0.cs(13,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(13, 19, 13, 27).WithArguments("MyApp.SampleViewModel", "LastName"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"),
+
+ // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_Multiple_OnlyTriggersOnFirstOne()
+ {
+ string original = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public class SampleViewModel : ObservableObject
+ {
+ public string FirstName
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ private string _lastName;
+
+ public string LastName
+ {
+ get => _lastName;
+ set => SetProperty(ref _lastName, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ public partial string FirstName { get; set; }
+
+ private string _lastName;
+
+ public string LastName
+ {
+ get => _lastName;
+ set => SetProperty(ref _lastName, value);
+ }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 28).WithArguments("MyApp.SampleViewModel", "FirstName"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_Multiple_OnlyTriggersOnSecondOne()
+ {
+ string original = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public class SampleViewModel : ObservableObject
+ {
+ private string _firstName;
+
+ public string FirstName
+ {
+ get => _firstName;
+ set => SetProperty(ref _firstName, value);
+ }
+
+ public string LastName
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ private string _firstName;
+
+ public string FirstName
+ {
+ get => _firstName;
+ set => SetProperty(ref _firstName, value);
+ }
+
+ [ObservableProperty]
+ public partial string LastName { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(15,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(15, 19, 15, 27).WithArguments("MyApp.SampleViewModel", "LastName"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(16,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(16, 27, 16, 35).WithArguments("MyApp.SampleViewModel.LastName"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_WithinPartialType()
+ {
+ string original = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public string Name
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ public partial string Name { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 23).WithArguments("MyApp.SampleViewModel", "Name"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_WithinPartialType_Multiple()
+ {
+ string original = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ public string FirstName
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ public string LastName
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ public partial string FirstName { get; set; }
+
+ [ObservableProperty]
+ public partial string LastName { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 28).WithArguments("MyApp.SampleViewModel", "FirstName"),
+
+ // /0/Test0.cs(13,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(13, 19, 13, 27).WithArguments("MyApp.SampleViewModel", "LastName"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"),
+
+ // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_Multiple_WithMissingUsingDirective()
+ {
+ string original = """
+ namespace MyApp;
+
+ public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
+ {
+ public string FirstName
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ public string LastName
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ public string PhoneNumber
+ {
+ get;
+ set => SetProperty(ref field, value);
+ }
+ }
+ """;
+
+ string @fixed = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject
+ {
+ [ObservableProperty]
+ public partial string FirstName { get; set; }
+
+ [ObservableProperty]
+ public partial string LastName { get; set; }
+
+ [ObservableProperty]
+ public partial string PhoneNumber { get; set; }
+ }
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(5,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 19, 5, 28).WithArguments("MyApp.SampleViewModel", "FirstName"),
+
+ // /0/Test0.cs(11,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(11, 19, 11, 27).WithArguments("MyApp.SampleViewModel", "LastName"),
+
+ // /0/Test0.cs(17,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.PhoneNumber can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(17, 19, 17, 30).WithArguments("MyApp.SampleViewModel", "PhoneNumber"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"),
+
+ // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"),
+
+ // /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.PhoneNumber' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 38).WithArguments("MyApp.SampleViewModel.PhoneNumber"),
+ });
+
+ await test.RunAsync();
+ }
+
+ [TestMethod]
+ public async Task SimpleProperty_WithinPartialType_Multiple_MixedScenario()
+ {
+ string original = """
+ using System;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ [Test("This is an attribute")]
+ public string Prop1
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ // Single comment
+ public string Prop2
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ ///
+ /// This is a property.
+ ///
+ public string Prop3
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ ///
+ /// This is another property.
+ ///
+ [Test("Another attribute")]
+ public string Prop4
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ // Some other single comment
+ [Test("Yet another attribute")]
+ public string Prop5
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ [Test("Attribute without trivia")]
+ public string Prop6
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+
+ public string Prop7
+ {
+ get => field;
+ set => SetProperty(ref field, value);
+ }
+ }
+
+ public class TestAttribute(string text) : Attribute;
+ """;
+
+ string @fixed = """
+ using System;
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp;
+
+ public partial class SampleViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ [Test("This is an attribute")]
+ public partial string Prop1 { get; set; }
+
+ // Single comment
+ [ObservableProperty]
+ public partial string Prop2 { get; set; }
+
+ ///
+ /// This is a property.
+ ///
+ [ObservableProperty]
+ public partial string Prop3 { get; set; }
+
+ ///
+ /// This is another property.
+ ///
+ [ObservableProperty]
+ [Test("Another attribute")]
+ public partial string Prop4 { get; set; }
+
+ // Some other single comment
+ [ObservableProperty]
+ [Test("Yet another attribute")]
+ public partial string Prop5 { get; set; }
+
+ [ObservableProperty]
+ [Test("Attribute without trivia")]
+ public partial string Prop6 { get; set; }
+
+ [ObservableProperty]
+ public partial string Prop7 { get; set; }
+ }
+
+ public class TestAttribute(string text) : Attribute;
+ """;
+
+ CSharpCodeFixTest test = new(LanguageVersion.Preview)
+ {
+ TestCode = original,
+ FixedCode = @fixed,
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
+ };
+
+ test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly);
+ test.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(9,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop1 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(9, 19, 9, 24).WithArguments("MyApp.SampleViewModel", "Prop1"),
+
+ // /0/Test0.cs(16,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop2 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(16, 19, 16, 24).WithArguments("MyApp.SampleViewModel", "Prop2"),
+
+ // /0/Test0.cs(25,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop3 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(25, 19, 25, 24).WithArguments("MyApp.SampleViewModel", "Prop3"),
+
+ // /0/Test0.cs(35,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop4 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(35, 19, 35, 24).WithArguments("MyApp.SampleViewModel", "Prop4"),
+
+ // /0/Test0.cs(43,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop5 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(43, 19, 43, 24).WithArguments("MyApp.SampleViewModel", "Prop5"),
+
+ // /0/Test0.cs(50,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop6 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(50, 19, 50, 24).WithArguments("MyApp.SampleViewModel", "Prop6"),
+
+ // /0/Test0.cs(56,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop7 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)
+ CSharpCodeFixVerifier.Diagnostic().WithSpan(56, 19, 56, 24).WithArguments("MyApp.SampleViewModel", "Prop7"),
+ });
+
+ test.FixedState.ExpectedDiagnostics.AddRange(new[]
+ {
+ // /0/Test0.cs(10,27): error CS9248: Partial property 'SampleViewModel.Prop1' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(10, 27, 10, 32).WithArguments("MyApp.SampleViewModel.Prop1"),
+
+ // /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.Prop2' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 32).WithArguments("MyApp.SampleViewModel.Prop2"),
+
+ // /0/Test0.cs(20,27): error CS9248: Partial property 'SampleViewModel.Prop3' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(20, 27, 20, 32).WithArguments("MyApp.SampleViewModel.Prop3"),
+
+ // /0/Test0.cs(27,27): error CS9248: Partial property 'SampleViewModel.Prop4' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(27, 27, 27, 32).WithArguments("MyApp.SampleViewModel.Prop4"),
+
+ // /0/Test0.cs(32,27): error CS9248: Partial property 'SampleViewModel.Prop5' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(32, 27, 32, 32).WithArguments("MyApp.SampleViewModel.Prop5"),
+
+ // /0/Test0.cs(36,27): error CS9248: Partial property 'SampleViewModel.Prop6' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(36, 27, 36, 32).WithArguments("MyApp.SampleViewModel.Prop6"),
+
+ // /0/Test0.cs(39,27): error CS9248: Partial property 'SampleViewModel.Prop7' must have an implementation part.
+ DiagnosticResult.CompilerError("CS9248").WithSpan(39, 27, 39, 32).WithArguments("MyApp.SampleViewModel.Prop7"),
+ });
+
+ await test.RunAsync();
+ }
+}