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(); + } +}