Skip to content

Commit 673801a

Browse files
committed
Update generator for unconstrained generic nullability
1 parent d2fe8a0 commit 673801a

File tree

3 files changed

+850
-23
lines changed

3 files changed

+850
-23
lines changed

src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
1818
/// <param name="NotifyPropertyChangedRecipients">Whether or not the generated property also broadcasts changes.</param>
1919
/// <param name="NotifyDataErrorInfo">Whether or not the generated property also validates its value.</param>
2020
/// <param name="IsOldPropertyValueDirectlyReferenced">Whether the old property value is being directly referenced.</param>
21-
/// <param name="IsReferenceType">Indicates whether the property is of a reference type.</param>
21+
/// <param name="IsReferenceTypeOrUnconstraindTypeParameter">Indicates whether the property is of a reference type or an unconstrained type parameter.</param>
2222
/// <param name="IncludeMemberNotNullOnSetAccessor">Indicates whether to include nullability annotations on the setter.</param>
2323
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated property.</param>
2424
internal sealed record PropertyInfo(
@@ -31,6 +31,6 @@ internal sealed record PropertyInfo(
3131
bool NotifyPropertyChangedRecipients,
3232
bool NotifyDataErrorInfo,
3333
bool IsOldPropertyValueDirectlyReferenced,
34-
bool IsReferenceType,
34+
bool IsReferenceTypeOrUnconstraindTypeParameter,
3535
bool IncludeMemberNotNullOnSetAccessor,
3636
EquatableArray<AttributeInfo> ForwardedAttributes);

src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,13 @@ public static bool TryGetInfo(
112112
bool hasOrInheritsClassLevelNotifyDataErrorInfo = false;
113113
bool hasAnyValidationAttributes = false;
114114
bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName);
115-
bool isReferenceType = fieldSymbol.Type.IsReferenceType;
116-
bool includeMemberNotNullOnSetAccessor = GetIncludeMemberNotNullOnSetAccessor(fieldSymbol, semanticModel);
115+
116+
// Get the nullability info for the property
117+
GetNullabilityInfo(
118+
fieldSymbol,
119+
semanticModel,
120+
out bool isReferenceTypeOrUnconstraindTypeParameter,
121+
out bool includeMemberNotNullOnSetAccessor);
117122

118123
// Track the property changing event for the property, if the type supports it
119124
if (shouldInvokeOnPropertyChanging)
@@ -262,7 +267,7 @@ public static bool TryGetInfo(
262267
notifyRecipients,
263268
notifyDataErrorInfo,
264269
isOldPropertyValueDirectlyReferenced,
265-
isReferenceType,
270+
isReferenceTypeOrUnconstraindTypeParameter,
266271
includeMemberNotNullOnSetAccessor,
267272
forwardedAttributes.ToImmutable());
268273

@@ -671,13 +676,24 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo
671676
}
672677

673678
/// <summary>
674-
/// Checks whether <see cref="MemberNotNullAttribute"/> should be used on the setter.
679+
/// Gets the nullability info on the generated property
675680
/// </summary>
676681
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
677682
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
678-
/// <returns>Whether <see cref="MemberNotNullAttribute"/> should be used on the setter.</returns>
679-
private static bool GetIncludeMemberNotNullOnSetAccessor(IFieldSymbol fieldSymbol, SemanticModel semanticModel)
683+
/// <param name="isReferenceTypeOrUnconstraindTypeParameter">Whether the property type supports nullability.</param>
684+
/// <param name="includeMemberNotNullOnSetAccessor">Whether <see cref="MemberNotNullAttribute"/> should be used on the setter.</param>
685+
/// <returns></returns>
686+
private static void GetNullabilityInfo(
687+
IFieldSymbol fieldSymbol,
688+
SemanticModel semanticModel,
689+
out bool isReferenceTypeOrUnconstraindTypeParameter,
690+
out bool includeMemberNotNullOnSetAccessor)
680691
{
692+
// We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases.
693+
// This will cover both reference types as well T when the constraints are not struct or unmanaged.
694+
// If this is true, it means the field storage can potentially be in a null state (even if not annotated).
695+
isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType;
696+
681697
// This is used to avoid nullability warnings when setting the property from a constructor, in case the field
682698
// was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler.
683699
// Consider this example:
@@ -695,8 +711,10 @@ private static bool GetIncludeMemberNotNullOnSetAccessor(IFieldSymbol fieldSymbo
695711
//
696712
// The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name
697713
// is set, the compiler can determine that the name backing field is also being set (to a non null value).
698-
return
699-
fieldSymbol.Type is { NullableAnnotation: not NullableAnnotation.Annotated, IsReferenceType: true } &&
714+
// Of course, this can only be the case if the field type is also of a type that could be in a null state.
715+
includeMemberNotNullOnSetAccessor =
716+
isReferenceTypeOrUnconstraindTypeParameter &&
717+
fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated &&
700718
semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute");
701719
}
702720

@@ -1001,7 +1019,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
10011019
// happen when the property is first set to some value that is not null (but the backing field would still be so).
10021020
// As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability
10031021
// annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it.
1004-
TypeSyntax oldValueTypeSyntax = propertyInfo.IsReferenceType switch
1022+
TypeSyntax oldValueTypeSyntax = propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch
10051023
{
10061024
true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?")
10071025
=> IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"),

0 commit comments

Comments
 (0)