Skip to content

Commit

Permalink
Handle adding using directives if needed
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergio0694 committed Dec 11, 2024
1 parent df714ea commit a91e7a8
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,9 +33,9 @@ public sealed class UsePartialPropertyForSemiAutoPropertyCodeFixer : CodeFixProv
public override ImmutableArray<string> FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoPropertyId);

/// <inheritdoc/>
public override FixAllProvider? GetFixAllProvider()
public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider()
{
return WellKnownFixAllProviders.BatchFixer;
return new FixAllProvider();
}

/// <inheritdoc/>
Expand All @@ -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);
}
Expand All @@ -64,14 +80,47 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
/// <param name="document">The original document being fixed.</param>
/// <param name="root">The original tree root belonging to the current document.</param>
/// <param name="propertyDeclaration">The <see cref="PropertyDeclarationSyntax"/> for the property being updated.</param>
/// <param name="observablePropertySymbol">The <see cref="INamedTypeSymbol"/> for <c>[ObservableProperty]</c>.</param>
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
private static async Task<Document> ConvertToPartialProperty(Document document, SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration)
private static async Task<Document> 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());
}

/// <summary>
/// Applies the code fix to a target identifier and returns an updated document.
/// </summary>
/// <param name="propertyDeclaration">The <see cref="PropertyDeclarationSyntax"/> for the property being updated.</param>
/// <param name="observablePropertyAttributeList">The <see cref="AttributeListSyntax"/> with the attribute to add.</param>
/// <param name="syntaxEditor">The <see cref="SyntaxEditor"/> instance to use.</param>
/// <returns>An updated document with the applied code fix, and <paramref name="propertyDeclaration"/> being replaced with a partial property.</returns>
private static void ConvertToPartialProperty(
PropertyDeclarationSyntax propertyDeclaration,
AttributeListSyntax observablePropertyAttributeList,
SyntaxEditor syntaxEditor)
{
// Start setting up the updated attribute lists
SyntaxList<AttributeListSyntax> attributeLists = propertyDeclaration.AttributeLists;

Expand Down Expand Up @@ -120,10 +169,7 @@ private static async Task<Document> 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<TypeDeclarationSyntax>()!;
Expand All @@ -132,10 +178,61 @@ private static async Task<Document> 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());
/// <summary>
/// A custom <see cref="FixAllProvider"/> with the logic from <see cref="UsePartialPropertyForSemiAutoPropertyCodeFixer"/>.
/// </summary>
private sealed class FixAllProvider : DocumentBasedFixAllProvider
{
/// <inheritdoc/>
protected override async Task<Document?> FixAllAsync(FixAllContext fixAllContext, Document document, ImmutableArray<Diagnostic> 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());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand Down

0 comments on commit a91e7a8

Please sign in to comment.