Skip to content

Commit fd9faea

Browse files
committed
Fix nullability checks in type parameter constraints
1 parent f7f3e86 commit fd9faea

File tree

3 files changed

+202
-1
lines changed

3 files changed

+202
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 Microsoft.CodeAnalysis;
6+
7+
namespace CommunityToolkit.GeneratedDependencyProperty.Extensions;
8+
9+
/// <summary>
10+
/// Extension methods for <see cref="ITypeParameterSymbol"/> types.
11+
/// </summary>
12+
internal static class ITypeParameterSymbolExtensions
13+
{
14+
/// <summary>
15+
/// Checks whether a given type parameter is a reference type.
16+
/// </summary>
17+
/// <param name="symbol">The input <see cref="ITypeParameterSymbol"/> instance to check.</param>
18+
/// <returns>Whether the input type parameter is a reference type.</returns>
19+
public static bool IsReferenceTypeOrIndirectlyConstrainedToReferenceType(this ITypeParameterSymbol symbol)
20+
{
21+
// The type is definitely a reference type (e.g. it has the 'class' constraint)
22+
if (symbol.IsReferenceType)
23+
{
24+
return true;
25+
}
26+
27+
// The type is definitely a value type (e.g. it has the 'struct' constraint)
28+
if (symbol.IsValueType)
29+
{
30+
return false;
31+
}
32+
33+
foreach (ITypeSymbol constraintType in symbol.ConstraintTypes)
34+
{
35+
// Recurse on the type parameter first (e. g. we might indirectly be getting a 'class' constraint)
36+
if (constraintType is ITypeParameterSymbol typeParameter &&
37+
typeParameter.IsReferenceTypeOrIndirectlyConstrainedToReferenceType())
38+
{
39+
return true;
40+
}
41+
42+
// Special constraint type that type parameters can derive from. Note that for concrete enum
43+
// types, the 'Enum' constraint isn't sufficient, they'd also have e.g. 'struct', which is
44+
// already checked before. If a type parameter only has 'Enum', then it should be considered
45+
// a reference type.
46+
if (constraintType.SpecialType is SpecialType.System_Delegate or SpecialType.System_Enum)
47+
{
48+
return true;
49+
}
50+
51+
// Only check for classes (an interface doesn't guarantee the type argument will be a reference type)
52+
if (constraintType.TypeKind is TypeKind.Class)
53+
{
54+
return true;
55+
}
56+
}
57+
58+
return false;
59+
}
60+
}

components/DependencyPropertyGenerator/CommunityToolkit.DependencyPropertyGenerator.SourceGenerators/Extensions/ITypeSymbolExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static bool IsDefaultValueNull(this ITypeSymbol symbol)
2525
// If we do have a type parameter, check that it does have some reference type constraint on it.
2626
if (symbol is ITypeParameterSymbol typeParameter)
2727
{
28-
return typeParameter.HasReferenceTypeConstraint;
28+
return typeParameter.IsReferenceTypeOrIndirectlyConstrainedToReferenceType();
2929
}
3030

3131
return symbol is { IsValueType: false } or INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T };

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

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1765,6 +1765,68 @@ public partial class MyObject<T1, T2, T3, T4, T5> : DependencyObject
17651765
await CSharpAnalyzerTest<InvalidPropertyDefaultValueTypeAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
17661766
}
17671767

1768+
[TestMethod]
1769+
[DataRow("where T : class", "null")]
1770+
[DataRow("where T : class", "default(T)")]
1771+
[DataRow("where T : TOther where TOther : class", "null")]
1772+
[DataRow("where T : class where TOther : class", "default(TOther)")]
1773+
[DataRow("where T : Delegate", "null")]
1774+
[DataRow("where T : Enum", "null")]
1775+
[DataRow("where T : DependencyObject", "null")]
1776+
[DataRow("where T : DependencyObject", "default(T)")]
1777+
[DataRow("where T : TOther where TOther : Delegate", "null")]
1778+
[DataRow("where T : TOther where TOther : Enum", "null")]
1779+
[DataRow("where T : TOther where TOther : DependencyObject", "null")]
1780+
[DataRow("where T : DependencyObject where TOther : class", "default(TOther)")]
1781+
public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ConstrainedExplicitNull_DoesNotWarn(
1782+
string typeConstraints,
1783+
string defaultValue)
1784+
{
1785+
string source = $$"""
1786+
using System;
1787+
using CommunityToolkit.WinUI;
1788+
using Windows.UI.Xaml;
1789+
1790+
#nullable enable
1791+
1792+
namespace MyApp;
1793+
1794+
public partial class MyObject<T, TOther> : DependencyObject {{typeConstraints}}
1795+
{
1796+
[GeneratedDependencyProperty(DefaultValue = {{defaultValue}})]
1797+
public partial T {|CS9248:Name|} { get; set; }
1798+
}
1799+
""";
1800+
1801+
await CSharpAnalyzerTest<InvalidPropertyDefaultValueTypeAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
1802+
}
1803+
1804+
[TestMethod]
1805+
[DataRow("where T : struct")]
1806+
[DataRow("where T : unmanaged")]
1807+
[DataRow("where T : struct, Enum")]
1808+
[DataRow("where T : unmanaged, Enum")]
1809+
public async Task InvalidPropertyDefaultValueTypeAnalyzer_TypeParameter_ConstrainedExplicitNull_Warns(string typeConstraints)
1810+
{
1811+
string source = $$"""
1812+
using System;
1813+
using CommunityToolkit.WinUI;
1814+
using Windows.UI.Xaml;
1815+
1816+
#nullable enable
1817+
1818+
namespace MyApp;
1819+
1820+
public partial class MyObject<T, TOther> : DependencyObject {{typeConstraints}}
1821+
{
1822+
[GeneratedDependencyProperty({|WCTDPG0010:DefaultValue = null|})]
1823+
public partial T {|CS9248:Name|} { get; set; }
1824+
}
1825+
""";
1826+
1827+
await CSharpAnalyzerTest<InvalidPropertyDefaultValueTypeAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
1828+
}
1829+
17681830
[TestMethod]
17691831
[DataRow("string", "42")]
17701832
[DataRow("string", "3.14")]
@@ -3226,6 +3288,85 @@ public void Dispose()
32263288
await CSharpAnalyzerTest<UseGeneratedDependencyPropertyOnManualPropertyAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
32273289
}
32283290

3291+
// Regression test for a case found in the Microsoft Store
3292+
[TestMethod]
3293+
[DataRow("where T : class", "null")]
3294+
[DataRow("where T : class", "default(T)")]
3295+
[DataRow("where T : TOther where TOther : class", "null")]
3296+
[DataRow("where T : class where TOther : class", "default(TOther)")]
3297+
[DataRow("where T : Delegate", "null")]
3298+
[DataRow("where T : Enum", "null")]
3299+
[DataRow("where T : DependencyObject", "null")]
3300+
[DataRow("where T : DependencyObject", "default(T)")]
3301+
[DataRow("where T : TOther where TOther : Delegate", "null")]
3302+
[DataRow("where T : TOther where TOther : Enum", "null")]
3303+
[DataRow("where T : TOther where TOther : DependencyObject", "null")]
3304+
[DataRow("where T : DependencyObject where TOther : class", "default(TOther)")]
3305+
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithNullConstrainedGeneric_Warns(
3306+
string typeConstraints,
3307+
string defaultValue)
3308+
{
3309+
string source = $$"""
3310+
using System;
3311+
using Windows.UI.Xaml;
3312+
3313+
#nullable enable
3314+
3315+
namespace MyApp;
3316+
3317+
public partial class MyObject<T, TOther> : DependencyObject {{typeConstraints}}
3318+
{
3319+
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
3320+
name: "Name",
3321+
propertyType: typeof(T),
3322+
ownerType: typeof(MyObject<T, TOther>),
3323+
typeMetadata: new PropertyMetadata({{defaultValue}}));
3324+
3325+
public T {|WCTDPG0017:Name|}
3326+
{
3327+
get => (T?)GetValue(NameProperty);
3328+
set => SetValue(NameProperty, value);
3329+
}
3330+
}
3331+
""";
3332+
3333+
await CSharpAnalyzerTest<UseGeneratedDependencyPropertyOnManualPropertyAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
3334+
}
3335+
3336+
[TestMethod]
3337+
[DataRow("where T : struct")]
3338+
[DataRow("where T : unmanaged")]
3339+
[DataRow("where T : struct, Enum")]
3340+
[DataRow("where T : unmanaged, Enum")]
3341+
public async Task UseGeneratedDependencyPropertyOnManualPropertyAnalyzer_ValidProperty_WithNullConstrainedGeneric_WCTDPG0031_DoesNotWarn(string typeConstraints)
3342+
{
3343+
string source = $$"""
3344+
using System;
3345+
using Windows.UI.Xaml;
3346+
3347+
#nullable enable
3348+
3349+
namespace MyApp;
3350+
3351+
public partial class MyObject<T, TOther> : DependencyObject {{typeConstraints}}
3352+
{
3353+
public static readonly DependencyProperty NameProperty = DependencyProperty.Register(
3354+
name: "Name",
3355+
propertyType: typeof(T),
3356+
ownerType: typeof(MyObject<T, TOther>),
3357+
typeMetadata: new PropertyMetadata({|WCTDPG0031:null|}));
3358+
3359+
public T {|WCTDPG0017:Name|}
3360+
{
3361+
get => (T)GetValue(NameProperty);
3362+
set => SetValue(NameProperty, value);
3363+
}
3364+
}
3365+
""";
3366+
3367+
await CSharpAnalyzerTest<UseGeneratedDependencyPropertyOnManualPropertyAnalyzer>.VerifyAnalyzerAsync(source, LanguageVersion.CSharp13);
3368+
}
3369+
32293370
[TestMethod]
32303371
[DataRow("private static readonly")]
32313372
[DataRow("public static")]

0 commit comments

Comments
 (0)