Skip to content

Commit

Permalink
Handle 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 866492b commit 26fdeed
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using CommunityToolkit.GeneratedDependencyProperty.Constants;
using CommunityToolkit.GeneratedDependencyProperty.Extensions;
using CommunityToolkit.GeneratedDependencyProperty.Helpers;
using CommunityToolkit.GeneratedDependencyProperty.Models;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
Expand Down Expand Up @@ -50,6 +51,11 @@ public sealed class UseGeneratedDependencyPropertyOnManualPropertyAnalyzer : Dia
/// </summary>
private static readonly ObjectPool<Stack<FieldFlags>> FieldFlagsStackPool = new(CreateFlagsStack<FieldFlags>);

/// <summary>
/// The property name for the serialized property value, if present.
/// </summary>
public const string DefaultValuePropertyName = "DefaultValue";

/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [UseGeneratedDependencyPropertyForManualProperty];

Expand Down Expand Up @@ -386,10 +392,48 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla
return;
}

// For now, just check that the metadata is 'null'
// First, check if the metadata is 'null' (simplest case)
if (propertyMetadataArgument.Value.ConstantValue is not { HasValue: true, Value: null })
{
return;
// Next, check if the argument is 'new PropertyMetadata(...)' with the default value for the property type
if (propertyMetadataArgument.Value is not IObjectCreationOperation { Arguments: [{ } defaultValueArgument] } objectCreationOperation)
{
return;
}

// Make sure the object being created is actually 'PropertyMetadata'
if (!SymbolEqualityComparer.Default.Equals(objectCreationOperation.Type, propertyMetadataSymbol))
{
return;
}

// The argument should be a conversion operation (boxing)
if (defaultValueArgument.Value is not IConversionOperation { IsTryCast: false, Type.SpecialType: SpecialType.System_Object } conversionOperation)
{
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())
{
// If that is not the case, check if it's some constant value we can forward
if (!TypedConstantInfo.TryCreate(conversionOperation.Operand, out fieldFlags.DefaultValue))
{
// As a last resort, check if this is explicitly a 'default(T)' expression
if (conversionOperation.Operand is not IDefaultValueOperation { Type: { } defaultValueExpressionType })
{
return;
}

// Also make sure the type matches the property type (it's not technically guaranteed).
// If this succeeds, we can safely convert the property, the generated code will be fine.
if (!SymbolEqualityComparer.Default.Equals(defaultValueExpressionType, propertyTypeSymbol))
{
return;
}
}
}
}

// Find the parent field for the operation (we're guaranteed to only fine one)
Expand Down Expand Up @@ -448,6 +492,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla
UseGeneratedDependencyPropertyForManualProperty,
pair.Key.Locations.FirstOrDefault(),
[fieldLocation],
ImmutableDictionary.Create<string, string?>().Add(DefaultValuePropertyName, fieldFlags.DefaultValue?.ToString()),
pair.Key));
}
}
Expand All @@ -467,6 +512,7 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla
{
fieldFlags.PropertyName = null;
fieldFlags.PropertyType = null;
fieldFlags.DefaultValue = null;
fieldFlags.FieldLocation = null;

fieldFlagsStack.Push(fieldFlags);
Expand Down Expand Up @@ -536,6 +582,11 @@ private sealed class FieldFlags
/// </summary>
public ITypeSymbol? PropertyType;

/// <summary>
/// The default value to use (not present if it does not need to be set explicitly).
/// </summary>
public TypedConstantInfo? DefaultValue;

/// <summary>
/// The location of the target field being initialized.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using Microsoft.CodeAnalysis;

namespace CommunityToolkit.GeneratedDependencyProperty.Extensions;

/// <summary>
/// Extension methods for <see cref="IOperation"/> types.
/// </summary>
internal static class IOperationExtensions
{
/// <summary>
/// Checks whether a given operation represents a default constant value.
/// </summary>
/// <param name="operation">The input <see cref="IOperation"/> instance.</param>
/// <returns>Whether <paramref name="operation"/> represents a default constant value.</returns>
public static bool IsConstantValueDefault(this IOperation operation)
{
if (operation is not { Type: not null, ConstantValue.HasValue: true })
{
return false;
}

// Easy check for reference types
if (operation is { Type.IsReferenceType: true, ConstantValue.Value: null })
{
return true;
}

// Equivalent check for nullable value types too
if (operation is { Type.SpecialType: SpecialType.System_Nullable_T, ConstantValue.Value: null })
{
return true;
}

// Manually match for known primitive types
return (operation.Type.SpecialType, operation.ConstantValue.Value) switch
{
(SpecialType.System_Byte, default(byte)) or
(SpecialType.System_Char, default(char)) or
(SpecialType.System_Int16, default(short)) or
(SpecialType.System_UInt16, default(ushort)) or
(SpecialType.System_Int32, default(int)) or
(SpecialType.System_UInt32, default(uint)) or
(SpecialType.System_Int64, default(long)) or
(SpecialType.System_UInt64, default(ulong)) or
(SpecialType.System_Boolean, default(bool)) => true,
(SpecialType.System_Single, float x) when BitConverter.DoubleToInt64Bits(x) == 0 => true,
(SpecialType.System_Double, double x) when BitConverter.DoubleToInt64Bits(x) == 0 => true,
_ => false
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis;

Expand Down Expand Up @@ -57,4 +58,51 @@ public static TypedConstantInfo Create(TypedConstant arg)
_ => throw new ArgumentException("Invalid typed constant type"),
};
}

/// <summary>
/// Creates a new <see cref="TypedConstantInfo"/> instance from a given <see cref="TypedConstant"/> value.
/// </summary>
/// <param name="arg">The input <see cref="TypedConstant"/> value.</param>
/// <returns>A <see cref="TypedConstantInfo"/> instance representing <paramref name="arg"/>.</returns>
/// <exception cref="ArgumentException">Thrown if the input argument is not valid.</exception>
public static bool TryCreate(IOperation operation, [NotNullWhen(true)] out TypedConstantInfo? result)
{
// Validate that we do have some constant value
if (operation is not { Type: { } operationType, ConstantValue.HasValue: true })
{
result = null;

return false;
}

if (operation.ConstantValue.Value is null)
{
result = new Null();

return true;
}

// Handle all known possible constant values
result = (operationType, operation.ConstantValue.Value) switch
{
({ SpecialType: SpecialType.System_String }, string text) => new Primitive.String(text),
({ SpecialType: SpecialType.System_Boolean}, bool flag) => new Primitive.Boolean(flag),
(_, byte b) => new Primitive.Of<byte>(b),
(_, char c) => new Primitive.Of<char>(c),
(_, double d) => new Primitive.Of<double>(d),
(_, float f) => new Primitive.Of<float>(f),
(_, int i) => new Primitive.Of<int>(i),
(_, long l) => new Primitive.Of<long>(l),
(_, sbyte sb) => new Primitive.Of<sbyte>(sb),
(_, short sh) => new Primitive.Of<short>(sh),
(_, uint ui) => new Primitive.Of<uint>(ui),
(_, ulong ul) => new Primitive.Of<ulong>(ul),
(_, ushort ush) => new Primitive.Of<ushort>(ush),
(_, ITypeSymbol type) => new Type(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)),
(INamedTypeSymbol { TypeKind: TypeKind.Enum}, object value) => new Enum(operationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), value),
_ => throw new ArgumentException("Invalid typed constant type"),
};

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,6 @@ public string? Name
[DataRow("\"Name\"", "typeof(string)", "typeof(string)", "null")]
[DataRow("\"Name\"", "typeof(string)", "typeof(Control)", "null")]
[DataRow("\"Name\"", "typeof(string)", "typeof(DependencyObject)", "null")]
[DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(42)")]
[DataRow("\"Name\"", "typeof(string)", "typeof(MyControl)", "new PropertyMetadata(null, (d, e) => { })")]
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_InvalidRegisterArguments_DoesNotWarn(
string name,
Expand Down Expand Up @@ -1423,4 +1422,55 @@ public partial class MyControl : Control

await CSharpAnalyzerTest<UseGeneratedDependencyPropertyOnManualPropertyAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
}

[TestMethod]
[DataRow("string", "string", "null")]
[DataRow("string", "string", "default(string)")]
[DataRow("string", "string", "(string)null")]
[DataRow("string", "string", "\"\"")]
[DataRow("string", "string", "\"Hello\"")]
[DataRow("string", "string?", "null")]
[DataRow("object", "object", "null")]
[DataRow("object", "object?", "null")]
[DataRow("int", "int", "0")]
[DataRow("int", "int", "42")]
[DataRow("int", "int", "default(int)")]
[DataRow("int?", "int?", "null")]
[DataRow("int?", "int?", "0")]
[DataRow("int?", "int?", "42")]
[DataRow("int?", "int?", "default(int?)")]
[DataRow("int?", "int?", "null")]
[DataRow("System.TimeSpan", "System.TimeSpan", "default(System.TimeSpan)")]
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_Warns(
string dependencyPropertyType,
string propertyType,
string defaultValueExpression)
{
string source = $$"""
using CommunityToolkit.WinUI;
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}} {|WCTDP0017:Name|}
{
get => ({{propertyType}})GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
}
""";

await CSharpAnalyzerTest<UseGeneratedDependencyPropertyOnManualPropertyAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
}
}

0 comments on commit 26fdeed

Please sign in to comment.