Skip to content

Commit c11bb0e

Browse files
committed
Add [MemberNotNull] to [ObservableProperty] set accessor
1 parent 24388b8 commit c11bb0e

File tree

2 files changed

+57
-6
lines changed

2 files changed

+57
-6
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
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>
2121
/// <param name="IsReferenceType">Indicates whether the property is of a reference type.</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,
@@ -31,4 +32,5 @@ internal sealed record PropertyInfo(
3132
bool NotifyDataErrorInfo,
3233
bool IsOldPropertyValueDirectlyReferenced,
3334
bool IsReferenceType,
35+
bool IncludeMemberNotNullOnSetAccessor,
3436
EquatableArray<AttributeInfo> ForwardedAttributes);

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

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ public static bool TryGetInfo(
113113
bool hasAnyValidationAttributes = false;
114114
bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName);
115115
bool isReferenceType = fieldSymbol.Type.IsReferenceType;
116+
bool includeMemberNotNullOnSetAccessor = GetIncludeMemberNotNullOnSetAccessor(fieldSymbol, semanticModel);
116117

117118
// Track the property changing event for the property, if the type supports it
118119
if (shouldInvokeOnPropertyChanging)
@@ -262,6 +263,7 @@ public static bool TryGetInfo(
262263
notifyDataErrorInfo,
263264
isOldPropertyValueDirectlyReferenced,
264265
isReferenceType,
266+
includeMemberNotNullOnSetAccessor,
265267
forwardedAttributes.ToImmutable());
266268

267269
diagnostics = builder.ToImmutable();
@@ -668,6 +670,36 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo
668670
return false;
669671
}
670672

673+
/// <summary>
674+
/// Checks whether <see cref="MemberNotNullAttribute"/> should be used on the setter.
675+
/// </summary>
676+
/// <param name="fieldSymbol">The input <see cref="IFieldSymbol"/> instance to process.</param>
677+
/// <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)
680+
{
681+
// This is used to avoid nullability warnings when setting the property from a constructor, in case the field
682+
// was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler.
683+
// Consider this example:
684+
//
685+
// partial class MyViewModel : ObservableObject
686+
// {
687+
// public MyViewModel()
688+
// {
689+
// Name = "Bob";
690+
// }
691+
//
692+
// [ObservableProperty]
693+
// private string name;
694+
// }
695+
//
696+
// The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name
697+
// 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 } &&
700+
semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute");
701+
}
702+
671703
/// <summary>
672704
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
673705
/// </summary>
@@ -880,6 +912,27 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
880912
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
881913
.ToImmutableArray();
882914

915+
// Prepare the setter for the generated property:
916+
//
917+
// set
918+
// {
919+
// <BODY>
920+
// }
921+
AccessorDeclarationSyntax setAccessor = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithBody(Block(setterIfStatement));
922+
923+
// Add the [MemberNotNull] attribute if needed:
924+
//
925+
// [MemberNotNull("<FIELD_NAME>")]
926+
// <SET_ACCESSOR>
927+
if (propertyInfo.IncludeMemberNotNullOnSetAccessor)
928+
{
929+
setAccessor = setAccessor.AddAttributeLists(
930+
AttributeList(SingletonSeparatedList(
931+
Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.MemberNotNull"))
932+
.AddArgumentListArguments(
933+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.FieldName)))))));
934+
}
935+
883936
// Construct the generated property as follows:
884937
//
885938
// /// <inheritdoc cref="<FIELD_NAME>"/>
@@ -889,10 +942,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
889942
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
890943
// {
891944
// get => <FIELD_NAME>;
892-
// set
893-
// {
894-
// <BODY>
895-
// }
945+
// <SET_ACCESSOR>
896946
// }
897947
return
898948
PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName))
@@ -910,8 +960,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
910960
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
911961
.WithExpressionBody(ArrowExpressionClause(IdentifierName(propertyInfo.FieldName)))
912962
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken)),
913-
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
914-
.WithBody(Block(setterIfStatement)));
963+
setAccessor);
915964
}
916965

917966
/// <summary>

0 commit comments

Comments
 (0)