Skip to content

Commit 441abbb

Browse files
committed
Add fixers project and implement a fixer for FieldReferenceForObservablePropertyFieldAnalyzer
This adds a fixer for MVVMTK0034, that simply swaps the field reference for what will be generated for the property. It leaves the qualification intact, basically just doing a text swap. Because it's very a very simple fixer, the built-in batch fixall provider works just fine and does not need special handling. While I implemented the fixer and added some tests, I did not actually set up all the msbuild goop for properly packaging this as part of the existing packages (as, frankly, the multi-targeting and msbuild setup in this repo is very complicated and I didn't feel like trying to grok exactly how to add it given the setup). The fixer assembly needs to be put next to the analyzer assemblies. This PR will be editable by maintainers, so feel free to implement this in-place in the PR itself. Fixes #577.
1 parent 302b8a9 commit 441abbb

9 files changed

+406
-2
lines changed

dotnet Community Toolkit.sln

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Exter
8181
EndProject
8282
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj", "{4FCD501C-1BB5-465C-AD19-356DAB6600C6}"
8383
EndProject
84+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.Fixers", "src\CommunityToolkit.Mvvm.Fixers\CommunityToolkit.Mvvm.Fixers.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}"
85+
EndProject
8486
Global
8587
GlobalSection(SolutionConfigurationPlatforms) = preSolution
8688
Debug|Any CPU = Debug|Any CPU
@@ -435,6 +437,26 @@ Global
435437
{4FCD501C-1BB5-465C-AD19-356DAB6600C6}.Release|x64.Build.0 = Release|Any CPU
436438
{4FCD501C-1BB5-465C-AD19-356DAB6600C6}.Release|x86.ActiveCfg = Release|Any CPU
437439
{4FCD501C-1BB5-465C-AD19-356DAB6600C6}.Release|x86.Build.0 = Release|Any CPU
440+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
441+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|Any CPU.Build.0 = Debug|Any CPU
442+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|ARM.ActiveCfg = Debug|Any CPU
443+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|ARM.Build.0 = Debug|Any CPU
444+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|ARM64.ActiveCfg = Debug|Any CPU
445+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|ARM64.Build.0 = Debug|Any CPU
446+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|x64.ActiveCfg = Debug|Any CPU
447+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|x64.Build.0 = Debug|Any CPU
448+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|x86.ActiveCfg = Debug|Any CPU
449+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Debug|x86.Build.0 = Debug|Any CPU
450+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|Any CPU.ActiveCfg = Release|Any CPU
451+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|Any CPU.Build.0 = Release|Any CPU
452+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|ARM.ActiveCfg = Release|Any CPU
453+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|ARM.Build.0 = Release|Any CPU
454+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|ARM64.ActiveCfg = Release|Any CPU
455+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|ARM64.Build.0 = Release|Any CPU
456+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x64.ActiveCfg = Release|Any CPU
457+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x64.Build.0 = Release|Any CPU
458+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x86.ActiveCfg = Release|Any CPU
459+
{E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x86.Build.0 = Release|Any CPU
438460
EndGlobalSection
439461
GlobalSection(SolutionProperties) = preSolution
440462
HideSolutionNode = FALSE
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
9+
</ItemGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj" />
13+
</ItemGroup>
14+
15+
</Project>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Immutable;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using CommunityToolkit.Mvvm.SourceGenerators;
11+
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
12+
using Microsoft.CodeAnalysis;
13+
using Microsoft.CodeAnalysis.CodeActions;
14+
using Microsoft.CodeAnalysis.CodeFixes;
15+
using Microsoft.CodeAnalysis.CSharp;
16+
using Microsoft.CodeAnalysis.CSharp.Syntax;
17+
18+
namespace CommunityToolkit.Mvvm.Fixers;
19+
20+
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
21+
[ExportCodeFixProvider(LanguageNames.CSharp)]
22+
public class FieldReferenceForObservablePropertyFieldFixer : CodeFixProvider
23+
{
24+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticDescriptors.FieldReferenceForObservablePropertyFieldId);
25+
26+
public override FixAllProvider? GetFixAllProvider()
27+
{
28+
return WellKnownFixAllProviders.BatchFixer;
29+
}
30+
31+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
32+
{
33+
Diagnostic diagnostic = context.Diagnostics[0];
34+
Microsoft.CodeAnalysis.Text.TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
35+
36+
string? propertyName = diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey];
37+
string? fieldName = diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey];
38+
39+
if (propertyName == null || fieldName == null)
40+
{
41+
return;
42+
}
43+
44+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
45+
Debug.Assert(root != null);
46+
47+
IdentifierNameSyntax fieldReference = root!.FindNode(diagnosticSpan).DescendantNodesAndSelf().OfType<IdentifierNameSyntax>().FirstOrDefault(i => i.ToString() == fieldName);
48+
49+
if (fieldReference == null)
50+
{
51+
return;
52+
}
53+
54+
context.RegisterCodeFix(
55+
CodeAction.Create(
56+
title: "Reference property",
57+
createChangedDocument: c => UpdateReference(context.Document, fieldReference, propertyName, c),
58+
equivalenceKey: "Reference property"),
59+
diagnostic);
60+
61+
}
62+
63+
private async Task<Document> UpdateReference(Document document, IdentifierNameSyntax fieldReference, string propertyName, CancellationToken cancellationToken)
64+
{
65+
IdentifierNameSyntax propertyReference = SyntaxFactory.IdentifierName(propertyName);
66+
SyntaxNode originalRoot = await fieldReference.SyntaxTree.GetRootAsync(cancellationToken);
67+
SyntaxTree updatedTree = originalRoot.ReplaceNode(fieldReference, propertyReference).SyntaxTree;
68+
return document.WithSyntaxRoot(await updatedTree.GetRootAsync(cancellationToken));
69+
}
70+
}

src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
<Compile Include="$(MSBuildThisFileDirectory)Input\Models\CommandInfo.cs" />
6666
<Compile Include="$(MSBuildThisFileDirectory)Input\RelayCommandGenerator.cs" />
6767
<Compile Include="$(MSBuildThisFileDirectory)Input\RelayCommandGenerator.Execute.cs" />
68+
<Compile Include="$(MSBuildThisFileDirectory)InternalsVisibleTo.cs" />
6869
<Compile Include="$(MSBuildThisFileDirectory)Messaging\IMessengerRegisterAllGenerator.cs" />
6970
<Compile Include="$(MSBuildThisFileDirectory)Messaging\IMessengerRegisterAllGenerator.Execute.cs" />
7071
<Compile Include="$(MSBuildThisFileDirectory)Messaging\Models\RecipientInfo.cs" />

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
1616
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1717
public sealed class FieldReferenceForObservablePropertyFieldAnalyzer : DiagnosticAnalyzer
1818
{
19+
internal const string PropertyNameKey = "PropertyName";
20+
internal const string FieldNameKey = "FieldName";
21+
1922
/// <inheritdoc/>
2023
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(FieldReferenceForObservablePropertyFieldWarning);
2124

@@ -59,7 +62,13 @@ public override void Initialize(AnalysisContext context)
5962
SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbol))
6063
{
6164
// Emit a warning to redirect users to access the generated property instead
62-
context.ReportDiagnostic(Diagnostic.Create(FieldReferenceForObservablePropertyFieldWarning, context.Operation.Syntax.GetLocation(), fieldSymbol));
65+
context.ReportDiagnostic(Diagnostic.Create(
66+
FieldReferenceForObservablePropertyFieldWarning,
67+
context.Operation.Syntax.GetLocation(),
68+
ImmutableDictionary.Create<string, string?>()
69+
.Add(PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol))
70+
.Add(FieldNameKey, fieldSymbol.Name),
71+
fieldSymbol));
6372

6473
return;
6574
}

src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,14 +543,16 @@ internal static class DiagnosticDescriptors
543543
"reduce the binary size of the application (the attributes are only meant to support cases where the annotated types are already inheriting from a different type).",
544544
helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0033");
545545

546+
public const string FieldReferenceForObservablePropertyFieldId = "MVVMTK0034";
547+
546548
/// <summary>
547549
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a field with <c>[ObservableProperty]</c> is being directly referenced.
548550
/// <para>
549551
/// Format: <c>"The field {0} is annotated with [ObservableProperty] and should not be directly referenced (use the generated property instead)"</c>.
550552
/// </para>
551553
/// </summary>
552554
public static readonly DiagnosticDescriptor FieldReferenceForObservablePropertyFieldWarning = new DiagnosticDescriptor(
553-
id: "MVVMTK0034",
555+
id: FieldReferenceForObservablePropertyFieldId,
554556
title: "Direct field reference to [ObservableProperty] backing field",
555557
messageFormat: "The field {0} is annotated with [ObservableProperty] and should not be directly referenced (use the generated property instead)",
556558
category: typeof(ObservablePropertyGenerator).FullName,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Runtime.CompilerServices;
6+
7+
[assembly: InternalsVisibleTo("CommunityToolkit.Mvvm.Fixers, PublicKey=002400000480000094000000060200000024000052534131000400000100010041753af735ae6140c9508567666c51c6ab929806adb0d210694b30ab142a060237bc741f9682e7d8d4310364b4bba4ee89cc9d3d5ce7e5583587e8ea44dca09977996582875e71fb54fa7b170798d853d5d8010b07219633bdb761d01ac924da44576d6180cdceae537973982bb461c541541d58417a3794e34f45e6f2d129e2")]

tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66

77
<ItemGroup>
88
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.MSTest" Version="1.1.1" />
9+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.1" />
10+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.MSTest" Version="1.1.1" />
911
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" />
1012
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
1113
<PackageReference Include="MSTest.TestAdapter" Version="3.0.1" />
1214
<PackageReference Include="MSTest.TestFramework" Version="3.0.1" />
1315
</ItemGroup>
1416

1517
<ItemGroup>
18+
<ProjectReference Include="..\..\src\CommunityToolkit.Mvvm.Fixers\CommunityToolkit.Mvvm.Fixers.csproj" />
1619
<ProjectReference Include="..\..\src\CommunityToolkit.Mvvm\CommunityToolkit.Mvvm.csproj" />
1720
<ProjectReference Include="..\..\src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj" />
1821
</ItemGroup>

0 commit comments

Comments
 (0)