Skip to content

Commit 6de259d

Browse files
authored
Feature Add Code Fixer for [Reactive] (#23)
1 parent 2e7fad7 commit 6de259d

13 files changed

+230
-6
lines changed

src/Directory.Packages.props

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
55
</PropertyGroup>
66
<ItemGroup>
7+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />
78
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
89
<PackageVersion Include="xunit" Version="2.9.0" />
910
<PackageVersion Include="xunit.runner.console" Version="2.9.0" />
@@ -14,18 +15,15 @@
1415
<PackageVersion Include="PublicApiGenerator" Version="11.1.0" />
1516
<PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
1617
<PackageVersion Include="Verify.Xunit" Version="26.1.2" />
17-
1818
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
1919
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.6.139" />
2020
<PackageVersion Include="stylecop.analyzers" Version="1.2.0-beta.556" />
2121
<PackageVersion Include="Roslynator.Analyzers" Version="4.12.4" />
22-
2322
<PackageVersion Include="ReactiveUI" Version="20.1.1" />
2423
<PackageVersion Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
2524
<PackageVersion Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
26-
2725
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
2826
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
2927
<PackageVersion Include="PolySharp" Version="1.14.1" />
3028
</ItemGroup>
31-
</Project>
29+
</Project>

src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public partial class TestViewModel : ReactiveObject
2626
[JsonInclude]
2727
[Reactive]
2828
[DataMember]
29-
private int _test1Property;
29+
private int _test1Property = 10;
3030

3131
/// <summary>
3232
/// Initializes a new instance of the <see cref="TestViewModel"/> class.
@@ -77,6 +77,15 @@ public TestViewModel()
7777
/// </value>
7878
public static TestViewModel Instance { get; } = new();
7979

80+
/// <summary>
81+
/// Gets or sets the test property.
82+
/// </summary>
83+
/// <value>
84+
/// The test property.
85+
/// </value>
86+
[JsonInclude]
87+
public string? TestProperty { get; set; } = "Test";
88+
8089
/// <summary>
8190
/// Gets the can execute test1.
8291
/// </summary>

src/ReactiveUI.SourceGenerators/AnalyzerReleases.Shipped.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ RXUISG0012 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error |
2222
RXUISG0013 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0013
2323
RXUISG0014 | ReactiveUI.SourceGenerators.ObservableAsPropertyGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0014
2424
RXUISG0015 | ReactiveUI.SourceGenerators.ReactiveGenerator | Error | See https://www.reactiveui.net/errors/RXUISG0015
25+
RXUISG0016 | ReactiveUI.SourceGenerators.PropertyToReactiveFieldCodeFixProvider | Info | See https://www.reactiveui.net/errors/RXUISG0016
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using System.Collections.Immutable;
7+
using System.Linq;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp;
10+
using Microsoft.CodeAnalysis.CSharp.Syntax;
11+
using Microsoft.CodeAnalysis.Diagnostics;
12+
using ReactiveUI.SourceGenerators.Diagnostics;
13+
14+
namespace ReactiveUI.SourceGenerators.CodeAnalyzers
15+
{
16+
/// <summary>
17+
/// PropertyToFieldAnalyzer.
18+
/// </summary>
19+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
20+
public class PropertyToReactiveFieldAnalyzer : DiagnosticAnalyzer
21+
{
22+
/// <summary>
23+
/// Gets the supported diagnostics.
24+
/// </summary>
25+
/// <value>
26+
/// The supported diagnostics.
27+
/// </value>
28+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
29+
ImmutableArray.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule);
30+
31+
/// <summary>
32+
/// Initializes the specified context.
33+
/// </summary>
34+
/// <param name="context">The context.</param>
35+
public override void Initialize(AnalysisContext context)
36+
{
37+
context?.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
38+
context?.EnableConcurrentExecution();
39+
context?.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.PropertyDeclaration);
40+
}
41+
42+
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
43+
{
44+
var propertyDeclaration = (PropertyDeclarationSyntax)context.Node;
45+
var isAutoProperty = propertyDeclaration.ExpressionBody == null && (propertyDeclaration.AccessorList?.Accessors.All(a => a.Body == null && a.ExpressionBody == null) != false);
46+
47+
if (isAutoProperty && propertyDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword) && !propertyDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword))
48+
{
49+
var diagnostic = Diagnostic.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule, propertyDeclaration.GetLocation());
50+
context.ReportDiagnostic(diagnostic);
51+
}
52+
}
53+
}
54+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using System.Collections.Immutable;
7+
using System.Composition;
8+
using System.Linq;
9+
using System.Threading.Tasks;
10+
using Microsoft.CodeAnalysis;
11+
using Microsoft.CodeAnalysis.CodeActions;
12+
using Microsoft.CodeAnalysis.CodeFixes;
13+
using Microsoft.CodeAnalysis.CSharp;
14+
using Microsoft.CodeAnalysis.CSharp.Syntax;
15+
using ReactiveUI.SourceGenerators.Diagnostics;
16+
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
17+
18+
namespace ReactiveUI.SourceGenerators.CodeAnalyzers
19+
{
20+
/// <summary>
21+
/// PropertyToFieldCodeFixProvider.
22+
/// </summary>
23+
/// <seealso cref="CodeFixProvider" />
24+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PropertyToReactiveFieldCodeFixProvider))]
25+
[Shared]
26+
public class PropertyToReactiveFieldCodeFixProvider : CodeFixProvider
27+
{
28+
/// <summary>
29+
/// Gets a list of diagnostic IDs that this provider can provide fixes for.
30+
/// </summary>
31+
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(DiagnosticDescriptors.PropertyToReactiveFieldRule.Id);
32+
33+
/// <summary>
34+
/// Gets an optional <see cref="T:Microsoft.CodeAnalysis.CodeFixes.FixAllProvider" /> that can fix all/multiple occurrences of diagnostics fixed by this code fix provider.
35+
/// Return null if the provider doesn't support fix all/multiple occurrences.
36+
/// Otherwise, you can return any of the well known fix all providers from <see cref="T:Microsoft.CodeAnalysis.CodeFixes.WellKnownFixAllProviders" /> or implement your own fix all provider.
37+
/// </summary>
38+
/// <returns>FixAllProvider.</returns>
39+
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
40+
41+
/// <summary>
42+
/// Computes one or more fixes for the specified <see cref="T:Microsoft.CodeAnalysis.CodeFixes.CodeFixContext" />.
43+
/// </summary>
44+
/// <param name="context">A <see cref="T:Microsoft.CodeAnalysis.CodeFixes.CodeFixContext" /> containing context information about the diagnostics to fix.
45+
/// The context must only contain diagnostics with a <see cref="P:Microsoft.CodeAnalysis.Diagnostic.Id" /> included in the <see cref="P:Microsoft.CodeAnalysis.CodeFixes.CodeFixProvider.FixableDiagnosticIds" /> for the current provider.</param>
46+
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
47+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
48+
{
49+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
50+
var diagnostic = context.Diagnostics[0];
51+
var diagnosticSpan = diagnostic.Location.SourceSpan;
52+
53+
// Find the property declaration syntax node
54+
var propertyDeclaration = root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().First();
55+
56+
var fieldName = propertyDeclaration?.Identifier.Text;
57+
fieldName = "_" + fieldName?.Substring(0, 1).ToLower() + fieldName?.Substring(1);
58+
59+
var attributeSyntaxes =
60+
propertyDeclaration!.AttributeLists
61+
.Select(static a => AttributeList(a.Attributes)).ToList();
62+
attributeSyntaxes.Add(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ReactiveUI.SourceGenerators.Reactive")))));
63+
64+
SyntaxList<AttributeListSyntax> al = new(attributeSyntaxes);
65+
66+
// Create a new field declaration syntax node
67+
var fieldDeclaration = FieldDeclaration(
68+
VariableDeclaration(propertyDeclaration!.Type)
69+
.WithVariables(SingletonSeparatedList(
70+
VariableDeclarator(fieldName).WithInitializer(propertyDeclaration.Initializer))))
71+
.WithAttributeLists(al)
72+
.WithLeadingTrivia(propertyDeclaration.GetLeadingTrivia())
73+
.WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword)));
74+
75+
// Replace the property with the field
76+
var newRoot = root?.ReplaceNode(propertyDeclaration, fieldDeclaration);
77+
78+
// Apply the code fix
79+
context.RegisterCodeFix(
80+
CodeAction.Create("Convert to Reactive field", c => Task.FromResult(context.Document.WithSyntaxRoot(newRoot!))),
81+
diagnostic);
82+
}
83+
}
84+
}

src/ReactiveUI.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// See the LICENSE file in the project root for full license information.
55

66
using Microsoft.CodeAnalysis;
7+
using ReactiveUI.SourceGenerators.CodeAnalyzers;
78

89
#pragma warning disable IDE0090 // Use 'new DiagnosticDescriptor(...)'
910

@@ -250,4 +251,17 @@ internal static class DiagnosticDescriptors
250251
isEnabledByDefault: true,
251252
description: "The fields annotated with [Reactive] cannot result in a property name or have a type that would cause conflicts with other generated members.",
252253
helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0015");
254+
255+
/// <summary>
256+
/// The property to field rule.
257+
/// </summary>
258+
public static readonly DiagnosticDescriptor PropertyToReactiveFieldRule = new(
259+
id: "RXUISG0016",
260+
title: "Property To Reactive Field, change to [Reactive] private type _fieldName;",
261+
messageFormat: "Replace the property {0} with a INPC Reactive Property for ReactiveUI",
262+
category: typeof(PropertyToReactiveFieldCodeFixProvider).FullName,
263+
defaultSeverity: DiagnosticSeverity.Info,
264+
isEnabledByDefault: true,
265+
description: "Used to create a Read Write INPC Reactive Property for ReactiveUI, annotated with [Reactive].",
266+
helpLinkUri: "https://www.reactiveui.net/errors/RXUISG0016");
253267
}

src/ReactiveUI.SourceGenerators/Diagnostics/SuppressionDescriptors.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,9 @@ internal static class SuppressionDescriptors
2323
id: "RXUISPR0003",
2424
suppressedDiagnosticId: "CA1822",
2525
justification: "Methods using [ReactiveCommand] do not need to be static");
26+
27+
public static readonly SuppressionDescriptor ReactiveFieldsShouldNotBeReadOnly = new(
28+
id: "RXUISPR0004",
29+
suppressedDiagnosticId: "RCS1169",
30+
justification: "Fields using [Reactive] do not need to be ReadOnly");
2631
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions
1818
/// </summary>
1919
/// <seealso cref="DiagnosticSuppressor" />
2020
[DiagnosticAnalyzer(LanguageNames.CSharp)]
21-
public sealed class ReactiveCommandMethodDoesNotNeedToBeStatisDiagnosticSuppressor : DiagnosticSuppressor
21+
public sealed class ReactiveCommandMethodDoesNotNeedToBeStaticDiagnosticSuppressor : DiagnosticSuppressor
2222
{
2323
/// <inheritdoc/>
2424
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(ReactiveCommandDoesNotAccessInstanceData);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using System.Collections.Immutable;
7+
using Microsoft.CodeAnalysis;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using ReactiveUI.SourceGenerators.Extensions;
12+
using static ReactiveUI.SourceGenerators.Diagnostics.SuppressionDescriptors;
13+
14+
namespace ReactiveUI.SourceGenerators.Diagnostics.Suppressions
15+
{
16+
/// <summary>
17+
/// Reactive Attribute ReadOnly Field Target Diagnostic Suppressor.
18+
/// </summary>
19+
/// <seealso cref="DiagnosticSuppressor" />
20+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
21+
public sealed class ReactiveFieldDoesNotNeedToBeReadOnlyDiagnosticSuppressor : DiagnosticSuppressor
22+
{
23+
/// <inheritdoc/>
24+
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => ImmutableArray.Create(ReactiveFieldsShouldNotBeReadOnly);
25+
26+
/// <inheritdoc/>
27+
public override void ReportSuppressions(SuppressionAnalysisContext context)
28+
{
29+
foreach (var diagnostic in context.ReportedDiagnostics)
30+
{
31+
var syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan);
32+
33+
// Check that the target is a method declaration, which is the case we're looking for
34+
if (syntaxNode is FieldDeclarationSyntax fieldDeclaration)
35+
{
36+
var semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree);
37+
38+
// Get the method symbol from the first variable declaration
39+
var declaredSymbol = semanticModel.GetDeclaredSymbol(fieldDeclaration, context.CancellationToken);
40+
41+
// Check if the method is using [Reactive], in which case we should suppress the warning
42+
if (declaredSymbol is IFieldSymbol fieldSymbol &&
43+
semanticModel.Compilation.GetTypeByMetadataName("ReactiveUI.SourceGenerators.ReactiveAttribute") is INamedTypeSymbol reactiveSymbol &&
44+
fieldSymbol.HasAttributeWithType(reactiveSymbol))
45+
{
46+
context.ReportSuppression(Suppression.Create(ReactiveFieldsShouldNotBeReadOnly, diagnostic));
47+
}
48+
}
49+
}
50+
}
51+
}
52+
}

src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ internal static bool GetFieldInfoFromClass(
142142
var typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations();
143143
var fieldName = fieldSymbol.Name;
144144
var propertyName = GetGeneratedPropertyName(fieldSymbol);
145+
var initializer = fieldSyntax.Declaration.Variables.FirstOrDefault()?.Initializer;
145146

146147
// Check for name collisions
147148
if (fieldName == propertyName)
@@ -270,6 +271,7 @@ internal static bool GetFieldInfoFromClass(
270271
typeNameWithNullabilityAnnotations,
271272
fieldName,
272273
propertyName,
274+
initializer,
273275
isReferenceTypeOrUnconstraindTypeParameter,
274276
includeMemberNotNullOnSetAccessor,
275277
forwardedAttributes.ToImmutable());

0 commit comments

Comments
 (0)