Skip to content

Commit

Permalink
Handle even more default properties in analyzer
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergio0694 committed Dec 12, 2024
1 parent 26fdeed commit bebdcac
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
return;
}

// Retrieve the properties passed by the analyzer
string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName];

SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

// Get the property declaration and the field declaration from the target diagnostic
Expand All @@ -69,7 +72,13 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
context.RegisterCodeFix(
CodeAction.Create(
title: "Use a partial property",
createChangedDocument: token => ConvertToPartialProperty(context.Document, semanticModel, root, propertyDeclaration, fieldDeclaration),
createChangedDocument: token => ConvertToPartialProperty(
context.Document,
semanticModel,
root,
propertyDeclaration,
fieldDeclaration,
defaultValue),
equivalenceKey: "Use a partial property"),
diagnostic);
}
Expand Down Expand Up @@ -113,13 +122,15 @@ private static bool TryGetGeneratedObservablePropertyAttributeList(
/// <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="fieldDeclaration">The <see cref="FieldDeclarationSyntax"/> for the declared property to remove.</param>
/// <param name="defaultValueExpression">The expression for the default value of the property, if present</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,
SemanticModel semanticModel,
SyntaxNode root,
PropertyDeclarationSyntax propertyDeclaration,
FieldDeclarationSyntax fieldDeclaration)
FieldDeclarationSyntax fieldDeclaration,
string? defaultValueExpression)
{
await Task.CompletedTask;

Expand All @@ -136,7 +147,8 @@ private static async Task<Document> ConvertToPartialProperty(
propertyDeclaration,
fieldDeclaration,
observablePropertyAttributeList,
syntaxEditor);
syntaxEditor,
defaultValueExpression);

// Create the new document with the single change
return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot());
Expand All @@ -149,13 +161,28 @@ private static async Task<Document> ConvertToPartialProperty(
/// <param name="fieldDeclaration">The <see cref="FieldDeclarationSyntax"/> for the declared property to remove.</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>
/// <param name="defaultValueExpression">The expression for the default value of the property, if present</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,
FieldDeclarationSyntax fieldDeclaration,
AttributeListSyntax observablePropertyAttributeList,
SyntaxEditor syntaxEditor)
SyntaxEditor syntaxEditor,
string? defaultValueExpression)
{
// If we do have a default value expression, set it in the attribute.
// We extract the generated attribute so we can add the new argument.
// It's important to reuse it, as it has the "add usings" annotation.
if (defaultValueExpression is not null)
{
observablePropertyAttributeList =
AttributeList(SingletonSeparatedList(
observablePropertyAttributeList.Attributes[0]
.AddArgumentListArguments(
AttributeArgument(ParseExpression(defaultValueExpression))
.WithNameEquals(NameEquals(IdentifierName("DefaultValue"))))));
}

// Start setting up the updated attribute lists
SyntaxList<AttributeListSyntax> attributeLists = propertyDeclaration.AttributeLists;

Expand Down Expand Up @@ -264,11 +291,15 @@ private sealed class FixAllProvider : DocumentBasedFixAllProvider
continue;
}

// Retrieve the properties passed by the analyzer
string? defaultValue = diagnostic.Properties[UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.DefaultValuePropertyName];

ConvertToPartialProperty(
propertyDeclaration,
fieldDeclaration,
observablePropertyAttributeList,
syntaxEditor);
syntaxEditor,
defaultValue);
}

return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,14 +413,26 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla
return;
}

// Check whether the value is a default constant value.
// If it is, then the property is valid (no explicit value).
if (!conversionOperation.Operand.IsConstantValueDefault())
bool isNullableValueType = propertyTypeSymbol is INamedTypeSymbol { IsValueType: true, IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T };

// Check whether the value is a default constant value. If it is, then the property is valid (no explicit value).
// We need to special case nullable value types, as the default value for the underlying type is not the actual default.
if (!conversionOperation.Operand.IsConstantValueDefault() || isNullableValueType)
{
// If that is not the case, check if it's some constant value we can forward
if (!TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue))
// The value is just 'null' with no type, special case this one and skip the other checks below
if (conversionOperation.Operand is { Type: null, ConstantValue: { HasValue: true, Value: null } })
{
// This is only allowed for reference or nullable types. This 'null' is redundant, but support it nonetheless.
// It's not that uncommon for especially legacy codebases to have this kind of pattern in dependency properties.
if (!propertyTypeSymbol.IsReferenceType && !isNullableValueType)
{
return;
}
}
else if (!TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue))
{
// As a last resort, check if this is explicitly a 'default(T)' expression
// If that is not the case, check if it's some constant value we can forward. In this case, we did not
// retrieve it. As a last resort, check if this is explicitly a 'default(T)' expression.
if (conversionOperation.Operand is not IDefaultValueOperation { Type: { } defaultValueExpressionType })
{
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1225,7 +1225,6 @@ public partial class MyControl : Control
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_FieldNotInitialized_DoesNotWarn()
{
const string source = """
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
Expand All @@ -1252,7 +1251,6 @@ public string? Name
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_FieldWithDifferentName_DoesNotWarn()
{
const string source = """
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
Expand Down Expand Up @@ -1297,7 +1295,6 @@ public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_Invalid
string typeMetadata)
{
string source = $$"""
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
Expand Down Expand Up @@ -1328,7 +1325,6 @@ public string? Name
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_MissingGetter_DoesNotWarn()
{
const string source = """
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
Expand Down Expand Up @@ -1358,7 +1354,6 @@ public string? Name
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_MissingSetter_DoesNotWarn()
{
const string source = """
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
Expand Down Expand Up @@ -1396,7 +1391,6 @@ public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidPr
string propertyType)
{
string source = $$"""
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
Expand Down Expand Up @@ -1447,7 +1441,6 @@ public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidPr
string defaultValueExpression)
{
string source = $$"""
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer
[DataRow("object", "object?")]
[DataRow("int", "int")]
[DataRow("int?", "int?")]
public async Task SimpleProperty(string underlyingType, string propertyType)
public async Task SimpleProperty(string dependencyPropertyType, string propertyType)
{
string original = $$"""
using Windows.UI.Xaml;
Expand All @@ -42,7 +42,7 @@ public class MyControl : Control
{
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
name: nameof(Name),
propertyType: typeof({{underlyingType}}),
propertyType: typeof({{dependencyPropertyType}}),
ownerType: typeof(MyControl),
typeMetadata: null);
Expand Down Expand Up @@ -83,4 +83,145 @@ public partial class MyControl : Control

await test.RunAsync();
}

[TestMethod]
[DataRow("string", "string", "null")]
[DataRow("string", "string", "default(string)")]
[DataRow("string", "string", "(string)null")]
[DataRow("string", "string?", "null")]
[DataRow("object", "object", "null")]
[DataRow("object", "object?", "null")]
[DataRow("int", "int", "0")]
[DataRow("int", "int", "default(int)")]
[DataRow("int?", "int?", "null")]
[DataRow("int?", "int?", "default(int?)")]
[DataRow("int?", "int?", "null")]
[DataRow("System.TimeSpan", "System.TimeSpan", "default(System.TimeSpan)")]
public async Task SimpleProperty_WithExplicitValue_DefaultValue(
string dependencyPropertyType,
string propertyType,
string defaultValueExpression)
{
string original = $$"""
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#nullable enable
namespace MyApp;
public partial class MyControl : Control
{
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
name: "Name",
propertyType: typeof({{dependencyPropertyType}}),
ownerType: typeof(MyControl),
typeMetadata: new PropertyMetadata({{defaultValueExpression}}));
public {{propertyType}} [|Name|]
{
get => ({{propertyType}})GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
}
""";

string @fixed = $$"""
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#nullable enable
namespace MyApp;
public partial class MyControl : Control
{
[GeneratedDependencyProperty]
public partial {{propertyType}} {|CS9248:Name|} { get; set; }
}
""";

CSharpCodeFixTest test = new(LanguageVersion.Preview)
{
TestCode = original,
FixedCode = @fixed,
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestState = { AdditionalReferences =
{
MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location),
MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location),
MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)
}}
};

await test.RunAsync();
}

[TestMethod]
[DataRow("string", "string", "\"\"")]
[DataRow("string", "string", "\"Hello\"")]
[DataRow("int", "int", "42")]
[DataRow("int?", "int?", "0")]
[DataRow("int?", "int?", "42")]
public async Task SimpleProperty_WithExplicitValue_NotDefault(
string dependencyPropertyType,
string propertyType,
string defaultValueExpression)
{
string original = $$"""
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#nullable enable
namespace MyApp;
public partial class MyControl : Control
{
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
name: "Name",
propertyType: typeof({{dependencyPropertyType}}),
ownerType: typeof(MyControl),
typeMetadata: new PropertyMetadata({{defaultValueExpression}}));
public {{propertyType}} [|Name|]
{
get => ({{propertyType}})GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
}
""";

string @fixed = $$"""
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#nullable enable
namespace MyApp;
public partial class MyControl : Control
{
[GeneratedDependencyProperty(DefaultValue = {{defaultValueExpression}})]
public partial {{propertyType}} {|CS9248:Name|} { get; set; }
}
""";

CSharpCodeFixTest test = new(LanguageVersion.Preview)
{
TestCode = original,
FixedCode = @fixed,
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
TestState = { AdditionalReferences =
{
MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location),
MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location),
MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)
}}
};

await test.RunAsync();
}
}

0 comments on commit bebdcac

Please sign in to comment.