From c11bb0eaca3af78656b2ac64356fc229fbcfb7d5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 14 Mar 2023 13:53:32 +0100 Subject: [PATCH 1/3] Add [MemberNotNull] to [ObservableProperty] set accessor --- .../ComponentModel/Models/PropertyInfo.cs | 2 + .../ObservablePropertyGenerator.Execute.cs | 61 +++++++++++++++++-- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 59e5b33fe..2aedc18d2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -19,6 +19,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// Whether or not the generated property also validates its value. /// Whether the old property value is being directly referenced. /// Indicates whether the property is of a reference type. +/// Indicates whether to include nullability annotations on the setter. /// The sequence of forwarded attributes for the generated property. internal sealed record PropertyInfo( string TypeNameWithNullabilityAnnotations, @@ -31,4 +32,5 @@ internal sealed record PropertyInfo( bool NotifyDataErrorInfo, bool IsOldPropertyValueDirectlyReferenced, bool IsReferenceType, + bool IncludeMemberNotNullOnSetAccessor, EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index c6d35b1d8..758af4f52 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -113,6 +113,7 @@ public static bool TryGetInfo( bool hasAnyValidationAttributes = false; bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName); bool isReferenceType = fieldSymbol.Type.IsReferenceType; + bool includeMemberNotNullOnSetAccessor = GetIncludeMemberNotNullOnSetAccessor(fieldSymbol, semanticModel); // Track the property changing event for the property, if the type supports it if (shouldInvokeOnPropertyChanging) @@ -262,6 +263,7 @@ public static bool TryGetInfo( notifyDataErrorInfo, isOldPropertyValueDirectlyReferenced, isReferenceType, + includeMemberNotNullOnSetAccessor, forwardedAttributes.ToImmutable()); diagnostics = builder.ToImmutable(); @@ -668,6 +670,36 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo return false; } + /// + /// Checks whether should be used on the setter. + /// + /// The input instance to process. + /// The instance for the current run. + /// Whether should be used on the setter. + private static bool GetIncludeMemberNotNullOnSetAccessor(IFieldSymbol fieldSymbol, SemanticModel semanticModel) + { + // This is used to avoid nullability warnings when setting the property from a constructor, in case the field + // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. + // Consider this example: + // + // partial class MyViewModel : ObservableObject + // { + // public MyViewModel() + // { + // Name = "Bob"; + // } + // + // [ObservableProperty] + // private string name; + // } + // + // The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name + // is set, the compiler can determine that the name backing field is also being set (to a non null value). + return + fieldSymbol.Type is { NullableAnnotation: not NullableAnnotation.Annotated, IsReferenceType: true } && + semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); + } + /// /// Gets a instance with the cached args for property changing notifications. /// @@ -880,6 +912,27 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) .ToImmutableArray(); + // Prepare the setter for the generated property: + // + // set + // { + // + // } + AccessorDeclarationSyntax setAccessor = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithBody(Block(setterIfStatement)); + + // Add the [MemberNotNull] attribute if needed: + // + // [MemberNotNull("")] + // + if (propertyInfo.IncludeMemberNotNullOnSetAccessor) + { + setAccessor = setAccessor.AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.MemberNotNull")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.FieldName))))))); + } + // Construct the generated property as follows: // // /// @@ -889,10 +942,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // public // { // get => ; - // set - // { - // - // } + // // } return PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName)) @@ -910,8 +960,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithExpressionBody(ArrowExpressionClause(IdentifierName(propertyInfo.FieldName))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), - AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) - .WithBody(Block(setterIfStatement))); + setAccessor); } /// From d2fe8a0eb04624a69d44863ed8dbed380066b6ec Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 14 Mar 2023 14:07:59 +0100 Subject: [PATCH 2/3] Add unit tests for [MemberNotNull] generation --- .../Test_SourceGeneratorsCodegen.cs | 210 +++++++++++++++++- .../Test_ObservablePropertyAttribute.cs | 29 +++ 2 files changed, 238 insertions(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index e82b77ff4..63fb19186 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -22,7 +22,6 @@ public class Test_SourceGeneratorsCodegen public void ObservablePropertyWithPartialMethodWithPreviousValuesNotUsed_DoesNotGenerateFieldReadAndMarksOldValueAsNullable() { string source = """ - using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable @@ -36,6 +35,7 @@ partial class MyViewModel : ObservableObject } """; +#if NET6_0_OR_GREATER string result = """ // #pragma warning disable @@ -50,6 +50,7 @@ partial class MyViewModel public string Name { get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] set { if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) @@ -90,6 +91,213 @@ public string Name } } """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public string Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnNameChanging(string? oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithNullableReferenceType_DoesNotEmitMemberNotNullAttribute() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + private string? name; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public string? Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnNameChanging(string? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnNameChanging(string? oldValue, string? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnNameChanged(string? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnNameChanged(string? oldValue, string? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithNonNullableValueType_DoesNotEmitMemberNotNullAttribute() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + private Guid id; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public global::System.Guid Id + { + get => id; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(id, value)) + { + OnIdChanging(value); + OnIdChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Id); + id = value; + OnIdChanged(value); + OnIdChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Id); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnIdChanging(global::System.Guid value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnIdChanging(global::System.Guid oldValue, global::System.Guid newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnIdChanged(global::System.Guid value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnIdChanged(global::System.Guid oldValue, global::System.Guid newValue); + } + } + """; VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index e485793df..c6404a87f 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs @@ -6,6 +6,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif using System.Linq; using System.Reflection; #if NET6_0_OR_GREATER @@ -1037,6 +1040,17 @@ public void Test_ObservableProperty_ModelWithObservablePropertyWithUnderscoreAnd Assert.IsTrue(model.IsReadOnly); } +#if NET6_0_OR_GREATER + [TestMethod] + public void Test_ObservableProperty_MemberNotNullAttributeIsPresent() + { + MemberNotNullAttribute? attribute = typeof(ModelWithNonNullableObservableProperty).GetProperty(nameof(ModelWithNonNullableObservableProperty.Name))!.SetMethod!.GetCustomAttribute(); + + Assert.IsNotNull(attribute); + CollectionAssert.AreEqual(new[] { nameof(ModelWithNonNullableObservableProperty.name) }, attribute.Members); + } +#endif + public abstract partial class BaseViewModel : ObservableObject { public string? Content { get; set; } @@ -1664,4 +1678,19 @@ private partial class ModelWithObservablePropertyWithUnderscoreAndUppercase : Ob [ObservableProperty] private bool _IsReadOnly; } + +#if NET6_0_OR_GREATER + // See https://github.com/CommunityToolkit/dotnet/issues/645 + // This viewmodel is here only to double check no warnings are emitted when the attribute is present + public partial class ModelWithNonNullableObservableProperty : ObservableObject + { + public ModelWithNonNullableObservableProperty() + { + Name = "Bob"; + } + + [ObservableProperty] + internal string name; + } +#endif } From 673801af53b58dfe5da346cfc3b4439637e32aa1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 14 Mar 2023 16:01:56 +0100 Subject: [PATCH 3/3] Update generator for unconstrained generic nullability --- .../ComponentModel/Models/PropertyInfo.cs | 4 +- .../ObservablePropertyGenerator.Execute.cs | 36 +- .../Test_SourceGeneratorsCodegen.cs | 833 +++++++++++++++++- 3 files changed, 850 insertions(+), 23 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 2aedc18d2..2bf62d0de 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -18,7 +18,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// Whether or not the generated property also broadcasts changes. /// Whether or not the generated property also validates its value. /// Whether the old property value is being directly referenced. -/// Indicates whether the property is of a reference type. +/// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether to include nullability annotations on the setter. /// The sequence of forwarded attributes for the generated property. internal sealed record PropertyInfo( @@ -31,6 +31,6 @@ internal sealed record PropertyInfo( bool NotifyPropertyChangedRecipients, bool NotifyDataErrorInfo, bool IsOldPropertyValueDirectlyReferenced, - bool IsReferenceType, + bool IsReferenceTypeOrUnconstraindTypeParameter, bool IncludeMemberNotNullOnSetAccessor, EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 758af4f52..5def3ccb8 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -112,8 +112,13 @@ public static bool TryGetInfo( bool hasOrInheritsClassLevelNotifyDataErrorInfo = false; bool hasAnyValidationAttributes = false; bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName); - bool isReferenceType = fieldSymbol.Type.IsReferenceType; - bool includeMemberNotNullOnSetAccessor = GetIncludeMemberNotNullOnSetAccessor(fieldSymbol, semanticModel); + + // Get the nullability info for the property + GetNullabilityInfo( + fieldSymbol, + semanticModel, + out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool includeMemberNotNullOnSetAccessor); // Track the property changing event for the property, if the type supports it if (shouldInvokeOnPropertyChanging) @@ -262,7 +267,7 @@ public static bool TryGetInfo( notifyRecipients, notifyDataErrorInfo, isOldPropertyValueDirectlyReferenced, - isReferenceType, + isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor, forwardedAttributes.ToImmutable()); @@ -671,13 +676,24 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo } /// - /// Checks whether should be used on the setter. + /// Gets the nullability info on the generated property /// /// The input instance to process. /// The instance for the current run. - /// Whether should be used on the setter. - private static bool GetIncludeMemberNotNullOnSetAccessor(IFieldSymbol fieldSymbol, SemanticModel semanticModel) + /// Whether the property type supports nullability. + /// Whether should be used on the setter. + /// + private static void GetNullabilityInfo( + IFieldSymbol fieldSymbol, + SemanticModel semanticModel, + out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool includeMemberNotNullOnSetAccessor) { + // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. + // This will cover both reference types as well T when the constraints are not struct or unmanaged. + // If this is true, it means the field storage can potentially be in a null state (even if not annotated). + isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType; + // This is used to avoid nullability warnings when setting the property from a constructor, in case the field // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. // Consider this example: @@ -695,8 +711,10 @@ private static bool GetIncludeMemberNotNullOnSetAccessor(IFieldSymbol fieldSymbo // // The [MemberNotNull] attribute is needed on the setter for the generated Name property so that when Name // is set, the compiler can determine that the name backing field is also being set (to a non null value). - return - fieldSymbol.Type is { NullableAnnotation: not NullableAnnotation.Annotated, IsReferenceType: true } && + // Of course, this can only be the case if the field type is also of a type that could be in a null state. + includeMemberNotNullOnSetAccessor = + isReferenceTypeOrUnconstraindTypeParameter && + fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } @@ -1001,7 +1019,7 @@ public static ImmutableArray GetOnPropertyChangeMethods // happen when the property is first set to some value that is not null (but the backing field would still be so). // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. - TypeSyntax oldValueTypeSyntax = propertyInfo.IsReferenceType switch + TypeSyntax oldValueTypeSyntax = propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch { true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"), diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index 63fb19186..aa8aa0d17 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -19,7 +19,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; public class Test_SourceGeneratorsCodegen { [TestMethod] - public void ObservablePropertyWithPartialMethodWithPreviousValuesNotUsed_DoesNotGenerateFieldReadAndMarksOldValueAsNullable() + public void ObservablePropertyWithNonNullableReferenceType_EmitsMemberNotNullAttribute() { string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -31,7 +31,7 @@ namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] - private string name = null!; + private string name; } """; @@ -151,6 +151,566 @@ public string Name VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } + [TestMethod] + public void ObservablePropertyWithNonNullableValueType_DoesNotEmitMemberNotNullAttribute() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + private Guid id; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public global::System.Guid Id + { + get => id; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(id, value)) + { + OnIdChanging(value); + OnIdChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Id); + id = value; + OnIdChanged(value); + OnIdChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Id); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnIdChanging(global::System.Guid value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnIdChanging(global::System.Guid oldValue, global::System.Guid newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnIdChanged(global::System.Guid value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnIdChanged(global::System.Guid oldValue, global::System.Guid newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithNonNullableUnconstrainedGenericType_EmitsMemberNotNullAttribute() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + private T content; + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Content + { + get => content; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("content")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? oldValue, T newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Content + { + get => content; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? oldValue, T newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + + // Also test with C# 8 to double check that using a nullable unconstrained type parameter doesn't cause issues, but just a (suppressed) warning + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.CSharp8, ("MyApp.MyViewModel`1.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithNonNullableConstrainedNotNullGenericType_EmitsMemberNotNullAttribute() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + where T : notnull + { + [ObservableProperty] + private T content; + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Content + { + get => content; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("content")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? oldValue, T newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Content + { + get => content; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? oldValue, T newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithNonNullableConstrainedReferenceTypeGenericType_EmitsMemberNotNullAttribute() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + where T : class + { + [ObservableProperty] + private T content; + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Content + { + get => content; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("content")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? oldValue, T newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Content + { + get => content; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? oldValue, T newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithNonNullableConstrainedValueTypeGenericType_DoesNotEmitMemberNotNullAttribute() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + where T : struct + { + [ObservableProperty] + private T content; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Content + { + get => content; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T oldValue, T newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + [TestMethod] public void ObservablePropertyWithNullableReferenceType_DoesNotEmitMemberNotNullAttribute() { @@ -227,7 +787,7 @@ public string? Name } [TestMethod] - public void ObservablePropertyWithNonNullableValueType_DoesNotEmitMemberNotNullAttribute() + public void ObservablePropertyWithNullableValueType_DoesNotEmitMemberNotNullAttribute() { string source = """ using System; @@ -240,7 +800,7 @@ namespace MyApp; partial class MyViewModel : ObservableObject { [ObservableProperty] - private Guid id; + private Guid? id; } """; @@ -255,12 +815,12 @@ partial class MyViewModel /// [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - public global::System.Guid Id + public global::System.Guid? Id { get => id; set { - if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(id, value)) + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(id, value)) { OnIdChanging(value); OnIdChanging(default, value); @@ -277,24 +837,24 @@ partial class MyViewModel /// The new property value being set. /// This method is invoked right before the value of is changed. [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] - partial void OnIdChanging(global::System.Guid value); + partial void OnIdChanging(global::System.Guid? value); /// Executes the logic for when is changing. /// The previous property value that is being replaced. /// The new property value being set. /// This method is invoked right before the value of is changed. [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] - partial void OnIdChanging(global::System.Guid oldValue, global::System.Guid newValue); + partial void OnIdChanging(global::System.Guid? oldValue, global::System.Guid? newValue); /// Executes the logic for when just changed. /// The new property value that was set. /// This method is invoked right after the value of is changed. [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] - partial void OnIdChanged(global::System.Guid value); + partial void OnIdChanged(global::System.Guid? value); /// Executes the logic for when just changed. /// The previous property value that was replaced. /// The new property value that was set. /// This method is invoked right after the value of is changed. [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] - partial void OnIdChanged(global::System.Guid oldValue, global::System.Guid newValue); + partial void OnIdChanged(global::System.Guid? oldValue, global::System.Guid? newValue); } } """; @@ -302,6 +862,236 @@ partial class MyViewModel VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } + [TestMethod] + public void ObservablePropertyWithNullableUnconstrainedGenericType_DoesNotEmitMemberNotNullAttribute() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + private T? content; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T? Content + { + get => content; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? oldValue, T? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? oldValue, T? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithNullableConstrainedReferenceTypeGenericType_DoesNotEmitMemberNotNullAttribute() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + where T : class + { + [ObservableProperty] + private T? content; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T? Content + { + get => content; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? oldValue, T? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? oldValue, T? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithNullableConstrainedValueTypeGenericType_DoesNotEmitMemberNotNullAttribute() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + where T : struct + { + [ObservableProperty] + private T? content; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T? Content + { + get => content; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(content, value)) + { + OnContentChanging(value); + OnContentChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Content); + content = value; + OnContentChanged(value); + OnContentChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Content); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanging(T? oldValue, T? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.2.0.0")] + partial void OnContentChanged(T? oldValue, T? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + // See https://github.com/CommunityToolkit/dotnet/issues/601 [TestMethod] public void ObservablePropertyWithForwardedAttributesWithNumberLiterals_PreservesType() @@ -732,6 +1522,18 @@ public string? A /// The generators to apply to the input syntax tree. /// The source files to compare. private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, params (string Filename, string Text)[] results) + { + VerifyGenerateSources(source, generators, LanguageVersion.CSharp10, results); + } + + /// + /// Generates the requested sources + /// + /// The input source to process. + /// The generators to apply to the input syntax tree. + /// The language version to use. + /// The source files to compare. + private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, LanguageVersion languageVersion, params (string Filename, string Text)[] results) { // Ensure CommunityToolkit.Mvvm and System.ComponentModel.DataAnnotations are loaded Type observableObjectType = typeof(ObservableObject); @@ -744,7 +1546,7 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() let reference = MetadataReference.CreateFromFile(assembly.Location) select reference; - SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10)); + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion)); // Create a syntax tree with the input source CSharpCompilation compilation = CSharpCompilation.Create( @@ -763,7 +1565,14 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() foreach ((string filename, string text) in results) { - SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filename); + string filePath = filename; + +#if !ROSLYN_4_3_1_OR_GREATER + // Adjust the filenames for the legacy Roslyn 4.0 + filePath = filePath.Replace('`', '_'); +#endif + + SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filePath); Assert.AreEqual(text, generatedTree.ToString()); }