Skip to content

Commit

Permalink
draft
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergio0694 committed Dec 13, 2024
1 parent 3241ea5 commit 5eaaa84
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public override void Initialize(AnalysisContext context)
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();


context.RegisterCompilationStartAction(static context =>
{
// Get the XAML mode to use
Expand Down Expand Up @@ -465,6 +466,10 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla
}
}
}
else
{

}

// Find the parent field for the operation (we're guaranteed to only fine one)
if (context.Operation.Syntax.FirstAncestor<FieldDeclarationSyntax>()?.GetLocation() is not Location fieldLocation)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// 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 System.Diagnostics.CodeAnalysis;
using CommunityToolkit.GeneratedDependencyProperty.Helpers;
using Microsoft.CodeAnalysis;

namespace CommunityToolkit.GeneratedDependencyProperty.Extensions;

/// <summary>
/// Extension methods for <see cref="ITypeSymbol"/> types.
/// </summary>
internal static class ITypeSymbolExtensions
{
/// <summary>
/// Checks whether a given type has a default value of <see langword="null"/>.
/// </summary>
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance to check.</param>
/// <returns>Whether the default value of <paramref name="symbol"/> is <see langword="null"/>.</returns>
public static bool IsDefaultValueNull(this ITypeSymbol symbol)
{
return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T };
}

/// <summary>
/// Tries to get the default value of a given enum type.
/// </summary>
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance to check.</param>
/// <param name="value">The resulting default value for <paramref name="symbol"/>, if it was an enum type.</param>
/// <returns>Whether <paramref name="value"/> was retrieved successfully.</returns>
public static bool TryGetDefaultValueForEnumType(this ITypeSymbol symbol, [NotNullWhen(true)] out object? value)
{
if (symbol.TypeKind is not TypeKind.Enum)
{
value = default;

return false;
}

// The default value of the enum is the value of its first constant field
foreach (ISymbol memberSymbol in symbol.GetMembers())
{
if (memberSymbol is IFieldSymbol { IsConst: true, ConstantValue: object defaultValue })
{
value = defaultValue;

return true;
}
}

value = default;

return false;
}

/// <summary>
/// Checks whether or not a given type symbol has a specified fully qualified metadata name.
/// </summary>
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance to check.</param>
/// <param name="name">The full name to check.</param>
/// <returns>Whether <paramref name="symbol"/> has a full name equals to <paramref name="name"/>.</returns>
public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name)
{
using ImmutableArrayBuilder<char> builder = new();

symbol.AppendFullyQualifiedMetadataName(in builder);

return builder.WrittenSpan.SequenceEqual(name.AsSpan());
}

/// <summary>
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits from a specified type.
/// </summary>
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
/// <param name="baseTypeSymbol">The <see cref="ITypeSymbol"/> instance to check for inheritance from.</param>
/// <returns>Whether or not <paramref name="typeSymbol"/> inherits from <paramref name="baseTypeSymbol"/>.</returns>
public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
{
INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType;

while (currentBaseTypeSymbol is not null)
{
if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol))
{
return true;
}

currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType;
}

return false;
}

/// <summary>
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits from a specified type.
/// </summary>
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
/// <param name="name">The full name of the type to check for inheritance.</param>
/// <returns>Whether or not <paramref name="typeSymbol"/> inherits from <paramref name="name"/>.</returns>
public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name)
{
INamedTypeSymbol? baseType = typeSymbol.BaseType;

while (baseType is not null)
{
if (baseType.HasFullyQualifiedMetadataName(name))
{
return true;
}

baseType = baseType.BaseType;
}

return false;
}

/// <summary>
/// Gets the fully qualified metadata name for a given <see cref="ITypeSymbol"/> instance.
/// </summary>
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance.</param>
/// <returns>The fully qualified metadata name for <paramref name="symbol"/>.</returns>
public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol)
{
using ImmutableArrayBuilder<char> builder = new();

symbol.AppendFullyQualifiedMetadataName(in builder);

return builder.ToString();
}

/// <summary>
/// Appends the fully qualified metadata name for a given symbol to a target builder.
/// </summary>
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance.</param>
/// <param name="builder">The target <see cref="ImmutableArrayBuilder{T}"/> instance.</param>
public static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, ref readonly ImmutableArrayBuilder<char> builder)
{
static void BuildFrom(ISymbol? symbol, ref readonly ImmutableArrayBuilder<char> builder)
{
switch (symbol)
{
// Namespaces that are nested also append a leading '.'
case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }:
BuildFrom(symbol.ContainingNamespace, in builder);
builder.Add('.');
builder.AddRange(symbol.MetadataName.AsSpan());
break;

// Other namespaces (i.e. the one right before global) skip the leading '.'
case INamespaceSymbol { IsGlobalNamespace: false }:
builder.AddRange(symbol.MetadataName.AsSpan());
break;

// Types with no namespace just have their metadata name directly written
case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }:
builder.AddRange(symbol.MetadataName.AsSpan());
break;

// Types with a containing non-global namespace also append a leading '.'
case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }:
BuildFrom(namespaceSymbol, in builder);
builder.Add('.');
builder.AddRange(symbol.MetadataName.AsSpan());
break;

// Nested types append a leading '+'
case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }:
BuildFrom(typeSymbol, in builder);
builder.Add('+');
builder.AddRange(symbol.MetadataName.AsSpan());
break;
default:
break;
}
}

BuildFrom(symbol, in builder);
}

/// <summary>
/// Checks whether a given type is contained in a namespace with a specified name.
/// </summary>
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance.</param>
/// <param name="namespaceName">The namespace to check.</param>
/// <returns>Whether <paramref name="symbol"/> is contained within <paramref name="namespaceName"/>.</returns>
public static bool IsContainedInNamespace(this ITypeSymbol symbol, string? namespaceName)
{
static void BuildFrom(INamespaceSymbol? symbol, ref readonly ImmutableArrayBuilder<char> builder)
{
switch (symbol)
{
// Namespaces that are nested also append a leading '.'
case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }:
BuildFrom(symbol.ContainingNamespace, in builder);
builder.Add('.');
builder.AddRange(symbol.MetadataName.AsSpan());
break;

// Other namespaces (i.e. the one right before global) skip the leading '.'
case INamespaceSymbol { IsGlobalNamespace: false }:
builder.AddRange(symbol.MetadataName.AsSpan());
break;
default:
break;
}
}

// Special case for no containing namespace
if (symbol.ContainingNamespace is not { } containingNamespace)
{
return namespaceName is null;
}

// Special case if the type is directly in the global namespace
if (containingNamespace.IsGlobalNamespace)
{
return containingNamespace.MetadataName == namespaceName;
}

using ImmutableArrayBuilder<char> builder = new();

BuildFrom(containingNamespace, in builder);

return builder.WrittenSpan.SequenceEqual(namespaceName.AsSpan());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,6 @@ public string? Name
[TestMethod]
[DataRow("global::System.TimeSpan", "global::System.TimeSpan", "global::System.TimeSpan.FromSeconds(1)")]
[DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "global::System.TimeSpan.FromSeconds(1)")]
[DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")]
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_DoesNotWarn(
string dependencyPropertyType,
string propertyType,
Expand Down Expand Up @@ -1425,21 +1424,21 @@ public enum MyEnum { A, B, C }
[DataRow("object", "object?")]
[DataRow("int", "int")]
[DataRow("int?", "int?")]
[DataRow("global::System.TimeSpan", "global::System.TimeSpan", "null")]
[DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "default(global::System.TimeSpan?)")]
[DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "null")]
[DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "default(global::System.DateTimeOffset?)")]
[DataRow("global::System.Guid?", "global::System.Guid?", "default(global::System.Guid?)")]
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?", "default(global::System.Collections.Generic.KeyValuePair<int, float>?)")]
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?", "null")]
[DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct", "default(global::MyApp.MyStruct)")]
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "null")]
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "default(global::MyApp.MyStruct?)")]
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "default(global::MyApp.MyEnum)")]
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "null")]
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")]
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "null")]
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "default(global::MyApp.MyClass)")]
[DataRow("global::System.TimeSpan", "global::System.TimeSpan")]
[DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")]
[DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset")]
[DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?")]
[DataRow("global::System.Guid?", "global::System.Guid?")]
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?")]
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?" )]
[DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")]
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")]
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")]
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")]
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")]
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")]
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")]
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")]
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_Warns(
string dependencyPropertyType,
string propertyType)
Expand Down Expand Up @@ -1504,6 +1503,7 @@ public class MyClass { }
[DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "default(global::Windows.Foundation.Size)")]
[DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")]
[DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Visible")]
[DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")]
[DataRow("global::System.TimeSpan", "global::System.TimeSpan", "default(System.TimeSpan)")]
[DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "default(global::System.DateTimeOffset)")]
[DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ public class Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer
[DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")]
[DataRow("global::System.Guid?", "global::System.Guid?")]
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?")]
[DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")]
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")]
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")]
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")]
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")]
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass?")]
public async Task SimpleProperty(string dependencyPropertyType, string propertyType)
{
string original = $$"""
Expand All @@ -86,6 +86,7 @@ public class MyControl : Control
public struct MyStruct { public string X { get; set; } }
public enum MyEnum { A, B, C }
public class MyClass { }
""";

string @fixed = $$"""
Expand All @@ -101,6 +102,75 @@ public partial class MyControl : Control
public partial {{propertyType}} {|CS9248:Name|} { get; set; }
}
public struct MyStruct { public string X { get; set; } }
public enum MyEnum { A, B, C }
public class MyClass { }
""";

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

await test.RunAsync();
}

// These are custom value types, on properties where the metadata was set to 'null'. In this case, the
// default value would just be 'null', as XAML can't default initialize them. To preserve behavior,
// we must include an explicit default value. This will warn when the code is recompiled, but that
// is expected, because this specific scenario was (1) niche, and (2) kinda busted already anyway.
[TestMethod]
[DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")]
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")]
public async Task SimpleProperty_ExplicitNull(string dependencyPropertyType, string propertyType)
{
string original = $$"""
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace MyApp;
public class MyControl : Control
{
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
name: nameof(Name),
propertyType: typeof({{dependencyPropertyType}}),
ownerType: typeof(MyControl),
typeMetadata: null);
public {{propertyType}} [|Name|]
{
get => ({{propertyType}})GetValue(NameProperty);
set => SetValue(NameProperty, value);
}
}
public struct MyStruct { public string X { get; set; } }
public enum MyEnum { A, B, C }
""";

string @fixed = $$"""
using CommunityToolkit.WinUI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
namespace MyApp;
public partial class MyControl : Control
{
[GeneratedDependencyProperty(DefaultValue = null)]
public partial {{propertyType}} {|CS9248:Name|} { get; set; }
}
public struct MyStruct { public string X { get; set; } }
public enum MyEnum { A, B, C }
""";
Expand Down

0 comments on commit 5eaaa84

Please sign in to comment.