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);
+ }
}