diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index b0bf76a5a..f11f523ea 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -96,3 +96,4 @@ MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0052 MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053 MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054 +MVVMTK0055 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0055 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index f02990ae1..3c3b58a31 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -41,6 +41,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index c2c8feed6..1fee5b622 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -15,6 +15,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// The type name for the generated property, including nullability annotations. /// The field name. /// The generated property name. +/// The list of additional modifiers for the property (they are values). /// The accessibility of the property. /// The accessibility of the accessor. /// The accessibility of the accessor. @@ -23,7 +24,6 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// The sequence of commands to notify. /// Whether or not the generated property also broadcasts changes. /// Whether or not the generated property also validates its value. -/// Whether or not the generated property should be marked as required. /// 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. @@ -34,6 +34,7 @@ internal sealed record PropertyInfo( string TypeNameWithNullabilityAnnotations, string FieldName, string PropertyName, + EquatableArray PropertyModifers, Accessibility PropertyAccessibility, Accessibility GetterAccessibility, Accessibility SetterAccessibility, @@ -42,7 +43,6 @@ internal sealed record PropertyInfo( EquatableArray NotifiedCommandNames, bool NotifyPropertyChangedRecipients, bool NotifyDataErrorInfo, - bool IsRequired, bool IsOldPropertyValueDirectlyReferenced, bool IsReferenceTypeOrUnconstrainedTypeParameter, bool IncludeMemberNotNullOnSetAccessor, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index e127859d7..17a97d5bd 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; @@ -98,7 +99,7 @@ public static bool IsCandidateValidForCompilation(MemberDeclarationSyntax node, public static bool IsCandidateSymbolValid(ISymbol memberSymbol) { #if ROSLYN_4_12_0_OR_GREATER - // We only need additional checks for properties (Roslyn already validates things for fields in our scenarios) + // We only need these additional checks for properties (Roslyn already validates things for fields in our scenarios) if (memberSymbol is IPropertySymbol propertySymbol) { // Ensure that the property declaration is a partial definition with no implementation @@ -115,6 +116,14 @@ public static bool IsCandidateSymbolValid(ISymbol memberSymbol) } #endif + // Pointer types are never allowed in either case + if (memberSymbol is + IPropertySymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer } or + IFieldSymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer }) + { + return false; + } + // We assume all other cases are supported (other failure cases will be detected later) return true; } @@ -362,6 +371,9 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + // Get all additional modifiers for the member + ImmutableArray propertyModifiers = GetPropertyModifiers(memberSyntax); + // Retrieve the accessibility values for all components if (!TryGetAccessibilityModifiers( memberSyntax, @@ -378,16 +390,12 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); - // Check whether the property should be required - bool isRequired = GetIsRequiredProperty(memberSymbol); - - token.ThrowIfCancellationRequested(); - propertyInfo = new PropertyInfo( memberSyntax.Kind(), typeNameWithNullabilityAnnotations, fieldName, propertyName, + propertyModifiers.AsUnderlyingType(), propertyAccessibility, getterAccessibility, setterAccessibility, @@ -396,7 +404,6 @@ public static bool TryGetInfo( notifiedCommandNames.ToImmutable(), notifyRecipients, notifyDataErrorInfo, - isRequired, isOldPropertyValueDirectlyReferenced, isReferenceTypeOrUnconstrainedTypeParameter, includeMemberNotNullOnSetAccessor, @@ -970,6 +977,45 @@ private static void GatherLegacyForwardedAttributes( } } + /// + /// Gathers all allowed property modifiers that should be forwarded to the generated property. + /// + /// The instance to process. + /// The returned set of property modifiers, if any. + private static ImmutableArray GetPropertyModifiers(MemberDeclarationSyntax memberSyntax) + { + // Fields never need to carry additional modifiers along + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + return ImmutableArray.Empty; + } + + // We only allow a subset of all possible modifiers (aside from the accessibility modifiers) + ReadOnlySpan candidateKinds = + [ + SyntaxKind.NewKeyword, + SyntaxKind.VirtualKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.OverrideKeyword, +#if ROSLYN_4_3_1_OR_GREATER + SyntaxKind.RequiredKeyword +#endif + ]; + + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + + // Track all modifiers from the allowed set on the input property declaration + foreach (SyntaxKind kind in candidateKinds) + { + if (memberSyntax.Modifiers.Any(kind)) + { + builder.Add(kind); + } + } + + return builder.ToImmutable(); + } + /// /// Tries to get the accessibility of the property and accessors, if possible. /// If the target member is not a property, it will use the defaults. @@ -1043,20 +1089,6 @@ private static bool TryGetAccessibilityModifiers( return true; } - /// - /// Checks whether an input member is a required property. - /// - /// The input instance to process. - /// Whether is a required property. - private static bool GetIsRequiredProperty(ISymbol memberSymbol) - { -#if ROSLYN_4_3_1_OR_GREATER - return memberSymbol is IPropertySymbol { IsRequired: true }; -#else - return false; -#endif - } - /// /// Gets a instance with the cached args for property changing notifications. /// @@ -1395,13 +1427,11 @@ private static SyntaxTokenList GetPropertyModifiers(PropertyInfo propertyInfo) { SyntaxTokenList propertyModifiers = propertyInfo.PropertyAccessibility.ToSyntaxTokenList(); -#if ROSLYN_4_3_1_OR_GREATER - // Add the 'required' modifier if the original member also had it - if (propertyInfo.IsRequired) + // Add all gathered modifiers + foreach (SyntaxKind modifier in propertyInfo.PropertyModifers.AsImmutableArray().FromUnderlyingType()) { - propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.RequiredKeyword)); + propertyModifiers = propertyModifiers.Add(Token(modifier)); } -#endif // Add the 'partial' modifier if the original member is a partial property if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..5ece6f03e --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used with pointer types. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPointerTypeObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidObservablePropertyDeclarationReturnsPointerLikeType); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the [ObservableProperty] symbol + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure that we have a valid target symbol to analyze + if (context.Symbol is not (IFieldSymbol or IPropertySymbol)) + { + return; + } + + // If the property is not using [ObservableProperty], there's nothing to do + if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Emit a diagnostic if the type is a pointer type + if (context.Symbol is + IPropertySymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer } or + IFieldSymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer }) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyDeclarationReturnsPointerLikeType, + observablePropertyAttribute.GetLocation(), + context.Symbol.ContainingType, + context.Symbol.Name)); + } + }, SymbolKind.Field, SymbolKind.Property); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 36835788a..2de37f901 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -907,4 +907,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "A property using [ObservableProperty] returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0054"); + + /// + /// Gets a for when [ObservableProperty] is used on a property that returns a pointer type. + /// + /// Format: "The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)". + /// + /// + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsPointerLikeType = new DiagnosticDescriptor( + id: "MVVMTK0055", + title: "Using [ObservableProperty] on a property that returns pointer-like", + messageFormat: """The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [ObservableProperty] returns a pointer-like value ([ObservableProperty] must be used on properties of a non pointer-like type).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0055"); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs index a178dcc82..da890724e 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -12,6 +14,30 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; /// internal static class SyntaxKindExtensions { + /// + /// Converts an of values to one of their underlying type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray AsUnderlyingType(this ImmutableArray array) + { + ushort[]? underlyingArray = (ushort[]?)(object?)Unsafe.As, SyntaxKind[]?>(ref array); + + return Unsafe.As>(ref underlyingArray); + } + + /// + /// Converts an of values to one of their real type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray FromUnderlyingType(this ImmutableArray array) + { + SyntaxKind[]? typedArray = (SyntaxKind[]?)(object?)Unsafe.As, ushort[]?>(ref array); + + return Unsafe.As>(ref typedArray); + } + /// /// Converts a value to either "field" or "property" based on the kind. /// diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs index 9d7092888..978017735 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -21,7 +21,6 @@ partial class Test_SourceGeneratorsCodegen public void ObservablePropertyWithValueType_OnPartialProperty_WithNoModifiers_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -97,7 +96,6 @@ partial int Number public void ObservablePropertyWithValueType_OnPartialProperty_RequiredProperty_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -168,11 +166,245 @@ private set VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); } + // See https://github.com/CommunityToolkit/dotnet/issues/1013 + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_NewProperty_WorksCorrectly() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class BaseViewModel : ObservableObject + { + public int Number { get; private set; } + } + + partial class MyViewModel : BaseViewModel + { + [ObservableProperty] + public new partial int Number { get; private set; } + } + """; + + 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] + public new partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// 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 OnNumberChanging(int 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 OnNumberChanging(int oldValue, int 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 OnNumberChanged(int 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 OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_VirtualProperty_WorksCorrectly() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public virtual partial int Number { get; private set; } + } + """; + + 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] + public virtual partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// 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 OnNumberChanging(int 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 OnNumberChanging(int oldValue, int 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 OnNumberChanged(int 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 OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + [DataRow("override")] + [DataRow("sealed override")] + public void ObservablePropertyWithValueType_OnPartialProperty_OverrideProperty_WorksCorrectly(string modifiers) + { + string source = $$""" + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class BaseViewModel : ObservableObject + { + public virtual partial int Number { get; private set; } + } + + partial class MyViewModel : BaseViewModel + { + [ObservableProperty] + public {{modifiers}} partial int Number { get; private set; } + } + """; + + 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] + public {{modifiers}} partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// 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 OnNumberChanging(int 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 OnNumberChanging(int oldValue, int 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 OnNumberChanged(int 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 OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + [TestMethod] public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly1() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -247,7 +479,6 @@ private set public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly2() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -397,7 +628,6 @@ private protected set public void ObservablePropertyWithReferenceType_NotNullable_OnPartialProperty_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable @@ -474,7 +704,6 @@ public partial string Name public void ObservablePropertyWithReferenceType_Nullable_OnPartialProperty_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable @@ -551,7 +780,6 @@ public partial string? Name public void ObservableProperty_OnPartialProperty_AlsoNotifyPropertyChange_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -631,7 +859,6 @@ public partial string Name public void ObservableProperty_OnPartialProperty_AlsoNotifyCanExecuteChange_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -711,7 +938,6 @@ public partial string Name public void ObservableProperty_OnPartialProperty_AlsoNotifyRecipients_WorksCorrectly() { string source = """ - using System; using System.Windows.Input; using CommunityToolkit.Mvvm.ComponentModel; @@ -790,7 +1016,6 @@ public partial string Name public void ObservableProperty_OnPartialProperty_AlsoNotifyDataErrorInfo_WorksCorrectly() { string source = """ - using System; using System.ComponentModel.DataAnnotations; using System.Windows.Input; using CommunityToolkit.Mvvm.ComponentModel; diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index a92b3d589..91c35db10 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -998,4 +998,61 @@ public partial class SampleViewModel : ObservableObject await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); } + + [TestMethod] + public async Task InvalidPointerTypeObservablePropertyAttributeAnalyzer_ReturnsValidType_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string {|CS9248:Name|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPointerTypeObservablePropertyAttributeAnalyzer_ReturnsPointerType_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public unsafe partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0055:ObservableProperty|}] + public partial int* {|CS9248:Name|} { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPointerTypeObservablePropertyAttributeAnalyzer_ReturnsFunctionPointerType_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public unsafe partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0055:ObservableProperty|}] + public partial delegate* unmanaged[Stdcall] {|CS9248:Name|} { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } }