Skip to content

Commit b8df95a

Browse files
authored
Merge pull request #646 from CommunityToolkit/dev/observable-property-membernotnull
Add [MemberNotNull] to [ObservableProperty] set accessors
2 parents 24388b8 + 673801a commit b8df95a

File tree

4 files changed

+1137
-22
lines changed

4 files changed

+1137
-22
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ 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>
22+
/// <param name="IncludeMemberNotNullOnSetAccessor">Indicates whether to include nullability annotations on the setter.</param>
2223
/// <param name="ForwardedAttributes">The sequence of forwarded attributes for the generated property.</param>
2324
internal sealed record PropertyInfo(
2425
string TypeNameWithNullabilityAnnotations,
@@ -30,5 +31,6 @@ internal sealed record PropertyInfo(
3031
bool NotifyPropertyChangedRecipients,
3132
bool NotifyDataErrorInfo,
3233
bool IsOldPropertyValueDirectlyReferenced,
33-
bool IsReferenceType,
34+
bool IsReferenceTypeOrUnconstraindTypeParameter,
35+
bool IncludeMemberNotNullOnSetAccessor,
3436
EquatableArray<AttributeInfo> ForwardedAttributes);

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

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +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;
115+
116+
// Get the nullability info for the property
117+
GetNullabilityInfo(
118+
fieldSymbol,
119+
semanticModel,
120+
out bool isReferenceTypeOrUnconstraindTypeParameter,
121+
out bool includeMemberNotNullOnSetAccessor);
116122

117123
// Track the property changing event for the property, if the type supports it
118124
if (shouldInvokeOnPropertyChanging)
@@ -261,7 +267,8 @@ public static bool TryGetInfo(
261267
notifyRecipients,
262268
notifyDataErrorInfo,
263269
isOldPropertyValueDirectlyReferenced,
264-
isReferenceType,
270+
isReferenceTypeOrUnconstraindTypeParameter,
271+
includeMemberNotNullOnSetAccessor,
265272
forwardedAttributes.ToImmutable());
266273

267274
diagnostics = builder.ToImmutable();
@@ -668,6 +675,49 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo
668675
return false;
669676
}
670677

678+
/// <summary>
679+
/// Gets the nullability info on the generated property
680+
/// </summary>
681+
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
682+
/// <param name="semanticModel">The <see cref="SemanticModel"/> instance for the current run.</param>
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)
691+
{
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+
697+
// This is used to avoid nullability warnings when setting the property from a constructor, in case the field
698+
// was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler.
699+
// Consider this example:
700+
//
701+
// partial class MyViewModel : ObservableObject
702+
// {
703+
// public MyViewModel()
704+
// {
705+
// Name = "Bob";
706+
// }
707+
//
708+
// [ObservableProperty]
709+
// private string name;
710+
// }
711+
//
712+
// The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name
713+
// is set, the compiler can determine that the name backing field is also being set (to a non null value).
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 &&
718+
semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute");
719+
}
720+
671721
/// <summary>
672722
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
673723
/// </summary>
@@ -880,6 +930,27 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
880930
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
881931
.ToImmutableArray();
882932

933+
// Prepare the setter for the generated property:
934+
//
935+
// set
936+
// {
937+
// <BODY>
938+
// }
939+
AccessorDeclarationSyntax setAccessor = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithBody(Block(setterIfStatement));
940+
941+
// Add the [MemberNotNull] attribute if needed:
942+
//
943+
// [MemberNotNull("<FIELD_NAME>")]
944+
// <SET_ACCESSOR>
945+
if (propertyInfo.IncludeMemberNotNullOnSetAccessor)
946+
{
947+
setAccessor = setAccessor.AddAttributeLists(
948+
AttributeList(SingletonSeparatedList(
949+
Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.MemberNotNull"))
950+
.AddArgumentListArguments(
951+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.FieldName)))))));
952+
}
953+
883954
// Construct the generated property as follows:
884955
//
885956
// /// <inheritdoc cref="<FIELD_NAME>"/>
@@ -889,10 +960,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
889960
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
890961
// {
891962
// get => <FIELD_NAME>;
892-
// set
893-
// {
894-
// <BODY>
895-
// }
963+
// <SET_ACCESSOR>
896964
// }
897965
return
898966
PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName))
@@ -910,8 +978,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
910978
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
911979
.WithExpressionBody(ArrowExpressionClause(IdentifierName(propertyInfo.FieldName)))
912980
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
913-
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
914-
.WithBody(Block(setterIfStatement)));
981+
setAccessor);
915982
}
916983

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

0 commit comments

Comments
 (0)