Skip to content

Commit f9ee156

Browse files
authored
Merge pull request #578 from 333fred/fixer
Add fixers project and implement a fixer for FieldReferenceForObservablePropertyFieldAnalyzer
2 parents 302b8a9 + e7a79c3 commit f9ee156

File tree

10 files changed

+452
-4
lines changed

10 files changed

+452
-4
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.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.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

src/CommunityToolkit.HighPerformance/CommunityToolkit.HighPerformance.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
3737
</ItemGroup>
3838

39-
<!-- On .NET Standard 2.0, the unit test project also needs access to internals-->
39+
<!-- On .NET Standard 2.0, the unit test project also needs access to internals -->
4040
<ItemGroup>
4141
<InternalsVisibleTo Include="CommunityToolkit.HighPerformance.UnitTests, PublicKey=$(AssemblySignPublicKey)" />
4242
</ItemGroup>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
10+
</ItemGroup>
11+
12+
<ItemGroup>
13+
<ProjectReference Include="..\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj" />
14+
</ItemGroup>
15+
16+
</Project>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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.Composition;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using CommunityToolkit.Mvvm.SourceGenerators;
10+
using CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
11+
using Microsoft.CodeAnalysis;
12+
using Microsoft.CodeAnalysis.CodeActions;
13+
using Microsoft.CodeAnalysis.CodeFixes;
14+
using Microsoft.CodeAnalysis.CSharp;
15+
using Microsoft.CodeAnalysis.CSharp.Syntax;
16+
using Microsoft.CodeAnalysis.Text;
17+
18+
namespace CommunityToolkit.Mvvm.CodeFixers;
19+
20+
/// <summary>
21+
/// A code fixer that automatically updates references to fields with <c>[ObservableProperty]</c> to reference the generated property instead.
22+
/// </summary>
23+
[ExportCodeFixProvider(LanguageNames.CSharp)]
24+
[Shared]
25+
public sealed class FieldReferenceForObservablePropertyFieldCodeFixer : CodeFixProvider
26+
{
27+
/// <inheritdoc/>
28+
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(DiagnosticDescriptors.FieldReferenceForObservablePropertyFieldId);
29+
30+
/// <inheritdoc/>
31+
public override FixAllProvider? GetFixAllProvider()
32+
{
33+
return WellKnownFixAllProviders.BatchFixer;
34+
}
35+
36+
/// <inheritdoc/>
37+
public override async Task RegisterCodeFixesAsync(CodeFixContext context)
38+
{
39+
Diagnostic diagnostic = context.Diagnostics[0];
40+
TextSpan diagnosticSpan = diagnostic.Location.SourceSpan;
41+
42+
// Retrieve the properties passed by the analyzer
43+
if (diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey] is not string fieldName ||
44+
diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey] is not string propertyName)
45+
{
46+
return;
47+
}
48+
49+
SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
50+
51+
foreach (SyntaxNode syntaxNode in root!.FindNode(diagnosticSpan).DescendantNodesAndSelf())
52+
{
53+
// Find the first descendant node from the source of the diagnostic that is an identifier with the target name
54+
if (syntaxNode is IdentifierNameSyntax { Identifier.Text: string identifierName } identifierNameSyntax &&
55+
identifierName == fieldName)
56+
{
57+
// Register the code fix to update the field reference to use the generated property instead
58+
context.RegisterCodeFix(
59+
CodeAction.Create(
60+
title: "Reference property",
61+
createChangedDocument: token => UpdateReference(context.Document, identifierNameSyntax, propertyName, token),
62+
equivalenceKey: "Reference property"),
63+
diagnostic);
64+
65+
return;
66+
}
67+
}
68+
}
69+
70+
/// <summary>
71+
/// Applies the code fix to a target identifier and returns an updated document.
72+
/// </summary>
73+
/// <param name="document">The original document being fixed.</param>
74+
/// <param name="fieldReference">The <see cref="IdentifierNameSyntax"/> corresponding to the field reference to update.</param>
75+
/// <param name="propertyName">The name of the generated property.</param>
76+
/// <param name="cancellationToken">The cancellation token for the operation.</param>
77+
/// <returns>An updated document with the applied code fix, and <paramref name="fieldReference"/> being replaced with a property reference.</returns>
78+
private static async Task<Document> UpdateReference(Document document, IdentifierNameSyntax fieldReference, string propertyName, CancellationToken cancellationToken)
79+
{
80+
IdentifierNameSyntax propertyReference = SyntaxFactory.IdentifierName(propertyName);
81+
SyntaxNode originalRoot = await fieldReference.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
82+
SyntaxTree updatedTree = originalRoot.ReplaceNode(fieldReference, propertyReference).SyntaxTree;
83+
84+
return document.WithSyntaxRoot(await updatedTree.GetRootAsync(cancellationToken).ConfigureAwait(false));
85+
}
86+
}

src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@
3535
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MvvmToolkitSourceGeneratorRoslynVersion)" PrivateAssets="all" Pack="false" />
3636
</ItemGroup>
3737

38+
<!-- Give access to the code fixers project for the exported diagnostic ids and properties -->
39+
<ItemGroup>
40+
<InternalsVisibleTo Include="CommunityToolkit.Mvvm.CodeFixers, PublicKey=$(AssemblySignPublicKey)" />
41+
</ItemGroup>
42+
3843
</Project>

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@ namespace CommunityToolkit.Mvvm.SourceGenerators;
1616
[DiagnosticAnalyzer(LanguageNames.CSharp)]
1717
public sealed class FieldReferenceForObservablePropertyFieldAnalyzer : DiagnosticAnalyzer
1818
{
19+
/// <summary>
20+
/// The key for the name of the target field to update.
21+
/// </summary>
22+
internal const string FieldNameKey = "FieldName";
23+
24+
/// <summary>
25+
/// The key for the name of the generated property to update a field reference to.
26+
/// </summary>
27+
internal const string PropertyNameKey = "PropertyName";
28+
1929
/// <inheritdoc/>
2030
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(FieldReferenceForObservablePropertyFieldWarning);
2131

@@ -59,7 +69,13 @@ public override void Initialize(AnalysisContext context)
5969
SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbol))
6070
{
6171
// Emit a warning to redirect users to access the generated property instead
62-
context.ReportDiagnostic(Diagnostic.Create(FieldReferenceForObservablePropertyFieldWarning, context.Operation.Syntax.GetLocation(), fieldSymbol));
72+
context.ReportDiagnostic(Diagnostic.Create(
73+
FieldReferenceForObservablePropertyFieldWarning,
74+
context.Operation.Syntax.GetLocation(),
75+
ImmutableDictionary.Create<string, string?>()
76+
.Add(FieldNameKey, fieldSymbol.Name)
77+
.Add(PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)),
78+
fieldSymbol));
6379

6480
return;
6581
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics;
1414
/// </summary>
1515
internal static class DiagnosticDescriptors
1616
{
17+
/// <summary>
18+
/// The diagnostic id for <see cref="FieldReferenceForObservablePropertyFieldWarning"/>.
19+
/// </summary>
20+
public const string FieldReferenceForObservablePropertyFieldId = "MVVMTK0034";
21+
1722
/// <summary>
1823
/// Gets a <see cref="DiagnosticDescriptor"/> indicating when a duplicate declaration of <see cref="INotifyPropertyChanged"/> would happen.
1924
/// <para>
@@ -550,7 +555,7 @@ internal static class DiagnosticDescriptors
550555
/// </para>
551556
/// </summary>
552557
public static readonly DiagnosticDescriptor FieldReferenceForObservablePropertyFieldWarning = new DiagnosticDescriptor(
553-
id: "MVVMTK0034",
558+
id: FieldReferenceForObservablePropertyFieldId,
554559
title: "Direct field reference to [ObservableProperty] backing field",
555560
messageFormat: "The field {0} is annotated with [ObservableProperty] and should not be directly referenced (use the generated property instead)",
556561
category: typeof(ObservablePropertyGenerator).FullName,

src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@
3434
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
3535
</ItemGroup>
3636

37-
<!-- Reference the various multi-targeted versions of the source generator project (one per Roslyn version) -->
37+
<!-- Reference the various multi-targeted versions of the source generator project (one per Roslyn version), and the code fixer -->
3838
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
3939
<ProjectReference Include="..\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj" ReferenceOutputAssembly="false" />
4040
<ProjectReference Include="..\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj" ReferenceOutputAssembly="false" />
41+
<ProjectReference Include="..\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.csproj" ReferenceOutputAssembly="false" />
4142
</ItemGroup>
4243

4344
<!-- Add the [InternalsVisibleTo] attribute for the test project -->
@@ -73,9 +74,17 @@
7374
<!--
7475
Pack the source generator to the right package folders (each matching the target Roslyn version).
7576
Roslyn will automatically load the highest version compatible with Roslyn's version in the SDK.
77+
Also pack the code fixer along with each target analyzer, the multi-targeting take care of it.
78+
Note: the code fixer is not currently multi-targeting, as there are no Roslyn APIs it needs from
79+
versions later than 4.0.1. As such, we can just use a single project (without the shared project
80+
and two multi-targeted ones), and pack the resulting assembly twice along with the generators.
81+
Even though the fixer only references the 4.0.1 generator target, both versions export the same
82+
APIs that the code fixer project needs, and Roslyn versions are also forward compatible.
7683
-->
7784
<None Include="..\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401\bin\$(Configuration)\netstandard2.0\CommunityToolkit.Mvvm.SourceGenerators.dll" PackagePath="analyzers\dotnet\roslyn4.0\cs" Pack="true" Visible="false" />
7885
<None Include="..\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431\bin\$(Configuration)\netstandard2.0\CommunityToolkit.Mvvm.SourceGenerators.dll" PackagePath="analyzers\dotnet\roslyn4.3\cs" Pack="true" Visible="false" />
86+
<None Include="..\CommunityToolkit.Mvvm.CodeFixers\bin\$(Configuration)\netstandard2.0\CommunityToolkit.Mvvm.CodeFixers.dll" PackagePath="analyzers\dotnet\roslyn4.0\cs" Pack="true" Visible="false" />
87+
<None Include="..\CommunityToolkit.Mvvm.CodeFixers\bin\$(Configuration)\netstandard2.0\CommunityToolkit.Mvvm.CodeFixers.dll" PackagePath="analyzers\dotnet\roslyn4.3\cs" Pack="true" Visible="false" />
7988
</ItemGroup>
8089

8190
</Project>

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,6 +6,8 @@
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" />
@@ -14,6 +16,7 @@
1416

1517
<ItemGroup>
1618
<ProjectReference Include="..\..\src\CommunityToolkit.Mvvm\CommunityToolkit.Mvvm.csproj" />
19+
<ProjectReference Include="..\..\src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.csproj" />
1720
<ProjectReference Include="..\..\src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj" />
1821
</ItemGroup>
1922

0 commit comments

Comments
 (0)