From df714eaa6c194b5f2b8a46713541939d6321b0ce Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 18:50:40 -0800 Subject: [PATCH] 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 37778a15..ecb6502a 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 7fd8ba72..a41af842 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() {