diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 2bf62d0de..8c3bca4da 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -20,6 +20,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// Whether the old property value is being directly referenced. /// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether to include nullability annotations on the setter. +/// Indicates whether to annotate the setter as requiring unreferenced code. /// The sequence of forwarded attributes for the generated property. internal sealed record PropertyInfo( string TypeNameWithNullabilityAnnotations, @@ -33,4 +34,5 @@ internal sealed record PropertyInfo( bool IsOldPropertyValueDirectlyReferenced, bool IsReferenceTypeOrUnconstraindTypeParameter, bool IncludeMemberNotNullOnSetAccessor, + bool IncludeRequiresUnreferencedCodeOnSetAccessor, EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 6aefd581a..d029295bd 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -297,6 +297,13 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + // We should generate [RequiresUnreferencedCode] on the setter if [NotifyDataErrorInfo] was used and the attribute is available + bool includeRequiresUnreferencedCodeOnSetAccessor = + notifyDataErrorInfo && + semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute"); + + token.ThrowIfCancellationRequested(); + // Prepare the effective property changing/changed names. For the property changing names, // there are two possible cases: if the mode is disabled, then there are no names to report // at all. If the mode is enabled, then the list is just the same as for property changed. @@ -321,6 +328,7 @@ public static bool TryGetInfo( isOldPropertyValueDirectlyReferenced, isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor, + includeRequiresUnreferencedCodeOnSetAccessor, forwardedAttributes.ToImmutable()); diagnostics = builder.ToImmutable(); @@ -1044,6 +1052,19 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.FieldName))))))); } + // Add the [RequiresUnreferencedCode] attribute if needed: + // + // [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + // + if (propertyInfo.IncludeRequiresUnreferencedCodeOnSetAccessor) + { + setAccessor = setAccessor.AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("The type of the current instance cannot be statically discovered."))))))); + } + // Construct the generated property as follows: // // /// diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index f98b7e917..c3e48fba6 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -2733,6 +2733,291 @@ internal static class __KnownINotifyPropertyChangedArgs VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result), ("__KnownINotifyPropertyChangingArgs.g.cs", null), ("__KnownINotifyPropertyChangedArgs.g.cs", changedArgs)); } + [TestMethod] + public void ObservableProperty_NotifyDataErrorInfo_EmitsTrimAnnotationsWhenNeeded() + { + string source = """ + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableValidator + { + [ObservableProperty] + [NotifyDataErrorInfo] + [Required] + private string? name; + } + """; + +#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", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.DataAnnotations.RequiredAttribute()] + public string? Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + 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; + ValidateProperty(value, "Name"); + 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", )] + 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", )] + 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", )] + 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", )] + partial void OnNameChanged(string? oldValue, string? newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.DataAnnotations.RequiredAttribute()] + 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; + ValidateProperty(value, "Name"); + 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", )] + 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", )] + 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", )] + 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", )] + partial void OnNameChanged(string? oldValue, string? newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_NotifyDataErrorInfo_EmitsTrimAnnotationsWhenNeeded_AppendToNullableAttribute() + { + string source = """ + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableValidator + { + [ObservableProperty] + [NotifyDataErrorInfo] + [Required] + private string name; + } + """; + +#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", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.DataAnnotations.RequiredAttribute()] + public string Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] + [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + 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; + ValidateProperty(value, "Name"); + 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", )] + 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", )] + 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", )] + 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", )] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.DataAnnotations.RequiredAttribute()] + 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; + ValidateProperty(value, "Name"); + 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", )] + 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", )] + 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", )] + 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", )] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + /// /// Generates the requested sources ///