@@ -112,7 +112,13 @@ public static bool TryGetInfo(
112
112
bool hasOrInheritsClassLevelNotifyDataErrorInfo = false ;
113
113
bool hasAnyValidationAttributes = false ;
114
114
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 ) ;
116
122
117
123
// Track the property changing event for the property, if the type supports it
118
124
if ( shouldInvokeOnPropertyChanging )
@@ -261,7 +267,8 @@ public static bool TryGetInfo(
261
267
notifyRecipients ,
262
268
notifyDataErrorInfo ,
263
269
isOldPropertyValueDirectlyReferenced ,
264
- isReferenceType ,
270
+ isReferenceTypeOrUnconstraindTypeParameter ,
271
+ includeMemberNotNullOnSetAccessor ,
265
272
forwardedAttributes . ToImmutable ( ) ) ;
266
273
267
274
diagnostics = builder . ToImmutable ( ) ;
@@ -668,6 +675,49 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo
668
675
return false ;
669
676
}
670
677
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
+
671
721
/// <summary>
672
722
/// Gets a <see cref="CompilationUnitSyntax"/> instance with the cached args for property changing notifications.
673
723
/// </summary>
@@ -880,6 +930,27 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
880
930
. Select ( static a => AttributeList ( SingletonSeparatedList ( a . GetSyntax ( ) ) ) )
881
931
. ToImmutableArray ( ) ;
882
932
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
+
883
954
// Construct the generated property as follows:
884
955
//
885
956
// /// <inheritdoc cref="<FIELD_NAME>"/>
@@ -889,10 +960,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
889
960
// public <FIELD_TYPE><NULLABLE_ANNOTATION?> <PROPERTY_NAME>
890
961
// {
891
962
// get => <FIELD_NAME>;
892
- // set
893
- // {
894
- // <BODY>
895
- // }
963
+ // <SET_ACCESSOR>
896
964
// }
897
965
return
898
966
PropertyDeclaration ( propertyType , Identifier ( propertyInfo . PropertyName ) )
@@ -910,8 +978,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
910
978
AccessorDeclaration ( SyntaxKind . GetAccessorDeclaration )
911
979
. WithExpressionBody ( ArrowExpressionClause ( IdentifierName ( propertyInfo . FieldName ) ) )
912
980
. WithSemicolonToken ( Token ( SyntaxKind . SemicolonToken ) ) ,
913
- AccessorDeclaration ( SyntaxKind . SetAccessorDeclaration )
914
- . WithBody ( Block ( setterIfStatement ) ) ) ;
981
+ setAccessor ) ;
915
982
}
916
983
917
984
/// <summary>
@@ -952,7 +1019,7 @@ public static ImmutableArray<MemberDeclarationSyntax> GetOnPropertyChangeMethods
952
1019
// happen when the property is first set to some value that is not null (but the backing field would still be so).
953
1020
// As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability
954
1021
// 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
956
1023
{
957
1024
true when ! propertyInfo . TypeNameWithNullabilityAnnotations . EndsWith ( "?" )
958
1025
=> IdentifierName ( $ "{ propertyInfo . TypeNameWithNullabilityAnnotations } ?") ,
0 commit comments