From e094757355af2fca4ccc8e79f1327fb2fa69f309 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 12:13:10 -0800 Subject: [PATCH 01/14] Skip generation for more invalid cases --- .../ObservablePropertyGenerator.Execute.cs | 41 ++++++++++++++++--- .../ObservablePropertyGenerator.cs | 8 ++++ ...evelObservablePropertyAttributeAnalyzer.cs | 8 ++++ 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 3505d05f7..e127859d7 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -71,7 +71,7 @@ static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? contain /// The instance to process. /// The instance for the current run. /// Whether is valid. - public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel semanticModel) + public static bool IsCandidateValidForCompilation(MemberDeclarationSyntax node, SemanticModel semanticModel) { // At least C# 8 is always required if (!semanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) @@ -90,6 +90,35 @@ public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel return true; } + /// + /// Performs additional checks before running the core generation logic. + /// + /// The input instance to process. + /// Whether is valid. + 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) + if (memberSymbol is IPropertySymbol propertySymbol) + { + // Ensure that the property declaration is a partial definition with no implementation + if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null }) + { + return false; + } + + // Also ignore all properties that have an invalid declaration + if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly || propertySymbol.Type.IsRefLikeType) + { + return false; + } + } +#endif + + // We assume all other cases are supported (other failure cases will be detected later) + return true; + } + /// /// Gets the candidate after the initial filtering. /// @@ -140,13 +169,11 @@ public static bool TryGetInfo( return false; } - using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); - // Validate the target type if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging)) { propertyInfo = null; - diagnostics = builder.ToImmutable(); + diagnostics = ImmutableArray.Empty; return false; } @@ -168,7 +195,7 @@ public static bool TryGetInfo( if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) { propertyInfo = null; - diagnostics = builder.ToImmutable(); + diagnostics = ImmutableArray.Empty; // If the generated property would collide, skip generating it entirely. This makes sure that // users only get the helpful diagnostic about the collision, and not the normal compiler error @@ -182,7 +209,7 @@ public static bool TryGetInfo( if (IsGeneratedPropertyInvalid(propertyName, GetPropertyType(memberSymbol))) { propertyInfo = null; - diagnostics = builder.ToImmutable(); + diagnostics = ImmutableArray.Empty; return false; } @@ -232,6 +259,8 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + // Gather attributes info foreach (AttributeData attributeData in memberSymbol.GetAttributes()) { diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 9957f3bbe..a9f70bb0b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -38,6 +38,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return default; } + // Validate the symbol as well before doing any work + if (!Execute.IsCandidateSymbolValid(context.TargetSymbol)) + { + return default; + } + + token.ThrowIfCancellationRequested(); + // Get the hierarchy info for the target symbol, and try to gather the property info HierarchyInfo hierarchy = HierarchyInfo.From(context.TargetSymbol.ContainingType); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs index 0b65550d0..3e13be64e 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -91,6 +91,14 @@ internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarati return false; } + // Static properties are not supported + if (property.Modifiers.Any(SyntaxKind.StaticKeyword)) + { + containingTypeNode = null; + + return false; + } + // The accessors must be a get and a set (with any accessibility) if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) From 1b51b6c8f8a7217a27d2febaded35542d1d013c0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 12:13:15 -0800 Subject: [PATCH 02/14] Add 'TryGetConstructorArgument' extension --- .../Extensions/AttributeDataExtensions.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs index 14f7498af..096e6456e 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -6,6 +6,7 @@ // more info in ThirdPartyNotices.txt in the root of the project. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -53,6 +54,29 @@ properties.Value.Value is T argumentValue && return null; } + /// + /// Tries to get a constructor argument at a given index from the input instance. + /// + /// The type of constructor argument to retrieve. + /// The target instance to get the argument from. + /// The index of the argument to try to retrieve. + /// The resulting argument, if it was found. + /// Whether or not an argument of type at position was found. + public static bool TryGetConstructorArgument(this AttributeData attributeData, int index, [NotNullWhen(true)] out T? result) + { + if (attributeData.ConstructorArguments.Length > index && + attributeData.ConstructorArguments[index].Value is T argument) + { + result = argument; + + return true; + } + + result = default; + + return false; + } + /// /// Gets a given named argument value from an instance, or a fallback value. /// From da6527e481051580b7c22dc4fff346054bd759ac Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 12:13:31 -0800 Subject: [PATCH 03/14] Add 'InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer' --- .../AnalyzerReleases.Shipped.md | 5 +- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...evelObservablePropertyAttributeAnalyzer.cs | 98 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 54 +++++++++- 4 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 730713ab0..b0bf76a5a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -92,4 +92,7 @@ MVVMTK0047 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator MVVMTK0048 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0048 MVVMTK0049 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0049 MVVMTK0050 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0050 -MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0050 +MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0051 +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 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index cc4351664..f02990ae1 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/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..c6b943f9c --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,98 @@ +// 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. + +#if ROSLYN_4_12_0_OR_GREATER + +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 on an invalid partial property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition, + InvalidObservablePropertyDeclarationReturnsByRef, + InvalidObservablePropertyDeclarationReturnsRefLikeType); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the [ObservableProperty] and [GeneratedCode] symbols + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute") is not { } generatedCodeAttributeSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using [ObservableProperty], there's nothing to do + if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Emit an error if the property is not a partial definition with no implementation... + if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null }) + { + // ...But only if it wasn't actually generated by the [ObservableProperty] generator. + bool isImplementationAllowed = + propertySymbol is { IsPartialDefinition: true, PartialImplementationPart: IPropertySymbol implementationPartSymbol } && + implementationPartSymbol.TryGetAttributeWithType(generatedCodeAttributeSymbol, out AttributeData? generatedCodeAttributeData) && + generatedCodeAttributeData.TryGetConstructorArgument(0, out string? toolName) && + toolName == typeof(ObservablePropertyGenerator).FullName; + + // Emit the diagnostic only for cases that were not valid generator outputs + if (!isImplementationAllowed) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition, + observablePropertyAttribute.GetLocation(), + context.Symbol)); + } + } + + // Emit an error if the property returns a value by ref + if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyDeclarationReturnsByRef, + observablePropertyAttribute.GetLocation(), + context.Symbol)); + } + + // Emit an error if the property type is a ref struct + if (propertySymbol.Type.IsRefLikeType) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyDeclarationReturnsRefLikeType, + observablePropertyAttribute.GetLocation(), + context.Symbol)); + } + }, SymbolKind.Property); + }); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index adbb28771..2ed0f7c44 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -718,17 +718,17 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when [ObservableProperty] is applied to a property with an invalid declaration. /// - /// Format: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be a partial property with a getter and a setter that is not init-only)". + /// Format: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be an instance (non static) partial property with a getter and a setter that is not init-only)". /// /// public static readonly DiagnosticDescriptor InvalidPropertyDeclarationForObservableProperty = new DiagnosticDescriptor( id: "MVVMTK0043", title: "Invalid property declaration for [ObservableProperty]", - messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be a partial property with a getter and a setter that is not init-only)", + messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be an instance (non static) partial property with a getter and a setter that is not init-only)", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Properties annotated with [ObservableProperty] must be partial properties with a getter and a setter that is not init-only.", + description: "Properties annotated with [ObservableProperty] must be instance (non static) partial properties with a getter and a setter that is not init-only.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0043"); /// @@ -859,4 +859,52 @@ internal static class DiagnosticDescriptors description: "This project producing one or more 'MVVMTK0045' warnings due to [ObservableProperty] being used on fields, which is not AOT compatible in WinRT scenarios, should set 'LangVersion' to 'preview' to enable partial properties and the associated code fixer (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0051", customTags: WellKnownDiagnosticTags.CompilationEnd); + + /// + /// Gets a for when [ObservableProperty] is used on a property that is not an incomplete partial definition. + /// + /// Format: "The property {0}.{1} is not an incomplete partial definition ([ObservableProperty] must be used on partial property definitions with no implementation part)". + /// + /// + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition = new( + id: "MVVMTK0052", + title: "Using [ObservableProperty] on an invalid property declaration (not incomplete partial definition)", + messageFormat: """The property {0}.{1} is not an incomplete partial definition ([ObservableProperty] must be used on partial property definitions with no implementation part)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [ObservableProperty] is not a partial implementation part ([ObservableProperty] must be used on partial property definitions with no implementation part).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0052"); + + /// + /// Gets a for when [ObservableProperty] is used on a property that returns a ref value. + /// + /// Format: "The property {0}.{1} returns a ref value ([ObservableProperty] must be used on properties returning a type by value)". + /// + /// + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsByRef = new( + id: "MVVMTK0053", + title: "Using [ObservableProperty] on a property that returns byref", + messageFormat: """The property {0}.{1} returns a ref value ([ObservableProperty] must be used on properties returning a type by value)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [ObservableProperty] returns a value by reference ([ObservableProperty] must be used on properties returning a type by value).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0053"); + + /// + /// Gets a for when [ObservableProperty] is used on a property that returns a byref-like value. + /// + /// Format: "The property {0}.{1} returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type)". + /// + /// + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsRefLikeType = new( + id: "MVVMTK0054", + title: "Using [ObservableProperty] on a property that returns byref-like", + messageFormat: """The property {0}.{1} returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + 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"); } From 59a9f4a6a45debac04be5fda66151fd9ca0b71e1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 12:48:46 -0800 Subject: [PATCH 04/14] Add more unit tests --- .../Test_SourceGeneratorsDiagnostics.cs | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 186fe98c8..3e9d2a650 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -254,6 +254,25 @@ public partial class SampleViewModel : ObservableObject } """; + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnStaticProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public static partial string {|CS9248:Name|} { get; set; } + } + } + """; + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); } @@ -798,4 +817,185 @@ await CSharpAnalyzerWithLanguageVersionTest(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnUnannotatedPartialPropertyWithImplementation_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + public partial string Name { get; set; } + + public partial string Name + { + get => field; + set { } + } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnImplementedProperty_GeneratedByMvvmToolkitGenerator_DoesNotWarn() + { + const string source = """ + using System.CodeDom.Compiler; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + + [GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "1.0.0")] + public partial string Name + { + get => field; + set { } + } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnImplementedProperty_GeneratedByAnotherGenerator_Warns() + { + const string source = """ + using System.CodeDom.Compiler; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0052:ObservableProperty|}] + public partial string Name { get; set; } + + [GeneratedCode("Some.Other.Generator", "1.0.0")] + public partial string Name + { + get => field; + set { } + } + } + } + """; + + // This test is having issues, let's invoke the analyzer directly to make it easier to narrow down the problem + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnImplementedProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0052:ObservableProperty|}] + public partial string Name { get; set; } + + public partial string Name + { + get => field; + set { } + } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_ReturnsByRef_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0053:ObservableProperty|}] + public partial ref int {|CS9248:Name|} { get; {|CS8147:set|}; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS8147", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_ReturnsByRefReadOnly_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0053:ObservableProperty|}] + public partial ref readonly int {|CS9248:Name|} { get; {|CS8147:set|}; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS8147", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_ReturnsByRefLike_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0054:ObservableProperty|}] + public partial Span {|CS9248:Name|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } } From 3d2a6a2c1d160544216f645b53acf67c345cd3a7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 12:48:54 -0800 Subject: [PATCH 05/14] Minor formatting fixes to analyzers --- .../ComponentModel/ObservablePropertyGenerator.cs | 1 - ...alPropertyLevelObservablePropertyAttributeAnalyzer.cs | 9 ++++++--- ...idPropertyLevelObservablePropertyAttributeAnalyzer.cs | 2 +- ...UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index a9f70bb0b..05bee8c83 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -9,7 +9,6 @@ using CommunityToolkit.Mvvm.SourceGenerators.Helpers; using CommunityToolkit.Mvvm.SourceGenerators.Models; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CommunityToolkit.Mvvm.SourceGenerators; diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs index c6b943f9c..cb6194419 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -69,7 +69,8 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create( InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition, observablePropertyAttribute.GetLocation(), - context.Symbol)); + propertySymbol.ContainingType, + propertySymbol.Name)); } } @@ -79,7 +80,8 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create( InvalidObservablePropertyDeclarationReturnsByRef, observablePropertyAttribute.GetLocation(), - context.Symbol)); + propertySymbol.ContainingType, + propertySymbol.Name)); } // Emit an error if the property type is a ref struct @@ -88,7 +90,8 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create( InvalidObservablePropertyDeclarationReturnsRefLikeType, observablePropertyAttribute.GetLocation(), - context.Symbol)); + propertySymbol.ContainingType, + propertySymbol.Name)); } }, SymbolKind.Property); }); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs index 3e13be64e..d0f28170f 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -60,7 +60,7 @@ public override void Initialize(AnalysisContext context) InvalidPropertyDeclarationForObservableProperty, observablePropertyAttribute.GetLocation(), propertySymbol.ContainingType, - propertySymbol)); + propertySymbol.Name)); } } }, SymbolKind.Property); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs index 29255adb5..2760ab023 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs @@ -51,7 +51,7 @@ public override void Initialize(AnalysisContext context) UnsupportedRoslynVersionForObservablePartialPropertySupport, propertySymbol.Locations.FirstOrDefault(), propertySymbol.ContainingType, - propertySymbol)); + propertySymbol.Name)); } }, SymbolKind.Property); }); From 3717a6fbb9a899e3ad072c09b147a69ec159c387 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 13:48:58 -0800 Subject: [PATCH 06/14] Add MVVM Toolkit test project for Roslyn 4.12 --- dotnet Community Toolkit.sln | 24 ++++++++++++++ ...tyToolkit.Mvvm.Roslyn4120.UnitTests.csproj | 31 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index dc557355a..7d7065f8b 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -91,6 +91,8 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.CodeF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.CodeFixers.Roslyn4120", "src\CommunityToolkit.Mvvm.CodeFixers.Roslyn4120\CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj", "{98572004-D29A-486E-8053-6D409557CE44}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.Roslyn4120.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4120.UnitTests\CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj", "{87BF1537-935A-414D-8318-458F61A6E562}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -525,6 +527,26 @@ Global {98572004-D29A-486E-8053-6D409557CE44}.Release|x64.Build.0 = Release|Any CPU {98572004-D29A-486E-8053-6D409557CE44}.Release|x86.ActiveCfg = Release|Any CPU {98572004-D29A-486E-8053-6D409557CE44}.Release|x86.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM64.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x64.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x64.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x86.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x86.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|Any CPU.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM64.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM64.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|x64.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|x64.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|x86.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -548,6 +570,7 @@ Global {ECFE93AA-4B98-4292-B3FA-9430D513B4F9} = {B30036C4-D514-4E5B-A323-587A061772CE} {4FCD501C-1BB5-465C-AD19-356DAB6600C6} = {B30036C4-D514-4E5B-A323-587A061772CE} {C342302D-A263-42D6-B8EE-01DEF8192690} = {B30036C4-D514-4E5B-A323-587A061772CE} + {87BF1537-935A-414D-8318-458F61A6E562} = {B30036C4-D514-4E5B-A323-587A061772CE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5403B0C4-F244-4F73-A35C-FE664D0F4345} @@ -556,6 +579,7 @@ Global tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{4fcd501c-1bb5-465c-ad19-356dab6600c6}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{5b44f7f1-dca2-4776-924e-a266f7bbf753}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{5e7f1212-a54b-40ca-98c5-1ff5cd1a1638}*SharedItemsImports = 13 + tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{87bf1537-935a-414d-8318-458f61a6e562}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{98572004-d29a-486e-8053-6d409557ce44}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{a2ebda90-b720-430d-83f5-c6bcc355232c}*SharedItemsImports = 13 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{ad9c3223-8e37-4fd4-a0d4-a45119551d3a}*SharedItemsImports = 5 diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj new file mode 100644 index 000000000..cfdec7b22 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj @@ -0,0 +1,31 @@ + + + + net472;net7.0;net8.0 + preview + true + $(DefineConstants);ROSLYN_4_12_0_OR_GREATER + + + $(NoWarn);MVVMTK0042 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 29ae95d56b2e31fd6028905472344e5e8943e72d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 13:49:22 -0800 Subject: [PATCH 07/14] Port '[ObservableProperty]' tests for partial properties --- ...ablePropertyAttribute_PartialProperties.cs | 1737 +++++++++++++++++ .../Test_INotifyPropertyChangedAttribute.cs | 31 + 2 files changed, 1768 insertions(+) create mode 100644 tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs new file mode 100644 index 000000000..ddeb5ebbf --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs @@ -0,0 +1,1737 @@ +// 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; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +#if NET6_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ExternalAssembly; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.Mvvm.Messaging.Messages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#pragma warning disable MVVMTK0032, MVVMTK0033, MVVMTK0034 + +namespace CommunityToolkit.Mvvm.UnitTests; + +// Note: this class is a copy of 'Test_ObservablePropertyAttribute', but using partial properties. +// The two implementations should be kept in sync for all tests, for parity, whenever possible. + +[TestClass] +public partial class Test_ObservablePropertyAttribute_PartialProperties +{ + [TestMethod] + public void Test_ObservablePropertyAttribute_Events() + { + SampleModel model = new(); + + (PropertyChangingEventArgs, int) changing = default; + (PropertyChangedEventArgs, int) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Data); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Data); + }; + + model.Data = 42; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changing.Item2, 0); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changed.Item2, 42); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4225 + [TestMethod] + public void Test_ObservablePropertyAttributeWithinRegion_Events() + { + SampleModel model = new(); + + (PropertyChangingEventArgs, int) changing = default; + (PropertyChangedEventArgs, int) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Counter); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Counter); + }; + + model.Counter = 42; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Counter)); + Assert.AreEqual(changing.Item2, 0); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Counter)); + Assert.AreEqual(changed.Item2, 42); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4225 + [TestMethod] + public void Test_ObservablePropertyAttributeRightBelowRegion_Events() + { + SampleModel model = new(); + + (PropertyChangingEventArgs, string?) changing = default; + (PropertyChangedEventArgs, string?) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Name); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Name); + }; + + model.Name = "Bob"; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Name)); + Assert.AreEqual(changing.Item2, null); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Name)); + Assert.AreEqual(changed.Item2, "Bob"); + } + + [TestMethod] + public void Test_NotifyPropertyChangedForAttribute_Events() + { + DependentPropertyModel model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.Name = "Bob"; + model.Surname = "Ross"; + + CollectionAssert.AreEqual(new[] { nameof(model.Name), nameof(model.FullName), nameof(model.Alias), nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_ValidationAttributes() + { + PropertyInfo nameProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Name))!; + + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 1); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 100); + + PropertyInfo ageProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Age))!; + + Assert.IsNotNull(ageProperty.GetCustomAttribute()); + Assert.AreEqual(ageProperty.GetCustomAttribute()!.Minimum, 0); + Assert.AreEqual(ageProperty.GetCustomAttribute()!.Maximum, 120); + + PropertyInfo emailProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Email))!; + + Assert.IsNotNull(emailProperty.GetCustomAttribute()); + + PropertyInfo comboProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.IfThisWorksThenThatsGreat))!; + + TestValidationAttribute testAttribute = comboProperty.GetCustomAttribute()!; + + Assert.IsNotNull(testAttribute); + Assert.IsNull(testAttribute.O); + Assert.AreEqual(testAttribute.T, typeof(SampleModel)); + Assert.AreEqual(testAttribute.Flag, true); + Assert.AreEqual(testAttribute.D, 6.28); + CollectionAssert.AreEqual(testAttribute.Names, new[] { "Bob", "Ross" }); + + object[]? nestedArray = (object[]?)testAttribute.NestedArray; + + Assert.IsNotNull(nestedArray); + Assert.AreEqual(nestedArray!.Length, 3); + Assert.AreEqual(nestedArray[0], 1); + Assert.AreEqual(nestedArray[1], "Hello"); + Assert.IsTrue(nestedArray[2] is int[]); + CollectionAssert.AreEqual((int[])nestedArray[2], new[] { 2, 3, 4 }); + + Assert.AreEqual(testAttribute.Animal, Animal.Llama); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4216 + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField() + { + ModelWithValueProperty model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.Value = "Hello world"; + + Assert.AreEqual(model.Value, "Hello world"); + + CollectionAssert.AreEqual(new[] { nameof(model.Value) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4216 + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributes() + { + ModelWithValuePropertyWithValidation model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + bool errorsChanged = false; + + model.ErrorsChanged += (s, e) => errorsChanged = true; + + model.Value = "Hello world"; + + Assert.AreEqual(model.Value, "Hello world"); + + // The [NotifyDataErrorInfo] attribute wasn't used, so the property shouldn't be validated + Assert.IsFalse(errorsChanged); + + CollectionAssert.AreEqual(new[] { nameof(model.Value) }, propertyNames); + } + + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributesAndValidation() + { + ModelWithValuePropertyWithAutomaticValidation model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + List errors = new(); + + model.ErrorsChanged += (s, e) => errors.Add(e); + + model.Value = "Bo"; + + Assert.IsTrue(model.HasErrors); + Assert.AreEqual(errors.Count, 1); + Assert.AreEqual(errors[0].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidation.Value)); + + model.Value = "Hello world"; + + Assert.IsFalse(model.HasErrors); + Assert.AreEqual(errors.Count, 2); + Assert.AreEqual(errors[1].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidation.Value)); + } + + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributesAndValidation_WithClassLevelAttribute() + { + ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + List errors = new(); + + model.ErrorsChanged += (s, e) => errors.Add(e); + + model.Value = "Bo"; + + Assert.IsTrue(model.HasErrors); + Assert.AreEqual(errors.Count, 1); + Assert.AreEqual(errors[0].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute.Value)); + + model.Value = "Hello world"; + + Assert.IsFalse(model.HasErrors); + Assert.AreEqual(errors.Count, 2); + Assert.AreEqual(errors[1].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute.Value)); + } + + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributesAndValidation_InheritingClassLevelAttribute() + { + ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + List errors = new(); + + model.ErrorsChanged += (s, e) => errors.Add(e); + + model.Value2 = "Bo"; + + Assert.IsTrue(model.HasErrors); + Assert.AreEqual(errors.Count, 1); + Assert.AreEqual(errors[0].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute.Value2)); + + model.Value2 = "Hello world"; + + Assert.IsFalse(model.HasErrors); + Assert.AreEqual(errors.Count, 2); + Assert.AreEqual(errors[1].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute.Value2)); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4184 + [TestMethod] + public void Test_GeneratedPropertiesWithValidationAttributesOverFields() + { + ViewModelWithValidatableGeneratedProperties model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + // Assign these fields directly to bypass the validation that is executed in the generated setters. + // We only need those generated properties to be there to check whether they are correctly detected. + model.First = "A"; + model.Last = "This is a very long name that exceeds the maximum length of 60 for this property"; + + Assert.IsFalse(model.HasErrors); + + model.RunValidation(); + + Assert.IsTrue(model.HasErrors); + + ValidationResult[] validationErrors = model.GetErrors().ToArray(); + + Assert.AreEqual(validationErrors.Length, 2); + + CollectionAssert.AreEqual(new[] { nameof(ViewModelWithValidatableGeneratedProperties.First) }, validationErrors[0].MemberNames.ToArray()); + CollectionAssert.AreEqual(new[] { nameof(ViewModelWithValidatableGeneratedProperties.Last) }, validationErrors[1].MemberNames.ToArray()); + } + + [TestMethod] + public void Test_NotifyPropertyChangedFor() + { + DependentPropertyModel model = new(); + + List propertyNames = new(); + int canExecuteRequests = 0; + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; + + model.Surname = "Ross"; + + Assert.AreEqual(1, canExecuteRequests); + + CollectionAssert.AreEqual(new[] { nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_NotifyPropertyChangedFor_GeneratedCommand() + { + DependentPropertyModel2 model = new(); + + List propertyNames = new(); + int canExecuteRequests = 0; + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.TestFromMethodCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; + + model.Text = "Ross"; + + Assert.AreEqual(1, canExecuteRequests); + + CollectionAssert.AreEqual(new[] { nameof(model.Text), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_NotifyPropertyChangedFor_IRelayCommandProperty() + { + DependentPropertyModel3 model = new(); + + List propertyNames = new(); + int canExecuteRequests = 0; + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; + + model.Text = "Ross"; + + Assert.AreEqual(1, canExecuteRequests); + + CollectionAssert.AreEqual(new[] { nameof(model.Text), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_NotifyPropertyChangedFor_IAsyncRelayCommandOfTProperty() + { + DependentPropertyModel4 model = new(); + + List propertyNames = new(); + int canExecuteRequests = 0; + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; + + model.Text = "Ross"; + + Assert.AreEqual(1, canExecuteRequests); + + CollectionAssert.AreEqual(new[] { nameof(model.Text), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_OnPropertyChangingAndChangedPartialMethods() + { + ViewModelWithImplementedUpdateMethods model = new(); + + model.Name = nameof(Test_OnPropertyChangingAndChangedPartialMethods); + + Assert.AreEqual(nameof(Test_OnPropertyChangingAndChangedPartialMethods), model.NameChangingValue); + Assert.AreEqual(nameof(Test_OnPropertyChangingAndChangedPartialMethods), model.NameChangedValue); + + model.Number = 99; + + Assert.AreEqual(99, model.NumberChangedValue); + } + + [TestMethod] + public void Test_OnPropertyChangingAndChangedPartialMethods_WithPreviousValues() + { + ViewModelWithImplementedUpdateMethods2 model = new(); + + Assert.AreEqual(null, model.Name); + Assert.AreEqual(0, model.Number); + + CollectionAssert.AreEqual(Array.Empty<(string, string)>(), model.OnNameChangingValues); + CollectionAssert.AreEqual(Array.Empty<(string, string)>(), model.OnNameChangedValues); + CollectionAssert.AreEqual(Array.Empty<(int, int)>(), model.OnNumberChangingValues); + CollectionAssert.AreEqual(Array.Empty<(int, int)>(), model.OnNumberChangedValues); + + model.Name = "Bob"; + + CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangingValues); + CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangedValues); + + Assert.AreEqual("Bob", model.Name); + + CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangingValues); + CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangedValues); + + model.Name = "Alice"; + + CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangingValues); + CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangedValues); + + Assert.AreEqual("Alice", model.Name); + + CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangingValues); + CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangedValues); + + model.Number = 42; + + CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangingValues); + CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangedValues); + + Assert.AreEqual(42, model.Number); + + CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangingValues); + CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangedValues); + + model.Number = 77; + + CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangingValues); + CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangedValues); + + Assert.AreEqual(77, model.Number); + + CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangingValues); + CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangedValues); + } + + [TestMethod] + public void Test_OnPropertyChangingAndChangedPartialMethodWithAdditionalValidation() + { + ViewModelWithImplementedUpdateMethodAndAdditionalValidation model = new(); + + // The actual validation is performed inside the model itself. + // This test validates that the order with which methods/events are generated is: + // - OnChanging(value); + // - OnPropertyChanging(); + // - field = value; + // - OnChanged(value); + // - OnPropertyChanged(); + model.Name = "B"; + + Assert.AreEqual("B", model.Name); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithObservableObject() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModel(messenger), + setter: static (model, value) => model.Name = value, + propertyName: nameof(BroadcastingViewModel.Name)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithObservableRecipientAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithAttribute(messenger), + setter: static (model, value) => model.Name = value, + propertyName: nameof(BroadcastingViewModelWithAttribute.Name)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithInheritedObservableRecipientAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithInheritedAttribute(messenger), + setter: static (model, value) => model.Name2 = value, + propertyName: nameof(BroadcastingViewModelWithInheritedAttribute.Name2)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithObservableObject_WithClassLevelAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithClassLevelAttribute(messenger), + setter: static (model, value) => model.Name = value, + propertyName: nameof(BroadcastingViewModelWithClassLevelAttribute.Name)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithObservableRecipientAttribute_WithClassLevelAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithAttributeAndClassLevelAttribute(messenger), + setter: static (model, value) => model.Name = value, + propertyName: nameof(BroadcastingViewModelWithAttributeAndClassLevelAttribute.Name)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithInheritedObservableRecipientAttribute_WithClassLevelAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithInheritedClassLevelAttribute(messenger), + setter: static (model, value) => model.Name2 = value, + propertyName: nameof(BroadcastingViewModelWithInheritedClassLevelAttribute.Name2)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithInheritedObservableRecipientAttributeAndClassLevelAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute(messenger), + setter: static (model, value) => model.Name2 = value, + propertyName: nameof(BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute.Name2)); + } + + private void Test_NotifyPropertyChangedRecipients_Test(Func factory, Action setter, string propertyName) + where T : notnull + { + IMessenger messenger = new StrongReferenceMessenger(); + + T model = factory(messenger); + + List<(object Sender, PropertyChangedMessage Message)> messages = new(); + + messenger.Register>(model, (r, m) => messages.Add((r, m))); + + setter(model, "Bob"); + + Assert.AreEqual(1, messages.Count); + Assert.AreSame(model, messages[0].Sender); + Assert.AreEqual(null, messages[0].Message.OldValue); + Assert.AreEqual("Bob", messages[0].Message.NewValue); + Assert.AreEqual(propertyName, messages[0].Message.PropertyName); + + setter(model, "Ross"); + + Assert.AreEqual(2, messages.Count); + Assert.AreSame(model, messages[1].Sender); + Assert.AreEqual("Bob", messages[1].Message.OldValue); + Assert.AreEqual("Ross", messages[1].Message.NewValue); + Assert.AreEqual(propertyName, messages[0].Message.PropertyName); + } + + [TestMethod] + public void Test_ObservableProperty_ObservableRecipientDoesNotBroadcastByDefault() + { + IMessenger messenger = new StrongReferenceMessenger(); + RecipientWithNonBroadcastingProperty model = new(messenger); + + List<(object Sender, PropertyChangedMessage Message)> messages = new(); + + messenger.Register>(model, (r, m) => messages.Add((r, m))); + + model.Name = "Bob"; + model.Name = "Alice"; + model.Name = null; + + // The [NotifyPropertyChangedRecipients] attribute wasn't used, so no messages should have been sent + Assert.AreEqual(messages.Count, 0); + } + +#if NET6_0_OR_GREATER + // See https://github.com/CommunityToolkit/dotnet/issues/155 + [TestMethod] + public void Test_ObservableProperty_NullabilityAnnotations_Simple() + { + // List? + NullabilityInfoContext context = new(); + NullabilityInfo info = context.Create(typeof(NullableRepro).GetProperty(nameof(NullableRepro.NullableList))!); + + Assert.AreEqual(typeof(List), info.Type); + Assert.AreEqual(NullabilityState.Nullable, info.ReadState); + Assert.AreEqual(NullabilityState.Nullable, info.WriteState); + Assert.AreEqual(1, info.GenericTypeArguments.Length); + + NullabilityInfo elementInfo = info.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(string), elementInfo.Type); + Assert.AreEqual(NullabilityState.Nullable, elementInfo.ReadState); + Assert.AreEqual(NullabilityState.Nullable, elementInfo.WriteState); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/155 + [TestMethod] + public void Test_ObservableProperty_NullabilityAnnotations_Complex() + { + // Foo.Bar?, StrongBox.Bar?>?>? + NullabilityInfoContext context = new(); + NullabilityInfo info = context.Create(typeof(NullableRepro).GetProperty(nameof(NullableRepro.NullableMess))!); + + Assert.AreEqual(typeof(Foo.Bar?, StrongBox.Bar?>?>), info.Type); + Assert.AreEqual(NullabilityState.Nullable, info.ReadState); + Assert.AreEqual(NullabilityState.Nullable, info.WriteState); + Assert.AreEqual(2, info.GenericTypeArguments.Length); + + NullabilityInfo leftInfo = info.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(Foo.Bar), leftInfo.Type); + Assert.AreEqual(NullabilityState.Nullable, leftInfo.ReadState); + Assert.AreEqual(NullabilityState.Nullable, leftInfo.WriteState); + Assert.AreEqual(3, leftInfo.GenericTypeArguments.Length); + + NullabilityInfo leftInfo0 = leftInfo.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(string), leftInfo0.Type); + Assert.AreEqual(NullabilityState.Nullable, leftInfo0.ReadState); + Assert.AreEqual(NullabilityState.Nullable, leftInfo0.WriteState); + + NullabilityInfo leftInfo1 = leftInfo.GenericTypeArguments[1]; + + Assert.AreEqual(typeof(int), leftInfo1.Type); + Assert.AreEqual(NullabilityState.NotNull, leftInfo1.ReadState); + Assert.AreEqual(NullabilityState.NotNull, leftInfo1.WriteState); + + NullabilityInfo leftInfo2 = leftInfo.GenericTypeArguments[2]; + + Assert.AreEqual(typeof(object), leftInfo2.Type); + Assert.AreEqual(NullabilityState.Nullable, leftInfo2.ReadState); + Assert.AreEqual(NullabilityState.Nullable, leftInfo2.WriteState); + + NullabilityInfo rightInfo = info.GenericTypeArguments[1]; + + Assert.AreEqual(typeof(StrongBox.Bar?>), rightInfo.Type); + Assert.AreEqual(NullabilityState.Nullable, rightInfo.ReadState); + Assert.AreEqual(NullabilityState.Nullable, rightInfo.WriteState); + Assert.AreEqual(1, rightInfo.GenericTypeArguments.Length); + + NullabilityInfo rightInnerInfo = rightInfo.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(Foo.Bar), rightInnerInfo.Type); + Assert.AreEqual(NullabilityState.Nullable, rightInnerInfo.ReadState); + Assert.AreEqual(NullabilityState.Nullable, rightInnerInfo.WriteState); + Assert.AreEqual(3, rightInnerInfo.GenericTypeArguments.Length); + + NullabilityInfo rightInfo0 = rightInnerInfo.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(int), rightInfo0.Type); + Assert.AreEqual(NullabilityState.NotNull, rightInfo0.ReadState); + Assert.AreEqual(NullabilityState.NotNull, rightInfo0.WriteState); + + NullabilityInfo rightInfo1 = rightInnerInfo.GenericTypeArguments[1]; + + Assert.AreEqual(typeof(string), rightInfo1.Type); + Assert.AreEqual(NullabilityState.Nullable, rightInfo1.ReadState); + Assert.AreEqual(NullabilityState.Nullable, rightInfo1.WriteState); + + NullabilityInfo rightInfo2 = rightInnerInfo.GenericTypeArguments[2]; + + Assert.AreEqual(typeof(object), rightInfo2.Type); + Assert.AreEqual(NullabilityState.NotNull, rightInfo2.ReadState); + Assert.AreEqual(NullabilityState.NotNull, rightInfo2.WriteState); + } +#endif + + // See https://github.com/CommunityToolkit/dotnet/issues/201 + [TestMethod] + public void Test_ObservableProperty_InheritedMembersAsAttributeTargets() + { + ConcreteViewModel model = new(); + + List propertyNames = new(); + List canExecuteChangedArgs = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.DoSomethingCommand.CanExecuteChanged += (s, _) => canExecuteChangedArgs.Add(s); + model.ManualCommand.CanExecuteChanged += (s, _) => canExecuteChangedArgs.Add(s); + + model.A = nameof(model.A); + model.B = nameof(model.B); + model.C = nameof(model.C); + model.D = nameof(model.D); + + CollectionAssert.AreEqual(new[] + { + nameof(model.A), + nameof(model.Content), + nameof(model.B), + nameof(model.SomeGeneratedProperty), + nameof(model.C), + nameof(model.D) + }, propertyNames); + + CollectionAssert.AreEqual(new[] { model.DoSomethingCommand, model.ManualCommand }, canExecuteChangedArgs); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/224 + [TestMethod] + public void Test_ObservableProperty_WithinGenericTypeWithMultipleTypeParameters() + { + ModelWithMultipleGenericParameters model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.Value = true; + model.TValue = 42; + model.UValue = "Hello"; + model.List = new List() { 420 }; + + Assert.AreEqual(model.Value, true); + Assert.AreEqual(model.TValue, 42); + Assert.AreEqual(model.UValue, "Hello"); + CollectionAssert.AreEqual(new[] { 420 }, model.List); + + CollectionAssert.AreEqual(new[] { nameof(model.Value), nameof(model.TValue), nameof(model.UValue), nameof(model.List) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/222 + [TestMethod] + public void Test_ObservableProperty_WithBaseViewModelWithObservableObjectAttributeInAnotherAssembly() + { + ModelWithObservablePropertyAndBaseClassInAnotherAssembly model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + Assert.AreEqual(model.OtherProperty, "Ok"); + + model.MyProperty = "A"; + model.OtherProperty = "B"; + + Assert.AreEqual(model.MyProperty, "A"); + Assert.AreEqual(model.OtherProperty, "B"); + + CollectionAssert.AreEqual(new[] { nameof(model.MyProperty), nameof(model.OtherProperty) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/230 + [TestMethod] + public void Test_ObservableProperty_ModelWithCultureAwarePropertyName() + { + ModelWithCultureAwarePropertyName model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.InputFolder = 42; + + Assert.AreEqual(model.InputFolder, 42); + + CollectionAssert.AreEqual(new[] { nameof(model.InputFolder) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/242 + [TestMethod] + public void Test_ObservableProperty_ModelWithNotifyPropertyChangedRecipientsAndDisplayAttributeLast() + { + IMessenger messenger = new StrongReferenceMessenger(); + ModelWithNotifyPropertyChangedRecipientsAndDisplayAttributeLast model = new(messenger); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + object newValue = new(); + bool isMessageReceived = false; + + messenger.Register>(this, (r, m) => + { + if (m.Sender != model) + { + Assert.Fail(); + } + + if (m.NewValue != newValue) + { + Assert.Fail(); + } + + isMessageReceived = true; + }); + + model.SomeProperty = newValue; + + Assert.AreEqual(model.SomeProperty, newValue); + Assert.IsTrue(isMessageReceived); + + CollectionAssert.AreEqual(new[] { nameof(model.SomeProperty) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/257 + [TestMethod] + public void Test_ObservableProperty_InheritedModelWithCommandUsingInheritedObservablePropertyForCanExecute() + { + InheritedModelWithCommandUsingInheritedObservablePropertyForCanExecute model = new(); + + Assert.IsFalse(model.SaveCommand.CanExecute(null)); + + model.CanSave = true; + + Assert.IsTrue(model.SaveCommand.CanExecute(null)); + } + + [TestMethod] + public void Test_ObservableProperty_ForwardsSpecialCasesDataAnnotationAttributes() + { + PropertyInfo propertyInfo = typeof(ModelWithAdditionalDataAnnotationAttributes).GetProperty(nameof(ModelWithAdditionalDataAnnotationAttributes.Name))!; + + DisplayAttribute? displayAttribute = (DisplayAttribute?)propertyInfo.GetCustomAttribute(typeof(DisplayAttribute)); + + Assert.IsNotNull(displayAttribute); + Assert.AreEqual(displayAttribute!.Name, "MyProperty"); + Assert.AreEqual(displayAttribute.ResourceType, typeof(List)); + Assert.AreEqual(displayAttribute.Prompt, "Foo bar baz"); + + KeyAttribute? keyAttribute = (KeyAttribute?)propertyInfo.GetCustomAttribute(typeof(KeyAttribute)); + + Assert.IsNotNull(keyAttribute); + + EditableAttribute? editableAttribute = (EditableAttribute?)propertyInfo.GetCustomAttribute(typeof(EditableAttribute)); + + Assert.IsNotNull(keyAttribute); + Assert.IsTrue(editableAttribute!.AllowEdit); + + UIHintAttribute? uiHintAttribute = (UIHintAttribute?)propertyInfo.GetCustomAttribute(typeof(UIHintAttribute)); + + Assert.IsNotNull(uiHintAttribute); + Assert.AreEqual(uiHintAttribute!.UIHint, "MyControl"); + Assert.AreEqual(uiHintAttribute.PresentationLayer, "WPF"); + Assert.AreEqual(uiHintAttribute.ControlParameters.Count, 3); + Assert.IsTrue(uiHintAttribute.ControlParameters.ContainsKey("Foo")); + Assert.IsTrue(uiHintAttribute.ControlParameters.ContainsKey("Bar")); + Assert.IsTrue(uiHintAttribute.ControlParameters.ContainsKey("Baz")); + Assert.AreEqual(uiHintAttribute.ControlParameters["Foo"], 42); + Assert.AreEqual(uiHintAttribute.ControlParameters["Bar"], 3.14); + Assert.AreEqual(uiHintAttribute.ControlParameters["Baz"], "Hello"); + + ScaffoldColumnAttribute? scaffoldColumnAttribute = (ScaffoldColumnAttribute?)propertyInfo.GetCustomAttribute(typeof(ScaffoldColumnAttribute)); + + Assert.IsNotNull(scaffoldColumnAttribute); + Assert.IsTrue(scaffoldColumnAttribute!.Scaffold); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/271 + [TestMethod] + public void Test_ObservableProperty_ModelWithObservablePropertyInRootNamespace() + { + ModelWithObservablePropertyInRootNamespace model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.Number = 3.14f; + + // We mostly just need to verify this class compiles fine with the right generated code + CollectionAssert.AreEqual(propertyNames, new[] { nameof(model.Number) }); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/272 + [TestMethod] + public void Test_ObservableProperty_WithCommandReferencingGeneratedPropertyFromOtherAssembly() + { + ModelWithOverriddenCommandMethodFromExternalBaseModel model = new(); + + Assert.IsFalse(model.HasSaved); + Assert.IsFalse(model.SaveCommand.CanExecute(null)); + + model.CanSave = true; + + Assert.IsTrue(model.SaveCommand.CanExecute(null)); + + model.SaveCommand.Execute(null); + + Assert.IsTrue(model.HasSaved); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/413 + [TestMethod] + public void Test_ObservableProperty_WithExplicitAttributeForProperty() + { + PropertyInfo nameProperty = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.Name))!; + + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 1); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 100); + + PropertyInfo lastNameProperty = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.LastName))!; + + Assert.IsNotNull(lastNameProperty.GetCustomAttribute()); + Assert.AreEqual(lastNameProperty.GetCustomAttribute()!.Name, "lastName"); + Assert.IsNotNull(lastNameProperty.GetCustomAttribute()); + + PropertyInfo justOneSimpleAttributeProperty = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.JustOneSimpleAttribute))!; + + Assert.IsNotNull(justOneSimpleAttributeProperty.GetCustomAttribute()); + + PropertyInfo someComplexValidationAttributeProperty = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.SomeComplexValidationAttribute))!; + + TestValidationAttribute testAttribute = someComplexValidationAttributeProperty.GetCustomAttribute()!; + + Assert.IsNotNull(testAttribute); + Assert.IsNull(testAttribute.O); + Assert.AreEqual(testAttribute.T, typeof(MyViewModelWithExplicitPropertyAttributes)); + Assert.AreEqual(testAttribute.Flag, true); + Assert.AreEqual(testAttribute.D, 6.28); + CollectionAssert.AreEqual(testAttribute.Names, new[] { "Bob", "Ross" }); + + object[]? nestedArray = (object[]?)testAttribute.NestedArray; + + Assert.IsNotNull(nestedArray); + Assert.AreEqual(nestedArray!.Length, 3); + Assert.AreEqual(nestedArray[0], 1); + Assert.AreEqual(nestedArray[1], "Hello"); + Assert.IsTrue(nestedArray[2] is int[]); + CollectionAssert.AreEqual((int[])nestedArray[2], new[] { 2, 3, 4 }); + + Assert.AreEqual(testAttribute.Animal, Animal.Llama); + + PropertyInfo someComplexRandomAttribute = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.SomeComplexRandomAttribute))!; + + Assert.IsNotNull(someComplexRandomAttribute.GetCustomAttribute()); + + PropertyInfoAttribute testAttribute2 = someComplexRandomAttribute.GetCustomAttribute()!; + + Assert.IsNotNull(testAttribute2); + Assert.IsNull(testAttribute2.O); + Assert.AreEqual(testAttribute2.T, typeof(MyViewModelWithExplicitPropertyAttributes)); + Assert.AreEqual(testAttribute2.Flag, true); + Assert.AreEqual(testAttribute2.D, 6.28); + Assert.IsNotNull(testAttribute2.Objects); + Assert.IsTrue(testAttribute2.Objects is object[]); + Assert.AreEqual(((object[])testAttribute2.Objects).Length, 1); + Assert.AreEqual(((object[])testAttribute2.Objects)[0], "Test"); + CollectionAssert.AreEqual(testAttribute2.Names, new[] { "Bob", "Ross" }); + + object[]? nestedArray2 = (object[]?)testAttribute2.NestedArray; + + Assert.IsNotNull(nestedArray2); + Assert.AreEqual(nestedArray2!.Length, 4); + Assert.AreEqual(nestedArray2[0], 1); + Assert.AreEqual(nestedArray2[1], "Hello"); + Assert.AreEqual(nestedArray2[2], 42); + Assert.IsNull(nestedArray2[3]); + + Assert.AreEqual(testAttribute2.Animal, (Animal)67); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/375 + [TestMethod] + public void Test_ObservableProperty_ModelWithObservablePropertyWithUnderscoreAndUppercase() + { + ModelWithObservablePropertyWithUnderscoreAndUppercase model = new(); + + Assert.IsFalse(model.IsReadOnly); + + // Just ensures this builds and the property is generated with the expected name + model.IsReadOnly = true; + + Assert.IsTrue(model.IsReadOnly); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void Test_ObservableProperty_ModelWithDependentPropertyAndPropertyChanging() + { + ModelWithDependentPropertyAndPropertyChanging model = new(); + + List changingArgs = new(); + List changedArgs = new(); + + model.PropertyChanging += (s, e) => changingArgs.Add(e.PropertyName); + model.PropertyChanged += (s, e) => changedArgs.Add(e.PropertyName); + + model.Name = "Bob"; + + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndPropertyChanging.Name), nameof(ModelWithDependentPropertyAndPropertyChanging.FullName) }, changingArgs); + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndPropertyChanging.Name), nameof(ModelWithDependentPropertyAndPropertyChanging.FullName) }, changedArgs); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void Test_ObservableProperty_ModelWithDependentPropertyAndNoPropertyChanging() + { + ModelWithDependentPropertyAndNoPropertyChanging model = new(); + + List changedArgs = new(); + + model.PropertyChanged += (s, e) => changedArgs.Add(e.PropertyName); + + model.Name = "Alice"; + + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndNoPropertyChanging.Name), nameof(ModelWithDependentPropertyAndNoPropertyChanging.FullName) }, changedArgs); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/731 + [TestMethod] + public void Test_ObservableProperty_ForwardedAttributesWithNegativeValues() + { + Assert.AreEqual(PositiveEnum.Something, + typeof(ModelWithForwardedAttributesWithNegativeValues) + .GetProperty(nameof(ModelWithForwardedAttributesWithNegativeValues.Test2))! + .GetCustomAttribute()! + .Value); + + Assert.AreEqual(NegativeEnum.Problem, + typeof(ModelWithForwardedAttributesWithNegativeValues) + .GetProperty(nameof(ModelWithForwardedAttributesWithNegativeValues.Test3))! + .GetCustomAttribute()! + .Value); + } + + public abstract partial class BaseViewModel : ObservableObject + { + public string? Content { get; set; } + + [ObservableProperty] + public partial string? SomeGeneratedProperty { get; set; } + + [RelayCommand] + private void DoSomething() + { + } + + public IRelayCommand ManualCommand { get; } = new RelayCommand(() => { }); + } + + public partial class ConcreteViewModel : BaseViewModel + { + // Inherited property + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(Content))] + public partial string? A { get; set; } + + // Inherited generated property + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SomeGeneratedProperty))] + public partial string? B { get; set; } + + // Inherited generated command + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(DoSomethingCommand))] + public partial string? C { get; set; } + + // Inherited manual command + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(ManualCommand))] + public partial string? D { get; set; } + } + + public partial class SampleModel : ObservableObject + { + /// + /// This is a sample data field within of type . + /// + [ObservableProperty] + public partial int Data { get; set; } + + #region More properties + + [ObservableProperty] + public partial int Counter { get; set; } + + #endregion + + [ObservableProperty] + public partial string? Name { get; set; } + } + + [INotifyPropertyChanged] + public sealed partial class DependentPropertyModel + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + [NotifyPropertyChangedFor(nameof(Alias))] + public partial string? Name { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName), nameof(Alias))] + [NotifyCanExecuteChangedFor(nameof(MyCommand))] + public partial string? Surname { get; set; } + + public string FullName => $"{Name} {Surname}"; + + public string Alias => $"{Name?.ToLower()}{Surname?.ToLower()}"; + + public RelayCommand MyCommand { get; } = new(() => { }); + } + + [INotifyPropertyChanged] + public sealed partial class DependentPropertyModel2 + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName), nameof(Alias))] + [NotifyCanExecuteChangedFor(nameof(TestFromMethodCommand))] + public partial string? Text { get; set; } + + public string FullName => ""; + + public string Alias => ""; + + public RelayCommand MyCommand { get; } = new(() => { }); + + [RelayCommand] + private void TestFromMethod() + { + } + } + + [INotifyPropertyChanged] + public sealed partial class DependentPropertyModel3 + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName), nameof(Alias))] + [NotifyCanExecuteChangedFor(nameof(MyCommand))] + public partial string? Text { get; set; } + + public string FullName => ""; + + public string Alias => ""; + + public IRelayCommand MyCommand { get; } = new RelayCommand(() => { }); + } + + [INotifyPropertyChanged] + public sealed partial class DependentPropertyModel4 + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName), nameof(Alias))] + [NotifyCanExecuteChangedFor(nameof(MyCommand))] + public partial string? Text { get; set; } + + public string FullName => ""; + + public string Alias => ""; + + public IAsyncRelayCommand MyCommand { get; } = new AsyncRelayCommand(_ => Task.CompletedTask); + } + + public partial class MyFormViewModel : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(1)] + [MaxLength(100)] + public partial string? Name { get; set; } + + [ObservableProperty] + [Range(0, 120)] + public partial int Age { get; set; } + + [ObservableProperty] + [EmailAddress] + public partial string? Email { get; set; } + + [ObservableProperty] + [TestValidation(null, typeof(SampleModel), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Animal.Llama)] + public partial int IfThisWorksThenThatsGreat { get; set; } + } + + private sealed class TestValidationAttribute : ValidationAttribute + { + public TestValidationAttribute(object? o, Type t, bool flag, double d, string[] names) + { + O = o; + T = t; + Flag = flag; + D = d; + Names = names; + } + + public object? O { get; } + + public Type T { get; } + + public bool Flag { get; } + + public double D { get; } + + public string[] Names { get; } + + public object? NestedArray { get; set; } + + public Animal Animal { get; set; } + } + + public enum Animal + { + Cat, + Dog, + Llama + } + + public partial class ModelWithValueProperty : ObservableObject + { + [ObservableProperty] + public partial string? Value { get; set; } + } + + public partial class ModelWithValuePropertyWithValidation : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(5)] + public partial string? Value { get; set; } + } + + public partial class ModelWithValuePropertyWithAutomaticValidation : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(5)] + [NotifyDataErrorInfo] + public partial string? Value { get; set; } + } + + [NotifyDataErrorInfo] + public partial class ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(5)] + public partial string? Value { get; set; } + } + + public partial class ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute : ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute + { + [ObservableProperty] + [Required] + [MinLength(5)] + public partial string? Value2 { get; set; } + } + + public partial class ViewModelWithValidatableGeneratedProperties : ObservableValidator + { + [Required] + [MinLength(2)] + [MaxLength(60)] + [Display(Name = "FirstName")] + [ObservableProperty] + public partial string First { get; set; } = "Bob"; + + [Display(Name = "LastName")] + [Required] + [MinLength(2)] + [MaxLength(60)] + [ObservableProperty] + public partial string Last { get; set; } = "Jones"; + + public void RunValidation() => ValidateAllProperties(); + } + + public partial class ViewModelWithImplementedUpdateMethods : ObservableObject + { + [ObservableProperty] + public partial string? Name { get; set; } = "Bob"; + + [ObservableProperty] + public partial int Number { get; set; } = 42; + + public string? NameChangingValue { get; private set; } + + public string? NameChangedValue { get; private set; } + + public int NumberChangedValue { get; private set; } + + partial void OnNameChanging(string? value) + { + NameChangingValue = value; + } + + partial void OnNameChanged(string? value) + { + NameChangedValue = value; + } + + partial void OnNumberChanged(int value) + { + NumberChangedValue = value; + } + } + + public partial class ViewModelWithImplementedUpdateMethods2 : ObservableObject + { + [ObservableProperty] + public partial string? Name { get; set; } + + [ObservableProperty] + public partial int Number { get; set; } + + public List<(string? Old, string? New)> OnNameChangingValues { get; } = new(); + + public List<(string? Old, string? New)> OnNameChangedValues { get; } = new(); + + public List<(int Old, int New)> OnNumberChangingValues { get; } = new(); + + public List<(int Old, int New)> OnNumberChangedValues { get; } = new(); + + partial void OnNameChanging(string? oldValue, string? newValue) + { + OnNameChangingValues.Add((oldValue, newValue)); + } + + partial void OnNameChanged(string? oldValue, string? newValue) + { + OnNameChangedValues.Add((oldValue, newValue)); + } + + partial void OnNumberChanging(int oldValue, int newValue) + { + OnNumberChangingValues.Add((oldValue, newValue)); + } + + partial void OnNumberChanged(int oldValue, int newValue) + { + OnNumberChangedValues.Add((oldValue, newValue)); + } + } + + public partial class ViewModelWithImplementedUpdateMethodAndAdditionalValidation : ObservableObject + { + private int step; + + [ObservableProperty] + public partial string? Name { get; set; } = "A"; + + partial void OnNameChanging(string? value) + { + Assert.AreEqual(0, this.step); + + this.step = 1; + + Assert.AreEqual("A", Name); + Assert.AreEqual("B", value); + } + + partial void OnNameChanged(string? value) + { + Assert.AreEqual(2, this.step); + + this.step = 3; + + Assert.AreEqual("B", Name); + Assert.AreEqual("B", value); + } + + protected override void OnPropertyChanging(PropertyChangingEventArgs e) + { + base.OnPropertyChanging(e); + + Assert.AreEqual(1, this.step); + + this.step = 2; + + Assert.AreEqual("A", Name); + Assert.AreEqual(nameof(Name), e.PropertyName); + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + Assert.AreEqual(3, this.step); + + Assert.AreEqual("B", Name); + Assert.AreEqual(nameof(Name), e.PropertyName); + } + } + + partial class BroadcastingViewModel : ObservableRecipient + { + public BroadcastingViewModel(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial string? Name { get; set; } + } + + partial class RecipientWithNonBroadcastingProperty : ObservableRecipient + { + public RecipientWithNonBroadcastingProperty(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + public partial string? Name { get; set; } + } + + [ObservableRecipient] + partial class BroadcastingViewModelWithAttribute : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial string? Name { get; set; } + } + + partial class BroadcastingViewModelWithInheritedAttribute : BroadcastingViewModelWithAttribute + { + public BroadcastingViewModelWithInheritedAttribute(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial string? Name2 { get; set; } + } + + [NotifyPropertyChangedRecipients] + partial class BroadcastingViewModelWithClassLevelAttribute : ObservableRecipient + { + public BroadcastingViewModelWithClassLevelAttribute(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + public partial string? Name { get; set; } + } + + partial class BroadcastingViewModelWithInheritedClassLevelAttribute : BroadcastingViewModelWithClassLevelAttribute + { + public BroadcastingViewModelWithInheritedClassLevelAttribute(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + public partial string? Name2 { get; set; } + } + + [ObservableRecipient] + [NotifyPropertyChangedRecipients] + partial class BroadcastingViewModelWithAttributeAndClassLevelAttribute : ObservableObject + { + [ObservableProperty] + public partial string? Name { get; set; } + } + + partial class BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute : BroadcastingViewModelWithAttributeAndClassLevelAttribute + { + public BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + public partial string? Name2 { get; set; } + } + +#if NET6_0_OR_GREATER + private partial class NullableRepro : ObservableObject + { + [ObservableProperty] + public partial List? NullableList { get; set; } + + [ObservableProperty] + public partial Foo.Bar?, StrongBox.Bar?>?>? NullableMess { get; set; } + } + + private class Foo + { + public class Bar + { + } + } +#endif + + partial class ModelWithObservablePropertyAndBaseClassInAnotherAssembly : ModelWithObservableObjectAttribute + { + [ObservableProperty] + public partial string? OtherProperty { get; set; } + + public ModelWithObservablePropertyAndBaseClassInAnotherAssembly() + { + OtherProperty = "Ok"; + } + } + + interface IValueHolder + { + public bool Value { get; } + } + + partial class ModelWithMultipleGenericParameters : ObservableObject, IValueHolder + { + [ObservableProperty] + public partial bool Value { get; set; } + + [ObservableProperty] + public partial T? TValue { get; set; } + + [ObservableProperty] + public partial U? UValue { get; set; } + + [ObservableProperty] + public partial List? List { get; set; } + } + + [ObservableObject] + partial class ModelWithCultureAwarePropertyName + { + // This starts with "i" as it's one of the characters that can change when converted to uppercase. + // For instance, when using the Turkish language pack, this would become "İnputFolder" if done wrong. + [ObservableProperty] + public partial int InputFolder { get; set; } + } + + [ObservableRecipient] + public sealed partial class ModelWithNotifyPropertyChangedRecipientsAndDisplayAttributeLast : ObservableValidator + { + [ObservableProperty] + [NotifyPropertyChangedRecipients] + [Display(Name = "Foo bar baz")] + public partial object? SomeProperty { get; set; } + } + + public abstract partial class BaseModelWithObservablePropertyAttribute : ObservableObject + { + [ObservableProperty] + public partial bool CanSave { get; set; } + + public abstract void Save(); + } + + public partial class InheritedModelWithCommandUsingInheritedObservablePropertyForCanExecute : BaseModelWithObservablePropertyAttribute + { + [RelayCommand(CanExecute = nameof(CanSave))] + public override void Save() + { + } + } + + public partial class ModelWithAdditionalDataAnnotationAttributes : ObservableValidator + { + [ObservableProperty] + [Display(Name = "MyProperty", ResourceType = typeof(List), Prompt = "Foo bar baz")] + [Key] + [Editable(true)] + [UIHint("MyControl", "WPF", new object[] { "Foo", 42, "Bar", 3.14, "Baz", "Hello" })] + [ScaffoldColumn(true)] + public partial string? Name { get; set; } + } + + public partial class ModelWithOverriddenCommandMethodFromExternalBaseModel : ModelWithObservablePropertyAndMethod + { + public bool HasSaved { get; private set; } + + [RelayCommand(CanExecute = nameof(CanSave))] + public override void Save() + { + HasSaved = true; + } + } + + public partial class MyViewModelWithExplicitPropertyAttributes : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(1)] + [MaxLength(100)] + public partial string? Name { get; set; } + [ObservableProperty] + [JsonPropertyName("lastName")] + [XmlIgnore] + public partial string? LastName { get; set; } + + [ObservableProperty] + [Test] + public partial string? JustOneSimpleAttribute { get; set; } + + [ObservableProperty] + [TestValidation(null, typeof(MyViewModelWithExplicitPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Animal.Llama)] + public partial int SomeComplexValidationAttribute { get; set; } + + [ObservableProperty] + [Test] + [PropertyInfo(null, typeof(MyViewModelWithExplicitPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, new object[] { "Test" }, NestedArray = new object[] { 1, "Hello", 42, null! }, Animal = (Animal)67)] + public partial int SomeComplexRandomAttribute { get; set; } + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class TestAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class PropertyInfoAttribute : Attribute + { + public PropertyInfoAttribute(object? o, Type t, bool flag, double d, string[] names, object[] objects) + { + O = o; + T = t; + Flag = flag; + D = d; + Names = names; + Objects = objects; + } + + public object? O { get; } + + public Type T { get; } + + public bool Flag { get; } + + public double D { get; } + + public string[] Names { get; } + + public object? Objects { get; set; } + + public object? NestedArray { get; set; } + + public Animal Animal { get; set; } + } + + private partial class ModelWithObservablePropertyWithUnderscoreAndUppercase : ObservableObject + { + [ObservableProperty] + public partial bool IsReadOnly { get; set; } + } + + private partial class ModelWithForwardedAttributesWithNegativeValues : ObservableObject + { + [ObservableProperty] + public partial bool Test1 { get; set; } + + [ObservableProperty] + [DefaultValue(PositiveEnum.Something)] + public partial PositiveEnum Test2 { get; set; } + + [ObservableProperty] + [DefaultValue(NegativeEnum.Problem)] + public partial NegativeEnum Test3 { get; set; } + + [ObservableProperty] + public partial int Test4 { get; set; } + + public ModelWithForwardedAttributesWithNegativeValues() + { + Test1 = true; + Test2 = PositiveEnum.Else; + } + } + + public enum PositiveEnum + { + Something = 0, + Else = 1 + } + + public enum NegativeEnum + { + Problem = -1, + OK = 0 + } + + private sealed partial class ModelWithDependentPropertyAndPropertyChanging : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + public partial string? Name { get; set; } + + public string? FullName => ""; + } + + [INotifyPropertyChanged(IncludeAdditionalHelperMethods = false)] + private sealed partial class ModelWithDependentPropertyAndNoPropertyChanging + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + public partial string? Name { get; set; } + + public string? FullName => ""; + } +} diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_INotifyPropertyChangedAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_INotifyPropertyChangedAttribute.cs index 095a9fcb0..d4992254e 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_INotifyPropertyChangedAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_INotifyPropertyChangedAttribute.cs @@ -105,6 +105,37 @@ public partial class SampleModelWithINPCAndObservableProperties private int y; } +#if ROSLYN_4_12_0_OR_GREATER + [TestMethod] + public void Test_INotifyPropertyChanged_WithGeneratedPartialProperties() + { + Assert.IsTrue(typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(SampleModelWithINPCAndObservablePartialProperties))); + Assert.IsFalse(typeof(INotifyPropertyChanging).IsAssignableFrom(typeof(SampleModelWithINPCAndObservablePartialProperties))); + + SampleModelWithINPCAndObservablePartialProperties model = new(); + List eventArgs = new(); + + model.PropertyChanged += (s, e) => eventArgs.Add(e); + + model.X = 42; + model.Y = 66; + + Assert.AreEqual(eventArgs.Count, 2); + Assert.AreEqual(eventArgs[0].PropertyName, nameof(SampleModelWithINPCAndObservablePartialProperties.X)); + Assert.AreEqual(eventArgs[1].PropertyName, nameof(SampleModelWithINPCAndObservablePartialProperties.Y)); + } + + [INotifyPropertyChanged] + public partial class SampleModelWithINPCAndObservablePartialProperties + { + [ObservableProperty] + public partial int X { get; set; } + + [ObservableProperty] + public partial int Y { get; set; } + } +#endif + [TestMethod] public void Test_INotifyPropertyChanged_WithGeneratedProperties_ExternalNetStandard20Assembly() { From 9b508c5699e16e5c9e748e49336261291eb9b4d2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 16:09:44 -0800 Subject: [PATCH 08/14] Ignore broken test --- .../Test_SourceGeneratorsDiagnostics.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 3e9d2a650..c60674fac 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -889,6 +889,7 @@ public partial string Name } [TestMethod] + [Ignore("The symbol callback is not being triggered correctly (see https://github.com/dotnet/roslyn/issues/76166)")] public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnImplementedProperty_GeneratedByAnotherGenerator_Warns() { const string source = """ From 4020c0332fb8e7fc7e05d59ee58619ec3ad5bf10 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 16:15:49 -0800 Subject: [PATCH 09/14] Update global.json to .NET 9.0.100 SDK --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 1880a952c..00b67caef 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.403", + "version": "9.0.100", "rollForward": "latestFeature", "allowPrerelease": false } From 633f32e3caabc9f6d8b1fcdfca93a6c8372b7a6e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 16:55:36 -0800 Subject: [PATCH 10/14] Remove 'Span' reference for NETFX test --- .../Test_SourceGeneratorsDiagnostics.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index c60674fac..aad1f1373 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -984,7 +984,6 @@ public partial class SampleViewModel : ObservableObject public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_ReturnsByRefLike_Warns() { const string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp @@ -992,8 +991,10 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [{|MVVMTK0054:ObservableProperty|}] - public partial Span {|CS9248:Name|} { get; set; } + public partial RefStruct {|CS9248:Name|} { get; set; } } + + public ref struct RefStruct; } """; From 2f3c842d1df6d75bc5e3f7ae01e45ff72ed5ece9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 18:22:09 -0800 Subject: [PATCH 11/14] Fixup message for 'MVVMTK0052' diagnostic --- .../Diagnostics/DiagnosticDescriptors.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 2ed0f7c44..5e4bb3304 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -873,7 +873,7 @@ internal static class DiagnosticDescriptors category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "A property using [ObservableProperty] is not a partial implementation part ([ObservableProperty] must be used on partial property definitions with no implementation part).", + description: "A property using [ObservableProperty] is not an incomplete partial definition part ([ObservableProperty] must be used on partial property definitions with no implementation part).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0052"); /// From f080c60378ab374ef6c10ff181d8a3f103e20b0f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 18:32:27 -0800 Subject: [PATCH 12/14] Stop after first invalid property diagnostic --- .../InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs index d0f28170f..e304690fa 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -61,6 +61,8 @@ public override void Initialize(AnalysisContext context) observablePropertyAttribute.GetLocation(), propertySymbol.ContainingType, propertySymbol.Name)); + + return; } } }, SymbolKind.Property); From aa9df8a3b7f628a19388ac1eb8295e7b349f5c33 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 23:20:08 -0800 Subject: [PATCH 13/14] Fixed reporting for 'MVVMTK0052' --- ...PartialPropertyLevelObservablePropertyAttributeAnalyzer.cs | 4 +++- .../Test_SourceGeneratorsDiagnostics.cs | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs index cb6194419..8b144049e 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -27,7 +27,9 @@ public sealed class InvalidPartialPropertyLevelObservablePropertyAttributeAnalyz /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + // This generator is intentionally also analyzing generated code, because Roslyn will interpret properties + // that have '[GeneratedCode]' on them as being generated (and the same will apply to all partial parts). + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index aad1f1373..a92b3d589 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -889,7 +889,6 @@ public partial string Name } [TestMethod] - [Ignore("The symbol callback is not being triggered correctly (see https://github.com/dotnet/roslyn/issues/76166)")] public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnImplementedProperty_GeneratedByAnotherGenerator_Warns() { const string source = """ @@ -913,8 +912,7 @@ public partial string Name } """; - // This test is having issues, let's invoke the analyzer directly to make it easier to narrow down the problem - await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); } [TestMethod] From 889cc08072d041866bc7a014b56196494e590203 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 2 Dec 2024 16:52:02 -0800 Subject: [PATCH 14/14] Fix consistency for 'new()' for diagnostics/suppressions --- .../Diagnostics/DiagnosticDescriptors.cs | 20 +++++++++---------- .../Diagnostics/SuppressionDescriptors.cs | 8 +++++--- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 5e4bb3304..36835788a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -689,7 +689,7 @@ internal static class DiagnosticDescriptors /// Format: "Using [ObservableProperty] on partial properties requires the C# language version to be set to 'preview', as support for the 'field' keyword is needed by the source generators to emit valid code (add preview to your .csproj/.props file)". /// /// - public static readonly DiagnosticDescriptor CSharpLanguageVersionIsNotPreviewForObservableProperty = new( + public static readonly DiagnosticDescriptor CSharpLanguageVersionIsNotPreviewForObservableProperty = new DiagnosticDescriptor( id: "MVVMTK0041", title: "C# language version is not 'preview'", messageFormat: """Using [ObservableProperty] on partial properties requires the C# language version to be set to 'preview', as support for the 'field' keyword is needed by the source generators to emit valid code (add preview to your .csproj/.props file)""", @@ -705,7 +705,7 @@ internal static class DiagnosticDescriptors /// Format: "The field {0}.{1} using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well)". /// /// - public static readonly DiagnosticDescriptor UseObservablePropertyOnPartialProperty = new( + public static readonly DiagnosticDescriptor UseObservablePropertyOnPartialProperty = new DiagnosticDescriptor( id: UseObservablePropertyOnPartialPropertyId, title: "Prefer using [ObservableProperty] on partial properties", messageFormat: """The field {0}.{1} using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well)""", @@ -753,7 +753,7 @@ internal static class DiagnosticDescriptors /// Format: "The field {0}.{1} using [ObservableProperty] will generate code that is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and a partial property should be used instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)". /// /// - public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatible = new( + public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatible = new DiagnosticDescriptor( id: WinRTObservablePropertyOnFieldsIsNotAotCompatibleId, title: "Using [ObservableProperty] on fields is not AOT compatible for WinRT", messageFormat: """The field {0}.{1} using [ObservableProperty] will generate code that is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and a partial property should be used instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)""", @@ -769,7 +769,7 @@ internal static class DiagnosticDescriptors /// Format: "The method {0} using [RelayCommand] within a type also using [GeneratedBindableCustomProperty], which is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator)". /// /// - public static readonly DiagnosticDescriptor WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible = new( + public static readonly DiagnosticDescriptor WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible = new DiagnosticDescriptor( id: "MVVMTK0046", title: "Using [RelayCommand] is not compatible with [GeneratedBindableCustomProperty]", messageFormat: """The method {0} using [RelayCommand] within a type also using [GeneratedBindableCustomProperty], which is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator)""", @@ -785,7 +785,7 @@ internal static class DiagnosticDescriptors /// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}: combining the two generators is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)". /// /// - public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField = new( + public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField = new DiagnosticDescriptor( id: "MVVMTK0047", title: "Using [GeneratedBindableCustomProperty] is not compatible with [ObservableProperty] on fields", messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}: combining the two generators is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)""", @@ -801,7 +801,7 @@ internal static class DiagnosticDescriptors /// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1}: combining the two generators is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)". /// /// - public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand = new( + public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand = new DiagnosticDescriptor( id: "MVVMTK0048", title: "Using [GeneratedBindableCustomProperty] is not compatible with [RelayCommand]", messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1}: combining the two generators is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)""", @@ -849,7 +849,7 @@ internal static class DiagnosticDescriptors /// Format: "This project produced one or more 'MVVMTK0045' warnings due to [ObservableProperty] being used on fields, which is not AOT compatible in WinRT scenarios, but it can't enable partial properties and the associated code fixer because 'LangVersion' is not set to 'preview' (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings)". /// /// - public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo = new( + public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo = new DiagnosticDescriptor( id: "MVVMTK0051", title: "Using [ObservableProperty] with WinRT and AOT requires 'LangVersion=preview'", messageFormat: """This project produced one or more 'MVVMTK0045' warnings due to [ObservableProperty] being used on fields, which is not AOT compatible in WinRT scenarios, but it can't enable partial properties and the associated code fixer because 'LangVersion' is not set to 'preview' (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings)""", @@ -866,7 +866,7 @@ internal static class DiagnosticDescriptors /// Format: "The property {0}.{1} is not an incomplete partial definition ([ObservableProperty] must be used on partial property definitions with no implementation part)". /// /// - public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition = new( + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition = new DiagnosticDescriptor( id: "MVVMTK0052", title: "Using [ObservableProperty] on an invalid property declaration (not incomplete partial definition)", messageFormat: """The property {0}.{1} is not an incomplete partial definition ([ObservableProperty] must be used on partial property definitions with no implementation part)""", @@ -882,7 +882,7 @@ internal static class DiagnosticDescriptors /// Format: "The property {0}.{1} returns a ref value ([ObservableProperty] must be used on properties returning a type by value)". /// /// - public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsByRef = new( + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsByRef = new DiagnosticDescriptor( id: "MVVMTK0053", title: "Using [ObservableProperty] on a property that returns byref", messageFormat: """The property {0}.{1} returns a ref value ([ObservableProperty] must be used on properties returning a type by value)""", @@ -898,7 +898,7 @@ internal static class DiagnosticDescriptors /// Format: "The property {0}.{1} returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type)". /// /// - public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsRefLikeType = new( + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsRefLikeType = new DiagnosticDescriptor( id: "MVVMTK0054", title: "Using [ObservableProperty] on a property that returns byref-like", messageFormat: """The property {0}.{1} returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type)""", diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs index 2f82c8b3a..774024c9f 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -4,6 +4,8 @@ using Microsoft.CodeAnalysis; +#pragma warning disable IDE0090 // Use 'new SuppressionDescriptor(...)' + namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics; /// @@ -14,7 +16,7 @@ internal static class SuppressionDescriptors /// /// Gets a for a field using [ObservableProperty] with an attribute list targeting a property. /// - public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new( + public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new SuppressionDescriptor( id: "MVVMTKSPR0001", suppressedDiagnosticId: "CS0657", justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); @@ -22,7 +24,7 @@ internal static class SuppressionDescriptors /// /// Gets a for a field using [ObservableProperty] with an attribute list targeting a get or set accessor. /// - public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyFieldAccessors = new( + public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyFieldAccessors = new SuppressionDescriptor( id: "MVVMTKSPR0001", suppressedDiagnosticId: "CS0658", justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); @@ -30,7 +32,7 @@ internal static class SuppressionDescriptors /// /// Gets a for a method using [RelayCommand] with an attribute list targeting a field or property. /// - public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new( + public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new SuppressionDescriptor( id: "MVVMTKSPR0002", suppressedDiagnosticId: "CS0657", justification: "Methods using [RelayCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties");