Skip to content

Commit 5eaaa84

Browse files
committed
draft
1 parent 3241ea5 commit 5eaaa84

File tree

4 files changed

+321
-18
lines changed

4 files changed

+321
-18
lines changed

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Diagnostics/Analyzers/UseGeneratedDependencyPropertyOnManualPropertyAnalyzer.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public override void Initialize(AnalysisContext context)
6565
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
6666
context.EnableConcurrentExecution();
6767

68+
6869
context.RegisterCompilationStartAction(static context =>
6970
{
7071
// Get the XAML mode to use
@@ -465,6 +466,10 @@ void HandleSetAccessor(IPropertySymbol propertySymbol, PropertyFlags propertyFla
465466
}
466467
}
467468
}
469+
else
470+
{
471+
472+
}
468473

469474
// Find the parent field for the operation (we're guaranteed to only fine one)
470475
if (context.Operation.Syntax.FirstAncestor<FieldDeclarationSyntax>()?.GetLocation() is not Location fieldLocation)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Diagnostics.CodeAnalysis;
7+
using CommunityToolkit.GeneratedDependencyProperty.Helpers;
8+
using Microsoft.CodeAnalysis;
9+
10+
namespace CommunityToolkit.GeneratedDependencyProperty.Extensions;
11+
12+
/// <summary>
13+
/// Extension methods for <see cref="ITypeSymbol"/> types.
14+
/// </summary>
15+
internal static class ITypeSymbolExtensions
16+
{
17+
/// <summary>
18+
/// Checks whether a given type has a default value of <see langword="null"/>.
19+
/// </summary>
20+
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance to check.</param>
21+
/// <returns>Whether the default value of <paramref name="symbol"/> is <see langword="null"/>.</returns>
22+
public static bool IsDefaultValueNull(this ITypeSymbol symbol)
23+
{
24+
return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T };
25+
}
26+
27+
/// <summary>
28+
/// Tries to get the default value of a given enum type.
29+
/// </summary>
30+
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance to check.</param>
31+
/// <param name="value">The resulting default value for <paramref name="symbol"/>, if it was an enum type.</param>
32+
/// <returns>Whether <paramref name="value"/> was retrieved successfully.</returns>
33+
public static bool TryGetDefaultValueForEnumType(this ITypeSymbol symbol, [NotNullWhen(true)] out object? value)
34+
{
35+
if (symbol.TypeKind is not TypeKind.Enum)
36+
{
37+
value = default;
38+
39+
return false;
40+
}
41+
42+
// The default value of the enum is the value of its first constant field
43+
foreach (ISymbol memberSymbol in symbol.GetMembers())
44+
{
45+
if (memberSymbol is IFieldSymbol { IsConst: true, ConstantValue: object defaultValue })
46+
{
47+
value = defaultValue;
48+
49+
return true;
50+
}
51+
}
52+
53+
value = default;
54+
55+
return false;
56+
}
57+
58+
/// <summary>
59+
/// Checks whether or not a given type symbol has a specified fully qualified metadata name.
60+
/// </summary>
61+
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance to check.</param>
62+
/// <param name="name">The full name to check.</param>
63+
/// <returns>Whether <paramref name="symbol"/> has a full name equals to <paramref name="name"/>.</returns>
64+
public static bool HasFullyQualifiedMetadataName(this ITypeSymbol symbol, string name)
65+
{
66+
using ImmutableArrayBuilder<char> builder = new();
67+
68+
symbol.AppendFullyQualifiedMetadataName(in builder);
69+
70+
return builder.WrittenSpan.SequenceEqual(name.AsSpan());
71+
}
72+
73+
/// <summary>
74+
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits from a specified type.
75+
/// </summary>
76+
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
77+
/// <param name="baseTypeSymbol">The <see cref="ITypeSymbol"/> instance to check for inheritance from.</param>
78+
/// <returns>Whether or not <paramref name="typeSymbol"/> inherits from <paramref name="baseTypeSymbol"/>.</returns>
79+
public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol)
80+
{
81+
INamedTypeSymbol? currentBaseTypeSymbol = typeSymbol.BaseType;
82+
83+
while (currentBaseTypeSymbol is not null)
84+
{
85+
if (SymbolEqualityComparer.Default.Equals(currentBaseTypeSymbol, baseTypeSymbol))
86+
{
87+
return true;
88+
}
89+
90+
currentBaseTypeSymbol = currentBaseTypeSymbol.BaseType;
91+
}
92+
93+
return false;
94+
}
95+
96+
/// <summary>
97+
/// Checks whether or not a given <see cref="ITypeSymbol"/> inherits from a specified type.
98+
/// </summary>
99+
/// <param name="typeSymbol">The target <see cref="ITypeSymbol"/> instance to check.</param>
100+
/// <param name="name">The full name of the type to check for inheritance.</param>
101+
/// <returns>Whether or not <paramref name="typeSymbol"/> inherits from <paramref name="name"/>.</returns>
102+
public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeSymbol, string name)
103+
{
104+
INamedTypeSymbol? baseType = typeSymbol.BaseType;
105+
106+
while (baseType is not null)
107+
{
108+
if (baseType.HasFullyQualifiedMetadataName(name))
109+
{
110+
return true;
111+
}
112+
113+
baseType = baseType.BaseType;
114+
}
115+
116+
return false;
117+
}
118+
119+
/// <summary>
120+
/// Gets the fully qualified metadata name for a given <see cref="ITypeSymbol"/> instance.
121+
/// </summary>
122+
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance.</param>
123+
/// <returns>The fully qualified metadata name for <paramref name="symbol"/>.</returns>
124+
public static string GetFullyQualifiedMetadataName(this ITypeSymbol symbol)
125+
{
126+
using ImmutableArrayBuilder<char> builder = new();
127+
128+
symbol.AppendFullyQualifiedMetadataName(in builder);
129+
130+
return builder.ToString();
131+
}
132+
133+
/// <summary>
134+
/// Appends the fully qualified metadata name for a given symbol to a target builder.
135+
/// </summary>
136+
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance.</param>
137+
/// <param name="builder">The target <see cref="ImmutableArrayBuilder{T}"/> instance.</param>
138+
public static void AppendFullyQualifiedMetadataName(this ITypeSymbol symbol, ref readonly ImmutableArrayBuilder<char> builder)
139+
{
140+
static void BuildFrom(ISymbol? symbol, ref readonly ImmutableArrayBuilder<char> builder)
141+
{
142+
switch (symbol)
143+
{
144+
// Namespaces that are nested also append a leading '.'
145+
case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }:
146+
BuildFrom(symbol.ContainingNamespace, in builder);
147+
builder.Add('.');
148+
builder.AddRange(symbol.MetadataName.AsSpan());
149+
break;
150+
151+
// Other namespaces (i.e. the one right before global) skip the leading '.'
152+
case INamespaceSymbol { IsGlobalNamespace: false }:
153+
builder.AddRange(symbol.MetadataName.AsSpan());
154+
break;
155+
156+
// Types with no namespace just have their metadata name directly written
157+
case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }:
158+
builder.AddRange(symbol.MetadataName.AsSpan());
159+
break;
160+
161+
// Types with a containing non-global namespace also append a leading '.'
162+
case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }:
163+
BuildFrom(namespaceSymbol, in builder);
164+
builder.Add('.');
165+
builder.AddRange(symbol.MetadataName.AsSpan());
166+
break;
167+
168+
// Nested types append a leading '+'
169+
case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }:
170+
BuildFrom(typeSymbol, in builder);
171+
builder.Add('+');
172+
builder.AddRange(symbol.MetadataName.AsSpan());
173+
break;
174+
default:
175+
break;
176+
}
177+
}
178+
179+
BuildFrom(symbol, in builder);
180+
}
181+
182+
/// <summary>
183+
/// Checks whether a given type is contained in a namespace with a specified name.
184+
/// </summary>
185+
/// <param name="symbol">The input <see cref="ITypeSymbol"/> instance.</param>
186+
/// <param name="namespaceName">The namespace to check.</param>
187+
/// <returns>Whether <paramref name="symbol"/> is contained within <paramref name="namespaceName"/>.</returns>
188+
public static bool IsContainedInNamespace(this ITypeSymbol symbol, string? namespaceName)
189+
{
190+
static void BuildFrom(INamespaceSymbol? symbol, ref readonly ImmutableArrayBuilder<char> builder)
191+
{
192+
switch (symbol)
193+
{
194+
// Namespaces that are nested also append a leading '.'
195+
case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }:
196+
BuildFrom(symbol.ContainingNamespace, in builder);
197+
builder.Add('.');
198+
builder.AddRange(symbol.MetadataName.AsSpan());
199+
break;
200+
201+
// Other namespaces (i.e. the one right before global) skip the leading '.'
202+
case INamespaceSymbol { IsGlobalNamespace: false }:
203+
builder.AddRange(symbol.MetadataName.AsSpan());
204+
break;
205+
default:
206+
break;
207+
}
208+
}
209+
210+
// Special case for no containing namespace
211+
if (symbol.ContainingNamespace is not { } containingNamespace)
212+
{
213+
return namespaceName is null;
214+
}
215+
216+
// Special case if the type is directly in the global namespace
217+
if (containingNamespace.IsGlobalNamespace)
218+
{
219+
return containingNamespace.MetadataName == namespaceName;
220+
}
221+
222+
using ImmutableArrayBuilder<char> builder = new();
223+
224+
BuildFrom(containingNamespace, in builder);
225+
226+
return builder.WrittenSpan.SequenceEqual(namespaceName.AsSpan());
227+
}
228+
}

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_Analyzers.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,7 +1382,6 @@ public string? Name
13821382
[TestMethod]
13831383
[DataRow("global::System.TimeSpan", "global::System.TimeSpan", "global::System.TimeSpan.FromSeconds(1)")]
13841384
[DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "global::System.TimeSpan.FromSeconds(1)")]
1385-
[DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")]
13861385
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_ExplicitDefaultValue_DoesNotWarn(
13871386
string dependencyPropertyType,
13881387
string propertyType,
@@ -1425,21 +1424,21 @@ public enum MyEnum { A, B, C }
14251424
[DataRow("object", "object?")]
14261425
[DataRow("int", "int")]
14271426
[DataRow("int?", "int?")]
1428-
[DataRow("global::System.TimeSpan", "global::System.TimeSpan", "null")]
1429-
[DataRow("global::System.TimeSpan?", "global::System.TimeSpan?", "default(global::System.TimeSpan?)")]
1430-
[DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "null")]
1431-
[DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "default(global::System.DateTimeOffset?)")]
1432-
[DataRow("global::System.Guid?", "global::System.Guid?", "default(global::System.Guid?)")]
1433-
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?", "default(global::System.Collections.Generic.KeyValuePair<int, float>?)")]
1434-
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?", "null")]
1435-
[DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct", "default(global::MyApp.MyStruct)")]
1436-
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "null")]
1437-
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?", "default(global::MyApp.MyStruct?)")]
1438-
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum", "default(global::MyApp.MyEnum)")]
1439-
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "null")]
1440-
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?", "default(global::MyApp.MyEnum?)")]
1441-
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "null")]
1442-
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass", "default(global::MyApp.MyClass)")]
1427+
[DataRow("global::System.TimeSpan", "global::System.TimeSpan")]
1428+
[DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")]
1429+
[DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset")]
1430+
[DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?")]
1431+
[DataRow("global::System.Guid?", "global::System.Guid?")]
1432+
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?")]
1433+
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?" )]
1434+
[DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")]
1435+
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")]
1436+
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")]
1437+
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")]
1438+
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")]
1439+
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")]
1440+
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")]
1441+
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")]
14431442
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_Warns(
14441443
string dependencyPropertyType,
14451444
string propertyType)
@@ -1504,6 +1503,7 @@ public class MyClass { }
15041503
[DataRow("global::Windows.Foundation.Size", "global::Windows.Foundation.Size", "default(global::Windows.Foundation.Size)")]
15051504
[DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "default(global::Windows.UI.Xaml.Visibility)")]
15061505
[DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Visible")]
1506+
[DataRow("global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility", "global::Windows.UI.Xaml.Visibility.Collapsed")]
15071507
[DataRow("global::System.TimeSpan", "global::System.TimeSpan", "default(System.TimeSpan)")]
15081508
[DataRow("global::System.DateTimeOffset", "global::System.DateTimeOffset", "default(global::System.DateTimeOffset)")]
15091509
[DataRow("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", "null")]

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.Tests/Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer.cs

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ public class Test_UseGeneratedDependencyPropertyOnManualPropertyCodeFixer
5757
[DataRow("global::System.TimeSpan?", "global::System.TimeSpan?")]
5858
[DataRow("global::System.Guid?", "global::System.Guid?")]
5959
[DataRow("global::System.Collections.Generic.KeyValuePair<int, float>?", "global::System.Collections.Generic.KeyValuePair<int, float>?")]
60-
[DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")]
6160
[DataRow("global::MyApp.MyStruct?", "global::MyApp.MyStruct?")]
62-
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")]
6361
[DataRow("global::MyApp.MyEnum?", "global::MyApp.MyEnum?")]
62+
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass")]
63+
[DataRow("global::MyApp.MyClass", "global::MyApp.MyClass?")]
6464
public async Task SimpleProperty(string dependencyPropertyType, string propertyType)
6565
{
6666
string original = $$"""
@@ -86,6 +86,7 @@ public class MyControl : Control
8686
8787
public struct MyStruct { public string X { get; set; } }
8888
public enum MyEnum { A, B, C }
89+
public class MyClass { }
8990
""";
9091

9192
string @fixed = $$"""
@@ -101,6 +102,75 @@ public partial class MyControl : Control
101102
public partial {{propertyType}} {|CS9248:Name|} { get; set; }
102103
}
103104
105+
public struct MyStruct { public string X { get; set; } }
106+
public enum MyEnum { A, B, C }
107+
public class MyClass { }
108+
""";
109+
110+
CSharpCodeFixTest test = new(LanguageVersion.Preview)
111+
{
112+
TestCode = original,
113+
FixedCode = @fixed,
114+
ReferenceAssemblies = ReferenceAssemblies.Net.Net80,
115+
TestState = { AdditionalReferences =
116+
{
117+
MetadataReference.CreateFromFile(typeof(Point).Assembly.Location),
118+
MetadataReference.CreateFromFile(typeof(ApplicationView).Assembly.Location),
119+
MetadataReference.CreateFromFile(typeof(DependencyProperty).Assembly.Location),
120+
MetadataReference.CreateFromFile(typeof(GeneratedDependencyPropertyAttribute).Assembly.Location)
121+
}}
122+
};
123+
124+
await test.RunAsync();
125+
}
126+
127+
// These are custom value types, on properties where the metadata was set to 'null'. In this case, the
128+
// default value would just be 'null', as XAML can't default initialize them. To preserve behavior,
129+
// we must include an explicit default value. This will warn when the code is recompiled, but that
130+
// is expected, because this specific scenario was (1) niche, and (2) kinda busted already anyway.
131+
[TestMethod]
132+
[DataRow("global::MyApp.MyStruct", "global::MyApp.MyStruct")]
133+
[DataRow("global::MyApp.MyEnum", "global::MyApp.MyEnum")]
134+
public async Task SimpleProperty_ExplicitNull(string dependencyPropertyType, string propertyType)
135+
{
136+
string original = $$"""
137+
using Windows.UI.Xaml;
138+
using Windows.UI.Xaml.Controls;
139+
140+
namespace MyApp;
141+
142+
public class MyControl : Control
143+
{
144+
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
145+
name: nameof(Name),
146+
propertyType: typeof({{dependencyPropertyType}}),
147+
ownerType: typeof(MyControl),
148+
typeMetadata: null);
149+
150+
public {{propertyType}} [|Name|]
151+
{
152+
get => ({{propertyType}})GetValue(NameProperty);
153+
set => SetValue(NameProperty, value);
154+
}
155+
}
156+
157+
public struct MyStruct { public string X { get; set; } }
158+
public enum MyEnum { A, B, C }
159+
""";
160+
161+
string @fixed = $$"""
162+
using CommunityToolkit.WinUI;
163+
using Windows.UI.Xaml;
164+
using Windows.UI.Xaml.Controls;
165+
166+
namespace MyApp;
167+
168+
public partial class MyControl : Control
169+
{
170+
[GeneratedDependencyProperty(DefaultValue = null)]
171+
public partial {{propertyType}} {|CS9248:Name|} { get; set; }
172+
}
173+
104174
public struct MyStruct { public string X { get; set; } }
105175
public enum MyEnum { A, B, C }
106176
""";

0 commit comments

Comments
 (0)