From fc429245ff935a35bc7007cdbc4a06735733df7e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Dec 2024 21:47:27 -0800 Subject: [PATCH 01/14] Add 'UseObservablePropertyOnSemiAutoPropertyAnalyzer' --- .../AnalyzerReleases.Shipped.md | 8 + ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 256 ++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 21 ++ 4 files changed, 286 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index f11f523ea..345f27de7 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -97,3 +97,11 @@ 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 + +## Release 8.4.1 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +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..31ad7f8cd 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -39,6 +39,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..5695b2aa0 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -0,0 +1,256 @@ +// 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.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +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 +{ + /// + 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 = new(SymbolEqualityComparer.Default); + + // 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; + } + + // Track the property for later + propertyMap.Add(propertySymbol, new bool[2]); + } + + // 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; + } + }); + + // 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)); + } + } + }); + }, 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; + } +} + +#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"); } From 7c6eeb6f7fb07692fd343155029242cdf24f01f0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Dec 2024 22:01:01 -0800 Subject: [PATCH 02/14] Add unit tests for new analyzer --- .../Test_SourceGeneratorsDiagnostics.cs | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) 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); + } } From 162ae4f24c55eaee46a214d7a764f5ef3a2a3779 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Dec 2024 23:01:34 -0800 Subject: [PATCH 03/14] Add 'UsePartialPropertyForSemiAutoPropertyCodeFixer' --- ...CommunityToolkit.Mvvm.CodeFixers.projitems | 1 + ...lPropertyForObservablePropertyCodeFixer.cs | 2 +- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 113 ++++++++++++++++++ ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 3 +- 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs 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..ba132fccc --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -0,0 +1,113 @@ +// 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.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 FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + 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) + { + // 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), + 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. + /// 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) + { + await Task.CompletedTask; + + // Get a new property that is partial and with semicolon token accessors + PropertyDeclarationSyntax updatedPropertyDeclaration = + propertyDeclaration + .AddModifiers(Token(SyntaxKind.PartialKeyword)) + .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty"))))) + .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)) + .WithAdditionalAnnotations(Formatter.Annotation) + ]))); + + // Create an editor to perform all mutations + SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services); + + editor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); + + // Find the parent type for the property + TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!; + + // Make sure it's partial + if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + TypeDeclarationSyntax updatedTypeDeclaration = typeDeclaration.AddModifiers(Token(SyntaxKind.PartialKeyword)); + + editor.ReplaceNode(typeDeclaration, updatedTypeDeclaration); + } + + return document.WithSyntaxRoot(editor.GetChangedRoot()); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs index 5695b2aa0..37778a157 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -212,7 +212,8 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create( UseObservablePropertyOnSemiAutoProperty, pair.Key.Locations.FirstOrDefault(), - pair.Key)); + pair.Key.ContainingType, + pair.Key.Name)); } } }); From 450fb9f9c565514beae522823fd90b908cfff4c5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Dec 2024 23:04:24 -0800 Subject: [PATCH 04/14] Add unit test for code fixer --- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs 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..02a539070 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -0,0 +1,76 @@ +// 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(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } +} From 71aeda87f67c305a1534008ee4f5997e0c2ad1dd Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 10 Dec 2024 08:51:44 +0100 Subject: [PATCH 05/14] Fix failing test --- .../UsePartialPropertyForSemiAutoPropertyCodeFixer.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index ba132fccc..f7e6f7edd 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -98,12 +98,11 @@ private static async Task ConvertToPartialProperty(Document document, // Find the parent type for the property TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!; - // Make sure it's partial + // 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)) { - TypeDeclarationSyntax updatedTypeDeclaration = typeDeclaration.AddModifiers(Token(SyntaxKind.PartialKeyword)); - - editor.ReplaceNode(typeDeclaration, updatedTypeDeclaration); + editor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); } return document.WithSyntaxRoot(editor.GetChangedRoot()); From 46cd488bdf0c5471be5f5c277a0b3ef34bacf2e6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 00:29:38 -0800 Subject: [PATCH 06/14] Add more unit tests for code fixer --- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 442 +++++++++++++++++- 1 file changed, 440 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index 02a539070..18b4a281b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -67,8 +67,446 @@ public partial class SampleViewModel : ObservableObject test.FixedState.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. - DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + // /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(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_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; } + } + """; + + 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(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_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.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() + { + 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.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_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); + } + } + + public class TestAttribute(string text) : Attribute; + """; + + string @fixed = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableAttribute] + [Test("This is an attribute")] + public partial string Prop1 { get; set; } + + // Single comment + [ObservableAttribute] + public partial string Prop2 { get; set; } + + /// + /// This is a property. + /// + [ObservableAttribute] + public partial string Prop3 { get; set; } + + /// + /// This is another property. + /// + [ObservableAttribute] + [Test("Another attribute")] + public partial string Prop4 { get; set; } + + // Some other single comment + [ObservableAttribute] + [Test("Yet another attribute")] + public partial string Prop5 { 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"), + }); + + 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(); From 5e3d65fedebef7c726ce95f66a7eb3e8cb153fb6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 00:46:43 -0800 Subject: [PATCH 07/14] Improve handling of attribute lists, fix tests --- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 31 +++++++++++++++- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 36 +++++++++++++------ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index f7e6f7edd..4c3cd3049 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -69,11 +69,40 @@ private static async Task ConvertToPartialProperty(Document document, { await Task.CompletedTask; + // Prepare the [ObservableProperty] attribute, which is always inserted first + AttributeListSyntax observablePropertyAttributeList = AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty")))); + + // 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)) - .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty"))))) + .WithoutTrivia() + .WithAttributeLists(attributeLists) .WithAdditionalAnnotations(Formatter.Annotation) .WithAccessorList(AccessorList(List( [ diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index 18b4a281b..a106be081 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -126,8 +126,8 @@ public partial class SampleViewModel : ObservableObject 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"), + // /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(); @@ -175,6 +175,8 @@ public partial class SampleViewModel : ObservableObject [field: Test("Targeting field")] public partial string Name { get; set; } } + + public class TestAttribute(string text) : Attribute; """; CSharpCodeFixTest test = new(LanguageVersion.Preview) @@ -193,8 +195,8 @@ public partial class SampleViewModel : ObservableObject 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"), + // /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(); @@ -447,29 +449,29 @@ namespace MyApp; public partial class SampleViewModel : ObservableObject { - [ObservableAttribute] + [ObservableProperty] [Test("This is an attribute")] public partial string Prop1 { get; set; } // Single comment - [ObservableAttribute] + [ObservableProperty] public partial string Prop2 { get; set; } /// /// This is a property. /// - [ObservableAttribute] + [ObservableProperty] public partial string Prop3 { get; set; } /// /// This is another property. /// - [ObservableAttribute] + [ObservableProperty] [Test("Another attribute")] public partial string Prop4 { get; set; } // Some other single comment - [ObservableAttribute] + [ObservableProperty] [Test("Yet another attribute")] public partial string Prop5 { get; set; } } @@ -505,8 +507,20 @@ public class TestAttribute(string text) : Attribute; 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"), + // /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"), }); await test.RunAsync(); From e3489068a95baa8695108b3dbee2960c29c02699 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 09:28:16 -0800 Subject: [PATCH 08/14] Only remove leading trivia, add tests --- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 2 +- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index 4c3cd3049..71ac68726 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -101,7 +101,7 @@ private static async Task ConvertToPartialProperty(Document document, PropertyDeclarationSyntax updatedPropertyDeclaration = propertyDeclaration .AddModifiers(Token(SyntaxKind.PartialKeyword)) - .WithoutTrivia() + .WithoutLeadingTrivia() .WithAttributeLists(attributeLists) .WithAdditionalAnnotations(Formatter.Annotation) .WithAccessorList(AccessorList(List( diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index a106be081..57351d88f 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -267,6 +267,75 @@ public partial class SampleViewModel : ObservableObject 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() { @@ -436,6 +505,19 @@ 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; @@ -474,6 +556,13 @@ public partial class SampleViewModel : ObservableObject [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; @@ -503,6 +592,12 @@ public class TestAttribute(string text) : Attribute; // /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[] From da8901493f151c1218b4d0af47421dad02da277e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 09:45:54 -0800 Subject: [PATCH 09/14] Fix leading trivia in accessors, add tests --- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 3 +- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 89 ++++++++++++++++++- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index 71ac68726..9bcddb458 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -116,8 +116,9 @@ private static async Task ConvertToPartialProperty(Document document, .WithBody(null) .WithExpressionBody(null) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia()) .WithAdditionalAnnotations(Formatter.Annotation) - ]))); + ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); // Create an editor to perform all mutations SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index 57351d88f..7fd8ba723 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -260,8 +260,80 @@ public partial class SampleViewModel : ObservableObject 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"), + // /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(); @@ -447,8 +519,11 @@ public partial class SampleViewModel : ObservableObject 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"), + // /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(); @@ -616,6 +691,12 @@ public class TestAttribute(string text) : Attribute; // /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(); From df714eaa6c194b5f2b8a46713541939d6321b0ce Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 18:50:40 -0800 Subject: [PATCH 10/14] Add support for 'get;' accessors too --- ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 30 +++++++++++ ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 53 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs index 37778a157..ecb6502a2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -10,6 +10,7 @@ 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; @@ -201,6 +202,35 @@ public override void Initialize(AnalysisContext context) } }); + // 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 => { diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index 7fd8ba723..a41af842d 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -74,6 +74,59 @@ public partial class SampleViewModel : ObservableObject 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_WithLeadingTrivia() { From a91e7a8b48fb4a6fdd4ead99f1f3fd728a5ddb93 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 19:35:51 -0800 Subject: [PATCH 11/14] Handle adding using directives if needed --- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 121 ++++++++++++++-- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 132 ++++++++++++++++++ 2 files changed, 241 insertions(+), 12 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index 9bcddb458..9b4d7e63d 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -15,6 +15,7 @@ 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; @@ -32,9 +33,9 @@ public sealed class UsePartialPropertyForSemiAutoPropertyCodeFixer : CodeFixProv public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoPropertyId); /// - public override FixAllProvider? GetFixAllProvider() + public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider() { - return WellKnownFixAllProviders.BatchFixer; + return new FixAllProvider(); } /// @@ -43,16 +44,31 @@ 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), + createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration, observablePropertySymbol), equivalenceKey: "Use a partial property"), diagnostic); } @@ -64,14 +80,47 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) /// 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) + private static async Task ConvertToPartialProperty( + Document document, + SyntaxNode root, + PropertyDeclarationSyntax propertyDeclaration, + INamedTypeSymbol observablePropertySymbol) { await Task.CompletedTask; - // Prepare the [ObservableProperty] attribute, which is always inserted first - AttributeListSyntax observablePropertyAttributeList = AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty")))); + 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; @@ -120,10 +169,7 @@ private static async Task ConvertToPartialProperty(Document document, .WithAdditionalAnnotations(Formatter.Annotation) ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); - // Create an editor to perform all mutations - SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services); - - editor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); + syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); // Find the parent type for the property TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!; @@ -132,10 +178,61 @@ private static async Task ConvertToPartialProperty(Document document, // 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)) { - editor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); + syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); } + } - return document.WithSyntaxRoot(editor.GetChangedRoot()); + /// + /// 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()); + } } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index a41af842d..e9cccaa91 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -127,6 +127,57 @@ public partial class SampleViewModel : ObservableObject 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() { @@ -582,6 +633,87 @@ public partial class SampleViewModel : ObservableObject 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() { From 76c41aa32c1aa74135420b08a57ebc6bf974852c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 20:13:24 -0800 Subject: [PATCH 12/14] Move MVVMTK0056 to 8.4.0 release --- .../AnalyzerReleases.Shipped.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 345f27de7..486e14158 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -97,11 +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 - -## Release 8.4.1 - -### New Rules - -Rule ID | Category | Severity | Notes ---------|----------|----------|------- MVVMTK0056 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0056 From a1bfe3d7f7f3e4cfa77152a0a2583fb1cd5797de Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 21:48:08 -0800 Subject: [PATCH 13/14] Port 'ObjectPool' type from Roslyn --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../Helpers/ObjectPool{T}.cs | 164 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 31ad7f8cd..f875a91df 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -89,6 +89,7 @@ + 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 From 576646598c300733405c4ceca7cf898aed46a719 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 22:17:15 -0800 Subject: [PATCH 14/14] Enable pooling for objects in new analyzer --- ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs index ecb6502a2..a36aad071 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -4,6 +4,7 @@ #if ROSLYN_4_12_0_OR_GREATER +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -14,6 +15,7 @@ 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; @@ -24,6 +26,22 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; [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); @@ -69,7 +87,8 @@ public override void Initialize(AnalysisContext context) return; } - Dictionary propertyMap = new(SymbolEqualityComparer.Default); + 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()) @@ -97,8 +116,13 @@ public override void Initialize(AnalysisContext context) 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, new bool[2]); + propertyMap.Add(propertySymbol, flags); } // We want to process both accessors, where we specifically need both the syntax @@ -246,6 +270,24 @@ public override void Initialize(AnalysisContext context) 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); }); @@ -282,6 +324,23 @@ private static bool TryGetSetPropertyMethodSymbol(INamedTypeSymbol observableObj 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