From ae935f514e0f7781d157144ae17787ae8d3a0cd4 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 28 Aug 2024 20:53:11 +0800 Subject: [PATCH 001/146] Update bug_report.yml --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1478f9285..9543006cc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -31,7 +31,6 @@ body: required: true attributes: label: Steps to reproduce - render: text description: Provide steps to reproduce the issue, or let us know why it can't be reproduced (e.g. more complex setup, environment, dependencies, etc...). If you want to insert a code snippet, make sure to properly format it (add 3 backticks ` at the start and end of your code, followed by the language in use, eg. "csharp") and to remove leading whitespace in each line (if you're pasting code from another IDE where it was indented). placeholder: | Example repro steps: From 9abe628c9f3ad3c67293e346f865fb0fb09b980a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Sep 2024 10:30:31 +0200 Subject: [PATCH 002/146] Mark old property value as nullable if needed --- .../ObservablePropertyGenerator.Execute.cs | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index d029295bd..52b7f44fe 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -834,9 +834,6 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf { using ImmutableArrayBuilder setterStatements = ImmutableArrayBuilder.Rent(); - // Get the property type syntax - TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); - string getterFieldIdentifierName; ExpressionSyntax getterFieldExpression; ExpressionSyntax setterFieldExpression; @@ -872,7 +869,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // __oldValue = ; setterStatements.Add( LocalDeclarationStatement( - VariableDeclaration(propertyType) + VariableDeclaration(GetMaybeNullPropertyType(propertyInfo)) .AddVariables( VariableDeclarator(Identifier("__oldValue")) .WithInitializer(EqualsValueClause(setterFieldExpression))))); @@ -1001,6 +998,9 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName)))))); } + // Get the property type syntax + TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); + // Generate the inner setter block as follows: // // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) @@ -1128,17 +1128,8 @@ public static ImmutableArray GetOnPropertyChangeMethods Comment($"/// This method is invoked right before the value of is changed.")), SyntaxKind.OpenBracketToken, TriviaList()))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); - // Prepare the nullable type for the previous property value. This is needed because if the type is a reference - // type, the previous value might be null even if the property type is not nullable, as the first invocation would - // happen when the property is first set to some value that is not null (but the backing field would still be so). - // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability - // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. - TypeSyntax oldValueTypeSyntax = propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch - { - true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") - => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"), - _ => parameterType - }; + // Get the type for the 'oldValue' parameter (which can be null on first invocation) + TypeSyntax oldValueTypeSyntax = GetMaybeNullPropertyType(propertyInfo); // Construct the generated method as follows: // @@ -1224,6 +1215,26 @@ public static ImmutableArray GetOnPropertyChangeMethods onPropertyChanged2Declaration); } + /// + /// Gets the for the type of a given property, when it can possibly be . + /// + /// The input instance to process. + /// The type of a given property, when it can possibly be + private static TypeSyntax GetMaybeNullPropertyType(PropertyInfo propertyInfo) + { + // Prepare the nullable type for the previous property value. This is needed because if the type is a reference + // type, the previous value might be null even if the property type is not nullable, as the first invocation would + // happen when the property is first set to some value that is not null (but the backing field would still be so). + // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability + // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. + return propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch + { + true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") + => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"), + _ => IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations) + }; + } + /// /// Gets a instance with the cached args of a specified type. /// From 8e23ac1a030aa5d9c91077dca3abc886ad22a61a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Sep 2024 10:30:42 +0200 Subject: [PATCH 003/146] Add unit tests for new nullable annotations --- .../Test_SourceGeneratorsCodegen.cs | 280 +++++++++++++++++- 1 file changed, 279 insertions(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index c3e48fba6..a1e954d85 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -2301,7 +2301,7 @@ partial class MyViewModel : ObservableObject [ObservableProperty] double @object; - partial void OnObjectChanged(object oldValue, object NewValue) + partial void OnObjectChanged(double oldValue, double NewValue) { } } @@ -3018,6 +3018,284 @@ public string Name VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } + [TestMethod] + public void ObservableProperty_NotNullableProperty_OfReferenceType_WithChangedMethods() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + string name; + + partial void OnNameChanged(string? oldValue, string NewValue) + { + } + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public string Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + string? __oldValue = name; + OnNameChanging(value); + OnNameChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public string Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + string? __oldValue = name; + OnNameChanging(value); + OnNameChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_NotNullableProperty_OfUnconstrainedGenericType_WithChangedMethods() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + T value; + + partial void OnValueChanged(T? oldValue, T NewValue) + { + } + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Value + { + get => value; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("value")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(this.value, value)) + { + T? __oldValue = this.value; + OnValueChanging(value); + OnValueChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Value); + this.value = value; + OnValueChanged(value); + OnValueChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Value); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T? oldValue, T newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Value + { + get => value; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(this.value, value)) + { + T? __oldValue = this.value; + OnValueChanging(value); + OnValueChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Value); + this.value = value; + OnValueChanged(value); + OnValueChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Value); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T? oldValue, T newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + /// /// Generates the requested sources /// From 9c0725a894f49efe4bf6a469c4b97f24006ce2ed Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Sep 2024 12:58:24 +0200 Subject: [PATCH 004/146] Add .targets to validate the Windows SDK version --- .../CommunityToolkit.Mvvm.WindowsSdk.targets | 62 +++++++++++++++++++ .../CommunityToolkit.Mvvm.csproj | 2 + .../CommunityToolkit.Mvvm.targets | 2 + 3 files changed, 66 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets new file mode 100644 index 000000000..5b7389ec7 --- /dev/null +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets @@ -0,0 +1,62 @@ + + + + + + true + + + + $(ResolveReferencesDependsOn);MvvmToolkitVerifyWindowsSdkPackageVersion + + + + + + + + + <_MvvmToolkitWindowsSdkPackageMinBuild>41 + + + + + <_MvvmToolkitWindowsSdkPackage Include="$(WindowsSdkPackageVersion)" Condition="'$(WindowsSdkPackageVersion)' != ''"> + $(WindowsSdkPackageVersion) + 10.0.$([System.Version]::Parse("$(WindowsSdkPackageVersion.Split('-')[0])").Build).$(_MvvmToolkitWindowsSdkPackageMinBuild) + + + + <_MvvmToolkitWindowsSdkPackage + Include="@(ResolvedFrameworkReference)" + Condition="'$(WindowsSdkPackageVersion)' == '' AND '@(ResolvedFrameworkReference)' != '' AND '%(Identity)' == 'Microsoft.Windows.SDK.NET.Ref'"> + %(ResolvedFrameworkReference.TargetingPackVersion) + 10.0.$([System.Version]::Parse("%(ResolvedFrameworkReference.TargetingPackVersion)").Build).$(_MvvmToolkitWindowsSdkPackageMinBuild) + + + + <_MvvmToolkitCompatibleWindowsSdkPackages + Include="@(_MvvmToolkitWindowsSdkPackage)" + Condition="'@(_MvvmToolkitWindowsSdkPackage)' != '' AND $([MSBuild]::VersionGreaterThanOrEquals(%(Referenced), %(Required)))" /> + + + + + <_MvvmToolkitWindowsSdkPackageRequired>@(_MvvmToolkitWindowsSdkPackage->'%(Required)') + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index ef94a0800..94c2f8bf0 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -99,9 +99,11 @@ + + + \ No newline at end of file From bbb4fb21bee84d30d32779571bf1a95f231bcfc3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Sep 2024 17:03:40 +0200 Subject: [PATCH 005/146] Suggest package versions based on configuration --- .../CommunityToolkit.Mvvm.WindowsSdk.targets | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets index 5b7389ec7..ee9b5c2c8 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets @@ -21,8 +21,20 @@ - - <_MvvmToolkitWindowsSdkPackageMinBuild>41 + + <_MvvmToolkitWindowsSdkPackageMinBuild>38 + <_MvvmToolkitWindowsSdkPackageRecommendedUwpBuild>39 + <_MvvmToolkitWindowsSdkPackageRecommendedWindowsAppSDKBuild>41 + + + <_MvvmToolkitWindowsSdkPackageRecommendedBuild>$(_MvvmToolkitWindowsSdkPackageMinBuild) + <_MvvmToolkitWindowsSdkPackageRecommendedBuild Condition="'$(UseUwp)' == 'true'">$(_MvvmToolkitWindowsSdkPackageRecommendedUwpBuild) + <_MvvmToolkitWindowsSdkPackageRecommendedBuild Condition="'$(UseUwp)' != 'true'">$(_MvvmToolkitWindowsSdkPackageRecommendedWindowsAppSDKBuild) @@ -30,6 +42,7 @@ <_MvvmToolkitWindowsSdkPackage Include="$(WindowsSdkPackageVersion)" Condition="'$(WindowsSdkPackageVersion)' != ''"> $(WindowsSdkPackageVersion) 10.0.$([System.Version]::Parse("$(WindowsSdkPackageVersion.Split('-')[0])").Build).$(_MvvmToolkitWindowsSdkPackageMinBuild) + 10.0.$([System.Version]::Parse("$(WindowsSdkPackageVersion.Split('-')[0])").Build).$(_MvvmToolkitWindowsSdkPackageRecommendedBuild) @@ -38,6 +51,7 @@ Condition="'$(WindowsSdkPackageVersion)' == '' AND '@(ResolvedFrameworkReference)' != '' AND '%(Identity)' == 'Microsoft.Windows.SDK.NET.Ref'"> %(ResolvedFrameworkReference.TargetingPackVersion) 10.0.$([System.Version]::Parse("%(ResolvedFrameworkReference.TargetingPackVersion)").Build).$(_MvvmToolkitWindowsSdkPackageMinBuild) + 10.0.$([System.Version]::Parse("%(ResolvedFrameworkReference.TargetingPackVersion)").Build).$(_MvvmToolkitWindowsSdkPackageRecommendedBuild) @@ -47,8 +61,9 @@ - + <_MvvmToolkitWindowsSdkPackageRequired>@(_MvvmToolkitWindowsSdkPackage->'%(Required)') + <_MvvmToolkitWindowsSdkPackageRecommended>@(_MvvmToolkitWindowsSdkPackage->'%(Recommended)') @@ -56,7 +71,7 @@ Condition="'@(_MvvmToolkitCompatibleWindowsSdkPackages)' == ''" Code="MVVMTKCFG0003" HelpLink="https://aka.ms/mvvmtoolkit/errors/mvvmtkcfg0003" - Text="This version of the MVVM Toolkit requires 'Microsoft.Windows.SDK.NET.Ref' version '$(_MvvmToolkitWindowsSdkPackageRequired)' or later. Please update to .NET SDK 8.0.109, 8.0.305 or 8.0.402 (or later). Alternatively, use a temporary 'Microsoft.Windows.SDK.NET.Ref' reference, which can be done by setting the 'WindowsSdkPackageVersion' property in your .csproj file to '$(_MvvmToolkitWindowsSdkPackageRequired)'." /> + Text="This version of the MVVM Toolkit requires 'Microsoft.Windows.SDK.NET.Ref' version '$(_MvvmToolkitWindowsSdkPackageRequired)' or later. Please update to .NET SDK 8.0.109, 8.0.305 or 8.0.402 (or later). Alternatively, use a temporary 'Microsoft.Windows.SDK.NET.Ref' reference, which can be done by setting the 'WindowsSdkPackageVersion' property in your .csproj file. For your project configuration, it is recommended to set the package version to '$(_MvvmToolkitWindowsSdkPackageRecommended)'." /> \ No newline at end of file From 67872c12def74796eb171b198302ea700aa19034 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 15 Sep 2024 12:37:59 +0200 Subject: [PATCH 006/146] Update SDK package checks for .NET 9 SDK --- .../CommunityToolkit.Mvvm.WindowsSdk.targets | 15 +++++++++++++-- .../CommunityToolkit.Mvvm.csproj | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets index ee9b5c2c8..1655776c5 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets @@ -45,10 +45,21 @@ 10.0.$([System.Version]::Parse("$(WindowsSdkPackageVersion.Split('-')[0])").Build).$(_MvvmToolkitWindowsSdkPackageRecommendedBuild) - + <_MvvmToolkitWindowsSdkPackage Include="@(ResolvedFrameworkReference)" - Condition="'$(WindowsSdkPackageVersion)' == '' AND '@(ResolvedFrameworkReference)' != '' AND '%(Identity)' == 'Microsoft.Windows.SDK.NET.Ref'"> + Condition="'$(WindowsSdkPackageVersion)' == '' AND + '@(ResolvedFrameworkReference)' != '' AND + ('%(Identity)' == 'Microsoft.Windows.SDK.NET.Ref' OR '%(Identity)' == 'Microsoft.Windows.SDK.NET.Ref.Windows')"> %(ResolvedFrameworkReference.TargetingPackVersion) 10.0.$([System.Version]::Parse("%(ResolvedFrameworkReference.TargetingPackVersion)").Build).$(_MvvmToolkitWindowsSdkPackageMinBuild) 10.0.$([System.Version]::Parse("%(ResolvedFrameworkReference.TargetingPackVersion)").Build).$(_MvvmToolkitWindowsSdkPackageRecommendedBuild) diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index 94c2f8bf0..d3f647efd 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -61,7 +61,7 @@ - + From 07971586a3901d71b19387794eb7dee6301e849c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Sep 2024 14:46:59 +0200 Subject: [PATCH 007/146] Add support for forwarding accessor attributes --- .../ComponentModel/Models/AttributeInfo.cs | 7 +++ .../ObservablePropertyGenerator.Execute.cs | 36 +++++++---- .../Input/Models/CommandInfo.cs | 6 +- .../Input/RelayCommandGenerator.Execute.cs | 60 ++++++++----------- 4 files changed, 59 insertions(+), 50 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs index 1009cd4c1..74b456063 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs @@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using CommunityToolkit.Mvvm.SourceGenerators.Helpers; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -17,10 +18,12 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// /// A model representing an attribute declaration. /// +/// Indicates the target of the attribute. /// The type name of the attribute. /// The values for all constructor arguments for the attribute. /// The values for all named arguments for the attribute. internal sealed record AttributeInfo( + SyntaxKind AttributeTarget, string TypeName, EquatableArray ConstructorArgumentInfo, EquatableArray<(string Name, TypedConstantInfo Value)> NamedArgumentInfo) @@ -50,6 +53,7 @@ public static AttributeInfo Create(AttributeData attributeData) } return new( + SyntaxKind.PropertyKeyword, typeName, constructorArguments.ToImmutable(), namedArguments.ToImmutable()); @@ -61,6 +65,7 @@ public static AttributeInfo Create(AttributeData attributeData) /// The symbol for the attribute type. /// The instance for the current run. /// The sequence of instances to process. + /// The kind of target for the attribute. /// The cancellation token for the current operation. /// The resulting instance, if available /// Whether a resulting instance could be created. @@ -68,6 +73,7 @@ public static bool TryCreate( INamedTypeSymbol typeSymbol, SemanticModel semanticModel, IEnumerable arguments, + SyntaxKind syntaxKind, CancellationToken token, [NotNullWhen(true)] out AttributeInfo? info) { @@ -105,6 +111,7 @@ public static bool TryCreate( } info = new AttributeInfo( + syntaxKind, typeName, constructorArguments.ToImmutable(), namedArguments.ToImmutable()); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 52b7f44fe..b127bc7a3 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -216,10 +216,10 @@ public static bool TryGetInfo( // Gather explicit forwarded attributes info foreach (AttributeListSyntax attributeList in fieldSyntax.AttributeLists) { - // Only look for attribute lists explicitly targeting the (generated) property. Roslyn will normally emit a - // CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic suppressor - // that recognizes uses of this target specifically to support [ObservableProperty]. - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword)) + // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will + // normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic + // suppressor that recognizes uses of this target specifically to support [ObservableProperty]. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.GetKeyword or SyntaxKind.SetKeyword) targetIdentifier) { continue; } @@ -256,7 +256,7 @@ public static bool TryGetInfo( IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo)) + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) { builder.Add( InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, @@ -1025,11 +1025,22 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf Argument(IdentifierName("value")))), Block(setterStatements.AsEnumerable())); - // Prepare the forwarded attributes, if any - ImmutableArray forwardedAttributes = + // Prepare the forwarded attributes, if any, for all targets + AttributeListSyntax[] forwardedPropertyAttributes = propertyInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.PropertyKeyword) .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); + .ToArray(); + AttributeListSyntax[] forwardedGetAccessorAttributes = + propertyInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.GetKeyword) + .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) + .ToArray(); + AttributeListSyntax[] forwardedSetAccessorAttributes = + propertyInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.SetKeyword) + .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) + .ToArray(); // Prepare the setter for the generated property: // @@ -1065,6 +1076,9 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("The type of the current instance cannot be statically discovered."))))))); } + // Also add any forwarded attributes + setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes); + // Construct the generated property as follows: // // /// @@ -1073,6 +1087,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // // public // { + // // get => ; // // } @@ -1086,12 +1101,13 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) - .AddAttributeLists(forwardedAttributes.ToArray()) + .AddAttributeLists(forwardedPropertyAttributes) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithExpressionBody(ArrowExpressionClause(getterFieldExpression)) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .AddAttributeLists(forwardedGetAccessorAttributes), setAccessor); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs index 16ca48c0a..70372a03f 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs @@ -23,8 +23,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models; /// Whether or not concurrent executions have been enabled. /// Whether or not exceptions should flow to the task scheduler. /// Whether or not to also generate a cancel command. -/// The sequence of forwarded attributes for the generated field. -/// The sequence of forwarded attributes for the generated property. +/// The sequence of forwarded attributes for the generated members. internal sealed record CommandInfo( string MethodName, string FieldName, @@ -39,5 +38,4 @@ internal sealed record CommandInfo( bool AllowConcurrentExecutions, bool FlowExceptionsToTaskScheduler, bool IncludeCancelCommand, - EquatableArray ForwardedFieldAttributes, - EquatableArray ForwardedPropertyAttributes); + EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs index e5b753160..d40086135 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs @@ -141,8 +141,7 @@ public static bool TryGetInfo( semanticModel, token, in builder, - out ImmutableArray fieldAttributes, - out ImmutableArray propertyAttributes); + out ImmutableArray forwardedAttributes); token.ThrowIfCancellationRequested(); @@ -160,8 +159,7 @@ public static bool TryGetInfo( allowConcurrentExecutions, flowExceptionsToTaskScheduler, generateCancelCommand, - fieldAttributes, - propertyAttributes); + forwardedAttributes); diagnostics = builder.ToImmutable(); @@ -196,16 +194,18 @@ public static ImmutableArray GetSyntax(CommandInfo comm : $"{commandInfo.DelegateType}<{string.Join(", ", commandInfo.DelegateTypeArguments)}>"; // Prepare the forwarded field attributes, if any - ImmutableArray forwardedFieldAttributes = - commandInfo.ForwardedFieldAttributes + AttributeListSyntax[] forwardedFieldAttributes = + commandInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.FieldKeyword) .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); + .ToArray(); // Also prepare any forwarded property attributes - ImmutableArray forwardedPropertyAttributes = - commandInfo.ForwardedPropertyAttributes + AttributeListSyntax[] forwardedPropertyAttributes = + commandInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.PropertyKeyword) .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); + .ToArray(); // Construct the generated field as follows: // @@ -225,7 +225,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))) - .AddAttributeLists(forwardedFieldAttributes.ToArray()); + .AddAttributeLists(forwardedFieldAttributes); // Prepares the argument to pass the underlying method to invoke using ImmutableArrayBuilder commandCreationArguments = ImmutableArrayBuilder.Rent(); @@ -332,7 +332,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) - .AddAttributeLists(forwardedPropertyAttributes.ToArray()) + .AddAttributeLists(forwardedPropertyAttributes) .WithExpressionBody( ArrowExpressionClause( AssignmentExpression( @@ -972,26 +972,22 @@ private static bool TryGetCanExecuteMemberFromGeneratedProperty( /// The instance for the current run. /// The cancellation token for the current operation. /// The current collection of gathered diagnostics. - /// The resulting field attributes to forward. - /// The resulting property attributes to forward. + /// The resulting attributes to forward. private static void GatherForwardedAttributes( IMethodSymbol methodSymbol, SemanticModel semanticModel, CancellationToken token, in ImmutableArrayBuilder diagnostics, - out ImmutableArray fieldAttributes, - out ImmutableArray propertyAttributes) + out ImmutableArray forwardedAttributes) { - using ImmutableArrayBuilder fieldAttributesInfo = ImmutableArrayBuilder.Rent(); - using ImmutableArrayBuilder propertyAttributesInfo = ImmutableArrayBuilder.Rent(); + using ImmutableArrayBuilder forwardedAttributesInfo = ImmutableArrayBuilder.Rent(); static void GatherForwardedAttributes( IMethodSymbol methodSymbol, SemanticModel semanticModel, CancellationToken token, in ImmutableArrayBuilder diagnostics, - in ImmutableArrayBuilder fieldAttributesInfo, - in ImmutableArrayBuilder propertyAttributesInfo) + in ImmutableArrayBuilder forwardedAttributesInfo) { // Get the single syntax reference for the input method symbol (there should be only one) if (methodSymbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference]) @@ -1009,7 +1005,7 @@ static void GatherForwardedAttributes( foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists) { // Same as in the [ObservableProperty] generator, except we're also looking for fields here - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword)) + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword) targetIdentifier) { continue; } @@ -1033,7 +1029,7 @@ static void GatherForwardedAttributes( IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo)) + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) { diagnostics.Add( InvalidFieldOrPropertyTargetedAttributeExpressionOnRelayCommandMethod, @@ -1044,15 +1040,8 @@ static void GatherForwardedAttributes( continue; } - // Add the new attribute info to the right builder - if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword)) - { - fieldAttributesInfo.Add(attributeInfo); - } - else - { - propertyAttributesInfo.Add(attributeInfo); - } + // Add the new attribute info to the builder + forwardedAttributesInfo.Add(attributeInfo); } } } @@ -1064,17 +1053,16 @@ static void GatherForwardedAttributes( IMethodSymbol partialImplementation = methodSymbol.PartialImplementationPart ?? methodSymbol; // We always give priority to the partial definition, to ensure a predictable and testable ordering - GatherForwardedAttributes(partialDefinition, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo); - GatherForwardedAttributes(partialImplementation, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo); + GatherForwardedAttributes(partialDefinition, semanticModel, token, in diagnostics, in forwardedAttributesInfo); + GatherForwardedAttributes(partialImplementation, semanticModel, token, in diagnostics, in forwardedAttributesInfo); } else { // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributes(methodSymbol, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo); + GatherForwardedAttributes(methodSymbol, semanticModel, token, in diagnostics, in forwardedAttributesInfo); } - fieldAttributes = fieldAttributesInfo.ToImmutable(); - propertyAttributes = propertyAttributesInfo.ToImmutable(); + forwardedAttributes = forwardedAttributesInfo.ToImmutable(); } } } From 5d9ce3ee16d410b0edc45e3db9b5b707b3ce6b5d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Sep 2024 15:36:25 +0200 Subject: [PATCH 008/146] Update diagnostic suppressor for forwarded attributes --- .../CommunityToolkit.Mvvm.SourceGenerators.projitems | 2 +- .../Diagnostics/SuppressionDescriptors.cs | 10 +++++++++- ...ttributeWithSupportedTargetDiagnosticSuppressor.cs} | 8 ++++---- 3 files changed, 14 insertions(+), 6 deletions(-) rename src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/{ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs => ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs} (85%) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index c4cbb324e..9dc77b7d1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -48,7 +48,7 @@ - + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs index 4a6c803a9..2f82c8b3a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -17,7 +17,15 @@ internal static class SuppressionDescriptors public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new( id: "MVVMTKSPR0001", suppressedDiagnosticId: "CS0657", - justification: "Fields using [ObservableProperty] can use [property:] attribute lists to forward attributes to the generated properties"); + justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); + + /// + /// Gets a for a field using [ObservableProperty] with an attribute list targeting a get or set accessor. + /// + public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyFieldAccessors = new( + id: "MVVMTKSPR0001", + suppressedDiagnosticId: "CS0658", + justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); /// /// Gets a for a method using [RelayCommand] with an attribute list targeting a field or property. diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs similarity index 85% rename from src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs rename to src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs index f27f3969a..0b9b4246d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs @@ -14,7 +14,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// /// -/// A diagnostic suppressor to suppress CS0657 warnings for fields with [ObservableProperty] using a [property:] attribute list. +/// A diagnostic suppressor to suppress CS0657 warnings for fields with [ObservableProperty] using a [property:] attribute list (or [set:] or [get:]). /// /// /// That is, this diagnostic suppressor will suppress the following diagnostic: @@ -29,10 +29,10 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// /// [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor +public sealed class ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor : DiagnosticSuppressor { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(PropertyAttributeListForObservablePropertyField); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(PropertyAttributeListForObservablePropertyField, PropertyAttributeListForObservablePropertyFieldAccessors); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -43,7 +43,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) // Check that the target is effectively [property:] over a field declaration with at least one variable, which is the only case we are interested in if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: FieldDeclarationSyntax { Declaration.Variables.Count: > 0 } fieldDeclaration } attributeTarget && - attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword)) + (attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword) || attributeTarget.Identifier.IsKind(SyntaxKind.GetKeyword) || attributeTarget.Identifier.IsKind(SyntaxKind.SetKeyword))) { SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); From c7e454a8c7aef4029d1b3e9e7577f6b099ab18ad Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Sep 2024 16:24:58 +0200 Subject: [PATCH 009/146] Add unit test for accessor attributes --- .../Test_SourceGeneratorsCodegen.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index a1e954d85..6997cfaa1 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -3296,6 +3296,104 @@ public T Value VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); } + [TestMethod] + public void ObservablePropertyWithForwardedAttributes_OnPropertyAccessors() + { + string source = """ + using System.ComponentModel; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [property: Test("Property1")] + [property: Test("Property2")] + [property: Test("Property3")] + [get: Test("Get1")] + [get: Test("Get2")] + [set: Test("Set1")] + [set: Test("Set2")] + private object? a; + } + + public class TestAttribute : Attribute + { + public TestAttribute(string value) + { + } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::MyApp.TestAttribute("Property1")] + [global::MyApp.TestAttribute("Property2")] + [global::MyApp.TestAttribute("Property3")] + public object? A + { + [global::MyApp.TestAttribute("Get1")] + [global::MyApp.TestAttribute("Get2")] + get => a; + [global::MyApp.TestAttribute("Set1")] + [global::MyApp.TestAttribute("Set2")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(a, value)) + { + OnAChanging(value); + OnAChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.A); + a = value; + OnAChanged(value); + OnAChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.A); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanging(object? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanging(object? oldValue, object? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanged(object? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanged(object? oldValue, object? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + /// /// Generates the requested sources /// From 8404e3ae451f775066c7cedcb8630632e089f40b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Sep 2024 21:58:46 +0200 Subject: [PATCH 010/146] Add unit test for updated diagnostic suppressor --- .../Test_ObservablePropertyAttribute.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index 495a9cd59..6b3e25796 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs @@ -1797,4 +1797,30 @@ private sealed partial class ModelWithDependentPropertyAndNoPropertyChanging public string? FullName => ""; } + +#if NET6_0_OR_GREATER + // See https://github.com/CommunityToolkit/dotnet/issues/939 + public partial class ModelWithSecondaryPropertySetFromGeneratedSetter_DoesNotWarn : ObservableObject + { + [ObservableProperty] + [set: MemberNotNull(nameof(B))] + private string a; + + // This type validates forwarding attributes on generated accessors. In particular, there should + // be no nullability warning on this constructor (CS8618), thanks to 'MemberNotNullAttribute("B")' + // being forwarded to the generated setter in the generated property (see linked issue). + public ModelWithSecondaryPropertySetFromGeneratedSetter_DoesNotWarn() + { + A = ""; + } + + public string B { get; private set; } + + [MemberNotNull(nameof(B))] + partial void OnAChanged(string? oldValue, string newValue) + { + B = ""; + } + } +#endif } From d52dec909734d21cc466adadee021cbbf4a07476 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 22:02:26 -0700 Subject: [PATCH 011/146] Fix suppressions for custom attribute targets --- ...ttributeWithSupportedTargetDiagnosticSuppressor.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs index 0b9b4246d..57bae864b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs @@ -55,7 +55,16 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol observablePropertySymbol && fieldSymbol.HasAttributeWithType(observablePropertySymbol)) { - context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyField, diagnostic)); + // Emit the right suppression based on the attribute modifier. For 'property:', Roslyn + // will emit the 'CS0657' warning, whereas for 'get:' or 'set:', it will emit 'CS0658'. + if (attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword)) + { + context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyField, diagnostic)); + } + else + { + context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyFieldAccessors, diagnostic)); + } } } } From 11d15c9dbd59e5ba6bbceb27dce39aeec3283e0a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:13:59 -0700 Subject: [PATCH 012/146] Add blank 'Roslyn4110' project --- dotnet Community Toolkit.sln | 23 +++++++++++++++++++ ...it.Mvvm.SourceGenerators.Roslyn4110.csproj | 6 +++++ 2 files changed, 29 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index 688e32367..f66684005 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -83,6 +83,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Exter EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj", "{FCC13AD5-CEB8-4CC1-8250-89B616D126F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -457,6 +459,26 @@ Global {E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x64.Build.0 = Release|Any CPU {E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x86.ActiveCfg = Release|Any CPU {E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x86.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM64.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x64.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x86.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|Any CPU.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM64.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM64.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x64.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x64.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -495,6 +517,7 @@ Global tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{ecfe93aa-4b98-4292-b3fa-9430d513b4f9}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{f3799252-7a66-4533-89d8-b3c312052d95}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{fb59ce88-7732-4a63-b5bd-ac5681b7da1a}*SharedItemsImports = 13 + src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{fcc13ad5-ceb8-4cc1-8250-89b616d126f2}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{fe3ea695-ea0f-4e5f-9257-e059aaa23b10}*SharedItemsImports = 5 EndGlobalSection EndGlobal diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj new file mode 100644 index 000000000..3cea30b35 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj @@ -0,0 +1,6 @@ + + + + + + From e27359fbb080d89811e1bbda25e2f45c7f6e1d10 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:22:39 -0700 Subject: [PATCH 013/146] Fix naming conventions for all Roslyn projects --- dotnet Community Toolkit.sln | 16 ++++++++-------- .../CommunityToolkit.Mvvm.CodeFixers.csproj | 2 +- ...lkit.Mvvm.SourceGenerators.Roslyn4001.csproj} | 0 ...lkit.Mvvm.SourceGenerators.Roslyn4031.csproj} | 0 .../CommunityToolkit.Mvvm.SourceGenerators.props | 10 ++++++---- .../CommunityToolkit.Mvvm.csproj | 8 ++++---- ...sableINotifyPropertyChanging.UnitTests.csproj | 2 +- ...lkit.Mvvm.ExternalAssembly.Roslyn4001.csproj} | 2 +- ...lkit.Mvvm.ExternalAssembly.Roslyn4031.csproj} | 2 +- ...nityToolkit.Mvvm.Roslyn4001.UnitTests.csproj} | 4 ++-- ...nityToolkit.Mvvm.Roslyn4031.UnitTests.csproj} | 4 ++-- ...SourceGenerators.Roslyn4001.UnitTests.csproj} | 2 +- ...ncVoidReturningRelayCommandMethodCodeFixer.cs | 2 +- ...singAttributeInsteadOfInheritanceCodeFixer.cs | 2 +- ...ferenceForObservablePropertyFieldCodeFixer.cs | 2 +- ...SourceGenerators.Roslyn4031.UnitTests.csproj} | 2 +- 16 files changed, 31 insertions(+), 29 deletions(-) rename src/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.csproj} (100%) rename src/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn431/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.csproj} (100%) rename tests/{CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj => CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj} (81%) rename tests/{CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj => CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj} (77%) rename tests/{CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj => CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj} (80%) rename tests/{CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj => CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj} (80%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj} (92%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests}/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs (98%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests}/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs (99%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests}/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs (99%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj} (92%) diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index f66684005..651a17bce 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -36,7 +36,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{88C6FFBE-3 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Diagnostics", "src\CommunityToolkit.Diagnostics\CommunityToolkit.Diagnostics.csproj", "{76F89522-CA28-458D-801D-947AB033A758}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn401", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj", "{E24D1146-5AD8-498F-A518-4890D8BF4937}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.csproj", "{E24D1146-5AD8-498F-A518-4890D8BF4937}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Diagnostics.UnitTests", "tests\CommunityToolkit.Diagnostics.UnitTests\CommunityToolkit.Diagnostics.UnitTests.csproj", "{35E48D4D-6433-4B70-98A9-BA544921EE04}" EndProject @@ -61,25 +61,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Inter EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.SourceGenerators", "src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.shproj", "{5E7F1212-A54B-40CA-98C5-1FF5CD1A1638}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn431", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj", "{DF455C40-B18E-4890-8758-7CCCB5CA7052}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.csproj", "{DF455C40-B18E-4890-8758-7CCCB5CA7052}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.UnitTests", "tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.shproj", "{B8DCD82E-B53B-4249-AD4E-F9B99ACB9334}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn401.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn401.UnitTests\CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj", "{AD9C3223-8E37-4FD4-A0D4-A45119551D3A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn4001.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4001.UnitTests\CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj", "{AD9C3223-8E37-4FD4-A0D4-A45119551D3A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn431.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn431.UnitTests\CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj", "{5B44F7F1-DCA2-4776-924E-A266F7BBF753}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn4031.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4031.UnitTests\CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj", "{5B44F7F1-DCA2-4776-924E-A266F7BBF753}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.SourceGenerators.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.shproj", "{FB59CE88-7732-4A63-B5BD-AC5681B7DA1A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj", "{F3799252-7A66-4533-89D8-B3C312052D95}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj", "{F3799252-7A66-4533-89D8-B3C312052D95}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj", "{FE3EA695-EA0F-4E5F-9257-E059AAA23B10}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj", "{FE3EA695-EA0F-4E5F-9257-E059AAA23B10}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.ExternalAssembly", "tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.shproj", "{E827A9CD-405F-43E4-84C7-68CC7E845CDC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj", "{ECFE93AA-4B98-4292-B3FA-9430D513B4F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj", "{ECFE93AA-4B98-4292-B3FA-9430D513B4F9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj", "{4FCD501C-1BB5-465C-AD19-356DAB6600C6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj", "{4FCD501C-1BB5-465C-AD19-356DAB6600C6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" EndProject diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj index 498bfc4b3..01020c625 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.csproj similarity index 100% rename from src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj rename to src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.csproj diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.csproj similarity index 100% rename from src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj rename to src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.csproj diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props index 6c5a90f5e..0a2e05c33 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props @@ -16,20 +16,22 @@ --> - - $(MSBuildProjectName.Substring(0, $([MSBuild]::Subtract($(MSBuildProjectName.Length), 10)))) + + $(MSBuildProjectName.Substring(0, $([MSBuild]::Subtract($(MSBuildProjectName.Length), 11)))) - $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 3)), 1)) - $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 2)), 1)) + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 4)), 1)) + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 3)), 2)) $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 1)), 1)) $(MvvmToolkitSourceGeneratorRoslynMajorVersion).$(MvvmToolkitSourceGeneratorRoslynMinorVersion).$(MvvmToolkitSourceGeneratorRoslynPatchVersion) $(DefineConstants);ROSLYN_4_3_1_OR_GREATER + $(DefineConstants);ROSLYN_4_11_0_OR_GREATER diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index d3f647efd..bb1b40b8e 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -66,8 +66,8 @@ - - + + @@ -116,8 +116,8 @@ Even though the fixer only references the 4.0.1 generator target, both versions export the same APIs that the code fixer project needs, and Roslyn versions are also forward compatible. --> - - + + diff --git a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj index 76f768ef0..9a4086826 100644 --- a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj @@ -12,7 +12,7 @@ - + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj similarity index 81% rename from tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj rename to tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj index ee36d9f08..ae18e53f9 100644 --- a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj +++ b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj similarity index 77% rename from tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj rename to tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj index 164e42e50..4a58a1151 100644 --- a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj +++ b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj similarity index 80% rename from tests/CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj index 115f23003..2ce1d0992 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj @@ -15,9 +15,9 @@ - + - + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj similarity index 80% rename from tests/CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj index c1973e0eb..460adb7b6 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj @@ -15,9 +15,9 @@ - + - + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj similarity index 92% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj index c1717ef47..584c015e3 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj @@ -16,7 +16,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs similarity index 98% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs index 5062572d1..cff9df992 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.AsyncVoidReturningRelayCommandMethodCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; [TestClass] public class Test_AsyncVoidReturningRelayCommandMethodCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs similarity index 99% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs index 018c5fc63..a2d3e920d 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.ClassUsingAttributeInsteadOfInheritanceCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; [TestClass] public class ClassUsingAttributeInsteadOfInheritanceCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs similarity index 99% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs index 596a11b66..9771ccd08 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.FieldReferenceForObservablePropertyFieldCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; [TestClass] public class Test_FieldReferenceForObservablePropertyFieldCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj similarity index 92% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index 7323b4a69..cbea561a0 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj @@ -15,7 +15,7 @@ - + From a67a31dfb0ca08986d6c2c16e69c73218e6a6ff8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:37:24 -0700 Subject: [PATCH 014/146] Add 'global.json' file --- global.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 000000000..1880a952c --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.403", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} \ No newline at end of file From 9c9061d50469ea538cd6377de119d76b9a615cdb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:42:05 -0700 Subject: [PATCH 015/146] Update .targets for Roslyn setup --- .../CommunityToolkit.Mvvm.SourceGenerators.targets | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets index aa4b8966f..1106146fa 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets @@ -86,6 +86,7 @@ removes and removes all analyzers except the highest version that is supported. The fallback is just Roslyn 4.0. --> + roslyn4.11 roslyn4.3 roslyn4.0 From f4fdb276e1ff472594ad0a9b11f35336d6efa118 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:42:13 -0700 Subject: [PATCH 016/146] Pack new source generator in MVVM Toolkit --- src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index bb1b40b8e..311b435de 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -68,6 +68,7 @@ + @@ -118,8 +119,10 @@ --> + + \ No newline at end of file From c8e794beeefdf89c95770727f149da6a588d4f5d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:57:43 -0700 Subject: [PATCH 017/146] Add logic to match partial properties --- .../ObservablePropertyGenerator.Execute.cs | 114 ++++++++++++++++++ .../ObservablePropertyGenerator.cs | 2 +- .../Extensions/SyntaxNodeExtensions.cs | 28 +++++ 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index b127bc7a3..007de9e02 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -30,6 +30,66 @@ partial class ObservablePropertyGenerator /// internal static class Execute { + /// + /// Checks whether an input syntax node is a candidate property declaration for the generator. + /// + /// The input syntax node to check. + /// The used to cancel the operation, if needed. + /// Whether is a candidate property declaration. + public static bool IsCandidatePropertyDeclaration(SyntaxNode node, CancellationToken token) + { + // Matches a valid field declaration, for legacy support + static bool IsCandidateField(SyntaxNode node) + { + return node is VariableDeclaratorSyntax { + Parent: VariableDeclarationSyntax { + Parent: FieldDeclarationSyntax { + Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }; + } + +#if ROSLYN_4_11_0_OR_GREATER + // Matches a valid partial property declaration + static bool IsCandidateProperty(SyntaxNode node) + { + // The node must be a property declaration with two accessors + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors } property) + { + return false; + } + + // The property must be partial (we'll check that it's a declaration from its symbol) + if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + 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)) + { + return false; + } + + return true; + } + + // We only support matching properties on Roslyn 4.11 and greater + if (!IsCandidateField(node) && !IsCandidateProperty(node)) + { + return false; + } +#else + // Otherwise, we only support matching fields + if (!IsCandidateField(node)) + { + return false; + } +#endif + + // The property must be in a type with a base type (as it must derive from ObservableObject) + return node.Parent?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; + } + /// /// Processes a given field. /// @@ -799,6 +859,60 @@ private static void GetNullabilityInfo( semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } + /// + /// Tries to get the accessibility of the property and accessors, if possible. + /// + /// The input node. + /// The input instance. + /// The accessibility of the property, if available. + /// The accessibility of the accessor, if available. + /// The accessibility of the accessor, if available. + /// Whether the property was valid and the accessibilities could be retrieved. + private static bool TryGetAccessibilityModifiers( + PropertyDeclarationSyntax node, + IPropertySymbol symbol, + out Accessibility declaredAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility) + { + declaredAccessibility = Accessibility.NotApplicable; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; + + // Ensure that we have a getter and a setter, and that the setter is not init-only + if (symbol is not { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + { + return false; + } + + // Track the property accessibility if explicitly set + if (node.Modifiers.Count > 0) + { + declaredAccessibility = symbol.DeclaredAccessibility; + } + + // Track the accessors accessibility, if explicitly set + foreach (AccessorDeclarationSyntax accessor in node.AccessorList?.Accessors ?? []) + { + if (accessor.Modifiers.Count == 0) + { + continue; + } + + switch (accessor.Kind()) + { + case SyntaxKind.GetAccessorDeclaration: + getterAccessibility = getMethod.DeclaredAccessibility; + break; + case SyntaxKind.SetAccessorDeclaration: + setterAccessibility = setMethod.DeclaredAccessibility; + break; + } + } + + return true; + } + /// /// Gets a instance with the cached args for property changing notifications. /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 68801d449..df24ac51a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -27,7 +27,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> propertyInfoWithErrors = context.ForAttributeWithMetadataNameAndOptions( "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute", - static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, + Execute.IsCandidatePropertyDeclaration, static (context, token) => { if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs index 52b7ccbfc..c7a08da9d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -30,4 +32,30 @@ public static bool IsFirstSyntaxDeclarationForSymbol(this SyntaxNode syntaxNode, syntaxReference.SyntaxTree == syntaxNode.SyntaxTree && syntaxReference.Span == syntaxNode.Span; } + + /// + /// Checks whether a given is a given type declaration with or potentially with any base types, using only syntax. + /// + /// The type of declaration to check for. + /// The input to check. + /// Whether is a given type declaration with or potentially with any base types. + public static bool IsTypeDeclarationWithOrPotentiallyWithBaseTypes(this SyntaxNode node) + where T : TypeDeclarationSyntax + { + // Immediately bail if the node is not a type declaration of the specified type + if (node is not T typeDeclaration) + { + return false; + } + + // If the base types list is not empty, the type can definitely has implemented interfaces + if (typeDeclaration.BaseList is { Types.Count: > 0 }) + { + return true; + } + + // If the base types list is empty, check if the type is partial. If it is, it means + // that there could be another partial declaration with a non-empty base types list. + return typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword); + } } From 76264f78759f70ae18fdbf146960674fd4935b8e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 21:27:49 -0700 Subject: [PATCH 018/146] Update the generator to also work on properties --- .../ObservablePropertyGenerator.Execute.cs | 337 +++++++++++------- .../ObservablePropertyGenerator.cs | 8 +- .../Extensions/CompilationExtensions.cs | 10 + 3 files changed, 232 insertions(+), 123 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 007de9e02..aac15ed5b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -39,27 +39,38 @@ internal static class Execute public static bool IsCandidatePropertyDeclaration(SyntaxNode node, CancellationToken token) { // Matches a valid field declaration, for legacy support - static bool IsCandidateField(SyntaxNode node) + static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) { - return node is VariableDeclaratorSyntax { - Parent: VariableDeclarationSyntax { - Parent: FieldDeclarationSyntax { - Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }; + // The node must represent a field declaration + if (node is not VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { AttributeLists.Count: > 0 } fieldNode } }) + { + containingTypeNode = null; + + return false; + } + + containingTypeNode = (TypeDeclarationSyntax?)fieldNode.Parent; + + return true; } #if ROSLYN_4_11_0_OR_GREATER // Matches a valid partial property declaration - static bool IsCandidateProperty(SyntaxNode node) + static bool IsCandidateProperty(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) { // The node must be a property declaration with two accessors - if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors } property) + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) { + containingTypeNode = null; + return false; } // The property must be partial (we'll check that it's a declaration from its symbol) if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) { + containingTypeNode = null; + return false; } @@ -67,43 +78,91 @@ static bool IsCandidateProperty(SyntaxNode node) if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) { + containingTypeNode = null; + return false; } + containingTypeNode = (TypeDeclarationSyntax?)property.Parent; + return true; } // We only support matching properties on Roslyn 4.11 and greater - if (!IsCandidateField(node) && !IsCandidateProperty(node)) + if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && !IsCandidateProperty(node, out parentNode)) { return false; } #else // Otherwise, we only support matching fields - if (!IsCandidateField(node)) + if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode)) { return false; } #endif - // The property must be in a type with a base type (as it must derive from ObservableObject) - return node.Parent?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; + // The candidate member must be in a type with a base type (as it must derive from ObservableObject) + return parentNode?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; } /// - /// Processes a given field. + /// Checks whether a given candidate node is valid given a compilation. /// - /// The instance to process. - /// The input instance to process. + /// The instance to process. + /// The instance for the current run. + /// Whether is valid. + public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel semanticModel) + { + // At least C# 8 is always required + if (!semanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) + { + return false; + } + + // If the target is a property, we only support using C# preview. + // This is because the generator is relying on the 'field' keyword. + if (node is PropertyDeclarationSyntax && !semanticModel.Compilation.IsLanguageVersionPreview()) + { + return false; + } + + // All other cases are supported, the syntax filter is already validating that + return true; + } + + /// + /// Gets the candidate after the initial filtering. + /// + /// The input syntax node to convert. + /// The resulting instance. + public static MemberDeclarationSyntax GetCandidateMemberDeclaration(SyntaxNode node) + { + // If the node is a property declaration, just return it directly. Note that we don't have + // to check whether we're using Roslyn 4.11 here, as if that's not the case all of these + // syntax nodes would already have pre-filtered well before this method could run at all. + if (node is PropertyDeclarationSyntax propertySyntax) + { + return propertySyntax; + } + + // Otherwise, assume all targets are field declarations + return (MemberDeclarationSyntax)node.Parent!.Parent!; + } + + /// + /// Processes a given field or property. + /// + /// The instance to process. + /// The input instance to process. /// The instance for the current run. /// The options in use for the generator. /// The cancellation token for the current operation. /// The resulting value, if successfully retrieved. /// The resulting diagnostics from the processing operation. - /// The resulting instance for , if successful. + /// The resulting instance for , if successful. public static bool TryGetInfo( - FieldDeclarationSyntax fieldSyntax, - IFieldSymbol fieldSymbol, + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, SemanticModel semanticModel, AnalyzerConfigOptions options, CancellationToken token, @@ -113,13 +172,13 @@ public static bool TryGetInfo( using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); // Validate the target type - if (!IsTargetTypeValid(fieldSymbol, out bool shouldInvokeOnPropertyChanging)) + if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging)) { builder.Add( InvalidContainingTypeForObservablePropertyFieldError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); propertyInfo = null; diagnostics = builder.ToImmutable(); @@ -135,18 +194,18 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Get the property type and name - string typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); - string fieldName = fieldSymbol.Name; - string propertyName = GetGeneratedPropertyName(fieldSymbol); + string typeNameWithNullabilityAnnotations = GetPropertyType(memberSymbol).GetFullyQualifiedNameWithNullabilityAnnotations(); + string fieldName = memberSymbol.Name; + string propertyName = GetGeneratedPropertyName(memberSymbol); // Check for name collisions if (fieldName == propertyName) { builder.Add( ObservablePropertyNameCollisionError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); propertyInfo = null; diagnostics = builder.ToImmutable(); @@ -160,13 +219,13 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Check for special cases that are explicitly not allowed - if (IsGeneratedPropertyInvalid(propertyName, fieldSymbol.Type)) + if (IsGeneratedPropertyInvalid(propertyName, GetPropertyType(memberSymbol))) { builder.Add( InvalidObservablePropertyError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); propertyInfo = null; diagnostics = builder.ToImmutable(); @@ -185,13 +244,13 @@ public static bool TryGetInfo( bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients = false; bool hasOrInheritsClassLevelNotifyDataErrorInfo = false; bool hasAnyValidationAttributes = false; - bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName); + bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(memberSymbol, propertyName); token.ThrowIfCancellationRequested(); // Get the nullability info for the property GetNullabilityInfo( - fieldSymbol, + memberSymbol, semanticModel, out bool isReferenceTypeOrUnconstraindTypeParameter, out bool includeMemberNotNullOnSetAccessor); @@ -202,7 +261,7 @@ public static bool TryGetInfo( propertyChangedNames.Add(propertyName); // Get the class-level [NotifyPropertyChangedRecipients] setting, if any - if (TryGetIsNotifyingRecipients(fieldSymbol, out bool isBroadcastTargetValid)) + if (TryGetIsNotifyingRecipients(memberSymbol, out bool isBroadcastTargetValid)) { notifyRecipients = isBroadcastTargetValid; hasOrInheritsClassLevelNotifyPropertyChangedRecipients = true; @@ -211,7 +270,7 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Get the class-level [NotifyDataErrorInfo] setting, if any - if (TryGetNotifyDataErrorInfo(fieldSymbol, out bool isValidationTargetValid)) + if (TryGetNotifyDataErrorInfo(memberSymbol, out bool isValidationTargetValid)) { notifyDataErrorInfo = isValidationTargetValid; hasOrInheritsClassLevelNotifyDataErrorInfo = true; @@ -220,19 +279,19 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Gather attributes info - foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) + foreach (AttributeData attributeData in memberSymbol.GetAttributes()) { token.ThrowIfCancellationRequested(); // Gather dependent property and command names - if (TryGatherDependentPropertyChangedNames(fieldSymbol, attributeData, in propertyChangedNames, in builder) || - TryGatherDependentCommandNames(fieldSymbol, attributeData, in notifiedCommandNames, in builder)) + if (TryGatherDependentPropertyChangedNames(memberSymbol, attributeData, in propertyChangedNames, in builder) || + TryGatherDependentCommandNames(memberSymbol, attributeData, in notifiedCommandNames, in builder)) { continue; } // Check whether the property should also notify recipients - if (TryGetIsNotifyingRecipients(fieldSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyPropertyChangedRecipients, out isBroadcastTargetValid)) + if (TryGetIsNotifyingRecipients(memberSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyPropertyChangedRecipients, out isBroadcastTargetValid)) { notifyRecipients = isBroadcastTargetValid; @@ -240,13 +299,20 @@ public static bool TryGetInfo( } // Check whether the property should also be validated - if (TryGetNotifyDataErrorInfo(fieldSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyDataErrorInfo, out isValidationTargetValid)) + if (TryGetNotifyDataErrorInfo(memberSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyDataErrorInfo, out isValidationTargetValid)) { notifyDataErrorInfo = isValidationTargetValid; continue; } + // The following checks only apply to fields, not properties. That is, attributes + // on partial properties are never forwarded, as they are already on the member. + if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) + { + continue; + } + // Track the current attribute for forwarding if it is a validation attribute if (attributeData.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) { @@ -274,8 +340,16 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Gather explicit forwarded attributes info - foreach (AttributeListSyntax attributeList in fieldSyntax.AttributeLists) + foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) { + // For properties, we never need to forward any attributes with explicit targets either, because + // they can already "just work" when used with 'field'. As for 'get' and 'set', they can just be + // added directly to the partial declarations of the property accessors. + if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) + { + continue; + } + // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will // normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic // suppressor that recognizes uses of this target specifically to support [ObservableProperty]. @@ -307,7 +381,7 @@ public static bool TryGetInfo( builder.Add( InvalidPropertyTargetedAttributeOnObservablePropertyField, attribute, - fieldSymbol, + memberSymbol, attribute.Name); continue; @@ -321,7 +395,7 @@ public static bool TryGetInfo( builder.Add( InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, attribute, - fieldSymbol, + memberSymbol, attribute.Name); continue; @@ -335,13 +409,13 @@ public static bool TryGetInfo( // Log the diagnostic for missing ObservableValidator, if needed if (hasAnyValidationAttributes && - !fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + !memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { builder.Add( MissingObservableValidatorInheritanceForValidationAttributeError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name, + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name, forwardedAttributes.Count); } @@ -350,9 +424,9 @@ public static bool TryGetInfo( { builder.Add( MissingValidationAttributesForNotifyDataErrorInfoError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); } token.ThrowIfCancellationRequested(); @@ -420,19 +494,19 @@ public static bool GetEnableINotifyPropertyChangingSupport(AnalyzerConfigOptions /// /// Validates the containing type for a given field being annotated. /// - /// The input instance to process. + /// The input instance to process. /// Whether or not property changing events should also be raised. - /// Whether or not the containing type for is valid. - private static bool IsTargetTypeValid(IFieldSymbol fieldSymbol, out bool shouldInvokeOnPropertyChanging) + /// Whether or not the containing type for is valid. + private static bool IsTargetTypeValid(ISymbol memberSymbol, out bool shouldInvokeOnPropertyChanging) { // The [ObservableProperty] attribute can only be used in types that are known to expose the necessary OnPropertyChanged and OnPropertyChanging methods. // That means that the containing type for the field needs to match one of the following conditions: // - It inherits from ObservableObject (in which case it also implements INotifyPropertyChanging). // - It has the [ObservableObject] attribute (on itself or any of its base types). // - It has the [INotifyPropertyChanged] attribute (on itself or any of its base types). - bool isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject"); - bool hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute"); - bool hasINotifyPropertyChangedAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"); + bool isObservableObject = memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject"); + bool hasObservableObjectAttribute = memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute"); + bool hasINotifyPropertyChangedAttribute = memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"); shouldInvokeOnPropertyChanging = isObservableObject || hasObservableObjectAttribute; @@ -465,13 +539,13 @@ private static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol /// /// Tries to gather dependent properties from the given attribute. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The target collection of dependent property names to populate. /// The current collection of gathered diagnostics. /// Whether or not was an attribute containing any dependent properties. private static bool TryGatherDependentPropertyChangedNames( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder propertyChangedNames, in ImmutableArrayBuilder diagnostics) @@ -479,16 +553,16 @@ private static bool TryGatherDependentPropertyChangedNames( // Validates a property name using existing properties bool IsPropertyNameValid(string propertyName) { - return fieldSymbol.ContainingType.GetAllMembers(propertyName).OfType().Any(); + return memberSymbol.ContainingType.GetAllMembers(propertyName).OfType().Any(); } // Validate a property name including generated properties too bool IsPropertyNameValidWithGeneratedMembers(string propertyName) { - foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers()) + foreach (ISymbol member in memberSymbol.ContainingType.GetAllMembers()) { if (member is IFieldSymbol otherFieldSymbol && - !SymbolEqualityComparer.Default.Equals(fieldSymbol, otherFieldSymbol) && + !SymbolEqualityComparer.Default.Equals(memberSymbol, otherFieldSymbol) && otherFieldSymbol.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") && propertyName == GetGeneratedPropertyName(otherFieldSymbol)) { @@ -515,9 +589,9 @@ bool IsPropertyNameValidWithGeneratedMembers(string propertyName) { diagnostics.Add( NotifyPropertyChangedForInvalidTargetError, - fieldSymbol, + memberSymbol, dependentPropertyName ?? "", - fieldSymbol.ContainingType); + memberSymbol.ContainingType); } } @@ -530,13 +604,13 @@ bool IsPropertyNameValidWithGeneratedMembers(string propertyName) /// /// Tries to gather dependent commands from the given attribute. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The target collection of dependent command names to populate. /// The current collection of gathered diagnostics. /// Whether or not was an attribute containing any dependent commands. private static bool TryGatherDependentCommandNames( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder notifiedCommandNames, in ImmutableArrayBuilder diagnostics) @@ -546,7 +620,7 @@ bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembe { // Each target must be a string matching the name of a property from the containing type of the annotated field, and the // property must be of type IRelayCommand, or any type that implements that interface (to avoid generating invalid code). - if (fieldSymbol.ContainingType.GetAllMembers(commandName).OfType().FirstOrDefault() is IPropertySymbol propertySymbol) + if (memberSymbol.ContainingType.GetAllMembers(commandName).OfType().FirstOrDefault() is IPropertySymbol propertySymbol) { // If there is a property member with the specified name, check that it's valid. If it isn't, the // target is definitely not valid, and the additional checks below can just be skipped. The property @@ -575,7 +649,7 @@ bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembe // Validate a command name including generated command too bool IsCommandNameValidWithGeneratedMembers(string commandName) { - foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers()) + foreach (ISymbol member in memberSymbol.ContainingType.GetAllMembers()) { if (member is IMethodSymbol methodSymbol && methodSymbol.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") && @@ -605,9 +679,9 @@ bool IsCommandNameValidWithGeneratedMembers(string commandName) { diagnostics.Add( NotifyCanExecuteChangedForInvalidTargetError, - fieldSymbol, + memberSymbol, commandName ?? "", - fieldSymbol.ContainingType); + memberSymbol.ContainingType); } } @@ -620,16 +694,16 @@ bool IsCommandNameValidWithGeneratedMembers(string commandName) /// /// Checks whether a given generated property should also notify recipients. /// - /// The input instance to process. + /// The input instance to process. /// Whether or not the the property is in a valid target that can notify recipients. - /// Whether or not the generated property for is in a type annotated with [NotifyPropertyChangedRecipients]. - private static bool TryGetIsNotifyingRecipients(IFieldSymbol fieldSymbol, out bool isBroadcastTargetValid) + /// Whether or not the generated property for is in a type annotated with [NotifyPropertyChangedRecipients]. + private static bool TryGetIsNotifyingRecipients(ISymbol memberSymbol, out bool isBroadcastTargetValid) { - if (fieldSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") == true) + if (memberSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") == true) { // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || - fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || + memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) { isBroadcastTargetValid = true; @@ -651,14 +725,14 @@ private static bool TryGetIsNotifyingRecipients(IFieldSymbol fieldSymbol, out bo /// /// Checks whether a given generated property should also notify recipients. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The current collection of gathered diagnostics. - /// Indicates wether the containing type of has or inherits [NotifyPropertyChangedRecipients]. + /// Indicates wether the containing type of has or inherits [NotifyPropertyChangedRecipients]. /// Whether or not the the property is in a valid target that can notify recipients. - /// Whether or not the generated property for used [NotifyPropertyChangedRecipients]. + /// Whether or not the generated property for used [NotifyPropertyChangedRecipients]. private static bool TryGetIsNotifyingRecipients( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder diagnostics, bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients, @@ -671,14 +745,14 @@ private static bool TryGetIsNotifyingRecipients( { diagnostics.Add( UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); } // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || - fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || + memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) { isBroadcastTargetValid = true; @@ -688,9 +762,9 @@ private static bool TryGetIsNotifyingRecipients( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); isBroadcastTargetValid = false; @@ -705,15 +779,15 @@ private static bool TryGetIsNotifyingRecipients( /// /// Checks whether a given generated property should also validate its value. /// - /// The input instance to process. + /// The input instance to process. /// Whether or not the the property is in a valid target that can validate values. - /// Whether or not the generated property for used [NotifyDataErrorInfo]. - private static bool TryGetNotifyDataErrorInfo(IFieldSymbol fieldSymbol, out bool isValidationTargetValid) + /// Whether or not the generated property for used [NotifyDataErrorInfo]. + private static bool TryGetNotifyDataErrorInfo(ISymbol memberSymbol, out bool isValidationTargetValid) { - if (fieldSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") == true) + if (memberSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") == true) { // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { isValidationTargetValid = true; @@ -734,14 +808,14 @@ private static bool TryGetNotifyDataErrorInfo(IFieldSymbol fieldSymbol, out bool /// /// Checks whether a given generated property should also validate its value. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The current collection of gathered diagnostics. - /// Indicates wether the containing type of has or inherits [NotifyDataErrorInfo]. + /// Indicates whether the containing type of has or inherits [NotifyDataErrorInfo]. /// Whether or not the the property is in a valid target that can validate values. - /// Whether or not the generated property for used [NotifyDataErrorInfo]. + /// Whether or not the generated property for used [NotifyDataErrorInfo]. private static bool TryGetNotifyDataErrorInfo( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder diagnostics, bool hasOrInheritsClassLevelNotifyDataErrorInfo, @@ -754,13 +828,13 @@ private static bool TryGetNotifyDataErrorInfo( { diagnostics.Add( UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); } // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { isValidationTargetValid = true; @@ -770,9 +844,9 @@ private static bool TryGetNotifyDataErrorInfo( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( MissingObservableValidatorInheritanceForNotifyDataErrorInfoError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); isValidationTargetValid = false; @@ -787,13 +861,13 @@ private static bool TryGetNotifyDataErrorInfo( /// /// Checks whether the generated code has to directly reference the old property value. /// - /// The input instance to process. + /// The input instance to process. /// The name of the property being generated. /// Whether the generated code needs direct access to the old property value. - private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbol, string propertyName) + private static bool IsOldPropertyValueDirectlyReferenced(ISymbol memberSymbol, string propertyName) { // Check OnChanging( oldValue, newValue) first - foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changing")) + foreach (ISymbol symbol in memberSymbol.ContainingType.GetMembers($"On{propertyName}Changing")) { // No need to be too specific as we're not expecting false positives (which also wouldn't really // cause any problems anyway, just produce slightly worse codegen). Just checking the number of @@ -805,7 +879,7 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo } // Do the same for OnChanged( oldValue, newValue) - foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changed")) + foreach (ISymbol symbol in memberSymbol.ContainingType.GetMembers($"On{propertyName}Changed")) { if (symbol is IMethodSymbol { Parameters.Length: 2 }) { @@ -819,13 +893,13 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo /// /// Gets the nullability info on the generated property /// - /// The input instance to process. + /// The input instance to process. /// The instance for the current run. /// Whether the property type supports nullability. /// Whether should be used on the setter. /// private static void GetNullabilityInfo( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, SemanticModel semanticModel, out bool isReferenceTypeOrUnconstraindTypeParameter, out bool includeMemberNotNullOnSetAccessor) @@ -833,7 +907,7 @@ private static void GetNullabilityInfo( // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. // This will cover both reference types as well T when the constraints are not struct or unmanaged. // If this is true, it means the field storage can potentially be in a null state (even if not annotated). - isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType; + isReferenceTypeOrUnconstraindTypeParameter = !GetPropertyType(memberSymbol).IsValueType; // This is used to avoid nullability warnings when setting the property from a constructor, in case the field // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. @@ -855,7 +929,7 @@ private static void GetNullabilityInfo( // Of course, this can only be the case if the field type is also of a type that could be in a null state. includeMemberNotNullOnSetAccessor = isReferenceTypeOrUnconstraindTypeParameter && - fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && + GetPropertyType(memberSymbol).NullableAnnotation != NullableAnnotation.Annotated && semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } @@ -1345,6 +1419,23 @@ public static ImmutableArray GetOnPropertyChangeMethods onPropertyChanged2Declaration); } + /// + /// Gets the for a given member symbol (it can be either a field or a property). + /// + /// The input instance to process. + /// The type of . + private static ITypeSymbol GetPropertyType(ISymbol memberSymbol) + { + // Check if the member is a property first + if (memberSymbol is IPropertySymbol propertySymbol) + { + return propertySymbol.Type; + } + + // Otherwise, the only possible case is a field symbol + return ((IFieldSymbol)memberSymbol).Type; + } + /// /// Gets the for the type of a given property, when it can possibly be . /// @@ -1479,13 +1570,19 @@ private static FieldDeclarationSyntax CreateFieldDeclaration(string fullyQualifi } /// - /// Get the generated property name for an input field. + /// Get the generated property name for an input field or property. /// - /// The input instance to process. - /// The generated property name for . - public static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol) + /// The input instance to process. + /// The generated property name for . + public static string GetGeneratedPropertyName(ISymbol memberSymbol) { - string propertyName = fieldSymbol.Name; + // If the input is a property, just always match the name exactly + if (memberSymbol is IPropertySymbol propertySymbol) + { + return propertySymbol.Name; + } + + string propertyName = memberSymbol.Name; if (propertyName.StartsWith("m_")) { diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index df24ac51a..4b650c716 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -30,12 +30,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Execute.IsCandidatePropertyDeclaration, static (context, token) => { - if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) + MemberDeclarationSyntax memberSyntax = Execute.GetCandidateMemberDeclaration(context.TargetNode); + + // Validate that the candidate is valid for the current compilation + if (!Execute.IsCandidateValidForCompilation(memberSyntax, context.SemanticModel)) { return default; } - FieldDeclarationSyntax fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; IFieldSymbol fieldSymbol = (IFieldSymbol)context.TargetSymbol; // Get the hierarchy info for the target symbol, and try to gather the property info @@ -44,7 +46,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); _ = Execute.TryGetInfo( - fieldDeclaration, + memberSyntax, fieldSymbol, context.SemanticModel, context.GlobalOptions, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs index ac05bdff6..8f3c06b93 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs @@ -27,6 +27,16 @@ public static bool HasLanguageVersionAtLeastEqualTo(this Compilation compilation return ((CSharpCompilation)compilation).LanguageVersion >= languageVersion; } + /// + /// Checks whether a given compilation (assumed to be for C#) is using the preview language version. + /// + /// The to consider for analysis. + /// Whether is using the preview language version. + public static bool IsLanguageVersionPreview(this Compilation compilation) + { + return ((CSharpCompilation)compilation).LanguageVersion == LanguageVersion.Preview; + } + /// /// /// Checks whether or not a type with a specified metadata name is accessible from a given instance. From 3df53f345436d14bef8b1e39346b4aa8890123d2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 21:39:53 -0700 Subject: [PATCH 019/146] Add 'RequiresCSharpLanguageVersionPreviewAnalyzer' --- .../AnalyzerReleases.Shipped.md | 8 +++ ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...resCSharpLanguageVersionPreviewAnalyzer.cs | 65 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 16 +++++ 4 files changed, 90 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 53e58bfea..ce431cea7 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -75,3 +75,11 @@ MVVMTK0039 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warn Rule ID | Category | Severity | Notes --------|----------|----------|------- MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0040 + +## Release 8.4.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 9dc77b7d1..19661727b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -46,6 +46,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs new file mode 100644 index 000000000..1b7a96114 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs @@ -0,0 +1,65 @@ +// 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_11_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 errors when a property using [ObservableProperty] on a partial property is in a project with the C# language version not set to preview. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class RequiresCSharpLanguageVersionPreviewAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [CSharpLanguageVersionIsNotPreviewForObservableProperty]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // If the language version is set to preview, we'll never emit diagnostics + if (context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We only want to target partial property definitions (also include non-partial ones for diagnostics) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null }) + { + return; + } + + // If the property is using [ObservableProperty], emit the diagnostic + if (context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + context.ReportDiagnostic(Diagnostic.Create( + CSharpLanguageVersionIsNotPreviewForObservableProperty, + observablePropertyAttribute.GetLocation(), + context.Symbol)); + } + }, SymbolKind.Property); + }); + } +} + +#endif \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 431e5da40..caa733463 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -674,4 +674,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "The backing fields of auto-properties cannot be annotated with [ObservableProperty] (the attribute can only be used directly on fields, and the generator will then handle generating the corresponding property).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0040"); + + /// + /// Gets a for a CanvasEffect property with invalid accessors. + /// + /// 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( + 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)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The C# language version must be set to 'preview' when using [ObservableProperty] on partial properties for the source generators to emit valid code (the preview option must be set in the .csproj/.props file).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0041"); } From fff5f91b627839471da02a409e134db6c9a6debc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 21:51:06 -0700 Subject: [PATCH 020/146] Suppress warnings about removed rules --- .../CommunityToolkit.Mvvm.SourceGenerators.props | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props index 0a2e05c33..c29a655e2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props @@ -32,6 +32,9 @@ $(DefineConstants);ROSLYN_4_3_1_OR_GREATER $(DefineConstants);ROSLYN_4_11_0_OR_GREATER + + + $(NoWarn);RS2003 From 41dc834719b92d1f565a412597caba12a756b28b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 22:54:28 -0700 Subject: [PATCH 021/146] Add blank test project for new generator --- dotnet Community Toolkit.sln | 24 +++++++++++++++++++ ...urceGenerators.Roslyn4110.UnitTests.csproj | 23 ++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index 651a17bce..8a94f3c65 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -85,6 +85,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj", "{FCC13AD5-CEB8-4CC1-8250-89B616D126F2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj", "{C342302D-A263-42D6-B8EE-01DEF8192690}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -479,6 +481,26 @@ Global {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x64.Build.0 = Release|Any CPU {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.ActiveCfg = Release|Any CPU {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM64.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x64.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x64.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x86.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x86.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|Any CPU.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM64.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM64.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x64.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x64.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x86.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -501,6 +523,7 @@ Global {E827A9CD-405F-43E4-84C7-68CC7E845CDC} = {B30036C4-D514-4E5B-A323-587A061772CE} {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5403B0C4-F244-4F73-A35C-FE664D0F4345} @@ -511,6 +534,7 @@ Global src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{5e7f1212-a54b-40ca-98c5-1ff5cd1a1638}*SharedItemsImports = 13 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{ad9c3223-8e37-4fd4-a0d4-a45119551d3a}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{b8dcd82e-b53b-4249-ad4e-f9b99acb9334}*SharedItemsImports = 13 + tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{c342302d-a263-42d6-b8ee-01def8192690}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{df455c40-b18e-4890-8758-7cccb5ca7052}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{e24d1146-5ad8-498f-a518-4890d8bf4937}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{e827a9cd-405f-43e4-84c7-68cc7e845cdc}*SharedItemsImports = 13 diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj new file mode 100644 index 000000000..73bec0bc0 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -0,0 +1,23 @@ + + + + net472;net6.0;net7.0;net8.0 + $(DefineConstants);ROSLYN_4_11_0_OR_GREATER + + + + + + + + + + + + + + + + + + \ No newline at end of file From 34a7c6f6080f092fac60df604f2eb79e7cae9fff Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 23:54:07 -0700 Subject: [PATCH 022/146] Update '[ObservableProperty]' attribute --- .../Attributes/ObservablePropertyAttribute.cs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs index 0e765267a..c6ae78763 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs @@ -8,7 +8,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// -/// An attribute that indicates that a given field should be wrapped by a generated observable property. +/// An attribute that indicates that a given partial property should be implemented by the source generator. /// In order to use this attribute, the containing type has to inherit from , or it /// must be using or . /// If the containing type also implements the (that is, if it either inherits from @@ -20,10 +20,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// partial class MyViewModel : ObservableObject /// { /// [ObservableProperty] -/// private string name; +/// public partial string name { get; set; } /// /// [ObservableProperty] -/// private bool isEnabled; +/// public partial bool IsEnabled { get; set; } /// } /// /// @@ -31,27 +31,43 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Name +/// public partial string Name /// { -/// get => name; -/// set => SetProperty(ref name, value); +/// get => field; +/// set => SetProperty(ref field, value); /// } /// -/// public bool IsEnabled +/// public partial bool IsEnabled /// { -/// get => isEnabled; -/// set => SetProperty(ref isEnabled, value); +/// get => field; +/// set => SetProperty(ref field, value); /// } /// } /// /// /// +/// +/// In order to use this attribute on partial properties, the .NET 9 SDK is required, and C# preview must +/// be used. If that is not available, this attribute can be used to annotate fields instead, like so: +/// +/// partial class MyViewModel : ObservableObject +/// { +/// [ObservableProperty] +/// private string name; +/// +/// [ObservableProperty] +/// private bool isEnabled; +/// } +/// +/// +/// /// The generated properties will automatically use the UpperCamelCase format for their names, /// which will be derived from the field names. The generator can also recognize fields using either /// the _lowerCamel or m_lowerCamel naming scheme. Otherwise, the first character in the /// source field name will be converted to uppercase (eg. isEnabled to IsEnabled). +/// /// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class ObservablePropertyAttribute : Attribute { } From bbd59ec247975817a9f9621ec24e83b2dc35a560 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 23:55:53 -0700 Subject: [PATCH 023/146] Add unit tests for new analyzer --- ...urceGenerators.Roslyn4110.UnitTests.csproj | 2 +- .../Test_SourceGeneratorsDiagnostics.cs | 50 +++++++++++++++++++ .../Test_SourceGeneratorsDiagnostics.cs | 2 +- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj index 73bec0bc0..394a10614 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -2,7 +2,7 @@ net472;net6.0;net7.0;net8.0 - $(DefineConstants);ROSLYN_4_11_0_OR_GREATER + $(DefineConstants);ROSLYN_4_3_1_OR_GREATER;ROSLYN_4_11_0_OR_GREATER diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs new file mode 100644 index 000000000..03a3f323d --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -0,0 +1,50 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +partial class Test_SourceGeneratorsDiagnostics +{ + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0041:ObservableProperty|}] + public string Bar { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp12); + } + + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsPreview_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public string Bar { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, languageVersion: LanguageVersion.Preview); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index fc767e712..0ab363974 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -19,7 +19,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] -public class Test_SourceGeneratorsDiagnostics +public partial class Test_SourceGeneratorsDiagnostics { [TestMethod] public void DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError_Explicit() From f970ba6c0b72bb44d06e666b578a4386d3bb806f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 19:23:39 -0700 Subject: [PATCH 024/146] Move forwarded attributes gathering to helper --- .../ObservablePropertyGenerator.Execute.cs | 158 +++++++++++------- 1 file changed, 93 insertions(+), 65 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index aac15ed5b..82415d7b1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -339,71 +339,15 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); - // Gather explicit forwarded attributes info - foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) - { - // For properties, we never need to forward any attributes with explicit targets either, because - // they can already "just work" when used with 'field'. As for 'get' and 'set', they can just be - // added directly to the partial declarations of the property accessors. - if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) - { - continue; - } - - // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will - // normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic - // suppressor that recognizes uses of this target specifically to support [ObservableProperty]. - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.GetKeyword or SyntaxKind.SetKeyword) targetIdentifier) - { - continue; - } - - token.ThrowIfCancellationRequested(); - - foreach (AttributeSyntax attribute in attributeList.Attributes) - { - // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. - // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: - // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not - // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. - // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. - // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. - // - We then go over each attribute argument expression and get the operation for it. This will still be available even - // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all - // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) - // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. - // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. - // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the - // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the - // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) - { - builder.Add( - InvalidPropertyTargetedAttributeOnObservablePropertyField, - attribute, - memberSymbol, - attribute.Name); - - continue; - } - - IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) - { - builder.Add( - InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, - attribute, - memberSymbol, - attribute.Name); - - continue; - } - - forwardedAttributes.Add(attributeInfo); - } - } + // Also gather any forwarded attributes on the annotated member, if it is a field. + // This method will not do anything for properties, as those don't support this. + GatherLegacyForwardedAttributes( + memberSyntax, + memberSymbol, + semanticModel, + in forwardedAttributes, + in builder, + token); token.ThrowIfCancellationRequested(); @@ -933,6 +877,90 @@ private static void GetNullabilityInfo( semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } + /// + /// Gathers all forwarded attributes from the given member syntax. + /// + /// The instance to process. + /// The input instance to process. + /// The instance for the current run. + /// The collection of forwarded attributes to add new ones to. + /// The current collection of gathered diagnostics. + /// The cancellation token for the current operation. + private static void GatherLegacyForwardedAttributes( + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, + SemanticModel semanticModel, + in ImmutableArrayBuilder forwardedAttributes, + in ImmutableArrayBuilder diagnostics, + CancellationToken token) + { + // For properties, we never need to forward any attributes with explicit targets either, because + // they can already "just work" when used with 'field'. As for 'get' and 'set', they can just be + // added directly to the partial declarations of the property accessors. + if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) + { + return; + } + + // Gather explicit forwarded attributes info + foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) + { + // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will + // normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic + // suppressor that recognizes uses of this target specifically to support [ObservableProperty]. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.GetKeyword or SyntaxKind.SetKeyword) targetIdentifier) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. + // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: + // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not + // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. + // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. + // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. + // - We then go over each attribute argument expression and get the operation for it. This will still be available even + // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all + // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) + // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. + // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. + // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the + // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the + // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) + { + diagnostics.Add( + InvalidPropertyTargetedAttributeOnObservablePropertyField, + attribute, + memberSymbol, + attribute.Name); + + continue; + } + + IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); + + // Try to extract the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) + { + diagnostics.Add( + InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, + attribute, + memberSymbol, + attribute.Name); + + continue; + } + + forwardedAttributes.Add(attributeInfo); + } + } + } + /// /// Tries to get the accessibility of the property and accessors, if possible. /// From e5b28029500302baa4a43edf41c75d81247f6d99 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 19:35:36 -0700 Subject: [PATCH 025/146] Gather accessibility information from nodes --- .../ComponentModel/Models/PropertyInfo.cs | 7 +++ .../ObservablePropertyGenerator.Execute.cs | 59 +++++++++++++++---- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 8c3bca4da..20e98ba13 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Mvvm.SourceGenerators.Helpers; +using Microsoft.CodeAnalysis; namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; @@ -12,6 +13,9 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// The type name for the generated property, including nullability annotations. /// The field name. /// The generated property name. +/// The accessibility of the property. +/// The accessibility of the accessor. +/// The accessibility of the accessor. /// The sequence of property changing properties to notify. /// The sequence of property changed properties to notify. /// The sequence of commands to notify. @@ -26,6 +30,9 @@ internal sealed record PropertyInfo( string TypeNameWithNullabilityAnnotations, string FieldName, string PropertyName, + Accessibility PropertyAccessibility, + Accessibility GetterAccessibility, + Accessibility SetterAccessibility, EquatableArray PropertyChangingNames, EquatableArray PropertyChangedNames, EquatableArray NotifiedCommandNames, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 82415d7b1..412b14349 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -394,10 +394,29 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + // Retrieve the accessibility values for all components + if (!TryGetAccessibilityModifiers( + memberSyntax, + memberSymbol, + out Accessibility propertyAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility)) + { + propertyInfo = null; + diagnostics = builder.ToImmutable(); + + return false; + } + + token.ThrowIfCancellationRequested(); + propertyInfo = new PropertyInfo( typeNameWithNullabilityAnnotations, fieldName, propertyName, + propertyAccessibility, + getterAccessibility, + setterAccessibility, effectivePropertyChangingNames, effectivePropertyChangedNames, notifiedCommandNames.ToImmutable(), @@ -963,38 +982,56 @@ private static void GatherLegacyForwardedAttributes( /// /// Tries to get the accessibility of the property and accessors, if possible. + /// If the target member is not a property, it will use the defaults. /// - /// The input node. - /// The input instance. - /// The accessibility of the property, if available. + /// The instance to process. + /// The input instance to process. + /// The accessibility of the property, if available. /// The accessibility of the accessor, if available. /// The accessibility of the accessor, if available. /// Whether the property was valid and the accessibilities could be retrieved. private static bool TryGetAccessibilityModifiers( - PropertyDeclarationSyntax node, - IPropertySymbol symbol, - out Accessibility declaredAccessibility, + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, + out Accessibility propertyAccessibility, out Accessibility getterAccessibility, out Accessibility setterAccessibility) { - declaredAccessibility = Accessibility.NotApplicable; + // For legacy support for fields, all accessibilities are public. + // To customize the accessibility, partial properties should be used. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + propertyAccessibility = Accessibility.Public; + getterAccessibility = Accessibility.Public; + setterAccessibility = Accessibility.Public; + + return true; + } + + propertyAccessibility = Accessibility.NotApplicable; getterAccessibility = Accessibility.NotApplicable; setterAccessibility = Accessibility.NotApplicable; // Ensure that we have a getter and a setter, and that the setter is not init-only - if (symbol is not { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + if (memberSymbol is not IPropertySymbol { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + { + return false; + } + + // At this point the node is definitely a property, just do a sanity check + if (memberSyntax is not PropertyDeclarationSyntax propertySyntax) { return false; } // Track the property accessibility if explicitly set - if (node.Modifiers.Count > 0) + if (propertySyntax.Modifiers.Count > 0) { - declaredAccessibility = symbol.DeclaredAccessibility; + propertyAccessibility = memberSymbol.DeclaredAccessibility; } // Track the accessors accessibility, if explicitly set - foreach (AccessorDeclarationSyntax accessor in node.AccessorList?.Accessors ?? []) + foreach (AccessorDeclarationSyntax accessor in propertySyntax.AccessorList?.Accessors ?? []) { if (accessor.Modifiers.Count == 0) { From 17263fe7ce8559bcc0766423de936772841fe03a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 21:11:29 -0700 Subject: [PATCH 026/146] Generalizing the generated accessibility modifiers --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 23 +++++++----- .../Extensions/AccessibilityExtensions.cs | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 19661727b..3ab9f89d6 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -52,6 +52,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 412b14349..40f749c17 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -997,13 +997,14 @@ private static bool TryGetAccessibilityModifiers( out Accessibility getterAccessibility, out Accessibility setterAccessibility) { - // For legacy support for fields, all accessibilities are public. - // To customize the accessibility, partial properties should be used. + // For legacy support for fields, the property that is generated is public, and neither + // accessors will have any accessibility modifiers. To customize the accessibility, + // partial properties should be used instead. if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) { propertyAccessibility = Accessibility.Public; - getterAccessibility = Accessibility.Public; - setterAccessibility = Accessibility.Public; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; return true; } @@ -1297,11 +1298,14 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // Prepare the setter for the generated property: // - // set + // set // { // // } - AccessorDeclarationSyntax setAccessor = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithBody(Block(setterIfStatement)); + AccessorDeclarationSyntax setAccessor = + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithModifiers(propertyInfo.SetterAccessibility.ToSyntaxTokenList()) + .WithBody(Block(setterIfStatement)); // Add the [MemberNotNull] attribute if needed: // @@ -1338,10 +1342,10 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // - // public + // // { // - // get => ; + // get => ; // // } return @@ -1355,9 +1359,10 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .AddAttributeLists(forwardedPropertyAttributes) - .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .WithModifiers(propertyInfo.PropertyAccessibility.ToSyntaxTokenList()) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithModifiers(propertyInfo.GetterAccessibility.ToSyntaxTokenList()) .WithExpressionBody(ArrowExpressionClause(getterFieldExpression)) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) .AddAttributeLists(forwardedGetAccessorAttributes), diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs new file mode 100644 index 000000000..09ef24907 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs @@ -0,0 +1,35 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AccessibilityExtensions +{ + /// + /// Converts a given value to the equivalent ."/> + /// + /// The input value to convert. + /// The representing the modifiers for . + public static SyntaxTokenList ToSyntaxTokenList(this Accessibility accessibility) + { + return accessibility switch + { + Accessibility.NotApplicable => TokenList(), + Accessibility.Private => TokenList(Token(SyntaxKind.PrivateKeyword)), + Accessibility.ProtectedAndInternal => TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.InternalKeyword)), + Accessibility.Protected => TokenList(Token(SyntaxKind.ProtectedKeyword)), + Accessibility.Internal => TokenList(Token(SyntaxKind.InternalKeyword)), + Accessibility.ProtectedOrInternal => TokenList(Token(SyntaxKind.ProtectedKeyword), Token(SyntaxKind.InternalKeyword)), + Accessibility.Public => TokenList(Token(SyntaxKind.PublicKeyword)), + _ => TokenList() + }; + } +} From 5f2ffcf4c0e43ef48f68dda23f708985a79478d5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 21:15:43 -0700 Subject: [PATCH 027/146] Don't emit an error for collisions for properties --- .../ComponentModel/Models/PropertyInfo.cs | 4 ++-- .../ObservablePropertyGenerator.Execute.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 20e98ba13..21f4f5e2d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -22,7 +22,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// Whether or not the generated property also broadcasts changes. /// Whether or not the generated property also validates its value. /// Whether the old property value is being directly referenced. -/// Indicates whether the property is of a reference type or an unconstrained type parameter. +/// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether to include nullability annotations on the setter. /// Indicates whether to annotate the setter as requiring unreferenced code. /// The sequence of forwarded attributes for the generated property. @@ -39,7 +39,7 @@ internal sealed record PropertyInfo( bool NotifyPropertyChangedRecipients, bool NotifyDataErrorInfo, bool IsOldPropertyValueDirectlyReferenced, - bool IsReferenceTypeOrUnconstraindTypeParameter, + bool IsReferenceTypeOrUnconstrainedTypeParameter, bool IncludeMemberNotNullOnSetAccessor, bool IncludeRequiresUnreferencedCodeOnSetAccessor, EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 40f749c17..1eb438977 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -198,8 +198,8 @@ public static bool TryGetInfo( string fieldName = memberSymbol.Name; string propertyName = GetGeneratedPropertyName(memberSymbol); - // Check for name collisions - if (fieldName == propertyName) + // Check for name collisions (only for fields) + if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) { builder.Add( ObservablePropertyNameCollisionError, @@ -252,7 +252,7 @@ public static bool TryGetInfo( GetNullabilityInfo( memberSymbol, semanticModel, - out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool isReferenceTypeOrUnconstrainedTypeParameter, out bool includeMemberNotNullOnSetAccessor); token.ThrowIfCancellationRequested(); @@ -423,7 +423,7 @@ public static bool TryGetInfo( notifyRecipients, notifyDataErrorInfo, isOldPropertyValueDirectlyReferenced, - isReferenceTypeOrUnconstraindTypeParameter, + isReferenceTypeOrUnconstrainedTypeParameter, includeMemberNotNullOnSetAccessor, includeRequiresUnreferencedCodeOnSetAccessor, forwardedAttributes.ToImmutable()); @@ -1518,7 +1518,7 @@ private static TypeSyntax GetMaybeNullPropertyType(PropertyInfo propertyInfo) // happen when the property is first set to some value that is not null (but the backing field would still be so). // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. - return propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch + return propertyInfo.IsReferenceTypeOrUnconstrainedTypeParameter switch { true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"), From 46d03e83b439decc7903c8d4f8f4b5bbd654a6aa Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 21:27:57 -0700 Subject: [PATCH 028/146] Update generation to account for properties --- .../ComponentModel/Models/PropertyInfo.cs | 3 ++ .../ObservablePropertyGenerator.Execute.cs | 35 ++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 21f4f5e2d..367d9a751 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -4,12 +4,14 @@ using CommunityToolkit.Mvvm.SourceGenerators.Helpers; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// /// A model representing an generated property /// +/// The syntax kind of the annotated member that triggered this property generation. /// The type name for the generated property, including nullability annotations. /// The field name. /// The generated property name. @@ -27,6 +29,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// Indicates whether to annotate the setter as requiring unreferenced code. /// The sequence of forwarded attributes for the generated property. internal sealed record PropertyInfo( + SyntaxKind AnnotatedMemberKind, string TypeNameWithNullabilityAnnotations, string FieldName, string PropertyName, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 1eb438977..f9838da46 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -411,6 +411,7 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); propertyInfo = new PropertyInfo( + memberSyntax.Kind(), typeNameWithNullabilityAnnotations, fieldName, propertyName, @@ -1092,11 +1093,17 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf ExpressionSyntax getterFieldExpression; ExpressionSyntax setterFieldExpression; - // In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments - // with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter. - if (propertyInfo.FieldName == "value") + // If the annotated member is a partial property, we always use the 'field' keyword + if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) { - // We only need to add "this." when referencing the field in the setter (getter and XML docs are not ambiguous) + getterFieldIdentifierName = "field"; + getterFieldExpression = setterFieldExpression = IdentifierName(getterFieldIdentifierName); + } + else if (propertyInfo.FieldName == "value") + { + // In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments + // with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter. We only need + // to add "this." when referencing the field in the setter (getter and XML docs are not ambiguous) getterFieldIdentifierName = "value"; getterFieldExpression = IdentifierName(getterFieldIdentifierName); setterFieldExpression = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), (IdentifierNameSyntax)getterFieldExpression); @@ -1116,6 +1123,13 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf getterFieldExpression = setterFieldExpression = IdentifierName(getterFieldIdentifierName); } + // Prepare the XML docs: + // - For partial properties, always just inherit from the partial declaration + // - For fields, inherit from them + string xmlSummary = propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration + ? "/// " + : $"/// "; + if (propertyInfo.NotifyPropertyChangedRecipients || propertyInfo.IsOldPropertyValueDirectlyReferenced) { // Store the old value for later. This code generates a statement as follows: @@ -1336,13 +1350,18 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // Also add any forwarded attributes setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes); + // Prepare the modifiers for the property + SyntaxTokenList propertyModifiers = propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration + ? propertyInfo.PropertyAccessibility.ToSyntaxTokenList().Add(Token(SyntaxKind.PartialKeyword)) + : propertyInfo.PropertyAccessibility.ToSyntaxTokenList(); + // Construct the generated property as follows: // - // /// + // // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // - // + // // { // // get => ; @@ -1356,10 +1375,10 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), + .WithOpenBracketToken(Token(TriviaList(Comment(xmlSummary)), SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .AddAttributeLists(forwardedPropertyAttributes) - .WithModifiers(propertyInfo.PropertyAccessibility.ToSyntaxTokenList()) + .WithModifiers(propertyModifiers) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithModifiers(propertyInfo.GetterAccessibility.ToSyntaxTokenList()) From 52ad254adef21c290e0a43d4bacd415a9095d5ce Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 22:00:03 -0700 Subject: [PATCH 029/146] Fix a generator crash when used on properties --- .../ComponentModel/ObservablePropertyGenerator.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 4b650c716..9957f3bbe 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -38,16 +38,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return default; } - IFieldSymbol fieldSymbol = (IFieldSymbol)context.TargetSymbol; - // Get the hierarchy info for the target symbol, and try to gather the property info - HierarchyInfo hierarchy = HierarchyInfo.From(fieldSymbol.ContainingType); + HierarchyInfo hierarchy = HierarchyInfo.From(context.TargetSymbol.ContainingType); token.ThrowIfCancellationRequested(); _ = Execute.TryGetInfo( memberSyntax, - fieldSymbol, + context.TargetSymbol, context.SemanticModel, context.GlobalOptions, token, From 8ca2f3c8cfb6507313bcc874df06f9dc61347da0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 22:00:28 -0700 Subject: [PATCH 030/146] Omit implicit accessibility modifiers --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 4 +-- .../Extensions/SyntaxTokenListExtensions.cs | 32 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 3ab9f89d6..5a44bf69d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -66,6 +66,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index f9838da46..63402cca2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -1027,7 +1027,7 @@ private static bool TryGetAccessibilityModifiers( } // Track the property accessibility if explicitly set - if (propertySyntax.Modifiers.Count > 0) + if (propertySyntax.Modifiers.ContainsAnyAccessibilityModifiers()) { propertyAccessibility = memberSymbol.DeclaredAccessibility; } @@ -1035,7 +1035,7 @@ private static bool TryGetAccessibilityModifiers( // Track the accessors accessibility, if explicitly set foreach (AccessorDeclarationSyntax accessor in propertySyntax.AccessorList?.Accessors ?? []) { - if (accessor.Modifiers.Count == 0) + if (!accessor.Modifiers.ContainsAnyAccessibilityModifiers()) { continue; } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs new file mode 100644 index 000000000..99c1ecb85 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxTokenListExtensions +{ + /// + /// Checks whether a given value contains any accessibility modifiers. + /// + /// The input value to check. + /// Whether contains any accessibility modifiers. + public static bool ContainsAnyAccessibilityModifiers(this SyntaxTokenList syntaxList) + { + foreach (SyntaxToken token in syntaxList) + { + if (SyntaxFacts.IsAccessibilityModifier(token.Kind())) + { + return true; + } + } + + return false; + } +} From db221edcf872dc9273a9d9ac71240e13f9ab40da Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 22:00:52 -0700 Subject: [PATCH 031/146] Update the targets of additional attributes --- .../NotifyCanExecuteChangedForAttribute.cs | 17 +++++++----- .../NotifyDataErrorInfoAttribute.cs | 22 +++++++++------ .../NotifyPropertyChangedForAttribute.cs | 27 ++++++++++--------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyCanExecuteChangedForAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyCanExecuteChangedForAttribute.cs index 748281015..33f0b3fb2 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyCanExecuteChangedForAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyCanExecuteChangedForAttribute.cs @@ -12,8 +12,8 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// An attribute that can be used to support properties in generated properties. When this attribute is /// used, the generated property setter will also call for the properties specified /// in the attribute data, causing the validation logic for the command to be executed again. This can be useful to keep the code compact -/// when there are one or more dependent commands that should also be notified when a property is updated. If this attribute is used in -/// a field without , it is ignored (just like ). +/// when there are one or more dependent commands that should also be notified when a property is updated. If this attribute is used on +/// a property without , it is ignored (just like ). /// /// In order to use this attribute, the target property has to implement the interface. /// @@ -24,7 +24,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// { /// [ObservableProperty] /// [NotifyCanExecuteChangedFor(nameof(GreetUserCommand))] -/// private string name; +/// public partial string Name { get; set; } /// /// public IRelayCommand GreetUserCommand { get; } /// } @@ -34,12 +34,12 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Name +/// public partial string Name /// { -/// get => name; +/// get => field; /// set /// { -/// if (SetProperty(ref name, value)) +/// if (SetProperty(ref field, value)) /// { /// GreetUserCommand.NotifyCanExecuteChanged(); /// } @@ -48,7 +48,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// } /// /// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)] +/// +/// Just like , this attribute can also be used on fields as well. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] public sealed class NotifyCanExecuteChangedForAttribute : Attribute { /// diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyDataErrorInfoAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyDataErrorInfoAttribute.cs index 2aa7b1159..6a741d601 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyDataErrorInfoAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyDataErrorInfoAttribute.cs @@ -8,7 +8,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// An attribute that can be used to support in generated properties, when applied to -/// fields contained in a type that is inheriting from and using any validation attributes. +/// partial properties contained in a type that is inheriting from and using any validation attributes. /// When this attribute is used, the generated property setter will also call . /// This allows generated properties to opt-in into validation behavior without having to fallback into a full explicit observable property. /// @@ -20,7 +20,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// [NotifyDataErrorInfo] /// [Required] /// [MinLength(2)] -/// private string username; +/// public partial string Username { get; set; } /// } /// /// @@ -28,17 +28,23 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// [Required] -/// [MinLength(2)] -/// public string Username +/// public partial string Username /// { -/// get => username; -/// set => SetProperty(ref username, value, validate: true); +/// get => field; +/// set => SetProperty(ref field, value, validate: true); /// } /// } /// /// -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +/// +/// +/// This attribute can also be used on a class, which will enable the validation on all generated properties contained in it. +/// +/// +/// Just like , this attribute can also be used on fields as well. +/// +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class NotifyDataErrorInfoAttribute : Attribute { } diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs index 09178f9f5..d963ed72c 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs @@ -13,7 +13,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// used, the generated property setter will also call (or the equivalent /// method in the target class) for the properties specified in the attribute data. This can be useful to keep the code compact when /// there are one or more dependent properties that should also be reported as updated when the value of the annotated observable -/// property is changed. If this attribute is used in a field without , it is ignored. +/// property is changed. If this attribute is used on a property without , it is ignored. /// /// In order to use this attribute, the containing type has to implement the interface /// and expose a method with the same signature as . If the containing @@ -27,11 +27,11 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// { /// [ObservableProperty] /// [NotifyPropertyChangedFor(nameof(FullName))] -/// private string name; +/// public partial string Name { get; set; } /// /// [ObservableProperty] /// [NotifyPropertyChangedFor(nameof(FullName))] -/// private string surname; +/// public partial string Surname { get; set; } /// /// public string FullName => $"{Name} {Surname}"; /// } @@ -41,17 +41,17 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Name +/// public partial string Name /// { -/// get => name; +/// get => field; /// set /// { -/// if (!EqualityComparer<string>.Default.Equals(name, value)) +/// if (!EqualityComparer<string>.Default.Equals(field, value)) /// { /// OnPropertyChanging(nameof(Name)); /// OnPropertyChanged(nameof(FullName)); /// -/// name = value; +/// field = value; /// /// OnPropertyChanged(nameof(Name)); /// OnPropertyChanged(nameof(FullName)); @@ -59,17 +59,17 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// } /// } /// -/// public string Surname +/// public partial string Surname /// { -/// get => surname; +/// get => field; /// set /// { -/// if (!EqualityComparer<string>.Default.Equals(name, value)) +/// if (!EqualityComparer<string>.Default.Equals(field, value)) /// { /// OnPropertyChanging(nameof(Surname)); /// OnPropertyChanged(nameof(FullName)); /// -/// surname = value; +/// field = value; /// /// OnPropertyChanged(nameof(Surname)); /// OnPropertyChanged(nameof(FullName)); @@ -79,7 +79,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// } /// /// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)] +/// +/// Just like , this attribute can also be used on fields as well. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] public sealed class NotifyPropertyChangedForAttribute : Attribute { /// From f9f77719a39b24e1c19e500571543d312070471f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 23:17:30 -0700 Subject: [PATCH 032/146] Fix handling notify data error info --- .../ObservablePropertyGenerator.Execute.cs | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 63402cca2..91d805706 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -306,34 +306,17 @@ public static bool TryGetInfo( continue; } - // The following checks only apply to fields, not properties. That is, attributes - // on partial properties are never forwarded, as they are already on the member. - if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) - { - continue; - } - // Track the current attribute for forwarding if it is a validation attribute if (attributeData.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) { hasAnyValidationAttributes = true; - forwardedAttributes.Add(AttributeInfo.Create(attributeData)); - } - - // Also track the current attribute for forwarding if it is of any of the following types: - // - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute) - // - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute) - // - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute) - // - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute) - // - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute) - if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || - attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + // Only forward the attribute if the target is a field. + // Otherwise, the attribute is already applied correctly. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + } } } @@ -922,6 +905,27 @@ private static void GatherLegacyForwardedAttributes( return; } + // Also track the current attribute for forwarding if it is of any of the following types: + // - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute) + // - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute) + // - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute) + // - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute) + // - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute) + // + // All of these have special handling and are always forwarded when a field is being targeted. + // That is because these attributes really only mean anything when used on generated properties. + foreach (AttributeData attributeData in memberSymbol.GetAttributes()) + { + if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || + attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true) + { + forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + } + } + // Gather explicit forwarded attributes info foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) { From 5d37b7322fc117ca46bb11e77869a1a4adb64626 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 00:32:21 -0700 Subject: [PATCH 033/146] Fix nullability for generated partial properties --- .../ObservablePropertyGenerator.Execute.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 91d805706..813dcd977 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -856,6 +856,18 @@ private static void GetNullabilityInfo( // If this is true, it means the field storage can potentially be in a null state (even if not annotated). isReferenceTypeOrUnconstraindTypeParameter = !GetPropertyType(memberSymbol).IsValueType; + // Special case if the target member is a partial property. In this case, the type should always match the + // declared type of the property declaration, and there is no need for the attribute on the setter. This + // is because assigning the property in the constructor will directly assign to the backing field, and not + // doing so from the constructor will cause Roslyn to emit a warning. Additionally, Roslyn can always see + // that the backing field is being assigned from the setter, so the attribute is just never needed here. + if (memberSymbol.Kind is SymbolKind.Property) + { + includeMemberNotNullOnSetAccessor = false; + + return; + } + // This is used to avoid nullability warnings when setting the property from a constructor, in case the field // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. // Consider this example: @@ -1141,7 +1153,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // __oldValue = ; setterStatements.Add( LocalDeclarationStatement( - VariableDeclaration(GetMaybeNullPropertyType(propertyInfo)) + VariableDeclaration(GetPropertyTypeForOldValue(propertyInfo)) .AddVariables( VariableDeclarator(Identifier("__oldValue")) .WithInitializer(EqualsValueClause(setterFieldExpression))))); @@ -1426,7 +1438,7 @@ public static ImmutableArray GetOnPropertyChangeMethods .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); // Get the type for the 'oldValue' parameter (which can be null on first invocation) - TypeSyntax oldValueTypeSyntax = GetMaybeNullPropertyType(propertyInfo); + TypeSyntax oldValueTypeSyntax = GetPropertyTypeForOldValue(propertyInfo); // Construct the generated method as follows: // @@ -1534,8 +1546,15 @@ private static ITypeSymbol GetPropertyType(ISymbol memberSymbol) /// /// The input instance to process. /// The type of a given property, when it can possibly be - private static TypeSyntax GetMaybeNullPropertyType(PropertyInfo propertyInfo) + private static TypeSyntax GetPropertyTypeForOldValue(PropertyInfo propertyInfo) { + // For partial properties, the old value always matches the exact property type. + // See additional notes for this in the 'GetNullabilityInfo' method above. + if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) + { + return IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); + } + // Prepare the nullable type for the previous property value. This is needed because if the type is a reference // type, the previous value might be null even if the property type is not nullable, as the first invocation would // happen when the property is first set to some value that is not null (but the backing field would still be so). From aabcd1596022545a8e3493036cb0c0d82f7a7fe4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 00:32:34 -0700 Subject: [PATCH 034/146] Add initial codegen tests for partial properties --- .../Test_SourceGeneratorsCodegen.cs | 718 ++++++++++++++++++ .../Test_SourceGeneratorsCodegen.cs | 2 +- 2 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs new file mode 100644 index 000000000..62539c403 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -0,0 +1,718 @@ +// 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.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +partial class Test_SourceGeneratorsCodegen +{ + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithNoModifiers_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + partial int Number { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + partial int Number + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly1() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly2() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + internal partial int Number { private get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal partial int Number + { + private get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithReferenceType_NotNullable_OnPartialProperty_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithReferenceType_Nullable_OnPartialProperty_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public partial string? Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyPropertyChange_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + public partial string Name { get; set; } + + public string FullName => ""; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FullName); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyCanExecuteChange_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + + namespace MyApp; + + partial class MyViewModel : ObservableRecipient + { + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(TestCommand))] + public partial string Name { get; set; } + + public IRelayCommand TestCommand => null; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + TestCommand.NotifyCanExecuteChanged(); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyRecipients_WorksCorrectly() + { + string source = """ + using System; + using System.Windows.Input; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableRecipient + { + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial string Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + string __oldValue = field; + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + Broadcast(__oldValue, value, "Name"); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyDataErrorInfo_WorksCorrectly() + { + string source = """ + using System; + using System.ComponentModel.DataAnnotations; + using System.Windows.Input; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableValidator + { + [ObservableProperty] + [NotifyDataErrorInfo] + [Required] + public partial string Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index 6997cfaa1..d3d44ec5b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -16,7 +16,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] -public class Test_SourceGeneratorsCodegen +public partial class Test_SourceGeneratorsCodegen { [TestMethod] public void ObservablePropertyWithNonNullableReferenceType_EmitsMemberNotNullAttribute() From 62c06f11131d59f0fb3863663c91fec45ddb5390 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 11:44:32 -0700 Subject: [PATCH 035/146] Add 'UseObservablePropertyOnPartialPropertyAnalyzer' --- .../AnalyzerReleases.Shipped.md | 1 + ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...resCSharpLanguageVersionPreviewAnalyzer.cs | 2 +- ...rvablePropertyOnPartialPropertyAnalyzer.cs | 70 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 21 ++++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index ce431cea7..d77a3a005 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -83,3 +83,4 @@ MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator Rule ID | Category | Severity | Notes --------|----------|----------|------- MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 +MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 5a44bf69d..5df7ef0e0 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -47,6 +47,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs index 1b7a96114..aa96c9bef 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs @@ -13,7 +13,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// -/// A diagnostic analyzer that generates errors when a property using [ObservableProperty] on a partial property is in a project with the C# language version not set to preview. +/// A diagnostic analyzer that generates errors when a property using [ObservableProperty] on a partial property is in a project with the C# language version not set to preview. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class RequiresCSharpLanguageVersionPreviewAnalyzer : DiagnosticAnalyzer diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs new file mode 100644 index 000000000..a45b0bde3 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -0,0 +1,70 @@ +// 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_11_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 a suggestion whenever [ObservableProperty] is used on a field when a partial property could be used instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseObservablePropertyOnPartialPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UseObservablePropertyOnPartialProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Using [ObservableProperty] on partial properties is only supported when using C# preview. + // As such, if that is not the case, return immediately, as no diagnostic should be produced. + if (!context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We're intentionally only looking for fields here + if (context.Symbol is not IFieldSymbol fieldSymbol) + { + return; + } + + // Check that we are in fact using [ObservableProperty] + if (!fieldSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Emit the diagnostic for this field to suggest changing to a partial property instead + context.ReportDiagnostic(Diagnostic.Create( + UseObservablePropertyOnPartialProperty, + observablePropertyAttribute.GetLocation(), + fieldSymbol.ContainingType, + fieldSymbol)); + }, SymbolKind.Field); + }); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index caa733463..f68a7ed72 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -34,6 +34,11 @@ internal static class DiagnosticDescriptors /// public const string AsyncVoidReturningRelayCommandMethodId = "MVVMTK0039"; + /// + /// The diagnostic id for . + /// + public const string UseObservablePropertyOnPartialPropertyId = "MVVMTK0042"; + /// /// Gets a indicating when a duplicate declaration of would happen. /// @@ -690,4 +695,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "The C# language version must be set to 'preview' when using [ObservableProperty] on partial properties for the source generators to emit valid code (the preview option must be set in the .csproj/.props file).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0041"); + + /// + /// Gets a for a CanvasEffect property with invalid accessors. + /// + /// 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( + 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)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Fields using [ObservableProperty] can be converted to partial properties instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0042"); } From 6342b83d7a710ac571f80891b0e89fa223c87328 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 11:48:22 -0700 Subject: [PATCH 036/146] Add unit tests for new analyzer --- .../Test_SourceGeneratorsDiagnostics.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 03a3f323d..718e566b1 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -47,4 +47,61 @@ public partial class SampleViewModel : ObservableObject await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, languageVersion: LanguageVersion.Preview); } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsNotPreview_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp12); + } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsPreview_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0042:ObservableProperty|}] + private string name; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsPreview_OnPartialProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public string Bar { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } } From efa90a3347fef8bdf80bae7dab0d3a84caa0eb9a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 13:34:34 -0700 Subject: [PATCH 037/146] Add 'InvalidPropertyLevelObservablePropertyAttributeAnalyzer' --- .../AnalyzerReleases.Shipped.md | 3 +- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 36 +----- ...evelObservablePropertyAttributeAnalyzer.cs | 122 ++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 16 +++ 5 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index d77a3a005..044a3783c 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -83,4 +83,5 @@ MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator Rule ID | Category | Severity | Notes --------|----------|----------|------- MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 -MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 +MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0042 +MVVMTK0043| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 5df7ef0e0..045c0ab14 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -47,6 +47,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 813dcd977..c4a08b831 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -55,41 +55,9 @@ static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? contain } #if ROSLYN_4_11_0_OR_GREATER - // Matches a valid partial property declaration - static bool IsCandidateProperty(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) - { - // The node must be a property declaration with two accessors - if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) - { - containingTypeNode = null; - - return false; - } - - // The property must be partial (we'll check that it's a declaration from its symbol) - if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) - { - 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)) - { - containingTypeNode = null; - - return false; - } - - containingTypeNode = (TypeDeclarationSyntax?)property.Parent; - - return true; - } - // We only support matching properties on Roslyn 4.11 and greater - if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && !IsCandidateProperty(node, out parentNode)) + if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && + !InvalidPropertyLevelObservablePropertyAttributeAnalyzer.IsValidCandidateProperty(node, out parentNode)) { return false; } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..8560dc1d1 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +#if ROSLYN_4_11_0_OR_GREATER +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +#endif +using Microsoft.CodeAnalysis; +#if ROSLYN_4_11_0_OR_GREATER +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +#endif +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 property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyLevelObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidPropertyDeclarationForObservableProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + +#if ROSLYN_4_11_0_OR_GREATER + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Don't analyze generated symbols, we only want to warn those + // that users have actually written on their own in source code. + if (context.IsGeneratedCode) + { + return; + } + + // We're intentionally only looking for properties here + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property isn't using [ObservableProperty], there's nothing to do + if (!propertySymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Check that the property has valid syntax + foreach (SyntaxReference propertyReference in propertySymbol.DeclaringSyntaxReferences) + { + SyntaxNode propertyNode = propertyReference.GetSyntax(context.CancellationToken); + + if (!IsValidCandidateProperty(propertyNode, out _)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationForObservableProperty, + observablePropertyAttribute.GetLocation(), + propertySymbol.ContainingType, + propertySymbol)); + } + } + }, SymbolKind.Property); + }); +#endif + } + +#if ROSLYN_4_11_0_OR_GREATER + /// + /// Checks whether a given property declaration has valid syntax. + /// + /// The input node to validate. + /// The resulting node for the containing type of the property, if valid. + /// Whether is a valid property. + internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) + { + // The node must be a property declaration with two accessors + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) + { + containingTypeNode = null; + + return false; + } + + // The property must be partial (we'll check that it's a declaration from its symbol) + if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + 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)) + { + containingTypeNode = null; + + return false; + } + + containingTypeNode = (TypeDeclarationSyntax?)property.Parent; + + return true; + } +#endif +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index f68a7ed72..b04ba72b8 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -711,4 +711,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Fields using [ObservableProperty] can be converted to partial properties instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0042"); + + /// + /// 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)". + /// + /// + 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)", + 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.", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0043"); } From 767c05b2552f0abb8ddc1541c266f6469b7fd106 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 13:35:04 -0700 Subject: [PATCH 038/146] Add 'UnsupportedRoslynVersionForPartialPropertyAnalyzer' --- .../AnalyzerReleases.Shipped.md | 1 + ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...RoslynVersionForPartialPropertyAnalyzer.cs | 60 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 16 +++++ 4 files changed, 78 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 044a3783c..cb19f6246 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -85,3 +85,4 @@ Rule ID | Category | Severity | Notes MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0042 MVVMTK0043| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 +MVVMTK0044| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 045c0ab14..9e2e2d555 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -46,6 +46,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs new file mode 100644 index 000000000..9a9c53f02 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs @@ -0,0 +1,60 @@ +// 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_11_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 a property, if the Roslyn version in use is not high enough. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UnsupportedRoslynVersionForPartialPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UnsupportedRoslynVersionForObservablePartialPropertySupport); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We're intentionally only looking for properties here + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property has [ObservableProperty], emit an error in all cases + if (propertySymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + context.ReportDiagnostic(Diagnostic.Create( + UnsupportedRoslynVersionForObservablePartialPropertySupport, + observablePropertyAttribute.GetLocation(), + propertySymbol.ContainingType, + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index b04ba72b8..5f7fb3883 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -727,4 +727,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Properties annotated with [ObservableProperty] must be partial properties with a getter and a setter that is not init-only.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0043"); + + /// + /// Gets a indicating when [ObservableProperty] is applied to a property when an unsupported version of Roslyn is used. + /// + /// Format: "The property {0}.{1} cannot be used to generate an observable property, as the current Roslyn version being used is not high enough (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK)". + /// + /// + public static readonly DiagnosticDescriptor UnsupportedRoslynVersionForObservablePartialPropertySupport = new DiagnosticDescriptor( + id: "MVVMTK0044", + title: "Unsupported Roslyn version for using [ObservableProperty] on partial properties", + messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as the current Roslyn version being used is not high enough (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK)", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Using [ObservableProperty] with (partial) properties requires a higher version of Roslyn (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0044"); } From eea59fd474db2c88948fc4c3a0f080fa4a7b4969 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 14:42:00 -0700 Subject: [PATCH 039/146] Fix handling of 'private protected' accessors --- .../Extensions/AccessibilityExtensions.cs | 2 +- .../Test_SourceGeneratorsCodegen.cs | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs index 09ef24907..cd7a8226b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs @@ -24,7 +24,7 @@ public static SyntaxTokenList ToSyntaxTokenList(this Accessibility accessibility { Accessibility.NotApplicable => TokenList(), Accessibility.Private => TokenList(Token(SyntaxKind.PrivateKeyword)), - Accessibility.ProtectedAndInternal => TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.InternalKeyword)), + Accessibility.ProtectedAndInternal => TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.ProtectedKeyword)), Accessibility.Protected => TokenList(Token(SyntaxKind.ProtectedKeyword)), Accessibility.Internal => TokenList(Token(SyntaxKind.InternalKeyword)), Accessibility.ProtectedOrInternal => TokenList(Token(SyntaxKind.ProtectedKeyword), Token(SyntaxKind.InternalKeyword)), diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs index 62539c403..dce57de5c 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -243,6 +243,81 @@ internal partial int Number } [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly3() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + protected internal partial string Name { get; private protected set; } + } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + protected internal partial string Name + { + get => field; + private protected set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + +[TestMethod] public void ObservablePropertyWithReferenceType_NotNullable_OnPartialProperty_WorksCorrectly() { string source = """ From 162bab5991c3015cfb6422fbab4f79125285163c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 14:42:24 -0700 Subject: [PATCH 040/146] Add unit tests for invalid property declarations --- ...evelObservablePropertyAttributeAnalyzer.cs | 12 +- ...RoslynVersionForPartialPropertyAnalyzer.cs | 32 ++++ ...urceGenerators.Roslyn4031.UnitTests.csproj | 4 + .../Test_SourceGeneratorsDiagnostics.cs | 139 +++++++++++++++++- .../Test_SourceGeneratorsDiagnostics.cs | 32 +++- 5 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs index 8560dc1d1..fdb349086 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -2,15 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Immutable; #if ROSLYN_4_11_0_OR_GREATER + +using System.Collections.Immutable; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; -#endif using Microsoft.CodeAnalysis; -#if ROSLYN_4_11_0_OR_GREATER using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -#endif using Microsoft.CodeAnalysis.Diagnostics; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; @@ -31,7 +29,6 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); -#if ROSLYN_4_11_0_OR_GREATER context.RegisterCompilationStartAction(static context => { // Get the symbol for [ObservableProperty] @@ -77,10 +74,8 @@ public override void Initialize(AnalysisContext context) } }, SymbolKind.Property); }); -#endif } -#if ROSLYN_4_11_0_OR_GREATER /// /// Checks whether a given property declaration has valid syntax. /// @@ -118,5 +113,6 @@ internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarati return true; } -#endif } + +#endif diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs new file mode 100644 index 000000000..b5cadd896 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs @@ -0,0 +1,32 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +[TestClass] +public class Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer +{ + [TestMethod] + public async Task UnsupportedRoslynVersionForPartialPropertyAnalyzer_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0044:ObservableProperty|}] + public string Bar { get; set; } + } + } + """; + + await Test_SourceGeneratorsDiagnostics.VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index cbea561a0..96233ae85 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj @@ -5,6 +5,10 @@ $(DefineConstants);ROSLYN_4_3_1_OR_GREATER + + + + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 718e566b1..1bb0b461c 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -21,7 +21,7 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [{|MVVMTK0041:ObservableProperty|}] - public string Bar { get; set; } + public string Name { get; set; } } } """; @@ -40,7 +40,7 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [ObservableProperty] - public string Bar { get; set; } + public string Name { get; set; } } } """; @@ -97,11 +97,144 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [ObservableProperty] - public string Bar { get; set; } + public string Name { get; set; } } } """; await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnValidPropertyDeclaration_DoesNotWarn1() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string {|CS9248:Name|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0103", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnValidPropertyDeclaration_DoesNotWarn2() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + internal partial string {|CS9248:Name|} { get; private set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0103", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnValidPropertyDeclaration_DoesNotWarn3() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + protected internal partial string {|CS9248:Name|} { get; private protected set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0103", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnNonPartialProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnReadOnlyProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { get; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnWriteOnlyProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnInitOnlyProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { get; init; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 0ab363974..6883440df 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1886,7 +1886,21 @@ internal static class IsExternalInit /// The type of the analyzer to test. /// The input source to process with diagnostic annotations. /// The language version to use to parse code and run tests. - private static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion) + internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion) + where TAnalyzer : DiagnosticAnalyzer, new() + { + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(markdownSource, languageVersion, [], []); + } + + /// + /// Verifies the diagnostic errors for a given analyzer, and that all available source generators can run successfully with the input source (including subsequent compilation). + /// + /// The type of the analyzer to test. + /// The input source to process with diagnostic annotations. + /// The language version to use to parse code and run tests. + /// The diagnostic ids to expect for the input source code. + /// The list of diagnostic ids to ignore in the final compilation. + internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion, string[] generatorDiagnosticsIds, string[] ignoredDiagnosticIds) where TAnalyzer : DiagnosticAnalyzer, new() { await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(markdownSource, languageVersion); @@ -1905,7 +1919,7 @@ private static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration "Foo()") string source = Regex.Replace(markdownSource, @"{\|((?:,?\w+)+):(.+)\|}", m => m.Groups[2].Value); - VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion)), generators, Array.Empty()); + VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion)), generators, generatorDiagnosticsIds, ignoredDiagnosticIds); } /// @@ -1914,12 +1928,12 @@ private static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGenerationThe generator type to use. /// The input source to process. /// The diagnostic ids to expect for the input source code. - private static void VerifyGeneratedDiagnostics(string source, params string[] diagnosticsIds) + internal static void VerifyGeneratedDiagnostics(string source, params string[] diagnosticsIds) where TGenerator : class, IIncrementalGenerator, new() { IIncrementalGenerator generator = new TGenerator(); - VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp8)), new[] { generator }, diagnosticsIds); + VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp8)), new[] { generator }, diagnosticsIds, []); } /// @@ -1928,7 +1942,8 @@ private static void VerifyGeneratedDiagnostics(string source, params /// The input source tree to process. /// The generators to apply to the input syntax tree. /// The diagnostic ids to expect for the input source code. - private static void VerifyGeneratedDiagnostics(SyntaxTree syntaxTree, IIncrementalGenerator[] generators, string[] generatorDiagnosticsIds) + /// The list of diagnostic ids to ignore in the final compilation. + internal static void VerifyGeneratedDiagnostics(SyntaxTree syntaxTree, IIncrementalGenerator[] generators, string[] generatorDiagnosticsIds, string[] ignoredDiagnosticIds) { // Ensure CommunityToolkit.Mvvm and System.ComponentModel.DataAnnotations are loaded Type observableObjectType = typeof(ObservableObject); @@ -1944,7 +1959,7 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() // Create a syntax tree with the input source CSharpCompilation compilation = CSharpCompilation.Create( "original", - new SyntaxTree[] { syntaxTree }, + [syntaxTree], references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); @@ -1963,7 +1978,10 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() // Compute diagnostics for the final compiled output (just include errors) List outputCompilationDiagnostics = outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToList(); - Assert.IsTrue(outputCompilationDiagnostics.Count == 0, $"resultingIds: {string.Join(", ", outputCompilationDiagnostics)}"); + // Filtered diagnostics + List filteredDiagnostics = outputCompilationDiagnostics.Where(diagnostic => !ignoredDiagnosticIds.Contains(diagnostic.Id)).ToList(); + + Assert.IsTrue(filteredDiagnostics.Count == 0, $"resultingIds: {string.Join(", ", filteredDiagnostics)}"); } GC.KeepAlive(observableObjectType); From 5e02b9acb25bf83264454157e306b2188f62dbd1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 17:09:22 -0700 Subject: [PATCH 041/146] Add 'UsePartialPropertyForObservablePropertyCodeFixer' --- ...lPropertyForObservablePropertyCodeFixer.cs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs new file mode 100644 index 000000000..0072e1aec --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.SourceGenerators; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.CodeFixers; + +/// +/// A code fixer that converts fields using [ObservableProperty] to partial properties. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UsePartialPropertyForObservablePropertyCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnPartialPropertyId); + + /// + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + // Retrieve the properties passed by the analyzer + if (diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey] is not string fieldName || + diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey] is not string propertyName) + { + return; + } + + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Get the field declaration from the target diagnostic (we only support individual fields, with a single declaration) + if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration && + identifierName == fieldName) + { + // Register the code fix to update the class declaration to inherit from ObservableObject instead + context.RegisterCodeFix( + CodeAction.Create( + title: "Use a partial property", + createChangedDocument: token => ConvertToPartialProperty(context.Document, root, fieldDeclaration, fieldName, propertyName, context.CancellationToken), + equivalenceKey: "Use a partial property"), + diagnostic); + } + } + + /// + /// Applies the code fix to a target identifier and returns an updated document. + /// + /// The original document being fixed. + /// The original tree root belonging to the current document. + /// The for the field being updated. + /// The name of the annotated field. + /// The name of the generated property. + /// The cancellation token for the operation. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static async Task ConvertToPartialProperty( + Document document, + SyntaxNode root, + FieldDeclarationSyntax fieldDeclaration, + string fieldName, + string propertyName, + CancellationToken cancellationToken) + { + // Get all attributes that were on the field. Here we only include those targeting either + // the field, or the property. Those targeting the accessors will be moved there directly. + AttributeListSyntax[] propertyAttributes = + fieldDeclaration + .AttributeLists + .Where(list => list.Target is null || list.Target.Kind() is SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) + .ToArray(); + + // Separately, also get all attributes for the property getters + AttributeListSyntax[] getterAttributes = + fieldDeclaration + .AttributeLists + .Where(list => list.Target?.Kind() is SyntaxKind.GetKeyword) + .ToArray(); + + // Also do the same for the setters + AttributeListSyntax[] setterAttributes = + fieldDeclaration + .AttributeLists + .Where(list => list.Target?.Kind() is SyntaxKind.SetKeyword) + .ToArray(); + + // Create the following property declaration: + // + // + // public partial + // { + // + // get; + // + // + // set; + // } + PropertyDeclarationSyntax propertyDeclaration = + PropertyDeclaration(fieldDeclaration.Declaration.Type, Identifier(propertyName)) + .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.PartialKeyword)) + .AddAttributeLists(propertyAttributes) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .AddAttributeLists(getterAttributes), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .AddAttributeLists(setterAttributes)); + + SyntaxTree updatedTree = root.ReplaceNode(fieldDeclaration, propertyDeclaration).SyntaxTree; + + return document.WithSyntaxRoot(await updatedTree.GetRootAsync(cancellationToken).ConfigureAwait(false)); + } +} From 9ba8f27919167d96846d29799e59a878c4793722 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 17:09:45 -0700 Subject: [PATCH 042/146] Add new helper for testing code fixers --- ...rsionTest{TAnalyzer,TCodeFix,TVerifier}.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs new file mode 100644 index 000000000..55127c5c8 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs @@ -0,0 +1,44 @@ +// 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 Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; + +/// +/// A custom that uses a specific C# language version to parse code. +/// +/// The type of the analyzer to produce diagnostics. +/// The type of code fix to test. +/// The type of verifier to use to validate the code fixer. +internal sealed class CSharpCodeFixWithLanguageVersionTest : CSharpCodeFixTest + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + where TVerifier : IVerifier, new() +{ + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion languageVersion; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The C# language version to use to parse code. + public CSharpCodeFixWithLanguageVersionTest(LanguageVersion languageVersion) + { + this.languageVersion = languageVersion; + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(this.languageVersion, DocumentationMode.Diagnose); + } +} From b98a6586bcff21b735e5d597d6c00bfb8ce3aa2e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 17:10:04 -0700 Subject: [PATCH 043/146] Add unit test for new code fixer --- ...rvablePropertyOnPartialPropertyAnalyzer.cs | 3 + ...oidReturningRelayCommandMethodCodeFixer.cs | 2 +- ...gAttributeInsteadOfInheritanceCodeFixer.cs | 2 +- ...enceForObservablePropertyFieldCodeFixer.cs | 2 +- ...urceGenerators.Roslyn4031.UnitTests.csproj | 1 + ...urceGenerators.Roslyn4110.UnitTests.csproj | 2 + ...lPropertyForObservablePropertyCodeFixer.cs | 69 +++++++++++++++++++ ....Mvvm.SourceGenerators.UnitTests.projitems | 1 + ...lyzerWithLanguageVersionTest{TAnalyzer}.cs | 2 +- 9 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs index a45b0bde3..16a42bc1c 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -60,6 +60,9 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create( UseObservablePropertyOnPartialProperty, observablePropertyAttribute.GetLocation(), + ImmutableDictionary.Create() + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name) + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)), fieldSymbol.ContainingType, fieldSymbol)); }, SymbolKind.Field); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs index cff9df992..c77c38b7b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.AsyncVoidReturningRelayCommandMethodCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class Test_AsyncVoidReturningRelayCommandMethodCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs index a2d3e920d..2e63a49f9 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.ClassUsingAttributeInsteadOfInheritanceCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class ClassUsingAttributeInsteadOfInheritanceCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs index 9771ccd08..665761405 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.FieldReferenceForObservablePropertyFieldCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class Test_FieldReferenceForObservablePropertyFieldCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index 96233ae85..9dbd39d92 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj index 394a10614..15077e2aa 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -7,6 +7,7 @@ + @@ -15,6 +16,7 @@ + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs new file mode 100644 index 000000000..10b9f710d --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -0,0 +1,69 @@ +// 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.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CSharpCodeFixTest = CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers.CSharpCodeFixWithLanguageVersionTest< + CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnPartialPropertyAnalyzer, + CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForObservablePropertyCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; +using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< + CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnPartialPropertyAnalyzer, + CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForObservablePropertyCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +[TestClass] +public class Test_UsePartialPropertyForObservablePropertyCodeFixer +{ + [TestMethod] + public async Task SimpleFieldWithNoReferences() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I") + }); + + await test.RunAsync(); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems index 881fdb625..8f141adb6 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems @@ -9,6 +9,7 @@ CommunityToolkit.Mvvm.SourceGenerators.UnitTests + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs index d76d8f356..8e41258ab 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs @@ -29,7 +29,7 @@ internal sealed class CSharpAnalyzerWithLanguageVersionTest : CSharpA private readonly LanguageVersion languageVersion; /// - /// Creates a new instance with the specified paramaters. + /// Creates a new instance with the specified parameters. /// /// The C# language version to use to parse code. private CSharpAnalyzerWithLanguageVersionTest(LanguageVersion languageVersion) From 8cdebe0a9c131830baa2cee377081e8900daa1ca Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 18:55:37 -0700 Subject: [PATCH 044/146] Fix attributes handling, add unit tests --- ...lPropertyForObservablePropertyCodeFixer.cs | 132 ++++++++++- .../Extensions/CompilationExtensions.cs | 3 + .../Extensions/SymbolInfoExtensions.cs | 2 +- ...lPropertyForObservablePropertyCodeFixer.cs | 223 +++++++++++++++++- 4 files changed, 352 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 0072e1aec..8814e9451 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -2,12 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.SourceGenerators; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -26,6 +28,30 @@ namespace CommunityToolkit.Mvvm.CodeFixers; [Shared] public sealed class UsePartialPropertyForObservablePropertyCodeFixer : CodeFixProvider { + /// + /// The mapping of well-known MVVM Toolkit attributes. + /// + private static readonly ImmutableDictionary MvvmToolkitAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("NotifyCanExecuteChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyCanExecuteChangedForAttribute"), + new KeyValuePair("NotifyDataErrorInfoAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute"), + new KeyValuePair("NotifyPropertyChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedForAttribute"), + new KeyValuePair("NotifyPropertyChangedRecipientsAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute"), + new KeyValuePair("ObservablePropertyAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") + }); + + /// + /// The mapping of well-known data annotation attributes. + /// + private static readonly ImmutableDictionary DataAnnotationsAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("UIHintAttribute", "System.ComponentModel.DataAnnotations.UIHintAttribute"), + new KeyValuePair("ScaffoldColumnAttribute", "System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute"), + new KeyValuePair("DisplayAttribute", "System.ComponentModel.DataAnnotations.DisplayAttribute"), + new KeyValuePair("EditableAttribute", "System.ComponentModel.DataAnnotations.EditableAttribute"), + new KeyValuePair("KeyAttribute", "System.ComponentModel.DataAnnotations.KeyAttribute") + }); + /// public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnPartialPropertyId); @@ -41,6 +67,12 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) Diagnostic diagnostic = context.Diagnostics[0]; TextSpan diagnosticSpan = context.Span; + // This code fixer needs the semantic model, so check that first + if (!context.Document.SupportsSemanticModel) + { + return; + } + // Retrieve the properties passed by the analyzer if (diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey] is not string fieldName || diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey] is not string propertyName) @@ -54,6 +86,13 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration && identifierName == fieldName) { + // We only support fields with up to one attribute per attribute list. + // This is so we can easily check one attribute when updating targets. + if (fieldDeclaration.AttributeLists.Any(static list => list.Attributes.Count > 1)) + { + return; + } + // Register the code fix to update the class declaration to inherit from ObservableObject instead context.RegisterCodeFix( CodeAction.Create( @@ -82,26 +121,107 @@ private static async Task ConvertToPartialProperty( string propertyName, CancellationToken cancellationToken) { + SemanticModel semanticModel = (await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false))!; + + // Try to get all necessary type symbols to process the attributes + if (!semanticModel.Compilation.TryBuildNamedTypeSymbolMap(MvvmToolkitAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary? toolkitTypeSymbols) || + !semanticModel.Compilation.TryBuildNamedTypeSymbolMap(DataAnnotationsAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary? annotationTypeSymbols)) + { + return document; + } + + // Also query [ValidationAttribute] + if (semanticModel.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") is not INamedTypeSymbol validationAttributeSymbol) + { + return document; + } + // Get all attributes that were on the field. Here we only include those targeting either // the field, or the property. Those targeting the accessors will be moved there directly. - AttributeListSyntax[] propertyAttributes = + List propertyAttributes = fieldDeclaration .AttributeLists - .Where(list => list.Target is null || list.Target.Kind() is SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) - .ToArray(); + .Where(list => list.Target is null || list.Target.Identifier.Kind() is not (SyntaxKind.GetKeyword or SyntaxKind.SetKeyword)) + .ToList(); + + // Fixup attribute lists as following: + // 1) If they have the 'field:' target, keep it (it's no longer the default) + // 2) If they have the 'property:' target, remove it (it's not needed anymore) + // 3) If they are from the MVVM Toolkit, remove the target (they'll apply to the property) + // 4) If they have no target and they are either a validation attribute, or any of the well-known + // data annotation attributes (which are automatically forwarded), leave them without a target. + // 5) If they have no target, add 'field:' to preserve the original behavior + // 5) Otherwise, leave them without changes (this will carry over invalid targets as-is) + for (int i = 0; i < propertyAttributes.Count; i++) + { + AttributeListSyntax attributeListSyntax = propertyAttributes[i]; + + // Special case: the list has no attributes. Just remove it entirely. + if (attributeListSyntax.Attributes is []) + { + propertyAttributes.RemoveAt(i--); + + continue; + } + + // Case 1 + if (attributeListSyntax.Target?.Identifier.IsKind(SyntaxKind.FieldKeyword) is true) + { + continue; + } + + // Case 2 + if (attributeListSyntax.Target?.Identifier.IsKind(SyntaxKind.PropertyKeyword) is true) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(null); + + continue; + } + + // Make sure we can retrieve the symbol for the attribute type. + // We are guaranteed to always find a single attribute in the list. + if (!semanticModel.GetSymbolInfo(attributeListSyntax.Attributes[0], cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol)) + { + return document; + } + + // Case 3 + if (toolkitTypeSymbols.ContainsValue(attributeSymbol)) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(null); + + continue; + } + + // Case 4 + if (annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol)) + { + continue; + } + + // Case 5 + if (attributeListSyntax.Target is null) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword))); + + continue; + } + } // Separately, also get all attributes for the property getters AttributeListSyntax[] getterAttributes = fieldDeclaration .AttributeLists - .Where(list => list.Target?.Kind() is SyntaxKind.GetKeyword) + .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.GetKeyword) + .Select(list => list.WithTarget(null)) .ToArray(); // Also do the same for the setters AttributeListSyntax[] setterAttributes = fieldDeclaration .AttributeLists - .Where(list => list.Target?.Kind() is SyntaxKind.SetKeyword) + .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.SetKeyword) + .Select(list => list.WithTarget(null)) .ToArray(); // Create the following property declaration: @@ -118,7 +238,7 @@ private static async Task ConvertToPartialProperty( PropertyDeclarationSyntax propertyDeclaration = PropertyDeclaration(fieldDeclaration.Declaration.Type, Identifier(propertyName)) .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.PartialKeyword)) - .AddAttributeLists(propertyAttributes) + .AddAttributeLists(propertyAttributes.ToArray()) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs index 8f3c06b93..6a584bbfb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs @@ -118,6 +118,9 @@ public static bool TryBuildNamedTypeSymbolMap( { ImmutableDictionary.Builder builder = ImmutableDictionary.CreateBuilder(); + // Ensure we always use the right comparer for values, when needed + builder.ValueComparer = SymbolEqualityComparer.Default; + foreach (KeyValuePair pair in typeNames) { if (compilation.GetTypeByMetadataName(pair.Value) is not INamedTypeSymbol attributeSymbol) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs index c87f9a8fb..4a81b6bee 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs @@ -19,7 +19,7 @@ internal static class SymbolInfoExtensions /// The resulting attribute type symbol, if correctly resolved. /// Whether is resolved to a symbol. /// - /// This can be used to ensure users haven't eg. spelled names incorrecty or missed a using directive. Normally, code would just + /// This can be used to ensure users haven't eg. spelled names incorrectly or missed a using directive. Normally, code would just /// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore /// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own. /// diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index 10b9f710d..7e23f160d 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -61,7 +61,228 @@ partial class C : ObservableObject test.FixedState.ExpectedDiagnostics.AddRange(new[] { // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. - DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I") + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithAdditionalAttributes1() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello")] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello")] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 24, 7, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithAdditionalAttributes2() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello1")] + [NotifyCanExecuteChangedFor("hello2")] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello1")] + [NotifyCanExecuteChangedFor("hello2")] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 24, 8, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithAdditionalAttributes3() + { + string original = """ + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [field: MinLength(1)] + [property: MinLength(2)] + private int i; + } + """; + + string @fixed = """ + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [field: MinLength(1)] + [MinLength(2)] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(9,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(9, 24, 9, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithAdditionalAttributes4() + { + string original = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [Test("This is on the field")] + [field: Test("This is also on a the field, but using 'field:'")] + [property: Test("This is on the property")] + [get: Test("This is on the getter")] + [set: Test("This is also on the setter")] + [set: Test("This is a second one on the setter")] + [ignored: Test("This should be ignored, but still carried over")] + private int i; + } + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class TestAttribute(string text) : Attribute; + """; + + string @fixed = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [field: Test("This is on the field")] + [field: Test("This is also on a the field, but using 'field:'")] + [Test("This is on the property")] + [ignored: Test("This should be ignored, but still carried over")] + public partial int I { [Test("This is on the getter")] + get; [Test("This is also on the setter")] + [Test("This is a second one on the setter")] + set; + } + } + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class TestAttribute(string text) : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 6, 7, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(12,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(12, 24, 12, 25).WithArguments("C.I"), }); await test.RunAsync(); From e6a5c091f8951eb76590882612618f22733d110e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 19:18:09 -0700 Subject: [PATCH 045/146] Add unit tests for comments in code fixer --- ...lPropertyForObservablePropertyCodeFixer.cs | 12 +- ...lPropertyForObservablePropertyCodeFixer.cs | 152 ++++++++++++++++++ 2 files changed, 160 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 8814e9451..4dfb27ba3 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -213,7 +214,7 @@ private static async Task ConvertToPartialProperty( fieldDeclaration .AttributeLists .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.GetKeyword) - .Select(list => list.WithTarget(null)) + .Select(list => list.WithTarget(null).WithAdditionalAnnotations(Formatter.Annotation)) .ToArray(); // Also do the same for the setters @@ -221,7 +222,7 @@ private static async Task ConvertToPartialProperty( fieldDeclaration .AttributeLists .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.SetKeyword) - .Select(list => list.WithTarget(null)) + .Select(list => list.WithTarget(null).WithAdditionalAnnotations(Formatter.Annotation)) .ToArray(); // Create the following property declaration: @@ -239,13 +240,16 @@ private static async Task ConvertToPartialProperty( PropertyDeclaration(fieldDeclaration.Declaration.Type, Identifier(propertyName)) .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.PartialKeyword)) .AddAttributeLists(propertyAttributes.ToArray()) + .WithAdditionalAnnotations(Formatter.Annotation) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) - .AddAttributeLists(getterAttributes), + .AddAttributeLists(getterAttributes) + .WithAdditionalAnnotations(Formatter.Annotation), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) - .AddAttributeLists(setterAttributes)); + .AddAttributeLists(setterAttributes) + .WithAdditionalAnnotations(Formatter.Annotation)); SyntaxTree updatedTree = root.ReplaceNode(fieldDeclaration, propertyDeclaration).SyntaxTree; diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index 7e23f160d..5520aaae5 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -287,4 +287,156 @@ public class TestAttribute(string text) : Attribute; await test.RunAsync(); } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithSimpleComment() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment + [ObservableProperty] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment + [ObservableProperty] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 24, 7, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithTwoLineComment() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment. + // This is more comment. + [ObservableProperty] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment. + // This is more comment. + [ObservableProperty] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 6, 7, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 24, 8, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithXmlComment() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + /// + /// Blah blah blah. + /// + /// Blah blah blah. + [ObservableProperty] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + /// + /// Blah blah blah. + /// + /// Blah blah blah. + [ObservableProperty] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(9,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(9, 6, 9, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(10, 24, 10, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } } From 00ebe428cd52f0aa611808c869cca7f16cff6915 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 19:43:22 -0700 Subject: [PATCH 046/146] Support updating field references as well --- ...lPropertyForObservablePropertyCodeFixer.cs | 34 ++++++++++- ...lPropertyForObservablePropertyCodeFixer.cs | 60 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 4dfb27ba3..ebeee4cc7 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; @@ -251,8 +252,37 @@ private static async Task ConvertToPartialProperty( .AddAttributeLists(setterAttributes) .WithAdditionalAnnotations(Formatter.Annotation)); - SyntaxTree updatedTree = root.ReplaceNode(fieldDeclaration, propertyDeclaration).SyntaxTree; + // Create an editor to perform all mutations. This allows to keep track of multiple + // replacements for nodes on the same original tree, which otherwise wouldn't work. + SyntaxEditor editor = new(root, document.Project.Solution.Workspace); - return document.WithSyntaxRoot(await updatedTree.GetRootAsync(cancellationToken).ConfigureAwait(false)); + editor.ReplaceNode(fieldDeclaration, propertyDeclaration); + + // Get the field declaration from the target diagnostic (we only support individual fields, with a single declaration) + foreach (SyntaxNode descendantNode in root.DescendantNodes()) + { + // We only care about identifier nodes + if (descendantNode is not IdentifierNameSyntax identifierSyntax) + { + continue; + } + + // Pre-filter to only match the field name we just replaced + if (identifierSyntax.Identifier.Text != fieldName) + { + continue; + } + + // Make sure the identifier actually refers to the field being replaced + if (semanticModel.GetSymbolInfo(identifierSyntax, cancellationToken).Symbol is not IFieldSymbol fieldSymbol) + { + continue; + } + + // Replace the field reference with a reference to the new property + editor.ReplaceNode(identifierSyntax, IdentifierName(propertyName)); + } + + return document.WithSyntaxRoot(editor.GetChangedRoot()); } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index 5520aaae5..f574a0836 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -439,4 +439,64 @@ partial class C : ObservableObject await test.RunAsync(); } + + [TestMethod] + public async Task SimpleFieldWithSomeReferences() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i; + + public void M() + { + i = 42; + } + + public int N() => i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } + + public void M() + { + I = 42; + } + + public int N() => I; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } } From 34538245d6774d0b7752090c203e4f07486c2f70 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 19:57:37 -0700 Subject: [PATCH 047/146] Improve messages in diagnostics when emitted on properties --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 2 + .../ObservablePropertyGenerator.Execute.cs | 14 +++- ...entObservablePropertyAttributesAnalyzer.cs | 13 +++- .../Diagnostics/DiagnosticDescriptors.cs | 76 +++++++++---------- .../Extensions/SymbolKindExtensions.cs | 30 ++++++++ .../Extensions/SyntaxKindExtensions.cs | 30 ++++++++ 6 files changed, 119 insertions(+), 46 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 9e2e2d555..988d87a15 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -69,6 +69,8 @@ + + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index c4a08b831..cf90fa8a5 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -143,8 +143,9 @@ public static bool TryGetInfo( if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging)) { builder.Add( - InvalidContainingTypeForObservablePropertyFieldError, + InvalidContainingTypeForObservablePropertyMemberError, memberSymbol, + memberSyntax.Kind().ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); @@ -309,6 +310,7 @@ public static bool TryGetInfo( builder.Add( MissingObservableValidatorInheritanceForValidationAttributeError, memberSymbol, + memberSyntax.Kind().ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name, forwardedAttributes.Count); @@ -320,6 +322,7 @@ public static bool TryGetInfo( builder.Add( MissingValidationAttributesForNotifyDataErrorInfoError, memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); } @@ -659,8 +662,9 @@ private static bool TryGetIsNotifyingRecipients( if (hasOrInheritsClassLevelNotifyPropertyChangedRecipients) { diagnostics.Add( - UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning, + UnnecessaryNotifyPropertyChangedRecipientsAttributeOnMemberWarning, memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); } @@ -676,7 +680,7 @@ private static bool TryGetIsNotifyingRecipients( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( - InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError, + InvalidContainingTypeForNotifyPropertyChangedRecipientsMemberError, memberSymbol, memberSymbol.ContainingType, memberSymbol.Name); @@ -742,8 +746,9 @@ private static bool TryGetNotifyDataErrorInfo( if (hasOrInheritsClassLevelNotifyDataErrorInfo) { diagnostics.Add( - UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning, + UnnecessaryNotifyDataErrorInfoAttributeOnMemberWarning, memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); } @@ -760,6 +765,7 @@ private static bool TryGetNotifyDataErrorInfo( diagnostics.Add( MissingObservableValidatorInheritanceForNotifyDataErrorInfoError, memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs index 1505b6865..064223911 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs @@ -40,7 +40,7 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); // Defer the registration so it can be skipped if C# 8.0 or more is not available. - // That is because in that case source generators are not supported at all anyaway. + // That is because in that case source generators are not supported at all anyway. context.RegisterCompilationStartAction(static context => { if (!context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) @@ -72,7 +72,7 @@ public override void Initialize(AnalysisContext context) foreach (AttributeData dependentAttribute in attributes) { - // Go over each attribute on the target symbol, anche check if any of them matches one of the trigger attributes. + // Go over each attribute on the target symbol, and check if any of them matches one of the trigger attributes. // The logic here is the same as the one in UnsupportedCSharpLanguageVersionAnalyzer. if (dependentAttribute.AttributeClass is { Name: string attributeName } dependentAttributeClass && typeSymbols.TryGetValue(attributeName, out INamedTypeSymbol? dependentAttributeSymbol) && @@ -89,13 +89,18 @@ public override void Initialize(AnalysisContext context) } } - context.ReportDiagnostic(Diagnostic.Create(FieldWithOrphanedDependentObservablePropertyAttributesError, context.Symbol.Locations.FirstOrDefault(), context.Symbol.ContainingType, context.Symbol.Name)); + context.ReportDiagnostic(Diagnostic.Create( + FieldWithOrphanedDependentObservablePropertyAttributesError, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol.Kind.ToFieldOrPropertyKeyword(), + context.Symbol.ContainingType, + context.Symbol.Name)); // Just like in UnsupportedCSharpLanguageVersionAnalyzer, stop if a diagnostic has been emitted for the current symbol return; } } - }, SymbolKind.Field); + }, SymbolKind.Field, SymbolKind.Property); }); } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 5f7fb3883..74986d3ce 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -122,17 +122,17 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when the target type doesn't inherit from the ObservableValidator class. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator". + /// Format: "The {0} {1}.{2} cannot be used to generate an observable property, as it has {3} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator". /// /// public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceForValidationAttributeError = new DiagnosticDescriptor( id: "MVVMTK0006", title: "Missing ObservableValidator inheritance", - messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator", + messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as it has {3} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Cannot apply [ObservableProperty] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", + description: "Cannot apply [ObservableProperty] to fields or properties with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0006"); /// @@ -325,35 +325,35 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0018"); /// - /// Gets a indicating when [ObservableProperty] is applied to a field in an invalid type. + /// Gets a indicating when [ObservableProperty] is applied to a field or property in an invalid type. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]". + /// Format: "The {0} {1}.{2} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]". /// /// - public static readonly DiagnosticDescriptor InvalidContainingTypeForObservablePropertyFieldError = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor InvalidContainingTypeForObservablePropertyMemberError = new DiagnosticDescriptor( id: "MVVMTK0019", - title: "Invalid containing type for [ObservableProperty] field", - messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]", + title: "Invalid containing type for [ObservableProperty] field or property", + messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Fields annotated with [ObservableProperty] must be contained in a type that inherits from ObservableObject or that is annotated with [ObservableObject] or [INotifyPropertyChanged] (including base types).", + description: "Fields and properties annotated with [ObservableProperty] must be contained in a type that inherits from ObservableObject or that is annotated with [ObservableObject] or [INotifyPropertyChanged] (including base types).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0019"); /// - /// Gets a indicating when [ObservableProperty] is applied to a field in an invalid type. + /// Gets a indicating when [ObservableProperty] is applied to a field or property in an invalid type. /// - /// Format: "The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]". + /// Format: "The {0} {1}.{2} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]". /// /// public static readonly DiagnosticDescriptor FieldWithOrphanedDependentObservablePropertyAttributesError = new DiagnosticDescriptor( id: "MVVMTK0020", title: "Invalid use of attributes dependent on [ObservableProperty]", - messageFormat: "The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]", + messageFormat: "The {0} {1}.{2} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Fields not annotated with [ObservableProperty] cannot use [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo].", + description: "Fields and properties not annotated with [ObservableProperty] cannot use [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo].", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0020"); /// @@ -373,19 +373,19 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0021"); /// - /// Gets a indicating when [NotifyPropertyChangedRecipients] is applied to a field in an invalid type. + /// Gets a indicating when [NotifyPropertyChangedRecipients] is applied to a field or property in an invalid type. /// - /// Format: "The field {0}.{1} cannot be annotated with [NotifyPropertyChangedRecipients], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]". + /// Format: "The {0} {1}.{2} cannot be annotated with [NotifyPropertyChangedRecipients], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]". /// /// - public static readonly DiagnosticDescriptor InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor InvalidContainingTypeForNotifyPropertyChangedRecipientsMemberError = new DiagnosticDescriptor( id: "MVVMTK0022", - title: "Invalid containing type for [ObservableProperty] field", - messageFormat: "The field {0}.{1} cannot be annotated with [NotifyPropertyChangedRecipients], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]", + title: "Invalid containing type for [ObservableProperty] field or property", + messageFormat: "The {0} {1}.{2} cannot be annotated with [NotifyPropertyChangedRecipients], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Fields annotated with [NotifyPropertyChangedRecipients] must be contained in a type that inherits from ObservableRecipient or that is annotated with [ObservableRecipient] (including base types).", + description: "Fields and properties annotated with [NotifyPropertyChangedRecipients] must be contained in a type that inherits from ObservableRecipient or that is annotated with [ObservableRecipient] (including base types).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0022"); /// @@ -423,33 +423,33 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when the target type doesn't inherit from the ObservableValidator class. /// - /// Format: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator". + /// Format: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator". /// /// public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceForNotifyDataErrorInfoError = new DiagnosticDescriptor( id: "MVVMTK0025", title: "Missing ObservableValidator inheritance", - messageFormat: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator", + messageFormat: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Cannot apply [NotifyDataErrorInfo] to fields that are declared in a type that doesn't inherit from ObservableValidator.", + description: "Cannot apply [NotifyDataErrorInfo] to fields and properties that are declared in a type that doesn't inherit from ObservableValidator.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0025"); /// - /// Gets a indicating when the target field uses [NotifyDataErrorInfo] but has no validation attributes. + /// Gets a indicating when the target field or property uses [NotifyDataErrorInfo] but has no validation attributes. /// - /// Format: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation". + /// Format: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation". /// /// public static readonly DiagnosticDescriptor MissingValidationAttributesForNotifyDataErrorInfoError = new DiagnosticDescriptor( id: "MVVMTK0026", title: "Missing validation attributes", - messageFormat: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation", + messageFormat: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Cannot apply [NotifyDataErrorInfo] to fields that don't have any validation attributes to use during validation.", + description: "Cannot apply [NotifyDataErrorInfo] to fields and properties that don't have any validation attributes to use during validation.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0026"); /// @@ -485,35 +485,35 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0028"); /// - /// Gets a indicating when [NotifyPropertyChangedRecipients] is applied to a field in a class with [NotifyPropertyChangedRecipients] used at the class-level. + /// Gets a indicating when [NotifyPropertyChangedRecipients] is applied to a field or property in a class with [NotifyPropertyChangedRecipients] used at the class-level. /// - /// Format: "The field {0}.{1} is annotated with [NotifyPropertyChangedRecipients], but that is not needed since its containing type already uses or inherits [NotifyPropertyChangedRecipients] at the class-level". + /// Format: "The {0} {1}.{2} is annotated with [NotifyPropertyChangedRecipients], but that is not needed since its containing type already uses or inherits [NotifyPropertyChangedRecipients] at the class-level". /// /// - public static readonly DiagnosticDescriptor UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor UnnecessaryNotifyPropertyChangedRecipientsAttributeOnMemberWarning = new DiagnosticDescriptor( id: "MVVMTK0029", - title: "Unnecessary [NotifyPropertyChangedRecipients] field annotation", - messageFormat: "The field {0}.{1} is annotated with [NotifyPropertyChangedRecipients], but that is not needed since its containing type already uses or inherits [NotifyPropertyChangedRecipients] at the class-level", + title: "Unnecessary [NotifyPropertyChangedRecipients] field or property annotation", + messageFormat: "The {0} {1}.{2} is annotated with [NotifyPropertyChangedRecipients], but that is not needed since its containing type already uses or inherits [NotifyPropertyChangedRecipients] at the class-level", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Annotating a field with [NotifyPropertyChangedRecipients] is not necessary if the containing type has or inherits [NotifyPropertyChangedRecipients] at the class-level.", + description: "Annotating a field or property with [NotifyPropertyChangedRecipients] is not necessary if the containing type has or inherits [NotifyPropertyChangedRecipients] at the class-level.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0029"); /// - /// Gets a indicating when [NotifyDataErrorInfo] is applied to a field in a class with [NotifyDataErrorInfo] used at the class-level. + /// Gets a indicating when [NotifyDataErrorInfo] is applied to a field or property in a class with [NotifyDataErrorInfo] used at the class-level. /// - /// Format: "The field {0}.{1} is annotated with [NotifyDataErrorInfo], but that is not needed since its containing type already uses or inherits [NotifyDataErrorInfo] at the class-level". + /// Format: "The {0} {1}.{2} is annotated with [NotifyDataErrorInfo], but that is not needed since its containing type already uses or inherits [NotifyDataErrorInfo] at the class-level". /// /// - public static readonly DiagnosticDescriptor UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor UnnecessaryNotifyDataErrorInfoAttributeOnMemberWarning = new DiagnosticDescriptor( id: "MVVMTK0030", - title: "Unnecessary [NotifyDataErrorInfo] field annotation", - messageFormat: "The field {0}.{1} is annotated with [NotifyDataErrorInfo], but that is not needed since its containing type already uses or inherits [NotifyDataErrorInfo] at the class-level", + title: "Unnecessary [NotifyDataErrorInfo] field or property annotation", + messageFormat: "The {0} {1}.{2} is annotated with [NotifyDataErrorInfo], but that is not needed since its containing type already uses or inherits [NotifyDataErrorInfo] at the class-level", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Annotating a field with [NotifyDataErrorInfo] is not necessary if the containing type has or inherits [NotifyDataErrorInfo] at the class-level.", + description: "Annotating a field or property with [NotifyDataErrorInfo] is not necessary if the containing type has or inherits [NotifyDataErrorInfo] at the class-level.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0030"); /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs new file mode 100644 index 000000000..398b17bd5 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs @@ -0,0 +1,30 @@ +// 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 Microsoft.CodeAnalysis; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SymbolKindExtensions +{ + /// + /// Converts a value to either "field" or "property" based on the kind. + /// + /// The input value. + /// Either "field" or "property" based on . + /// Thrown if is neither nor . + public static string ToFieldOrPropertyKeyword(this SymbolKind kind) + { + return kind switch + { + SymbolKind.Field => "field", + SymbolKind.Property => "property", + _ => throw new ArgumentException($"Unsupported symbol kind '{kind}' for field or property keyword conversion."), + }; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs new file mode 100644 index 000000000..a178dcc82 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs @@ -0,0 +1,30 @@ +// 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 Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxKindExtensions +{ + /// + /// Converts a value to either "field" or "property" based on the kind. + /// + /// The input value. + /// Either "field" or "property" based on . + /// Thrown if is neither nor . + public static string ToFieldOrPropertyKeyword(this SyntaxKind kind) + { + return kind switch + { + SyntaxKind.FieldDeclaration => "field", + SyntaxKind.PropertyDeclaration => "property", + _ => throw new ArgumentException($"Unsupported syntax kind '{kind}' for field or property keyword conversion."), + }; + } +} From 7b402b65966c8aa148cad8163217a9e256e0a6e7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 22:45:52 -0700 Subject: [PATCH 048/146] Add workaround for older Roslyn versions --- .../ObservablePropertyGenerator.Execute.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index cf90fa8a5..0cecdc6d5 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -137,6 +137,17 @@ public static bool TryGetInfo( [NotNullWhen(true)] out PropertyInfo? propertyInfo, out ImmutableArray diagnostics) { + // Special case for downlevel: if a field has the 'partial' modifier, ignore it. + // This is because older compilers might parse a partial property as a field. + // In that case, we ignore it here and rely on Roslyn producing a build error. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration) && memberSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + propertyInfo = null; + diagnostics = ImmutableArray.Empty; + + return false; + } + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); // Validate the target type From ca5d9ddd9a79baa0aeb39daffd70e49bf0006ebf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 22:46:16 -0700 Subject: [PATCH 049/146] Fix a unit test on .NET Framework --- .../Test_SourceGeneratorsCodegen.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs index dce57de5c..658d20aeb 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -730,6 +730,7 @@ partial class MyViewModel : ObservableValidator } """; +#if NET6_0_OR_GREATER string result = """ // #pragma warning disable @@ -787,6 +788,64 @@ public partial string Name } } """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; +#endif VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); } From 648788ca0deb5e5c24a44bc236f25145256f6670 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 22:59:33 -0700 Subject: [PATCH 050/146] Fix last remaining unit test, and test regex --- .../Test_SourceGeneratorsDiagnostics.cs | 17 ++++++++++++++++- .../Test_SourceGeneratorsDiagnostics.cs | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 1bb0b461c..18da79da2 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -222,6 +222,7 @@ public partial class SampleViewModel : ObservableObject [TestMethod] public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnInitOnlyProperty_Warns() { +#if NET6_0_OR_GREATER const string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -234,7 +235,21 @@ public partial class SampleViewModel : ObservableObject } } """; +#else + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { get; {|CS0518:init|}; } + } + } + """; +#endif - await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0518", "CS9248"]); } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 6883440df..605651520 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1917,7 +1917,7 @@ internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration "Foo()") - string source = Regex.Replace(markdownSource, @"{\|((?:,?\w+)+):(.+)\|}", m => m.Groups[2].Value); + string source = Regex.Replace(markdownSource, @"{\|((?:,?\w+)+):(.+?)\|}", m => m.Groups[2].Value); VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion)), generators, generatorDiagnosticsIds, ignoredDiagnosticIds); } From 9c9ff44c196fb595f05162d0acc93195d0cf1dd4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 23:00:55 -0700 Subject: [PATCH 051/146] Set version.json to 8.4.0 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 72763352d..b2cb92bde 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "8.3.0-build.{height}", + "version": "8.4.0-build.{height}", "publicReleaseRefSpec": [ "^refs/heads/main$", // we release out of main "^refs/heads/dev$", // we release out of dev From f75edabda8e4735534dd512cf1cbab08f66ecc7d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 23:49:04 -0700 Subject: [PATCH 052/146] Don't suggest partial properties for static fields --- ...rvablePropertyOnPartialPropertyAnalyzer.cs | 9 +++++++++ .../Test_SourceGeneratorsDiagnostics.cs | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs index 16a42bc1c..8fbe4c978 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -56,6 +56,15 @@ public override void Initialize(AnalysisContext context) return; } + // It's not really meant to be used this way, but technically speaking the generator also supports + // static fields. So for those users leveraging that (for whatever reason), make sure to skip those. + // Partial properties using [ObservableProperty] cannot be static, and we never want the code fixer + // to prompt the user, run, and then result in code that will fail to compile. + if (fieldSymbol.IsStatic) + { + return; + } + // Emit the diagnostic for this field to suggest changing to a partial property instead context.ReportDiagnostic(Diagnostic.Create( UseObservablePropertyOnPartialProperty, diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 18da79da2..e38182eaf 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -252,4 +252,23 @@ public partial class SampleViewModel : ObservableObject await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0518", "CS9248"]); } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsPreview_OnStaticField_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private static string name; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } } From 0a81f27b98cad335aebebc294fdf82a0b455681c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 25 Oct 2024 00:05:22 -0700 Subject: [PATCH 053/146] Support field initializers in code fixer --- ...lPropertyForObservablePropertyCodeFixer.cs | 6 + ...lPropertyForObservablePropertyCodeFixer.cs | 151 ++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index ebeee4cc7..f688fa8f8 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -252,6 +252,12 @@ private static async Task ConvertToPartialProperty( .AddAttributeLists(setterAttributes) .WithAdditionalAnnotations(Formatter.Annotation)); + // If the field has an initializer, preserve that on the property + if (fieldDeclaration.Declaration.Variables[0].Initializer is { } fieldInitializer) + { + propertyDeclaration = propertyDeclaration.WithInitializer(fieldInitializer).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + } + // Create an editor to perform all mutations. This allows to keep track of multiple // replacements for nodes on the same original tree, which otherwise wouldn't work. SyntaxEditor editor = new(root, document.Project.Solution.Workspace); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index f574a0836..6ecdabb23 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -499,4 +499,155 @@ public void M() await test.RunAsync(); } + + [TestMethod] + public async Task SimpleField_WithInitializer1() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i = 42; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } = 42; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + + // /0/Test0.cs(6,24): error CS8050: Only auto-implemented properties can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(6, 24, 6, 25), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleField_WithInitializer2() + { + string original = """ + using System.Collections.Generic; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private ICollection items = ["A", "B", "C"]; + } + """; + + string @fixed = """ + using System.Collections.Generic; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial ICollection Items { get; set; } = ["A", "B", "C"]; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.items 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.items"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,40): error CS8050: Only auto-implemented properties can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(7, 40, 7, 45), + + // /0/Test0.cs(7,40): error CS9248: Partial property 'C.Items' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 40, 7, 45).WithArguments("C.Items"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleField_WithInitializer3() + { + string original = """ + using System.Collections.Generic; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private ICollection items = new List { "A", "B", "C" }; + } + """; + + string @fixed = """ + using System.Collections.Generic; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial ICollection Items { get; set; } = new List { "A", "B", "C" }; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.items 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.items"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,40): error CS8050: Only auto-implemented properties can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(7, 40, 7, 45), + + // /0/Test0.cs(7,40): error CS9248: Partial property 'C.Items' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 40, 7, 45).WithArguments("C.Items"), + }); + + await test.RunAsync(); + } } From a7840b2c75e9bc23f26321e8bf00f60d2ad7b379 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 26 Oct 2024 11:08:34 -0700 Subject: [PATCH 054/146] Don't run analyzers on generated code --- .../ObservablePropertyGenerator.Execute.cs | 10 +--------- .../AsyncVoidReturningRelayCommandMethodAnalyzer.cs | 2 +- ...ldTargetedObservablePropertyAttributeAnalyzer.cs | 2 +- ...assUsingAttributeInsteadOfInheritanceAnalyzer.cs | 2 +- ...ldReferenceForObservablePropertyFieldAnalyzer.cs | 2 +- ...DependentObservablePropertyAttributesAnalyzer.cs | 2 +- ...lassLevelNotifyDataErrorInfoAttributeAnalyzer.cs | 2 +- ...ifyPropertyChangedRecipientsAttributeAnalyzer.cs | 2 +- ...pertyLevelObservablePropertyAttributeAnalyzer.cs | 13 +------------ .../RequiresCSharpLanguageVersionPreviewAnalyzer.cs | 2 +- .../UnsupportedCSharpLanguageVersionAnalyzer.cs | 2 +- ...portedRoslynVersionForPartialPropertyAnalyzer.cs | 2 +- ...seObservablePropertyOnPartialPropertyAnalyzer.cs | 2 +- 13 files changed, 13 insertions(+), 32 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 0cecdc6d5..6dc483e6b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -54,20 +54,12 @@ static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? contain return true; } -#if ROSLYN_4_11_0_OR_GREATER - // We only support matching properties on Roslyn 4.11 and greater + // Check that the target is a valid field or partial property if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && !InvalidPropertyLevelObservablePropertyAttributeAnalyzer.IsValidCandidateProperty(node, out parentNode)) { return false; } -#else - // Otherwise, we only support matching fields - if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode)) - { - return false; - } -#endif // The candidate member must be in a type with a base type (as it must derive from ObservableObject) return parentNode?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs index 8b8688d1e..164618ddf 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class AsyncVoidReturningRelayCommandMethodAnalyzer : DiagnosticAna /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs index 8740fe206..79390067b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs @@ -22,7 +22,7 @@ public sealed class AutoPropertyWithFieldTargetedObservablePropertyAttributeAnal /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs index 39419d2b0..13da28dd2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs @@ -54,7 +54,7 @@ public sealed class ClassUsingAttributeInsteadOfInheritanceAnalyzer : Diagnostic /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs index 599fa7cdb..df9052d36 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs @@ -32,7 +32,7 @@ public sealed class FieldReferenceForObservablePropertyFieldAnalyzer : Diagnosti /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs index 064223911..cc4796a23 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs @@ -36,7 +36,7 @@ public sealed class FieldWithOrphanedDependentObservablePropertyAttributesAnalyz /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); // Defer the registration so it can be skipped if C# 8.0 or more is not available. diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs index f4232ada0..bfdf087f1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer : Diag /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs index c9525ed3d..69d4cf0f8 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAna /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs index fdb349086..0b65550d0 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -2,8 +2,6 @@ // 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_11_0_OR_GREATER - using System.Collections.Immutable; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; @@ -26,7 +24,7 @@ public sealed class InvalidPropertyLevelObservablePropertyAttributeAnalyzer : Di /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => @@ -39,13 +37,6 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - // Don't analyze generated symbols, we only want to warn those - // that users have actually written on their own in source code. - if (context.IsGeneratedCode) - { - return; - } - // We're intentionally only looking for properties here if (context.Symbol is not IPropertySymbol propertySymbol) { @@ -114,5 +105,3 @@ internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarati return true; } } - -#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs index aa96c9bef..301b0e3f4 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs @@ -24,7 +24,7 @@ public sealed class RequiresCSharpLanguageVersionPreviewAnalyzer : DiagnosticAna /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs index af81bcac4..42312904b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs @@ -41,7 +41,7 @@ public sealed class UnsupportedCSharpLanguageVersionAnalyzer : DiagnosticAnalyze /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); // Defer the callback registration to when the compilation starts, so we can execute more diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs index 9a9c53f02..dc0512312 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs @@ -24,7 +24,7 @@ public sealed class UnsupportedRoslynVersionForPartialPropertyAnalyzer : Diagnos /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs index 8fbe4c978..68a77d6d1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -24,7 +24,7 @@ public sealed class UseObservablePropertyOnPartialPropertyAnalyzer : DiagnosticA /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => From f9c7bc3e8722c47f8cfb0f98c6862fa81f4700f7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 27 Oct 2024 14:54:54 -0700 Subject: [PATCH 055/146] Update NuGet packages to latest stable --- Directory.Build.props | 2 +- src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj | 4 ++-- .../CommunityToolkit.Common.UnitTests.csproj | 6 +++--- .../CommunityToolkit.Diagnostics.UnitTests.csproj | 6 +++--- .../CommunityToolkit.HighPerformance.UnitTests.csproj | 6 +++--- ...t.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj | 6 +++--- .../CommunityToolkit.Mvvm.Internals.UnitTests.csproj | 6 +++--- .../CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj | 8 ++++---- .../CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj | 8 ++++---- ...lkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj | 6 +++--- ...lkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj | 6 +++--- 11 files changed, 32 insertions(+), 32 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2c852ca11..6510902b7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -25,7 +25,7 @@ - + diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index 311b435de..f0246d204 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -9,7 +9,7 @@ because we need the source generators in CsWinRT to generate the supporting interop code for AOT). --> - 10.0.17763.41 + 10.0.17763.53 - + diff --git a/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj b/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj index db70cacd7..76bc41862 100644 --- a/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj +++ b/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj b/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj index 2c02dcade..f3a582df1 100644 --- a/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj +++ b/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj b/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj index ce059f06f..e49f035d4 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj @@ -11,9 +11,9 @@ - - - + + + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj index 9a4086826..63a4b907a 100644 --- a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj index 5c35d66d3..421cc2315 100644 --- a/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj index 2ce1d0992..c678777f5 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj @@ -7,11 +7,11 @@ - - - + + + - + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj index 460adb7b6..bd5ce9114 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj @@ -7,11 +7,11 @@ - - - + + + - + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj index 584c015e3..e66b04fda 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj @@ -8,9 +8,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index 9dbd39d92..9e59eeb93 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj @@ -13,9 +13,9 @@ - - - + + + From 0b5dfb2d242382e61e03971313ecb3417b6924eb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 27 Oct 2024 15:13:05 -0700 Subject: [PATCH 056/146] Add 'InvalidTargetObservablePropertyAttributeAnalyzer' --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 7 -- ...rgetObservablePropertyAttributeAnalyzer.cs | 72 +++++++++++++++++++ .../Test_SourceGeneratorsDiagnostics.cs | 65 ++++++++++++++++- 4 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 988d87a15..103d7cfbb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -40,6 +40,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 6dc483e6b..b8d0167ff 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -145,13 +145,6 @@ public static bool TryGetInfo( // Validate the target type if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging)) { - builder.Add( - InvalidContainingTypeForObservablePropertyMemberError, - memberSymbol, - memberSyntax.Kind().ToFieldOrPropertyKeyword(), - memberSymbol.ContainingType, - memberSymbol.Name); - propertyInfo = null; diagnostics = builder.ToImmutable(); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..bad2a67b9 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +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 when a field or property with [ObservableProperty] is not a valid target. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidTargetObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidContainingTypeForObservablePropertyMemberError); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the required symbols for the analyzer + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject") is not INamedTypeSymbol observableObjectSymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") is not INamedTypeSymbol observableObjectAttributeSymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute") is not INamedTypeSymbol notifyPropertyChangedAttributeSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Validate that we do have a field or a property + if (context.Symbol is not (IFieldSymbol or IPropertySymbol)) + { + return; + } + + // Ensure we do have the [ObservableProperty] attribute + if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? attributeDataobservablePropertyAttribute)) + { + return; + } + + // Same logic as in 'IsTargetTypeValid' in the generator + bool isObservableObject = context.Symbol.ContainingType.InheritsFromType(observableObjectSymbol); + bool hasObservableObjectAttribute = context.Symbol.ContainingType.HasOrInheritsAttributeWithType(observableObjectAttributeSymbol); + bool hasINotifyPropertyChangedAttribute = context.Symbol.ContainingType.HasOrInheritsAttributeWithType(notifyPropertyChangedAttributeSymbol); + + // Emit the diagnostic if the target is not valid + if (!isObservableObject && !hasObservableObjectAttribute && !hasINotifyPropertyChangedAttribute) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidContainingTypeForObservablePropertyMemberError, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol.Kind.ToFieldOrPropertyKeyword(), + context.Symbol.ContainingType, + context.Symbol.Name)); + } + }, SymbolKind.Field, SymbolKind.Property); + }); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 605651520..41d4dfb5b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1006,24 +1006,85 @@ public partial class A } [TestMethod] - public void InvalidContainingTypeForObservablePropertyFieldError() + public async Task InvalidContainingTypeForObservableProperty_OnField_Warns() { string source = """ + using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp { public partial class MyViewModel : INotifyPropertyChanged + { + [ObservableProperty] + public int {|MVVMTK0019:number|}; + + public event PropertyChangedEventHandler PropertyChanged; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task InvalidContainingTypeForObservableProperty_OnField_InValidType_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject { [ObservableProperty] public int number; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task InvalidContainingTypeForObservableProperty_OnPartialProperty_Warns() + { + string source = """ + using System.ComponentModel; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : INotifyPropertyChanged + { + [ObservableProperty] + public int {|MVVMTK0019:Number|} { get; set; } public event PropertyChangedEventHandler PropertyChanged; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0019"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task InvalidContainingTypeForObservableProperty_OnPartialProperty_InValidType_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public int Number { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); } [TestMethod] From ead5b1163bc0421516c58a5aeff910c9d919a29c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 27 Oct 2024 15:23:13 -0700 Subject: [PATCH 057/146] Add 'PropertyNameCollisionObservablePropertyAttributeAnalyzer' --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 6 -- ...rgetObservablePropertyAttributeAnalyzer.cs | 2 +- ...sionObservablePropertyAttributeAnalyzer.cs | 63 +++++++++++++++++++ .../Test_SourceGeneratorsDiagnostics.cs | 45 ++++++++++++- 5 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 103d7cfbb..c76af85f1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -40,6 +40,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index b8d0167ff..64ff45bcb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -166,12 +166,6 @@ public static bool TryGetInfo( // Check for name collisions (only for fields) if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) { - builder.Add( - ObservablePropertyNameCollisionError, - memberSymbol, - memberSymbol.ContainingType, - memberSymbol.Name); - propertyInfo = null; diagnostics = builder.ToImmutable(); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs index bad2a67b9..70c645227 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs @@ -46,7 +46,7 @@ public override void Initialize(AnalysisContext context) } // Ensure we do have the [ObservableProperty] attribute - if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? attributeDataobservablePropertyAttribute)) + if (!context.Symbol.HasAttributeWithType(observablePropertySymbol)) { return; } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..06894ac25 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +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 when a generated property from [ObservableProperty] would collide with the field name. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PropertyNameCollisionObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(ObservablePropertyNameCollisionError); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure we do have a valid field + if (context.Symbol is not IFieldSymbol fieldSymbol) + { + return; + } + + // We only care if the field has [ObservableProperty] + if (!fieldSymbol.HasAttributeWithType(observablePropertySymbol)) + { + return; + } + + // Emit the diagnostic if there is a name collision + if (fieldSymbol.Name == ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + ObservablePropertyNameCollisionError, + fieldSymbol.Locations.FirstOrDefault(), + fieldSymbol.ContainingType, + fieldSymbol.Name)); + } + }, SymbolKind.Field); + }); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 41d4dfb5b..cf6bf92dc 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -664,7 +664,7 @@ private async Task GreetUserAsync(User user) } [TestMethod] - public void NameCollisionForGeneratedObservableProperty() + public async Task NameCollisionForGeneratedObservableProperty_PascalCaseField_Warns() { string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -674,12 +674,51 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [ObservableProperty] - private string Name; + private string {|MVVMTK0014:Name|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0014"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task NameCollisionForGeneratedObservableProperty_CamelCaseField_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + // Using C# 9 here because the generated code will emit [MemberNotNull] on the property setter, which requires C# 9 + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); + } + + [TestMethod] + public async Task NameCollisionForGeneratedObservableProperty_PascalCaseProperty_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); } [TestMethod] From 143df8e1014d63309798684d302ee92c32d60898 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 27 Oct 2024 15:39:12 -0700 Subject: [PATCH 058/146] Add 'InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer' --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 10 +-- ...ertyObservablePropertyAttributeAnalyzer.cs | 75 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 6 +- .../Extensions/ITypeSymbolExtensions.cs | 21 +++++- .../Test_SourceGeneratorsDiagnostics.cs | 43 ++++++++--- 6 files changed, 132 insertions(+), 24 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index c76af85f1..86d4254a3 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -40,6 +40,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 64ff45bcb..ce3c5d98c 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -180,12 +180,6 @@ public static bool TryGetInfo( // Check for special cases that are explicitly not allowed if (IsGeneratedPropertyInvalid(propertyName, GetPropertyType(memberSymbol))) { - builder.Add( - InvalidObservablePropertyError, - memberSymbol, - memberSymbol.ContainingType, - memberSymbol.Name); - propertyInfo = null; diagnostics = builder.ToImmutable(); @@ -427,7 +421,7 @@ private static bool IsTargetTypeValid(ISymbol memberSymbol, out bool shouldInvok /// The property name. /// The property type. /// Whether the generated property is invalid. - private static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType) + public static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType) { // If the generated property name is called "Property" and the type is either object or it is PropertyChangedEventArgs or // PropertyChangingEventArgs (or a type derived from either of those two types), consider it invalid. This is needed because @@ -1493,7 +1487,7 @@ public static ImmutableArray GetOnPropertyChangeMethods /// /// The input instance to process. /// The type of . - private static ITypeSymbol GetPropertyType(ISymbol memberSymbol) + public static ITypeSymbol GetPropertyType(ISymbol memberSymbol) { // Check if the member is a property first if (memberSymbol is IPropertySymbol propertySymbol) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..e960cf47e --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +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 when a field or property with [ObservableProperty] is not valid (special cases) +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidObservablePropertyError); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] and the event args we need + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangedEventArgs") is not INamedTypeSymbol propertyChangedEventArgsSymbol || + context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangingEventArgs") is not INamedTypeSymbol propertyChangingEventArgsSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Validate that we do have a field or a property + if (context.Symbol is not (IFieldSymbol or IPropertySymbol)) + { + return; + } + + // Ensure we do have the [ObservableProperty] attribute + if (!context.Symbol.HasAttributeWithType(observablePropertySymbol)) + { + return; + } + + ITypeSymbol propertyType = ObservablePropertyGenerator.Execute.GetPropertyType(context.Symbol); + string propertyName = ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(context.Symbol); + + // Same logic as 'IsGeneratedPropertyInvalid' in the generator + if (propertyName == "Property") + { + // Check for collisions with the generated helpers and the property, only happens with these 3 types + if (propertyType.SpecialType == SpecialType.System_Object || + propertyType.HasOrInheritsFromType(propertyChangedEventArgsSymbol) || + propertyType.HasOrInheritsFromType(propertyChangingEventArgsSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyError, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol.Kind.ToFieldOrPropertyKeyword(), + context.Symbol.ContainingType, + context.Symbol.Name)); + } + } + }, SymbolKind.Field, SymbolKind.Property); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 74986d3ce..6ac4c7950 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -407,17 +407,17 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when a generated property created with [ObservableProperty] would cause conflicts with other generated members. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members". + /// Format: "The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members". /// /// public static readonly DiagnosticDescriptor InvalidObservablePropertyError = new DiagnosticDescriptor( id: "MVVMTK0024", title: "Invalid generated property declaration", - messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members", + messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "The fields annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.", + description: "The fields and properties annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0024"); /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index 6e976501b..b79f743b3 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -33,6 +33,25 @@ public static bool HasOrInheritsFromFullyQualifiedMetadataName(this ITypeSymbol return false; } + /// + /// Checks whether or not a given has or inherits from a specified type. + /// + /// The target instance to check. + /// The type to check for inheritance. + /// Whether or not is or inherits from . + public static bool HasOrInheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) + { + for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(currentType, baseTypeSymbol)) + { + return true; + } + } + + return false; + } + /// /// Checks whether or not a given inherits from a specified type. /// @@ -60,7 +79,7 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS /// Checks whether or not a given inherits from a specified type. /// /// The target instance to check. - /// The instane to check for inheritance from. + /// The instance to check for inheritance from. /// Whether or not inherits from . public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) { diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index cf6bf92dc..bef6d9c35 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1306,7 +1306,7 @@ private void GreetUser(object value) } [TestMethod] - public void InvalidObservablePropertyError_Object() + public async Task InvalidObservablePropertyError_Object() { string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -1316,16 +1316,35 @@ namespace MyApp public partial class MyViewModel : ObservableObject { [ObservableProperty] - public object property; + public object {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] - public void InvalidObservablePropertyError_PropertyChangingEventArgs() + public async Task InvalidObservablePropertyError_Object_WithProperty() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public object {|MVVMTK0024:Property|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidObservablePropertyError_PropertyChangingEventArgs() { string source = """ using System.ComponentModel; @@ -1336,16 +1355,16 @@ namespace MyApp public partial class MyViewModel : ObservableObject { [ObservableProperty] - public PropertyChangingEventArgs property; + public PropertyChangingEventArgs {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] - public void InvalidObservablePropertyError_PropertyChangedEventArgs() + public async Task InvalidObservablePropertyError_PropertyChangedEventArgs() { string source = """ using System.ComponentModel; @@ -1356,16 +1375,16 @@ namespace MyApp public partial class MyViewModel : ObservableObject { [ObservableProperty] - public PropertyChangedEventArgs property; + public PropertyChangedEventArgs {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] - public void InvalidObservablePropertyError_CustomTypeDerivedFromPropertyChangedEventArgs() + public async Task InvalidObservablePropertyError_CustomTypeDerivedFromPropertyChangedEventArgs() { string source = """ using System.ComponentModel; @@ -1384,12 +1403,12 @@ public MyPropertyChangedEventArgs(string propertyName) public partial class MyViewModel : ObservableObject { [ObservableProperty] - public MyPropertyChangedEventArgs property; + public MyPropertyChangedEventArgs {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] From a87344925ce72be01b1c2dbd05594500f1beebd7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 09:09:00 -0700 Subject: [PATCH 059/146] Fix targets for '[NotifyPropertyChangedRecipients]' --- .../NotifyPropertyChangedRecipientsAttribute.cs | 15 +++++++++------ .../Attributes/ObservablePropertyAttribute.cs | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedRecipientsAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedRecipientsAttribute.cs index f387acd8e..d758bbe17 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedRecipientsAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedRecipientsAttribute.cs @@ -7,7 +7,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// -/// An attribute that can be used to support in generated properties, when applied to fields +/// An attribute that can be used to support in generated properties, when applied to fields and properties /// contained in a type that is either inheriting from , or annotated with . /// When this attribute is used, the generated property setter will also call . /// This allows generated properties to opt-in into broadcasting behavior without having to fallback into a full explicit observable property. @@ -18,7 +18,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// { /// [ObservableProperty] /// [NotifyPropertyChangedRecipients] -/// private string username; +/// public partial string Username; /// } /// /// @@ -27,10 +27,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Username +/// public partial string Username /// { -/// get => username; -/// set => SetProperty(ref username, value, broadcast: true); +/// get => field; +/// set => SetProperty(ref field, value, broadcast: true); /// } /// } /// @@ -39,7 +39,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// This attribute can also be added to a class, and if so it will affect all generated properties in that type and inherited types. /// /// -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +/// +/// Just like , this attribute can also be used on fields as well. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class NotifyPropertyChangedRecipientsAttribute : Attribute { } diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs index c6ae78763..83311270c 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs @@ -20,7 +20,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// partial class MyViewModel : ObservableObject /// { /// [ObservableProperty] -/// public partial string name { get; set; } +/// public partial string Name { get; set; } /// /// [ObservableProperty] /// public partial bool IsEnabled { get; set; } From e82074f29002965b4fccac76ecec0b0860c348e7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 09:19:23 -0700 Subject: [PATCH 060/146] Support required properties in codegen --- .../ComponentModel/Models/PropertyInfo.cs | 2 + .../ObservablePropertyGenerator.Execute.cs | 53 ++++++++++++++++--- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 367d9a751..c2c8feed6 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -23,6 +23,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// The sequence of commands to notify. /// Whether or not the generated property also broadcasts changes. /// Whether or not the generated property also validates its value. +/// Whether or not the generated property should be marked as required. /// Whether the old property value is being directly referenced. /// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether to include nullability annotations on the setter. @@ -41,6 +42,7 @@ internal sealed record PropertyInfo( EquatableArray NotifiedCommandNames, bool NotifyPropertyChangedRecipients, bool NotifyDataErrorInfo, + bool IsRequired, bool IsOldPropertyValueDirectlyReferenced, bool IsReferenceTypeOrUnconstrainedTypeParameter, bool IncludeMemberNotNullOnSetAccessor, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index ce3c5d98c..751c4bbc6 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -348,6 +348,11 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + // Check whether the property should be required + bool isRequired = GetIsRequiredProperty(memberSymbol); + + token.ThrowIfCancellationRequested(); + propertyInfo = new PropertyInfo( memberSyntax.Kind(), typeNameWithNullabilityAnnotations, @@ -361,6 +366,7 @@ public static bool TryGetInfo( notifiedCommandNames.ToImmutable(), notifyRecipients, notifyDataErrorInfo, + isRequired, isOldPropertyValueDirectlyReferenced, isReferenceTypeOrUnconstrainedTypeParameter, includeMemberNotNullOnSetAccessor, @@ -1028,6 +1034,20 @@ private static bool TryGetAccessibilityModifiers( return true; } + /// + /// Checks whether an input member is a required property. + /// + /// The input instance to process. + /// Whether is a required property. + private static bool GetIsRequiredProperty(ISymbol memberSymbol) + { +#if ROSLYN_4_3_1_OR_GREATER + return memberSymbol is IPropertySymbol { IsRequired: true }; +#else + return false; +#endif + } + /// /// Gets a instance with the cached args for property changing notifications. /// @@ -1324,11 +1344,6 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // Also add any forwarded attributes setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes); - // Prepare the modifiers for the property - SyntaxTokenList propertyModifiers = propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration - ? propertyInfo.PropertyAccessibility.ToSyntaxTokenList().Add(Token(SyntaxKind.PartialKeyword)) - : propertyInfo.PropertyAccessibility.ToSyntaxTokenList(); - // Construct the generated property as follows: // // @@ -1352,7 +1367,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf .WithOpenBracketToken(Token(TriviaList(Comment(xmlSummary)), SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .AddAttributeLists(forwardedPropertyAttributes) - .WithModifiers(propertyModifiers) + .WithModifiers(GetPropertyModifiers(propertyInfo)) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithModifiers(propertyInfo.GetterAccessibility.ToSyntaxTokenList()) @@ -1362,6 +1377,32 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf setAccessor); } + /// + /// Gets all modifiers that need to be added to a generated property. + /// + /// The input instance to process. + /// The list of necessary modifiers for . + private static SyntaxTokenList GetPropertyModifiers(PropertyInfo propertyInfo) + { + SyntaxTokenList propertyModifiers = propertyInfo.PropertyAccessibility.ToSyntaxTokenList(); + +#if ROSLYN_4_3_1_OR_GREATER + // Add the 'required' modifier if the original member also had it + if (propertyInfo.IsRequired) + { + propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.RequiredKeyword)); + } +#endif + + // Add the 'partial' modifier if the original member is a partial property + if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) + { + propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.PartialKeyword)); + } + + return propertyModifiers; + } + /// /// Gets the instances for the OnPropertyChanging and OnPropertyChanged methods for the input field. /// From 73e7a88a4bd18ee88762742cb63fdbd8f1d16442 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 09:19:31 -0700 Subject: [PATCH 061/146] Add unit test for required properties --- .../Test_SourceGeneratorsCodegen.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs index 658d20aeb..9d7092888 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -92,6 +92,82 @@ partial int Number VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); } + // See https://github.com/CommunityToolkit/dotnet/issues/969 + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_RequiredProperty_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public required partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public required partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + [TestMethod] public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly1() { From f6fb902f9e8052d617fb1e898a41c48f298d6045 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 19:28:46 -0700 Subject: [PATCH 062/146] Rename 'CodeFixers' for Roslyn 4.0 --- dotnet Community Toolkit.sln | 2 +- .../AsyncVoidReturningRelayCommandMethodCodeFixer.cs | 0 .../ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs | 0 .../CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj} | 0 .../FieldReferenceForObservablePropertyFieldCodeFixer.cs | 0 .../UsePartialPropertyForObservablePropertyCodeFixer.cs | 0 src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj | 8 ++++---- ...lkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj | 2 +- ...lkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj | 2 +- 9 files changed, 7 insertions(+), 7 deletions(-) rename src/{CommunityToolkit.Mvvm.CodeFixers => CommunityToolkit.Mvvm.CodeFixers.Roslyn4001}/AsyncVoidReturningRelayCommandMethodCodeFixer.cs (100%) rename src/{CommunityToolkit.Mvvm.CodeFixers => CommunityToolkit.Mvvm.CodeFixers.Roslyn4001}/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs (100%) rename src/{CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj => CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj} (100%) rename src/{CommunityToolkit.Mvvm.CodeFixers => CommunityToolkit.Mvvm.CodeFixers.Roslyn4001}/FieldReferenceForObservablePropertyFieldCodeFixer.cs (100%) rename src/{CommunityToolkit.Mvvm.CodeFixers => CommunityToolkit.Mvvm.CodeFixers.Roslyn4001}/UsePartialPropertyForObservablePropertyCodeFixer.cs (100%) diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index 8a94f3c65..5f82f968f 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -81,7 +81,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Exter EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj", "{4FCD501C-1BB5-465C-AD19-356DAB6600C6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeFixers.Roslyn4001", "src\CommunityToolkit.Mvvm.CodeFixers.Roslyn4001\CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj", "{FCC13AD5-CEB8-4CC1-8250-89B616D126F2}" EndProject diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/AsyncVoidReturningRelayCommandMethodCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/AsyncVoidReturningRelayCommandMethodCodeFixer.cs similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers/AsyncVoidReturningRelayCommandMethodCodeFixer.cs rename to src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/AsyncVoidReturningRelayCommandMethodCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs rename to src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj rename to src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/FieldReferenceForObservablePropertyFieldCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/FieldReferenceForObservablePropertyFieldCodeFixer.cs similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers/FieldReferenceForObservablePropertyFieldCodeFixer.cs rename to src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/FieldReferenceForObservablePropertyFieldCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/UsePartialPropertyForObservablePropertyCodeFixer.cs similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs rename to src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/UsePartialPropertyForObservablePropertyCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index f0246d204..f0759fa6e 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -69,7 +69,7 @@ - + @@ -120,9 +120,9 @@ - - - + + + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj index e66b04fda..c466c17ea 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj @@ -15,7 +15,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj index 15077e2aa..051d5f697 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -16,7 +16,7 @@ - + From 8bc25ff4b7fb44d0b8c102259c3a97819d50ef6f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 19:30:37 -0700 Subject: [PATCH 063/146] Mode code fixer files to shared project --- dotnet Community Toolkit.sln | 4 ++++ ...ityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj | 2 ++ ...cVoidReturningRelayCommandMethodCodeFixer.cs | 0 ...ingAttributeInsteadOfInheritanceCodeFixer.cs | 0 .../CommunityToolkit.Mvvm.CodeFixers.projitems | 17 +++++++++++++++++ .../CommunityToolkit.Mvvm.CodeFixers.shproj | 13 +++++++++++++ ...erenceForObservablePropertyFieldCodeFixer.cs | 0 ...ialPropertyForObservablePropertyCodeFixer.cs | 0 8 files changed, 36 insertions(+) rename src/{CommunityToolkit.Mvvm.CodeFixers.Roslyn4001 => CommunityToolkit.Mvvm.CodeFixers}/AsyncVoidReturningRelayCommandMethodCodeFixer.cs (100%) rename src/{CommunityToolkit.Mvvm.CodeFixers.Roslyn4001 => CommunityToolkit.Mvvm.CodeFixers}/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs (100%) create mode 100644 src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems create mode 100644 src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.shproj rename src/{CommunityToolkit.Mvvm.CodeFixers.Roslyn4001 => CommunityToolkit.Mvvm.CodeFixers}/FieldReferenceForObservablePropertyFieldCodeFixer.cs (100%) rename src/{CommunityToolkit.Mvvm.CodeFixers.Roslyn4001 => CommunityToolkit.Mvvm.CodeFixers}/UsePartialPropertyForObservablePropertyCodeFixer.cs (100%) diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index 5f82f968f..60b0e4d9f 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -87,6 +87,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.Sourc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj", "{C342302D-A263-42D6-B8EE-01DEF8192690}" EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.shproj", "{A2EBDA90-B720-430D-83F5-C6BCC355232C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -532,11 +534,13 @@ 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 + 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 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{b8dcd82e-b53b-4249-ad4e-f9b99acb9334}*SharedItemsImports = 13 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{c342302d-a263-42d6-b8ee-01def8192690}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{df455c40-b18e-4890-8758-7cccb5ca7052}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{e24d1146-5ad8-498f-a518-4890d8bf4937}*SharedItemsImports = 5 + src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{e79dca2a-4c59-499f-85bd-f45215ed6b72}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{e827a9cd-405f-43e4-84c7-68cc7e845cdc}*SharedItemsImports = 13 tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{ecfe93aa-4b98-4292-b3fa-9430d513b4f9}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{f3799252-7a66-4533-89d8-b3c312052d95}*SharedItemsImports = 5 diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj index 01020c625..8ab074729 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj +++ b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj @@ -14,4 +14,6 @@ + + diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/AsyncVoidReturningRelayCommandMethodCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/AsyncVoidReturningRelayCommandMethodCodeFixer.cs similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/AsyncVoidReturningRelayCommandMethodCodeFixer.cs rename to src/CommunityToolkit.Mvvm.CodeFixers/AsyncVoidReturningRelayCommandMethodCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs rename to src/CommunityToolkit.Mvvm.CodeFixers/ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems new file mode 100644 index 000000000..79f5fa389 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems @@ -0,0 +1,17 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + a2ebda90-b720-430d-83f5-c6bcc355232c + + + CommunityToolkit.Mvvm.CodeFixers + + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.shproj b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.shproj new file mode 100644 index 000000000..f36e53832 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.shproj @@ -0,0 +1,13 @@ + + + + a2ebda90-b720-430d-83f5-c6bcc355232c + 14.0 + + + + + + + + diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/FieldReferenceForObservablePropertyFieldCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/FieldReferenceForObservablePropertyFieldCodeFixer.cs similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/FieldReferenceForObservablePropertyFieldCodeFixer.cs rename to src/CommunityToolkit.Mvvm.CodeFixers/FieldReferenceForObservablePropertyFieldCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/UsePartialPropertyForObservablePropertyCodeFixer.cs rename to src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs From 00320713e237c83da8df6aac36e3c6256570caf4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 19:38:12 -0700 Subject: [PATCH 064/146] Switch code fixers to shared project configuration --- ...yToolkit.Mvvm.CodeFixers.Roslyn4001.csproj | 15 +------- ...CommunityToolkit.Mvvm.CodeFixers.projitems | 3 ++ .../CommunityToolkit.Mvvm.CodeFixers.props | 37 +++++++++++++++++++ ...urceGenerators.Roslyn4001.UnitTests.csproj | 2 +- ...urceGenerators.Roslyn4110.UnitTests.csproj | 2 +- 5 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj index 8ab074729..9f443712d 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj +++ b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj @@ -1,19 +1,6 @@ - - netstandard2.0 - false - true - - - - - - - - - - + diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems index 79f5fa389..40339da45 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems @@ -14,4 +14,7 @@ + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props new file mode 100644 index 000000000..04381ec56 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props @@ -0,0 +1,37 @@ + + + + netstandard2.0 + false + true + + + + + + + $(MSBuildProjectName.Substring(0, $([MSBuild]::Subtract($(MSBuildProjectName.Length), 11)))) + + + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 10)))) + + + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 4)), 1)) + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 3)), 2)) + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 1)), 1)) + $(MvvmToolkitSourceGeneratorRoslynMajorVersion).$(MvvmToolkitSourceGeneratorRoslynMinorVersion).$(MvvmToolkitSourceGeneratorRoslynPatchVersion) + + + $(DefineConstants);ROSLYN_4_3_1_OR_GREATER + $(DefineConstants);ROSLYN_4_11_0_OR_GREATER + + + + + + + + + + + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj index c466c17ea..21b341ca9 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj @@ -15,7 +15,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj index 051d5f697..5423a8f69 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -16,7 +16,7 @@ - + From e4f8766f44f9af827a0b39544a4e3bb07b295365 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 19:40:48 -0700 Subject: [PATCH 065/146] Add code fixer project for Roslyn 4.11 --- dotnet Community Toolkit.sln | 23 +++++++++++++++++++ ...yToolkit.Mvvm.CodeFixers.Roslyn4110.csproj | 6 +++++ ...lPropertyForObservablePropertyCodeFixer.cs | 6 ++++- ...urceGenerators.Roslyn4110.UnitTests.csproj | 2 +- 4 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110.csproj diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index 60b0e4d9f..6d56ee910 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -89,6 +89,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.Sourc EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.shproj", "{A2EBDA90-B720-430D-83F5-C6BCC355232C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.CodeFixers.Roslyn4110", "src\CommunityToolkit.Mvvm.CodeFixers.Roslyn4110\CommunityToolkit.Mvvm.CodeFixers.Roslyn4110.csproj", "{98572004-D29A-486E-8053-6D409557CE44}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -503,6 +505,26 @@ Global {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x64.Build.0 = Release|Any CPU {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x86.ActiveCfg = Release|Any CPU {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x86.Build.0 = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|ARM.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|ARM.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|ARM64.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|x64.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|x64.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|x86.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|x86.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|Any CPU.Build.0 = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|ARM.ActiveCfg = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|ARM.Build.0 = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|ARM64.ActiveCfg = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|ARM64.Build.0 = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|x64.ActiveCfg = Release|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -534,6 +556,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 + 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 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{b8dcd82e-b53b-4249-ad4e-f9b99acb9334}*SharedItemsImports = 13 diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110.csproj b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110.csproj new file mode 100644 index 000000000..9f443712d --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index f688fa8f8..778a2ba92 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -2,6 +2,8 @@ // 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_11_0_OR_GREATER + using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; @@ -260,7 +262,7 @@ private static async Task ConvertToPartialProperty( // Create an editor to perform all mutations. This allows to keep track of multiple // replacements for nodes on the same original tree, which otherwise wouldn't work. - SyntaxEditor editor = new(root, document.Project.Solution.Workspace); + SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services); editor.ReplaceNode(fieldDeclaration, propertyDeclaration); @@ -292,3 +294,5 @@ private static async Task ConvertToPartialProperty( return document.WithSyntaxRoot(editor.GetChangedRoot()); } } + +#endif diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj index 5423a8f69..83566c52d 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -16,7 +16,7 @@ - + From bd4abe9b4d070d2c0f69127f533ad03e174416e5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 19:41:29 -0700 Subject: [PATCH 066/146] Build and pack Roslyn 4.11 code fixers --- src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index f0759fa6e..44b02cfa8 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -70,6 +70,7 @@ + @@ -122,7 +123,7 @@ - + \ No newline at end of file From bf49905b6b1deda0dbf89804f8a7b703d4523543 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 19:53:37 -0700 Subject: [PATCH 067/146] Handle 'required' on fields --- ...lPropertyForObservablePropertyCodeFixer.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 778a2ba92..f6e2684e1 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -231,7 +231,7 @@ private static async Task ConvertToPartialProperty( // Create the following property declaration: // // - // public partial + // // { // // get; @@ -241,7 +241,7 @@ private static async Task ConvertToPartialProperty( // } PropertyDeclarationSyntax propertyDeclaration = PropertyDeclaration(fieldDeclaration.Declaration.Type, Identifier(propertyName)) - .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.PartialKeyword)) + .WithModifiers(GetPropertyModifiers(fieldDeclaration)) .AddAttributeLists(propertyAttributes.ToArray()) .WithAdditionalAnnotations(Formatter.Annotation) .AddAccessorListAccessors( @@ -293,6 +293,25 @@ private static async Task ConvertToPartialProperty( return document.WithSyntaxRoot(editor.GetChangedRoot()); } + + /// + /// Gets all modifiers that need to be added to a generated property. + /// + /// The for the field being updated. + /// The list of necessary modifiers for . + private static SyntaxTokenList GetPropertyModifiers(FieldDeclarationSyntax fieldDeclaration) + { + SyntaxTokenList propertyModifiers = TokenList(Token(SyntaxKind.PublicKeyword)); + + // Add the 'required' modifier if the field also had it + if (fieldDeclaration.Modifiers.Any(SyntaxKind.RequiredKeyword)) + { + propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.RequiredKeyword)); + } + + // Always add 'partial' last + return propertyModifiers.Add(Token(SyntaxKind.PartialKeyword)); + } } #endif From fab668d11b0ef29a6f61e5ed79832ad3d374e459 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 19:53:44 -0700 Subject: [PATCH 068/146] Add unit test for 'required' fields --- ...lPropertyForObservablePropertyCodeFixer.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index 6ecdabb23..932c8096f 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -650,4 +650,51 @@ partial class C : ObservableObject await test.RunAsync(); } + + // See https://github.com/CommunityToolkit/dotnet/issues/971 + [TestMethod] + public async Task SimpleField_WithNoReferences_WithRequiredModifier() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + internal required string foo; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public required partial string Foo { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.foo 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.foo"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,36): error CS9248: Partial property 'C.Foo' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 36, 6, 39).WithArguments("C.Foo"), + }); + + await test.RunAsync(); + } } From d2ff32ca66eedd35b5b686fe675e039549c56b6d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 20:01:43 -0700 Subject: [PATCH 069/146] Remove 'this.' from partial property code fixer --- ...ePartialPropertyForObservablePropertyCodeFixer.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index f6e2684e1..403582ff0 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -287,8 +287,16 @@ private static async Task ConvertToPartialProperty( continue; } - // Replace the field reference with a reference to the new property - editor.ReplaceNode(identifierSyntax, IdentifierName(propertyName)); + // Special case for 'this.' accesses: we want to drop the 'this.' prefix + if (identifierSyntax.Parent is MemberAccessExpressionSyntax { Expression: ThisExpressionSyntax } thisExpressionSyntax) + { + editor.ReplaceNode(thisExpressionSyntax, IdentifierName(propertyName)); + } + else + { + // Replace the field reference with a reference to the new property + editor.ReplaceNode(identifierSyntax, IdentifierName(propertyName)); + } } return document.WithSyntaxRoot(editor.GetChangedRoot()); From 8f480bc4ef41b18ab3377767fa4234b3afeef174 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 28 Oct 2024 20:02:26 -0700 Subject: [PATCH 070/146] Add unit test for 'this.' expressions --- ...lPropertyForObservablePropertyCodeFixer.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index 932c8096f..c111ef8de 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -697,4 +697,74 @@ partial class C : ObservableObject await test.RunAsync(); } + + [TestMethod] + public async Task SimpleFieldWithSomeReferences_WithSomeThisExpressions() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i; + + public void M() + { + i = 42; + this.i = 42; + } + + public int N() => i; + + public int P() => this.i + Q(i) + Q(this.i); + + private int Q(int i) => this.i + i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } + + public void M() + { + I = 42; + I = 42; + } + + public int N() => I; + + public int P() => I + Q(I) + Q(I); + + private int Q(int i) => I + i; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } } From 1599d68b9b1fc42bed88931c47aa0f29acd352a4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 6 Nov 2024 21:36:53 -0800 Subject: [PATCH 071/146] Add 'CommunityToolkit.Mvvm.Windows.targets' --- .../CommunityToolkit.Mvvm.Windows.targets | 17 +++++++++++++++++ .../CommunityToolkit.Mvvm.csproj | 2 ++ .../CommunityToolkit.Mvvm.targets | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.Windows.targets diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.Windows.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.Windows.targets new file mode 100644 index 000000000..3602b1438 --- /dev/null +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.Windows.targets @@ -0,0 +1,17 @@ + + + + + <_MvvmToolkitIsUsingWindowsRuntimePack>false + <_MvvmToolkitIsUsingWindowsRuntimePack Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0-windows10.0.17763.0'))">true + + + + + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index 44b02cfa8..cddfad4af 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -101,10 +101,12 @@ + + diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets index 9006210d8..394a0a111 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets @@ -4,12 +4,14 @@ <_CommunityToolkitMvvmFeatureSwitchesTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.FeatureSwitches.targets <_CommunityToolkitMvvmSourceGeneratorsTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.SourceGenerators.targets + <_CommunityToolkitMvvmWindowsTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.Windows.targets <_CommunityToolkitMvvmWindowsSdkTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.WindowsSdk.targets + \ No newline at end of file From 24064de876fc4d08674fcbe64d9c3ada940ba615 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 6 Nov 2024 22:07:29 -0800 Subject: [PATCH 072/146] Add 'WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer' --- .../AnalyzerReleases.Shipped.md | 3 +- ...ityToolkit.Mvvm.SourceGenerators.projitems | 2 + ...rvablePropertyOnPartialPropertyAnalyzer.cs | 6 + ...pertyOnFieldsIsNotAotCompatibleAnalyzer.cs | 85 +++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 25 +++- .../AnalyzerConfigOptionsExtensions.cs | 114 ++++++++++++++++++ 6 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index cb19f6246..93a47db60 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -85,4 +85,5 @@ Rule ID | Category | Severity | Notes MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0042 MVVMTK0043| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 -MVVMTK0044| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 +MVVMTK0044| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0044 +MVVMTK0045| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0045 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 86d4254a3..4778a26f0 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -41,6 +41,7 @@ + @@ -58,6 +59,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs index 68a77d6d1..3fd76ce4c 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -36,6 +36,12 @@ public override void Initialize(AnalysisContext context) return; } + // If CsWinRT is in AOT-optimization mode, disable this analyzer, as the WinRT one will produce a warning instead + if (context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsCsWinRTAotOptimizerEnabled(context.Compilation)) + { + return; + } + // Get the symbol for [ObservableProperty] if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) { diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs new file mode 100644 index 000000000..735e189c5 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs @@ -0,0 +1,85 @@ +// 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_11_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 when [ObservableProperty] is used on a field in a scenario where it wouldn't be AOT compatible. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(WinRTObservablePropertyOnFieldsIsNotAotCompatible); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // This analyzer is only enabled in cases where CsWinRT is producing AOT-compatible code + if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsCsWinRTAotOptimizerEnabled(context.Compilation)) + { + return; + } + + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + bool isLanguageVersionPreview = context.Compilation.IsLanguageVersionPreview(); + + context.RegisterSymbolAction(context => + { + // Ensure we do have a valid field + if (context.Symbol is not IFieldSymbol fieldSymbol) + { + return; + } + + // Emit a diagnostic if the field is using the [ObservableProperty] attribute + if (fieldSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + // If the C# version is preview, we can include the necessary information to trigger the + // code fixer. If that is not the case, we shouldn't do that, to avoid the code fixer + // changing the code to invalid C# (as without the preview version, it wouldn't compile). + if (isLanguageVersionPreview) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTObservablePropertyOnFieldsIsNotAotCompatible, + observablePropertyAttribute.GetLocation(), + ImmutableDictionary.Create() + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name) + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)), + fieldSymbol.ContainingType, + fieldSymbol.Name)); + } + else + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTObservablePropertyOnFieldsIsNotAotCompatible, + observablePropertyAttribute.GetLocation(), + fieldSymbol.ContainingType, + fieldSymbol.Name)); + } + } + }, SymbolKind.Field); + }); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 6ac4c7950..102d29eea 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -39,6 +39,11 @@ internal static class DiagnosticDescriptors /// public const string UseObservablePropertyOnPartialPropertyId = "MVVMTK0042"; + /// + /// The diagnostic id for . + /// + public const string WinRTObservablePropertyOnFieldsIsNotAotCompatibleId = "MVVMTK0045"; + /// /// Gets a indicating when a duplicate declaration of would happen. /// @@ -681,7 +686,7 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0040"); /// - /// Gets a for a CanvasEffect property with invalid accessors. + /// Gets a for the C# language version not being sufficient for [ObservableProperty] on partial properties. /// /// 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)". /// @@ -697,7 +702,7 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0041"); /// - /// Gets a for a CanvasEffect property with invalid accessors. + /// Gets a for when [ObservableProperty] on a field should be converted to a partial property. /// /// 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)". /// @@ -743,4 +748,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Using [ObservableProperty] with (partial) properties requires a higher version of Roslyn (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0044"); + + /// + /// Gets a for a CanvasEffect property with invalid accessors. + /// + /// 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( + 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)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Fields using [ObservableProperty] will generate code that is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and partial properties should be used instead (as they allow the CsWinRT generators to correctly produce the necessary WinRT marshalling code).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0045"); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs new file mode 100644 index 000000000..01b8a8144 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs @@ -0,0 +1,114 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AnalyzerConfigOptionsExtensions +{ + /// + /// Checks whether the Windows runtime pack is being used (ie. if the target framework is net8.0-windows10.0.17763.0 or above). + /// + /// The input instance. + /// Whether the Windows runtime pack is being used. + public static bool IsUsingWindowsRuntimePack(this AnalyzerConfigOptions options) + { + return options.GetMSBuildBooleanPropertyValue("_MvvmToolkitIsUsingWindowsRuntimePack"); + } + + /// + /// Checks whether CsWinRT is configured in AOT support mode. + /// + /// The input instance. + /// The input instance in use. + /// Whether CsWinRT is configured in AOT support mode. + public static bool IsCsWinRTAotOptimizerEnabled(this AnalyzerConfigOptions options, Compilation compilation) + { + // If the runtime pack isn't being used, CsWinRT won't be used either. Technically speaking it's possible + // to reference CsWinRT without targeting Windows, but that's not a scenario that is supported anyway. + if (!options.IsUsingWindowsRuntimePack()) + { + return false; + } + + if (options.TryGetValue("build_property.CsWinRTAotOptimizerEnabled", out string? csWinRTAotOptimizerEnabled)) + { + // If the generators are in opt-in mode, we will not show warnings + if (string.Equals(csWinRTAotOptimizerEnabled, "OptIn", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // The automatic mode will generate marshalling code for all possible scenarios + if (string.Equals(csWinRTAotOptimizerEnabled, "Auto", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // The default value of "true" will run in automatic mode for some scenarios, which we have to check + if (bool.TryParse(csWinRTAotOptimizerEnabled, out bool isCsWinRTAotOptimizerEnabled) && isCsWinRTAotOptimizerEnabled) + { + // The CsWinRT generator will be enabled for AOT scenarios in the following cases: + // - The project is producing a WinRT component + // - The 'CsWinRTAotWarningLevel' is set to '2', ie. all marshalling code even for built-in types should be produced + // - The app is either UWP XAML or WinUI 3 (which is detected by the presence of the 'Button' type + // For additional reference, see the source code at https://github.com/microsoft/CsWinRT. + return + options.GetMSBuildBooleanPropertyValue("CsWinRTComponent") || + options.GetMSBuildInt32PropertyValue("CsWinRTAotWarningLevel") == 2 || + compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.Controls.Button") is not null || + compilation.GetTypeByMetadataName("Windows.UI.Xaml.Controls.Button") is not null; + + } + } + + return false; + } + + /// + /// Gets the boolean value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The default value to return if the property is not found or cannot be parsed. + /// The value of the target MSBuild property. + public static bool GetMSBuildBooleanPropertyValue(this AnalyzerConfigOptions options, string propertyName, bool defaultValue = false) + { + if (options.TryGetValue($"build_property.{propertyName}", out string? propertyValue)) + { + if (bool.TryParse(propertyValue, out bool booleanPropertyValue)) + { + return booleanPropertyValue; + } + } + + return defaultValue; + } + + /// + /// Gets the integer value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The default value to return if the property is not found or cannot be parsed. + /// The value of the target MSBuild property. + public static int GetMSBuildInt32PropertyValue(this AnalyzerConfigOptions options, string propertyName, int defaultValue = 0) + { + if (options.TryGetValue($"build_property.{propertyName}", out string? propertyValue)) + { + if (int.TryParse(propertyValue, out int int32PropertyValue)) + { + return int32PropertyValue; + } + } + + return defaultValue; + } +} From 729e96c108e025e51050411bacbe34951a2f6d82 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 6 Nov 2024 22:29:07 -0800 Subject: [PATCH 073/146] Add 'WinRTRelayCommandIsNotGeneratedBindablePropertyProviderCompatibleAnalyzer' --- .../AnalyzerReleases.Shipped.md | 1 + ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...indableCustomPropertyCompatibleAnalyzer.cs | 68 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 18 ++++- 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 93a47db60..28f1d0079 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -87,3 +87,4 @@ MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator MVVMTK0043| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 MVVMTK0044| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0044 MVVMTK0045| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0045 +MVVMTK0046 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0046 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 4778a26f0..2a06a532c 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/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs new file mode 100644 index 000000000..13dd28e73 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error when [RelayCommand] is used on a method inside a type with [GeneratedBindableCustomProperty]. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // This analyzer is only enabled when CsWinRT is also used + if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsUsingWindowsRuntimePack()) + { + return; + } + + // Get the symbol for [RelayCommand] and [GeneratedBindableCustomProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") is not INamedTypeSymbol relayCommandSymbol || + context.Compilation.GetTypeByMetadataName("WinRT.GeneratedBindableCustomPropertyAttribute") is not INamedTypeSymbol generatedBindableCustomPropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure we do have a valid method with a containing type we can reference + if (context.Symbol is not IMethodSymbol { ContainingType: INamedTypeSymbol typeSymbol } methodSymbol) + { + return; + } + + // If the method is not using [RelayCommand], we can skip it + if (!methodSymbol.TryGetAttributeWithType(relayCommandSymbol, out AttributeData? relayCommandAttribute)) + { + return; + } + + // If the containing type is using [GeneratedBindableCustomProperty], emit a warning + if (typeSymbol.HasAttributeWithType(generatedBindableCustomPropertySymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible, + relayCommandAttribute.GetLocation(), + methodSymbol)); + } + }, SymbolKind.Method); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 102d29eea..87dfbb719 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -750,7 +750,7 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0044"); /// - /// Gets a for a CanvasEffect property with invalid accessors. + /// Gets a for when [ObservableProperty] is used on a field in WinRT scenarios. /// /// 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)". /// @@ -764,4 +764,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Fields using [ObservableProperty] will generate code that is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and partial properties should be used instead (as they allow the CsWinRT generators to correctly produce the necessary WinRT marshalling code).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0045"); + + /// + /// Gets a for when [RelayCommand] is used on a method in types where [GeneratedBindableCustomProperty] is used. + /// + /// 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( + 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)""", + category: typeof(RelayCommandGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using [RelayCommand] on methods within a type also using [GeneratedBindableCustomProperty] 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).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0046"); } From 374b2d0fefb77b503db3633b21700c1c074e87f6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 6 Nov 2024 22:30:29 -0800 Subject: [PATCH 074/146] Use 'GetMSBuildBooleanPropertyValue' in more places --- .../ObservablePropertyGenerator.Execute.cs | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 751c4bbc6..a0a05c742 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -153,8 +153,9 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); - // Override the property changing support if explicitly disabled - shouldInvokeOnPropertyChanging &= GetEnableINotifyPropertyChangingSupport(options); + // Override the property changing support if explicitly disabled. + // This setting is enabled by default, for backwards compatibility. + shouldInvokeOnPropertyChanging &= options.GetMSBuildBooleanPropertyValue("MvvmToolkitEnableINotifyPropertyChangingSupport", defaultValue: true); token.ThrowIfCancellationRequested(); @@ -378,27 +379,6 @@ public static bool TryGetInfo( return true; } - /// - /// Gets the value for the "MvvmToolkitEnableINotifyPropertyChangingSupport" property. - /// - /// The options in use for the generator. - /// The value for the "MvvmToolkitEnableINotifyPropertyChangingSupport" property. - public static bool GetEnableINotifyPropertyChangingSupport(AnalyzerConfigOptions options) - { - if (options.TryGetValue("build_property.MvvmToolkitEnableINotifyPropertyChangingSupport", out string? propertyValue)) - { - if (bool.TryParse(propertyValue, out bool enableINotifyPropertyChangingSupport)) - { - return enableINotifyPropertyChangingSupport; - } - } - - // This setting is enabled by default, for backwards compatibility. - // Note that this path should never be reached, as the default - // value is also set in a .targets file bundled in the package. - return true; - } - /// /// Validates the containing type for a given field being annotated. /// From e2c33e78f4d9a33855e8054132807d2761441085 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 7 Nov 2024 11:33:53 -0800 Subject: [PATCH 075/146] Add 'WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer' --- .../AnalyzerReleases.Shipped.md | 8 +- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...leCustomPropertyWithBasesMemberAnalyzer.cs | 131 ++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 32 +++++ .../Extensions/INamedTypeSymbolExtensions.cs | 22 +++ 5 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 28f1d0079..6b2b3441d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -84,7 +84,9 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0042 -MVVMTK0043| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 -MVVMTK0044| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0044 -MVVMTK0045| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0045 +MVVMTK0043 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 +MVVMTK0044 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0044 +MVVMTK0045 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0045 MVVMTK0046 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0046 +MVVMTK0047 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0047 +MVVMTK0048 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0048 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 2a06a532c..11c513634 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/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs new file mode 100644 index 000000000..37454cd64 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs @@ -0,0 +1,131 @@ +// 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_11_0_OR_GREATER + +using System.Collections.Generic; +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 when [GeneratedBindableCustomProperty] is used on types with invalid generated base members. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField, + WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // This analyzer is only enabled when CsWinRT is also used + if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsUsingWindowsRuntimePack()) + { + return; + } + + // Get the symbol for [ObservableProperty], [RelayCommand] and [GeneratedBindableCustomProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") is not INamedTypeSymbol relayCommandSymbol || + context.Compilation.GetTypeByMetadataName("WinRT.GeneratedBindableCustomPropertyAttribute") is not INamedTypeSymbol generatedBindableCustomPropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure we do have a valid type + if (context.Symbol is not INamedTypeSymbol typeSymbol) + { + return; + } + + // We only care about it if it's using [GeneratedBindableCustomProperty] + if (!typeSymbol.TryGetAttributeWithType(generatedBindableCustomPropertySymbol, out AttributeData? generatedBindableCustomPropertyAttribute)) + { + return; + } + + // Warn on all [ObservableProperty] fields + foreach (IFieldSymbol fieldSymbol in FindObservablePropertyFields(typeSymbol, relayCommandSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField, + generatedBindableCustomPropertyAttribute.GetLocation(), + typeSymbol, + fieldSymbol.ContainingType, + fieldSymbol.Name)); + } + + // Warn on all [RelayCommand] methods + foreach (IMethodSymbol methodSymbol in FindRelayCommandMethods(typeSymbol, relayCommandSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand, + generatedBindableCustomPropertyAttribute.GetLocation(), + typeSymbol, + methodSymbol)); + } + }, SymbolKind.NamedType); + }); + } + + /// + /// Finds all methods in the base types that have the [RelayCommand] attribute. + /// + /// The instance to inspect. + /// The symbol for the [RelayCommand] + /// All instances for matching members. + private static IEnumerable FindRelayCommandMethods(INamedTypeSymbol typeSymbol, INamedTypeSymbol relayCommandSymbol) + { + // Check whether the base type (if any) is from the same assembly, and stop if it isn't. We do not + // want to include methods from the same type, as those will already be caught by another analyzer. + if (!SymbolEqualityComparer.Default.Equals(typeSymbol.ContainingAssembly, typeSymbol.BaseType)) + { + yield break; + } + + foreach (ISymbol memberSymbol in typeSymbol.BaseType.GetAllMembersFromSameAssembly()) + { + if (memberSymbol is IMethodSymbol methodSymbol && + methodSymbol.HasAttributeWithType(relayCommandSymbol)) + { + yield return methodSymbol; + } + } + } + + /// + /// Finds all fields in the base types that have the [ObservableProperty] attribute. + /// + /// The instance to inspect. + /// The symbol for the [ObservableProperty] + /// All instances for matching members. + private static IEnumerable FindObservablePropertyFields(INamedTypeSymbol typeSymbol, INamedTypeSymbol observablePropertySymbol) + { + foreach (ISymbol memberSymbol in typeSymbol.GetAllMembersFromSameAssembly()) + { + if (memberSymbol is IFieldSymbol fieldSymbol && + fieldSymbol.HasAttributeWithType(observablePropertySymbol)) + { + yield return fieldSymbol; + } + } + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 87dfbb719..13e4d7e65 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -780,4 +780,36 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Using [RelayCommand] on methods within a type also using [GeneratedBindableCustomProperty] 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).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0046"); + + /// + /// Gets a for when [GeneratedBindableCustomProperty] is used on a type that also uses [ObservableProperty] on any declared or inherited fields. + /// + /// 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( + id: "MVVMTK0047", + title: "Using [GeneratedBindableCustomProperty] is not compatible with [ObservableProperty] on fields", + messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its declared (or 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)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using [GeneratedBindableCustomProperty] on types that also use [ObservableProperty] on any declared (or inherited) fields 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).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0047"); + + /// + /// Gets a for when [GeneratedBindableCustomProperty] is used on a type that also uses [RelayCommand] on any declared or inherited methods. + /// + /// 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( + 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)""", + category: typeof(RelayCommandGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using [GeneratedBindableCustomProperty] on types that also use [RelayCommand] on any inherited methods 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).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0048"); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs index 8b847bcf0..367b16f78 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs @@ -28,6 +28,28 @@ public static IEnumerable GetAllMembers(this INamedTypeSymbol symbol) } } + /// + /// Gets all member symbols from a given instance, including inherited ones, only if they are declared in source. + /// + /// The input instance. + /// A sequence of all member symbols for . + public static IEnumerable GetAllMembersFromSameAssembly(this INamedTypeSymbol symbol) + { + for (INamedTypeSymbol? currentSymbol = symbol; currentSymbol is { SpecialType: not SpecialType.System_Object }; currentSymbol = currentSymbol.BaseType) + { + // Stop early when we reach a base type from another assembly + if (!SymbolEqualityComparer.Default.Equals(currentSymbol.ContainingAssembly, symbol.ContainingAssembly)) + { + yield break; + } + + foreach (ISymbol memberSymbol in currentSymbol.GetMembers()) + { + yield return memberSymbol; + } + } + } + /// /// Gets all member symbols from a given instance, including inherited ones. /// From 171ba41d6115b086b2ef6e7ef5036ccca3b53b33 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 7 Nov 2024 15:45:41 -0800 Subject: [PATCH 076/146] Tweaks to the '[ObservableProperty]' code fixer --- ...lPropertyForObservablePropertyCodeFixer.cs | 18 +++++++++-- ...pertyOnFieldsIsNotAotCompatibleAnalyzer.cs | 32 +++++-------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 403582ff0..43f5ca94c 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -57,7 +57,9 @@ public sealed class UsePartialPropertyForObservablePropertyCodeFixer : CodeFixPr }); /// - public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnPartialPropertyId); + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + UseObservablePropertyOnPartialPropertyId, + WinRTObservablePropertyOnFieldsIsNotAotCompatibleId); /// public override FixAllProvider? GetFixAllProvider() @@ -77,6 +79,14 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) return; } + SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!; + + // If the language is not preview, we cannot apply this code fix (as it would generate invalid C# code) + if (!semanticModel.Compilation.IsLanguageVersionPreview()) + { + return; + } + // Retrieve the properties passed by the analyzer if (diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey] is not string fieldName || diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey] is not string propertyName) @@ -101,7 +111,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create( title: "Use a partial property", - createChangedDocument: token => ConvertToPartialProperty(context.Document, root, fieldDeclaration, fieldName, propertyName, context.CancellationToken), + createChangedDocument: token => ConvertToPartialProperty(context.Document, root, fieldDeclaration, semanticModel, fieldName, propertyName, context.CancellationToken), equivalenceKey: "Use a partial property"), diagnostic); } @@ -113,6 +123,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) /// The original document being fixed. /// The original tree root belonging to the current document. /// The for the field being updated. + /// The semantic model for . /// The name of the annotated field. /// The name of the generated property. /// The cancellation token for the operation. @@ -121,11 +132,12 @@ private static async Task ConvertToPartialProperty( Document document, SyntaxNode root, FieldDeclarationSyntax fieldDeclaration, + SemanticModel semanticModel, string fieldName, string propertyName, CancellationToken cancellationToken) { - SemanticModel semanticModel = (await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false))!; + await Task.CompletedTask; // Try to get all necessary type symbols to process the attributes if (!semanticModel.Compilation.TryBuildNamedTypeSymbolMap(MvvmToolkitAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary? toolkitTypeSymbols) || diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs index 735e189c5..7c619c120 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs @@ -41,8 +41,6 @@ public override void Initialize(AnalysisContext context) return; } - bool isLanguageVersionPreview = context.Compilation.IsLanguageVersionPreview(); - context.RegisterSymbolAction(context => { // Ensure we do have a valid field @@ -54,28 +52,14 @@ public override void Initialize(AnalysisContext context) // Emit a diagnostic if the field is using the [ObservableProperty] attribute if (fieldSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) { - // If the C# version is preview, we can include the necessary information to trigger the - // code fixer. If that is not the case, we shouldn't do that, to avoid the code fixer - // changing the code to invalid C# (as without the preview version, it wouldn't compile). - if (isLanguageVersionPreview) - { - context.ReportDiagnostic(Diagnostic.Create( - WinRTObservablePropertyOnFieldsIsNotAotCompatible, - observablePropertyAttribute.GetLocation(), - ImmutableDictionary.Create() - .Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name) - .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)), - fieldSymbol.ContainingType, - fieldSymbol.Name)); - } - else - { - context.ReportDiagnostic(Diagnostic.Create( - WinRTObservablePropertyOnFieldsIsNotAotCompatible, - observablePropertyAttribute.GetLocation(), - fieldSymbol.ContainingType, - fieldSymbol.Name)); - } + context.ReportDiagnostic(Diagnostic.Create( + WinRTObservablePropertyOnFieldsIsNotAotCompatible, + observablePropertyAttribute.GetLocation(), + ImmutableDictionary.Create() + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name) + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)), + fieldSymbol.ContainingType, + fieldSymbol.Name)); } }, SymbolKind.Field); }); From f74269bcd099360afe5d18f93d65d5a0bcb7ccf3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 7 Nov 2024 17:07:03 -0800 Subject: [PATCH 077/146] Fix formatting in '[ObservableProperty]' diagnostics --- ...rvablePropertyOnPartialPropertyAnalyzer.cs | 2 +- ...lPropertyForObservablePropertyCodeFixer.cs | 56 +++++++++---------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs index 3fd76ce4c..caeeaaeff 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -79,7 +79,7 @@ public override void Initialize(AnalysisContext context) .Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name) .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)), fieldSymbol.ContainingType, - fieldSymbol)); + fieldSymbol.Name)); }, SymbolKind.Field); }); } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index c111ef8de..1236d5971 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -54,8 +54,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -102,8 +102,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -152,8 +152,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -204,8 +204,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -275,8 +275,8 @@ public class TestAttribute(string text) : Attribute; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(7,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 6, 7, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(7,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 6, 7, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -323,8 +323,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -373,8 +373,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(7,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 6, 7, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(7,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 6, 7, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -427,8 +427,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(9,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(9, 6, 9, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(9,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(9, 6, 9, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -487,8 +487,8 @@ public void M() test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -533,8 +533,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -584,8 +584,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.items 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.items"), + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.items 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "items"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -635,8 +635,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.items 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.items"), + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.items 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "items"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -685,8 +685,8 @@ partial class C : ObservableObject test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.foo 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.foo"), + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.foo 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "foo"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] @@ -755,8 +755,8 @@ public void M() test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); test.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i 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) - CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.i 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "i"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] From c6ca94dec39b0fe540f00117463a834fc55e4b17 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 7 Nov 2024 19:06:53 -0800 Subject: [PATCH 078/146] Minor code refactoring to internal extensions --- .../AnalyzerConfigOptionsExtensions.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs index 01b8a8144..7556bd45a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -38,7 +39,7 @@ public static bool IsCsWinRTAotOptimizerEnabled(this AnalyzerConfigOptions optio return false; } - if (options.TryGetValue("build_property.CsWinRTAotOptimizerEnabled", out string? csWinRTAotOptimizerEnabled)) + if (options.TryGetMSBuildStringPropertyValue("CsWinRTAotOptimizerEnabled", out string? csWinRTAotOptimizerEnabled)) { // If the generators are in opt-in mode, we will not show warnings if (string.Equals(csWinRTAotOptimizerEnabled, "OptIn", StringComparison.OrdinalIgnoreCase)) @@ -81,7 +82,7 @@ public static bool IsCsWinRTAotOptimizerEnabled(this AnalyzerConfigOptions optio /// The value of the target MSBuild property. public static bool GetMSBuildBooleanPropertyValue(this AnalyzerConfigOptions options, string propertyName, bool defaultValue = false) { - if (options.TryGetValue($"build_property.{propertyName}", out string? propertyValue)) + if (options.TryGetMSBuildStringPropertyValue(propertyName, out string? propertyValue)) { if (bool.TryParse(propertyValue, out bool booleanPropertyValue)) { @@ -101,7 +102,7 @@ public static bool GetMSBuildBooleanPropertyValue(this AnalyzerConfigOptions opt /// The value of the target MSBuild property. public static int GetMSBuildInt32PropertyValue(this AnalyzerConfigOptions options, string propertyName, int defaultValue = 0) { - if (options.TryGetValue($"build_property.{propertyName}", out string? propertyValue)) + if (options.TryGetMSBuildStringPropertyValue(propertyName, out string? propertyValue)) { if (int.TryParse(propertyValue, out int int32PropertyValue)) { @@ -111,4 +112,16 @@ public static int GetMSBuildInt32PropertyValue(this AnalyzerConfigOptions option return defaultValue; } + + /// + /// Tries to get a value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The resulting property value. + /// Whether the property value was retrieved.. + public static bool TryGetMSBuildStringPropertyValue(this AnalyzerConfigOptions options, string propertyName, [NotNullWhen(true)] out string? propertyValue) + { + return options.TryGetValue($"build_property.{propertyName}", out propertyValue); + } } From 7ba700ef6826d63807de83dfb1079435e41c2c87 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 7 Nov 2024 23:12:35 -0800 Subject: [PATCH 079/146] Fix some analyzer bugs --- ...dBindableCustomPropertyWithBasesMemberAnalyzer.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs index 37454cd64..af21f5d49 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs @@ -61,7 +61,7 @@ public override void Initialize(AnalysisContext context) } // Warn on all [ObservableProperty] fields - foreach (IFieldSymbol fieldSymbol in FindObservablePropertyFields(typeSymbol, relayCommandSymbol)) + foreach (IFieldSymbol fieldSymbol in FindObservablePropertyFields(typeSymbol, observablePropertySymbol)) { context.ReportDiagnostic(Diagnostic.Create( WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField, @@ -94,7 +94,7 @@ private static IEnumerable FindRelayCommandMethods(INamedTypeSymb { // Check whether the base type (if any) is from the same assembly, and stop if it isn't. We do not // want to include methods from the same type, as those will already be caught by another analyzer. - if (!SymbolEqualityComparer.Default.Equals(typeSymbol.ContainingAssembly, typeSymbol.BaseType)) + if (!SymbolEqualityComparer.Default.Equals(typeSymbol.ContainingAssembly, typeSymbol.BaseType?.ContainingAssembly)) { yield break; } @@ -117,7 +117,13 @@ private static IEnumerable FindRelayCommandMethods(INamedTypeSymb /// All instances for matching members. private static IEnumerable FindObservablePropertyFields(INamedTypeSymbol typeSymbol, INamedTypeSymbol observablePropertySymbol) { - foreach (ISymbol memberSymbol in typeSymbol.GetAllMembersFromSameAssembly()) + // Skip the base type if not from the same assembly, same as above + if (!SymbolEqualityComparer.Default.Equals(typeSymbol.ContainingAssembly, typeSymbol.BaseType?.ContainingAssembly)) + { + yield break; + } + + foreach (ISymbol memberSymbol in typeSymbol.BaseType.GetAllMembersFromSameAssembly()) { if (memberSymbol is IFieldSymbol fieldSymbol && fieldSymbol.HasAttributeWithType(observablePropertySymbol)) From ec0a331005078f6b332a39ada28d20f2d14e881b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 7 Nov 2024 23:12:51 -0800 Subject: [PATCH 080/146] Add unit tests for all WinRT analyzers --- .../Test_SourceGeneratorsDiagnostics.cs | 457 ++++++++++++++++++ ...lyzerWithLanguageVersionTest{TAnalyzer}.cs | 48 ++ 2 files changed, 505 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index e38182eaf..d7fdfc933 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Threading.Tasks; +using CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; using Microsoft.CodeAnalysis.CSharp; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -271,4 +272,460 @@ public partial class SampleViewModel : ObservableObject await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_CsWinRTAotOptimizerEnabled_Auto_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private static string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_NotTargetingWindows_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: []); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_OptIn_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_False_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "false")]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_Auto_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0045:ObservableProperty|}] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_NoXaml_Level1_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 1)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_NoXaml_Level2_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0045:ObservableProperty|}] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 2)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_NoXaml_1_Component_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0045:ObservableProperty|}] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 1), ("CsWinRTComponent", true)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_UwpXaml_Level1_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0045:ObservableProperty|}] + private string name; + } + } + + namespace Windows.UI.Xaml.Controls + { + public class Button; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 1)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_WinUIXaml_Level1_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0045:ObservableProperty|}] + private string name; + } + } + + namespace Microsoft.UI.Xaml.Controls + { + public class Button; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 1)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_NotTargetingWindows_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: []); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_Bindable_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty] + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0046:RelayCommand|}] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_NotTargetingWindows_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty] + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: []); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty] + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Field_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using WinRT; + + namespace MyApp + { + [{|MVVMTK0047:GeneratedBindableCustomProperty|}] + public partial class SampleViewModel : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Method_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [{|MVVMTK0048:GeneratedBindableCustomProperty|}] + public partial class SampleViewModel : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs index 8e41258ab..c44dbc154 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs @@ -2,6 +2,9 @@ // 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.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -10,6 +13,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; #if NET472 using System.ComponentModel.DataAnnotations; #endif @@ -59,8 +63,52 @@ public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVe #endif test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ObservableObject).Assembly.Location)); + test.SolutionTransforms.Add((solution, projectId) => + solution.AddAnalyzerConfigDocument(DocumentId.CreateNewId(projectId), + "UseMarshalType.editorconfig", + SourceText.From(""" + is_global = true + build_property.LibraryImportGenerator_UseMarshalType = true + """, + Encoding.UTF8), + filePath: "/UseMarshalType.editorconfig")); + test.ExpectedDiagnostics.AddRange(expected); return test.RunAsync(CancellationToken.None); } + + /// + /// The language version to use to run the test. + public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVersion, (string PropertyName, object PropertyValue)[] editorconfig) + { + CSharpAnalyzerWithLanguageVersionTest test = new(languageVersion) { TestCode = source }; + +#if NET8_0_OR_GREATER + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET6_0_OR_GREATER + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net60; +#else + test.TestState.ReferenceAssemblies = ReferenceAssemblies.NetFramework.Net472.Default; + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(RequiredAttribute).Assembly.Location)); +#endif + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ObservableObject).Assembly.Location)); + + // Add any editorconfig properties, if present + if (editorconfig.Length > 0) + { + test.SolutionTransforms.Add((solution, projectId) => + solution.AddAnalyzerConfigDocument( + DocumentId.CreateNewId(projectId), + "MvvmToolkitAnalyzers.editorconfig", + SourceText.From($""" + is_global = true + {string.Join(Environment.NewLine, editorconfig.Select(static p => $"build_property.{p.PropertyName} = {p.PropertyValue}"))} + """, + Encoding.UTF8), + filePath: "/MvvmToolkitAnalyzers.editorconfig")); + } + + return test.RunAsync(CancellationToken.None); + } } From a285a1eaf4fcf21424af722c42614515183b9722 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 7 Nov 2024 23:43:01 -0800 Subject: [PATCH 081/146] Embed .pdb files for all analyzer projects --- .../CommunityToolkit.Mvvm.CodeFixers.props | 3 +++ .../CommunityToolkit.Mvvm.SourceGenerators.props | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props index 04381ec56..5460cb1ea 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props @@ -4,6 +4,9 @@ netstandard2.0 false true + + + embedded diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props index c29a655e2..1624ac5f3 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props @@ -4,6 +4,15 @@ netstandard2.0 false true + + + embedded - + - netstandard2.0;netstandard2.1;net6.0;net8.0;net8.0-windows10.0.17763.0 + netstandard2.0;netstandard2.1;net8.0;net8.0-windows10.0.17763.0 - + diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 23f65c476..7239f4a2e 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -7,14 +7,6 @@ NETSTANDARD2_1_OR_GREATER - - - true - true - true - true - - true diff --git a/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj b/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj index 76bc41862..e47e06627 100644 --- a/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj +++ b/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 diff --git a/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj b/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj index f3a582df1..633cc0fa4 100644 --- a/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj +++ b/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj b/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj index e49f035d4..772b868c5 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 true $(NoWarn);CA2252 diff --git a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj index 63a4b907a..5853d5a5c 100644 --- a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 diff --git a/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj index 421cc2315..acb777815 100644 --- a/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj index c678777f5..eb46db98f 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 true diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj index bd5ce9114..b70464cb4 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 true diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj index 21b341ca9..3656cfdf1 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index 9e59eeb93..495ef1dca 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 $(DefineConstants);ROSLYN_4_3_1_OR_GREATER diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj index 83566c52d..a6b432501 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 $(DefineConstants);ROSLYN_4_3_1_OR_GREATER;ROSLYN_4_11_0_OR_GREATER From 15867433e86b627fb4c297e6f93cb2b9b75da6e2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 13 Nov 2024 08:58:32 -0800 Subject: [PATCH 088/146] Update code paths for .NET 7 as base --- .../Deferred/EventDeferral.cs | 29 +++++++++++++++++-- .../Test_ObservablePropertyAttribute.cs | 8 ++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/CommunityToolkit.Common/Deferred/EventDeferral.cs b/src/CommunityToolkit.Common/Deferred/EventDeferral.cs index d5a36b7cb..65660ff03 100644 --- a/src/CommunityToolkit.Common/Deferred/EventDeferral.cs +++ b/src/CommunityToolkit.Common/Deferred/EventDeferral.cs @@ -4,6 +4,9 @@ using System; using System.ComponentModel; +#if NET8_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif using System.Threading; using System.Threading.Tasks; @@ -16,8 +19,11 @@ namespace CommunityToolkit.Common.Deferred; /// public class EventDeferral : IDisposable { - // TODO: If/when .NET 6 is base, we can upgrade to non-generic version +#if NET8_0_OR_GREATER + private readonly TaskCompletionSource taskCompletionSource = new(); +#else private readonly TaskCompletionSource taskCompletionSource = new(); +#endif internal EventDeferral() { @@ -26,7 +32,14 @@ internal EventDeferral() /// /// Call when finished with the Deferral. /// - public void Complete() => this.taskCompletionSource.TrySetResult(null); + public void Complete() + { +#if NET8_0_OR_GREATER + this.taskCompletionSource.TrySetResult(); +#else + this.taskCompletionSource.TrySetResult(null); +#endif + } /// /// Waits for the to be completed by the event handler. @@ -38,9 +51,19 @@ internal EventDeferral() [Obsolete("This is an internal only method to be used by EventHandler extension classes, public callers should call GetDeferral() instead on the DeferredEventArgs.")] public async Task WaitForCompletion(CancellationToken cancellationToken) { - using (cancellationToken.Register(() => this.taskCompletionSource.TrySetCanceled())) + using (cancellationToken.Register( +#if NET8_0_OR_GREATER + callback: static obj => Unsafe.As(obj!).taskCompletionSource.TrySetCanceled(), +#else + callback: static obj => ((EventDeferral)obj).taskCompletionSource.TrySetCanceled(), +#endif + state: this)) { +#if NET8_0_OR_GREATER + await this.taskCompletionSource.Task; +#else _ = await this.taskCompletionSource.Task; +#endif } } diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index 6b3e25796..65ab55fe9 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs @@ -709,12 +709,8 @@ public void Test_ObservableProperty_NullabilityAnnotations_Complex() NullabilityInfo rightInfo2 = rightInnerInfo.GenericTypeArguments[2]; Assert.AreEqual(typeof(object), rightInfo2.Type); - //Assert.AreEqual(NullabilityState.NotNull, rightInfo2.ReadState); - //Assert.AreEqual(NullabilityState.NotNull, rightInfo2.WriteState); - - // The commented out lines are to work around a bug in the NullabilityInfo API in .NET 6. - // This has been fixed for .NET 7: https://github.com/dotnet/runtime/pull/63556. The test - // cases above can be uncommented when the .NET 7 target (or a more recent version) is added. + Assert.AreEqual(NullabilityState.NotNull, rightInfo2.ReadState); + Assert.AreEqual(NullabilityState.NotNull, rightInfo2.WriteState); } #endif From 70e469101ee89fd55b33171008a0c6136732c124 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 13 Nov 2024 18:42:37 -0800 Subject: [PATCH 089/146] Remove leftover analyzer test code --- ...CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs index c44dbc154..14a15ee60 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs @@ -63,16 +63,6 @@ public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVe #endif test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ObservableObject).Assembly.Location)); - test.SolutionTransforms.Add((solution, projectId) => - solution.AddAnalyzerConfigDocument(DocumentId.CreateNewId(projectId), - "UseMarshalType.editorconfig", - SourceText.From(""" - is_global = true - build_property.LibraryImportGenerator_UseMarshalType = true - """, - Encoding.UTF8), - filePath: "/UseMarshalType.editorconfig")); - test.ExpectedDiagnostics.AddRange(expected); return test.RunAsync(CancellationToken.None); From 1f88b1ed21cb310d62f62d05e7afc630a8675c3a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 13 Nov 2024 18:46:21 -0800 Subject: [PATCH 090/146] Skip MVVMTK0041 when properties are not partial --- ...resCSharpLanguageVersionPreviewAnalyzer.cs | 18 +++++- .../Test_SourceGeneratorsDiagnostics.cs | 55 ++++++++++++++++++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs index 301b0e3f4..f0b5793b2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs @@ -7,6 +7,8 @@ using System.Collections.Immutable; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; @@ -44,11 +46,25 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { // We only want to target partial property definitions (also include non-partial ones for diagnostics) - if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null }) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } partialProperty) { return; } + // Make sure to skip the warning if the property is not actually partial + if (partialProperty.DeclaringSyntaxReferences is [var syntaxReference]) + { + // Make sure we can find the syntax node, and that it's a property declaration + if (syntaxReference.GetSyntax(context.CancellationToken) is PropertyDeclarationSyntax propertyDeclarationSyntax) + { + // If the property is not partial, ignore it, as we'll already have a warning from the other analyzer here + if (!propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + return; + } + } + } + // If the property is using [ObservableProperty], emit the diagnostic if (context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) { diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 2fc686df1..66c6eea28 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; @@ -12,7 +13,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; partial class Test_SourceGeneratorsDiagnostics { [TestMethod] - public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_Warns() + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_DoesnNotWarn() { const string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -21,7 +22,7 @@ namespace MyApp { public partial class SampleViewModel : ObservableObject { - [{|MVVMTK0041:ObservableProperty|}] + [ObservableProperty] public string Name { get; set; } } } @@ -30,6 +31,32 @@ public partial class SampleViewModel : ObservableObject await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp12); } + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_Partial_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0041:ObservableProperty|}] + public partial string Name { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + + // /0/Test0.cs(8,31): error CS8703: The modifier 'partial' is not valid for this item in C# 12.0. Please use language version 'preview' or greater. + DiagnosticResult.CompilerError("CS8703").WithSpan(8, 31, 8, 35).WithArguments("partial", "12.0", "preview"), + // /0/Test0.cs(8,31): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 31, 8, 35).WithArguments("MyApp.SampleViewModel.Name")); + } + [TestMethod] public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsPreview_DoesNotWarn() { @@ -49,6 +76,30 @@ public partial class SampleViewModel : ObservableObject await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, languageVersion: LanguageVersion.Preview); } + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsPreview_Partial_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + + // /0/Test0.cs(8,31): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 31, 8, 35).WithArguments("MyApp.SampleViewModel.Name")); + } + [TestMethod] public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsNotPreview_DoesNotWarn() { From 77f1f335347c1548f5e36c159766b72b5724d4e3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 19 Nov 2024 15:45:41 -0800 Subject: [PATCH 091/146] Use 'ref readonly' in 'IndexOf' APIs --- .../Enumerables/SpanTokenizer{T}.cs | 5 ++++- .../Extensions/ReadOnlySpanExtensions.cs | 2 +- .../Extensions/SpanExtensions.cs | 9 +++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs b/src/CommunityToolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs index df06445ca..d2d7c33a6 100644 --- a/src/CommunityToolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs +++ b/src/CommunityToolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs @@ -73,7 +73,10 @@ public bool MoveNext() { this.start = newEnd; - int index = this.span.Slice(newEnd).IndexOf(this.separator); + // Here we're inside the 'CommunityToolkit.HighPerformance.Enumerables' namespace, so the + // 'MemoryExtensions' type from the .NET Community Toolkit would be bound instead. Because + // want the one from the BCL (to search by value), we can use its fully qualified name. + int index = System.MemoryExtensions.IndexOf(this.span.Slice(newEnd), this.separator); // Extract the current subsequence if (index >= 0) diff --git a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs index dfae52a13..e7e64c788 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -210,7 +210,7 @@ public static ReadOnlySpan2D AsSpan2D(this ReadOnlySpan span, int offse /// The reference to the target item to get the index for. /// The index of within , or -1. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int IndexOf(this ReadOnlySpan span, in T value) + public static unsafe int IndexOf(this ReadOnlySpan span, ref readonly T value) { ref T r0 = ref MemoryMarshal.GetReference(span); ref T r1 = ref Unsafe.AsRef(in value); diff --git a/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs index 7fb71727a..21f0725e7 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -148,10 +148,15 @@ public static Span Cast(this Span span) /// The reference to the target item to get the index for. /// The index of within , or -1. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int IndexOf(this Span span, ref T value) + public static unsafe int IndexOf(this Span span, ref readonly T value) { ref T r0 = ref MemoryMarshal.GetReference(span); - IntPtr byteOffset = Unsafe.ByteOffset(ref r0, ref value); + IntPtr byteOffset = +#if NET8_0_OR_GREATER + Unsafe.ByteOffset(ref r0, in value); +#else + Unsafe.ByteOffset(ref r0, ref Unsafe.AsRef(in value)); +#endif nint elementOffset = byteOffset / (nint)(uint)sizeof(T); From b8aa65ac4ed69ece620508b545eaf7e17a8c1185 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:14:46 +0000 Subject: [PATCH 092/146] Added stream over 'ReadOnlySequence' --- .../Extensions/ReadOnlySequenceExtensions.cs | 34 ++ .../Streams/MemoryStream.Validate.cs | 17 +- .../Streams/ReadOnlySequenceStream.cs | 308 +++++++++++++ .../Streams/Test_ReadOnlySequenceStream.cs | 407 ++++++++++++++++++ 4 files changed, 765 insertions(+), 1 deletion(-) create mode 100644 src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySequenceExtensions.cs create mode 100644 src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs create mode 100644 tests/CommunityToolkit.HighPerformance.UnitTests/Streams/Test_ReadOnlySequenceStream.cs diff --git a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySequenceExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySequenceExtensions.cs new file mode 100644 index 000000000..e4ca8f766 --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySequenceExtensions.cs @@ -0,0 +1,34 @@ +// 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 CommunityToolkit.HighPerformance.Streams; +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.HighPerformance; + +/// +/// Helpers for working with the type. +/// +public static class ReadOnlySequenceExtensions +{ + /// + /// Returns a wrapping the contents of the given of instance. + /// + /// The input of instance. + /// A wrapping the data within . + /// + /// Since this method only receives a instance, which does not track + /// the lifetime of its underlying buffer, it is responsibility of the caller to manage that. + /// In particular, the caller must ensure that the target buffer is not disposed as long + /// as the returned is in use, to avoid unexpected issues. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream AsStream(this ReadOnlySequence sequence) + { + return ReadOnlySequenceStream.Create(sequence); + } +} diff --git a/src/CommunityToolkit.HighPerformance/Streams/MemoryStream.Validate.cs b/src/CommunityToolkit.HighPerformance/Streams/MemoryStream.Validate.cs index e7dae1359..126d90a44 100644 --- a/src/CommunityToolkit.HighPerformance/Streams/MemoryStream.Validate.cs +++ b/src/CommunityToolkit.HighPerformance/Streams/MemoryStream.Validate.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; @@ -24,6 +25,20 @@ public static void ValidatePosition(long position, int length) } } + /// + /// Validates the argument (it needs to be in the [0, length]) range. + /// + /// The new value being set. + /// The maximum length of the target . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidatePosition(long position, long length) + { + if ((ulong)position > (ulong)length) + { + ThrowArgumentOutOfRangeExceptionForPosition(); + } + } + /// /// Validates the or arguments. /// @@ -31,7 +46,7 @@ public static void ValidatePosition(long position, int length) /// The offset within . /// The number of elements to process within . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidateBuffer(byte[]? buffer, int offset, int count) + public static void ValidateBuffer([NotNull] byte[]? buffer, int offset, int count) { if (buffer is null) { diff --git a/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs b/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs new file mode 100644 index 000000000..fbca23c13 --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs @@ -0,0 +1,308 @@ +// 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.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace CommunityToolkit.HighPerformance.Streams; + +/// +/// A implementation wrapping a of instance. +/// +internal sealed partial class ReadOnlySequenceStream : Stream +{ + /// + /// The instance currently in use. + /// + private readonly ReadOnlySequence source; + + /// + /// The current position within . + /// + private int position; + + /// + /// Indicates whether or not the current instance has been disposed + /// + private bool disposed; + + /// + /// Initializes a new instance of the class with the specified source. + /// + /// The source. + public ReadOnlySequenceStream(ReadOnlySequence source) + { + this.source = source; + } + + /// + public sealed override bool CanRead + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !this.disposed; + } + + /// + public sealed override bool CanSeek + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !this.disposed; + } + + /// + public sealed override bool CanWrite + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => false; + } + + /// + public sealed override long Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + MemoryStream.ValidateDisposed(this.disposed); + + return this.source.Length; + } + } + + /// + public sealed override long Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + MemoryStream.ValidateDisposed(this.disposed); + + return this.position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + MemoryStream.ValidateDisposed(this.disposed); + MemoryStream.ValidatePosition(value, this.source.Length); + + this.position = unchecked((int)value); + } + } + + /// + /// Creates a new from the input of instance. + /// + /// The input instance. + /// A wrapping the underlying data for . + public static Stream Create(ReadOnlySequence sequence) + { + return new ReadOnlySequenceStream(sequence); + } + + /// + public sealed override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + MemoryStream.ValidateDisposed(this.disposed); + + if (this.position >= this.source.Length) + { + return Task.CompletedTask; + } + + if (this.source.IsSingleSegment) + { + ReadOnlyMemory buffer = this.source.First.Slice(this.position); + + this.position = (int)this.source.Length; + + return destination.WriteAsync(buffer, cancellationToken).AsTask(); + } + + async Task CoreCopyToAsync(Stream destination, CancellationToken cancellationToken) + { + ReadOnlySequence sequence = this.source.Slice(this.position); + + this.position = (int)this.source.Length; + + foreach (ReadOnlyMemory segment in sequence) + { + await destination.WriteAsync(segment, cancellationToken).ConfigureAwait(false); + } + } + + return CoreCopyToAsync(destination, cancellationToken); + } + catch (OperationCanceledException e) + { + return Task.FromCanceled(e.CancellationToken); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + /// + public sealed override void Flush() + { + } + + /// + public sealed override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.CompletedTask; + } + + /// + public sealed override Task ReadAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + int result = Read(buffer, offset, count); + + return Task.FromResult(result); + } + catch (OperationCanceledException e) + { + return Task.FromCanceled(e.CancellationToken); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public sealed override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public sealed override long Seek(long offset, SeekOrigin origin) + { + MemoryStream.ValidateDisposed(this.disposed); + + long index = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => this.position + offset, + SeekOrigin.End => this.source.Length + offset, + _ => MemoryStream.ThrowArgumentExceptionForSeekOrigin() + }; + + MemoryStream.ValidatePosition(index, this.source.Length); + + this.position = unchecked((int)index); + + return index; + } + + /// + public sealed override void SetLength(long value) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public sealed override int Read(byte[]? buffer, int offset, int count) + { + MemoryStream.ValidateDisposed(this.disposed); + MemoryStream.ValidateBuffer(buffer, offset, count); + + if (this.position >= this.source.Length) + { + return 0; + } + + ReadOnlySequence sequence = this.source.Slice(this.position); + Span destination = buffer.AsSpan(offset, count); + int bytesCopied = 0; + + foreach (ReadOnlyMemory segment in sequence) + { + int bytesToCopy = Math.Min(segment.Length, destination.Length); + + segment.Span.Slice(0, bytesToCopy).CopyTo(destination); + + destination = destination.Slice(bytesToCopy); + bytesCopied += bytesToCopy; + + this.position += bytesToCopy; + + if (destination.Length == 0) + { + break; + } + } + + return bytesCopied; + } + + /// + public sealed override int ReadByte() + { + MemoryStream.ValidateDisposed(this.disposed); + + if (this.position == this.source.Length) + { + return -1; + } + + ReadOnlySequence sequence = this.source.Slice(this.position); + + this.position++; + + return sequence.First.Span[0]; + } + + /// + public sealed override void Write(byte[]? buffer, int offset, int count) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public sealed override void WriteByte(byte value) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + protected override void Dispose(bool disposing) + { + if (this.disposed) + { + return; + } + + this.disposed = true; + } +} diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Streams/Test_ReadOnlySequenceStream.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Streams/Test_ReadOnlySequenceStream.cs new file mode 100644 index 000000000..0cb053f2a --- /dev/null +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Streams/Test_ReadOnlySequenceStream.cs @@ -0,0 +1,407 @@ +// 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.Buffers; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.HighPerformance.UnitTests.Streams; + +[TestClass] +public partial class Test_ReadOnlySequenceStream +{ + [TestMethod] + public void Test_ReadOnlySequenceStream_Lifecycle() + { + ReadOnlySequence sequence = CreateReadOnlySequence(new byte[100]); + + Stream stream = sequence.AsStream(); + + Assert.IsTrue(stream.CanRead); + Assert.IsTrue(stream.CanSeek); + Assert.IsFalse(stream.CanWrite); + Assert.AreEqual(stream.Length, sequence.Length); + Assert.AreEqual(stream.Position, 0); + + stream.Dispose(); + + Assert.IsFalse(stream.CanRead); + Assert.IsFalse(stream.CanSeek); + Assert.IsFalse(stream.CanWrite); + + _ = Assert.ThrowsException(() => stream.Length); + _ = Assert.ThrowsException(() => stream.Position); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_Seek() + { + Stream stream = CreateReadOnlySequence(new byte[50], new byte[50]).AsStream(); + + Assert.AreEqual(stream.Position, 0); + + stream.Position = 42; + + Assert.AreEqual(stream.Position, 42); + + _ = Assert.ThrowsException(() => stream.Position = -1); + _ = Assert.ThrowsException(() => stream.Position = 120); + + _ = stream.Seek(0, SeekOrigin.Begin); + + _ = Assert.ThrowsException(() => stream.Seek(-1, SeekOrigin.Begin)); + _ = Assert.ThrowsException(() => stream.Seek(120, SeekOrigin.Begin)); + + Assert.AreEqual(stream.Position, 0); + + _ = stream.Seek(-1, SeekOrigin.End); + + _ = Assert.ThrowsException(() => stream.Seek(20, SeekOrigin.End)); + _ = Assert.ThrowsException(() => stream.Seek(-120, SeekOrigin.End)); + + Assert.AreEqual(stream.Position, stream.Length - 1); + + _ = stream.Seek(42, SeekOrigin.Begin); + _ = stream.Seek(20, SeekOrigin.Current); + _ = stream.Seek(-30, SeekOrigin.Current); + + _ = Assert.ThrowsException(() => stream.Seek(-64, SeekOrigin.Current)); + _ = Assert.ThrowsException(() => stream.Seek(80, SeekOrigin.Current)); + + Assert.AreEqual(stream.Position, 32); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_Read_Array() + { + Memory data = CreateRandomData(64); + + Stream stream = CreateReadOnlySequence(data).AsStream(); + + stream.Position = 0; + + byte[] result = new byte[data.Length]; + + int bytesRead = stream.Read(result, 0, result.Length); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result)); + + stream.Dispose(); + + _ = Assert.ThrowsException(() => stream.Read(result, 0, result.Length)); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_NotFromStart_Read_Array() + { + const int offset = 8; + + Memory data = CreateRandomData(64); + + Stream stream = CreateReadOnlySequence(data).AsStream(); + + stream.Position = offset; + + byte[] result = new byte[data.Length - offset]; + + int bytesRead = stream.Read(result, 0, result.Length); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, result.Length + offset); + Assert.IsTrue(data.Span.Slice(offset).SequenceEqual(result)); + + stream.Dispose(); + + _ = Assert.ThrowsException(() => stream.Read(result, 0, result.Length)); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_ReadByte() + { + Memory data = new byte[] { 1, 128, 255, 32 }; + + Stream stream = CreateReadOnlySequence(data.Slice(0,2), data.Slice(2, 2)).AsStream(); + + Span result = stackalloc byte[4]; + + foreach (ref byte value in result) + { + value = checked((byte)stream.ReadByte()); + } + + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result)); + + int exitCode = stream.ReadByte(); + + Assert.AreEqual(exitCode, -1); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_Read_Span() + { + Memory data = CreateRandomData(64); + + Stream stream = CreateReadOnlySequence(data).AsStream(); + + stream.Position = 0; + + Span result = new byte[data.Length]; + + int bytesRead = stream.Read(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_ReadAsync_Memory() + { + Memory data = CreateRandomData(64); + + Stream stream = CreateReadOnlySequence(data).AsStream(); + + Memory result = new byte[data.Length]; + + int bytesRead = await stream.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_SigleSegment_CopyTo() + { + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data).AsStream(); + + Stream destination = new byte[100].AsMemory().AsStream(); + + source.CopyTo(destination); + + Assert.AreEqual(source.Position, destination.Position); + + destination.Position = 0; + + Memory result = new byte[data.Length]; + + int bytesRead = destination.Read(result.Span); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_CopyTo() + { + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data.Slice(0, 32), data.Slice(32)).AsStream(); + + Stream destination = new byte[100].AsMemory().AsStream(); + + source.CopyTo(destination); + + Assert.AreEqual(source.Position, destination.Position); + + destination.Position = 0; + + Memory result = new byte[data.Length]; + + int bytesRead = destination.Read(result.Span); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_SigleSegment_CopyToAsync() + { + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data).AsStream(); + + Stream destination = new byte[100].AsMemory().AsStream(); + + await source.CopyToAsync(destination); + + Assert.AreEqual(source.Position, destination.Position); + + destination.Position = 0; + + Memory result = new byte[data.Length]; + + int bytesRead = await destination.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_SigleSegment_NotFromStart_CopyToAsync() + { + const int offset = 8; + + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data).AsStream(); + + source.Position = offset; + + Stream destination = new byte[100].AsMemory().AsStream(); + + await source.CopyToAsync(destination); + + Assert.AreEqual(source.Position, destination.Position + offset); + + destination.Position = 0; + + Memory result = new byte[data.Length - offset]; + + int bytesRead = await destination.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length - offset); + Assert.IsTrue(data.Span.Slice(offset).SequenceEqual(result.Span)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_MultipleSegments_CopyToAsync() + { + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data.Slice(0, 16), data.Slice(16, 16), data.Slice(32, 16), data.Slice(48, 16)).AsStream(); + + Stream destination = new byte[100].AsMemory().AsStream(); + + await source.CopyToAsync(destination); + + Assert.AreEqual(source.Position, destination.Position); + + destination.Position = 0; + + Memory result = new byte[data.Length]; + + int bytesRead = await destination.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_MultipleSegments_NotFromStart_CopyToAsync() + { + const int offset = 8; + + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data.Slice(0, 16), data.Slice(16, 16), data.Slice(32, 16), data.Slice(48, 16)).AsStream(); + + source.Position = offset; + + Stream destination = new byte[100].AsMemory().AsStream(); + + await source.CopyToAsync(destination); + + Assert.AreEqual(source.Position, destination.Position + offset); + + destination.Position = 0; + + Memory result = new byte[data.Length - offset]; + + int bytesRead = await destination.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length - offset); + Assert.IsTrue(data.Span.Slice(offset).SequenceEqual(result.Span)); + } + + /// + /// Creates a random array filled with random data. + /// + /// The number of array items to create. + /// The returned random array. + private static byte[] CreateRandomData(int count) + { + Random? random = new(DateTime.Now.Ticks.GetHashCode()); + + byte[] data = new byte[count]; + + foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan())) + { + n = (byte)random.Next(0, byte.MaxValue); + } + + return data; + } + + /// + /// Creates a value from the input segments. + /// + /// The input segments. + /// The resulting value. + private static ReadOnlySequence CreateReadOnlySequence(params ReadOnlyMemory[] segments) + { + if (segments is not { Length: > 0 }) + { + return ReadOnlySequence.Empty; + } + + if (segments.Length == 1) + { + return new(segments[0]); + } + + ReadOnlySequenceSegmentOfByte first = new(segments[0]); + ReadOnlySequenceSegmentOfByte last = first; + long length = first.Memory.Length; + + for (int i = 1; i < segments.Length; i++) + { + ReadOnlyMemory segment = segments[i]; + + length += segment.Length; + + last = last.Append(segment); + } + + return new(first, 0, last, (int)(length - last.RunningIndex)); + } + + /// + /// A custom that supports appending new segments. + /// + private sealed class ReadOnlySequenceSegmentOfByte : ReadOnlySequenceSegment + { + public ReadOnlySequenceSegmentOfByte(ReadOnlyMemory memory) + { + Memory = memory; + } + + public ReadOnlySequenceSegmentOfByte Append(ReadOnlyMemory memory) + { + ReadOnlySequenceSegmentOfByte nextSegment = new(memory) + { + RunningIndex = RunningIndex + Memory.Length + }; + + Next = nextSegment; + + return nextSegment; + } + } +} From 5aae6fc31e1aba7ca6e17985fef43817d5473c51 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 19 Nov 2024 16:34:30 -0800 Subject: [PATCH 093/146] Simplify 'ReadOnlySequenceStream.Dispose()' --- .../Streams/ReadOnlySequenceStream.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs b/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs index fbca23c13..8debbdc3d 100644 --- a/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs +++ b/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs @@ -298,11 +298,6 @@ public sealed override void WriteByte(byte value) /// protected override void Dispose(bool disposing) { - if (this.disposed) - { - return; - } - this.disposed = true; } } From bd4968143988162070c89ffe58534fe9df6f1eb6 Mon Sep 17 00:00:00 2001 From: Paulo Morgado <470455+paulomorgado@users.noreply.github.com> Date: Thu, 7 Dec 2023 18:14:46 +0000 Subject: [PATCH 094/146] Change the 'position' field to be 'long' --- .../Streams/ReadOnlySequenceStream.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs b/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs index 8debbdc3d..2e76f6a3f 100644 --- a/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs +++ b/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs @@ -24,7 +24,7 @@ internal sealed partial class ReadOnlySequenceStream : Stream /// /// The current position within . /// - private int position; + private long position; /// /// Indicates whether or not the current instance has been disposed @@ -90,7 +90,7 @@ public sealed override long Position MemoryStream.ValidateDisposed(this.disposed); MemoryStream.ValidatePosition(value, this.source.Length); - this.position = unchecked((int)value); + this.position = value; } } @@ -123,9 +123,9 @@ public sealed override Task CopyToAsync(Stream destination, int bufferSize, Canc if (this.source.IsSingleSegment) { - ReadOnlyMemory buffer = this.source.First.Slice(this.position); + ReadOnlyMemory buffer = this.source.First.Slice(unchecked((int)this.position)); - this.position = (int)this.source.Length; + this.position = this.source.Length; return destination.WriteAsync(buffer, cancellationToken).AsTask(); } @@ -134,7 +134,7 @@ async Task CoreCopyToAsync(Stream destination, CancellationToken cancellationTok { ReadOnlySequence sequence = this.source.Slice(this.position); - this.position = (int)this.source.Length; + this.position = this.source.Length; foreach (ReadOnlyMemory segment in sequence) { @@ -220,7 +220,7 @@ public sealed override long Seek(long offset, SeekOrigin origin) MemoryStream.ValidatePosition(index, this.source.Length); - this.position = unchecked((int)index); + this.position = index; return index; } @@ -253,6 +253,7 @@ public sealed override int Read(byte[]? buffer, int offset, int count) segment.Span.Slice(0, bytesToCopy).CopyTo(destination); destination = destination.Slice(bytesToCopy); + bytesCopied += bytesToCopy; this.position += bytesToCopy; From 3e13fa19260597e96c09639c8a089866036596ff Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 21 Nov 2024 23:07:16 -0800 Subject: [PATCH 095/146] Fix 'WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField' --- .../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 cfa7b9f4f..1bc3fcb44 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -790,7 +790,7 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField = new( id: "MVVMTK0047", title: "Using [GeneratedBindableCustomProperty] is not compatible with [ObservableProperty] on fields", - messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its declared (or 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)""", + 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)""", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, From 089592527742041ccb7b487672b51cdef3d66abb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 21 Nov 2024 23:22:44 -0800 Subject: [PATCH 096/146] Add diagnostic for '[ObservableProperty]' on WinRT AOT --- .../AnalyzerReleases.Shipped.md | 1 + ...pertyOnFieldsIsNotAotCompatibleAnalyzer.cs | 27 ++++++++++++++++++- .../Diagnostics/DiagnosticDescriptors.cs | 17 ++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index e5a0fecb0..730713ab0 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -92,3 +92,4 @@ 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 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs index f5d04d1bf..c1121edb4 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Linq; +using System.Threading; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; @@ -20,7 +21,9 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; public sealed class WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer : DiagnosticAnalyzer { /// - public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(WinRTObservablePropertyOnFieldsIsNotAotCompatible); + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + WinRTObservablePropertyOnFieldsIsNotAotCompatible, + WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo); /// public override void Initialize(AnalysisContext context) @@ -42,6 +45,9 @@ public override void Initialize(AnalysisContext context) return; } + // Track whether we produced any diagnostics, for the compilation end scenario + bool hasProducedAnyDiagnostics = false; + context.RegisterSymbolAction(context => { // Ensure we do have a valid field @@ -61,8 +67,27 @@ public override void Initialize(AnalysisContext context) .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)), fieldSymbol.ContainingType, fieldSymbol.Name)); + + // Notify that we did produce at least one diagnostic + Volatile.Write(ref hasProducedAnyDiagnostics, true); } }, SymbolKind.Field); + + // If C# preview is already in use, we can stop here. The last diagnostic is only needed when partial properties + // cannot be used, to inform developers that they'll need to bump the language version to enable the code fixer. + if (context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + context.RegisterCompilationEndAction(context => + { + // If we have produced at least one diagnostic, also emit the info message + if (Volatile.Read(ref hasProducedAnyDiagnostics)) + { + context.ReportDiagnostic(Diagnostic.Create(WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo, location: null)); + } + }); }); } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 1bc3fcb44..d770e290f 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -844,4 +844,21 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Using the [ObservableObject] attribute on types is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and they should derive from ObservableObject instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0050"); + + /// + /// Gets a for when [ObservableProperty] is used on a field in WinRT scenarios. + /// + /// 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( + 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)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "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 because (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); } From f877f7cf5341e748a8349343846098b2f13e53e1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 22 Nov 2024 11:37:14 -0800 Subject: [PATCH 097/146] Add unit test for new diagnostic --- ...pertyOnFieldsIsNotAotCompatibleAnalyzer.cs | 12 +++++----- .../Test_SourceGeneratorsDiagnostics.cs | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs index c1121edb4..33f61ce53 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs @@ -46,7 +46,7 @@ public override void Initialize(AnalysisContext context) } // Track whether we produced any diagnostics, for the compilation end scenario - bool hasProducedAnyDiagnostics = false; + AttributeData? firstObservablePropertyAttribute = null; context.RegisterSymbolAction(context => { @@ -57,7 +57,7 @@ public override void Initialize(AnalysisContext context) } // Emit a diagnostic if the field is using the [ObservableProperty] attribute - if (fieldSymbol.HasAttributeWithType(observablePropertySymbol)) + if (fieldSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) { context.ReportDiagnostic(Diagnostic.Create( WinRTObservablePropertyOnFieldsIsNotAotCompatible, @@ -69,7 +69,7 @@ public override void Initialize(AnalysisContext context) fieldSymbol.Name)); // Notify that we did produce at least one diagnostic - Volatile.Write(ref hasProducedAnyDiagnostics, true); + _ = Interlocked.CompareExchange(ref firstObservablePropertyAttribute, observablePropertyAttribute, null); } }, SymbolKind.Field); @@ -83,9 +83,11 @@ public override void Initialize(AnalysisContext context) context.RegisterCompilationEndAction(context => { // If we have produced at least one diagnostic, also emit the info message - if (Volatile.Read(ref hasProducedAnyDiagnostics)) + if (Volatile.Read(ref firstObservablePropertyAttribute) is { } observablePropertyAttribute) { - context.ReportDiagnostic(Diagnostic.Create(WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo, location: null)); + context.ReportDiagnostic(Diagnostic.Create( + WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo, + observablePropertyAttribute.GetLocation())); } }); }); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 66c6eea28..d1f55ddc1 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -434,6 +434,28 @@ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + [TestMethod] public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_NoXaml_Level1_DoesNotWarn() { From b50a83a3cb7ef68772a697dcab8932465b4edcd8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 22 Nov 2024 11:51:54 -0800 Subject: [PATCH 098/146] Use 'Volatile.Write' instead for better perf --- ...ObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs index 33f61ce53..0601499fd 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs @@ -68,8 +68,11 @@ public override void Initialize(AnalysisContext context) fieldSymbol.ContainingType, fieldSymbol.Name)); - // Notify that we did produce at least one diagnostic - _ = Interlocked.CompareExchange(ref firstObservablePropertyAttribute, observablePropertyAttribute, null); + // Notify that we did produce at least one diagnostic. Note: callbacks can run in parallel, so the order + // is not guaranteed. As such, there's no point in using an interlocked compare exchange operation here, + // since we couldn't rely on the value being written actually being the "first" occurrence anyway. + // So we can just do a normal volatile read for better performance. + Volatile.Write(ref firstObservablePropertyAttribute, observablePropertyAttribute); } }, SymbolKind.Field); From 6e528f0e30bdb4e0096264ca712b924399331c22 Mon Sep 17 00:00:00 2001 From: realybin <172874642+realybin@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:06:12 +0800 Subject: [PATCH 099/146] Fix minor typos in comments. --- src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs | 4 ++-- ...servableValidatorValidateAllPropertiesGenerator.Execute.cs | 2 +- .../Attributes/INotifyPropertyChangedAttribute.cs | 2 +- src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs | 2 +- src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs | 2 +- .../Internals/System/Collections.Generic/Dictionary2.cs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs b/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs index 3cb8f12e1..b1ef81e37 100644 --- a/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs +++ b/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs @@ -85,7 +85,7 @@ static void FindFactors(int size, int factor, out uint x, out uint y) // that we consider acceptable, and pick the best results produced. // The ratio between maps influences the number of objects being allocated, // as well as the multithreading performance when locking on maps. - // We still want to contraint this number to avoid situations where we + // We still want to constraint this number to avoid situations where we // have a way too high number of maps compared to total size. FindFactors(minimumSize, 2, out uint x2, out uint y2); FindFactors(minimumSize, 3, out uint x3, out uint y3); @@ -113,7 +113,7 @@ static void FindFactors(int size, int factor, out uint x, out uint y) // We preallocate the maps in advance, since each bucket only contains the // array field, which is not preinitialized, so the allocations are minimal. - // This lets us lock on each individual maps when retrieving a string instance. + // This lets us lock on each individual map when retrieving a string instance. foreach (ref FixedSizePriorityMap map in span) { map = new FixedSizePriorityMap((int)y2); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs index 684430db7..0b82211ba 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs @@ -181,7 +181,7 @@ public static CompilationUnitSyntax GetSyntax(ValidationInfo validationInfo) // full runtime type safety (as a safe cast is used to validate the input argument), and with less reflection needed. // Note that we're deliberately creating a new delegate instance here and not using code that could see the C# compiler // create a static class to cache a reusable delegate, because each generated method will only be called at most once, - // as the returned delegate will be cached by the MVVM Toolkit itself. So this ensures the the produced code is minimal, + // as the returned delegate will be cached by the MVVM Toolkit itself. So this ensures the produced code is minimal, // and that there will be no unnecessary static fields and objects being created and possibly never collected. // This code will produce a syntax tree as follows: // diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs index 360643eee..08b63b4c6 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs @@ -28,7 +28,7 @@ public sealed class INotifyPropertyChangedAttribute : Attribute { /// /// Gets or sets a value indicating whether or not to also generate all the additional helper methods that are found - /// in as well (eg. . + /// in as well (eg. ). /// If set to , only the event and /// the two overloads will be generated. /// The default value is . diff --git a/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs b/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs index a007d40eb..72720a899 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs @@ -15,7 +15,7 @@ namespace CommunityToolkit.Mvvm.Messaging; /// /// public sealed class LoginCompletedMessage { } /// -/// Then, register your a recipient for this message: +/// Then, register a recipient for this message: /// /// Messenger.Default.Register<MyRecipientType, LoginCompletedMessage>(this, (r, m) => /// { diff --git a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs index 6febfb2e1..532639c8f 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -435,7 +435,7 @@ public static TMessage Send(this IMessenger messenger, TMessage messag /// The type of token to identify what channel to use to send the message. /// The instance to use to send the message. /// The token indicating what channel to use. - /// The message that has been sen. + /// The message that has been sent. /// /// This method will automatically create a new instance /// just like , and then send it to the right recipients. diff --git a/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs b/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs index c3ac541a7..4e8bbf075 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs @@ -298,7 +298,7 @@ public bool MoveNext() while ((uint)this.index < (uint)this.count) { // We need to preemptively increment the current index so that we still correctly keep track - // of the current position in the dictionary even if the users doesn't access any of the + // of the current position in the dictionary even if the users don't access any of the // available properties in the enumerator. As this is a possibility, we can't rely on one of // them to increment the index before MoveNext is invoked again. We ditch the standard enumerator // API surface here to expose the Key/Value properties directly and minimize the memory copies. From 046dc4cade272d097b5bed4101855bc0aeb8199a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 22 Nov 2024 17:39:38 -0800 Subject: [PATCH 100/146] Minor diagnostics tweaks --- .../Diagnostics/DiagnosticDescriptors.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index d770e290f..adbb28771 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -634,8 +634,7 @@ internal static class DiagnosticDescriptors category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must be using valid expressions.", - helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0037"); + description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must be using valid expressions."); /// /// Gets a indicating when a method with [RelayCommand] is using an invalid attribute targeting the field or property. @@ -650,8 +649,7 @@ internal static class DiagnosticDescriptors category: typeof(RelayCommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must be using valid expressions.", - helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0038"); + description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must be using valid expressions."); /// /// Gets a indicating when a method with [RelayCommand] is async void. @@ -858,7 +856,7 @@ internal static class DiagnosticDescriptors category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Info, isEnabledByDefault: true, - description: "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 because (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings).", + 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); } From ee6f55dcb4b58d3db1e5e0ccc84050ebb488a00d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 22 Nov 2024 13:58:53 -0800 Subject: [PATCH 101/146] Update Roslyn to 4.12 for new generators --- dotnet Community Toolkit.sln | 6 +++--- .../CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj} | 0 .../CommunityToolkit.Mvvm.CodeFixers.props | 5 ++++- .../UsePartialPropertyForObservablePropertyCodeFixer.cs | 2 +- ...munityToolkit.Mvvm.SourceGenerators.Roslyn4120.csproj} | 0 .../CommunityToolkit.Mvvm.SourceGenerators.props | 7 +++++-- .../ComponentModel/ObservablePropertyGenerator.Execute.cs | 2 +- .../RequiresCSharpLanguageVersionPreviewAnalyzer.cs | 2 +- .../UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs | 2 +- .../UseObservablePropertyOnPartialPropertyAnalyzer.cs | 2 +- ...eratedBindableCustomPropertyWithBasesMemberAnalyzer.cs | 2 +- ...bservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs | 2 +- .../CommunityToolkit.Mvvm.SourceGenerators.targets | 2 +- src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj | 8 ++++---- ...kit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj} | 8 ++++---- .../Test_SourceGeneratorsCodegen.cs | 0 .../Test_SourceGeneratorsDiagnostics.cs | 0 ...st_UsePartialPropertyForObservablePropertyCodeFixer.cs | 0 18 files changed, 28 insertions(+), 22 deletions(-) rename src/{CommunityToolkit.Mvvm.CodeFixers.Roslyn4110/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110.csproj => CommunityToolkit.Mvvm.CodeFixers.Roslyn4120/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj} (100%) rename src/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.csproj} (100%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj} (82%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests}/Test_SourceGeneratorsCodegen.cs (100%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests}/Test_SourceGeneratorsDiagnostics.cs (100%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests}/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs (100%) diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index 6d56ee910..dc557355a 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -83,13 +83,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Exter EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeFixers.Roslyn4001", "src\CommunityToolkit.Mvvm.CodeFixers.Roslyn4001\CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj", "{FCC13AD5-CEB8-4CC1-8250-89B616D126F2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.csproj", "{FCC13AD5-CEB8-4CC1-8250-89B616D126F2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj", "{C342302D-A263-42D6-B8EE-01DEF8192690}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj", "{C342302D-A263-42D6-B8EE-01DEF8192690}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.shproj", "{A2EBDA90-B720-430D-83F5-C6BCC355232C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.CodeFixers.Roslyn4110", "src\CommunityToolkit.Mvvm.CodeFixers.Roslyn4110\CommunityToolkit.Mvvm.CodeFixers.Roslyn4110.csproj", "{98572004-D29A-486E-8053-6D409557CE44}" +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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110.csproj b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj similarity index 100% rename from src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110/CommunityToolkit.Mvvm.CodeFixers.Roslyn4110.csproj rename to src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props index 5460cb1ea..114924f1c 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props @@ -26,7 +26,10 @@ $(DefineConstants);ROSLYN_4_3_1_OR_GREATER - $(DefineConstants);ROSLYN_4_11_0_OR_GREATER + $(DefineConstants);ROSLYN_4_12_0_OR_GREATER + + + 4.12.0-3.final diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 43f5ca94c..168f43b5b 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if ROSLYN_4_11_0_OR_GREATER +#if ROSLYN_4_12_0_OR_GREATER using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.csproj similarity index 100% rename from src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj rename to src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.csproj diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props index 1624ac5f3..0e176df51 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props @@ -40,10 +40,13 @@ $(DefineConstants);ROSLYN_4_3_1_OR_GREATER - $(DefineConstants);ROSLYN_4_11_0_OR_GREATER + $(DefineConstants);ROSLYN_4_12_0_OR_GREATER - $(NoWarn);RS2003 + $(NoWarn);RS2003 + + + 4.12.0-3.final diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index a0a05c742..3505d05f7 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -98,7 +98,7 @@ public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel public static MemberDeclarationSyntax GetCandidateMemberDeclaration(SyntaxNode node) { // If the node is a property declaration, just return it directly. Note that we don't have - // to check whether we're using Roslyn 4.11 here, as if that's not the case all of these + // to check whether we're using Roslyn 4.12 here, as if that's not the case all of these // syntax nodes would already have pre-filtered well before this method could run at all. if (node is PropertyDeclarationSyntax propertySyntax) { diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs index f0b5793b2..7cb7cd233 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if ROSLYN_4_11_0_OR_GREATER +#if ROSLYN_4_12_0_OR_GREATER using System.Collections.Immutable; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs index f33a7ad4e..29255adb5 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if !ROSLYN_4_11_0_OR_GREATER +#if !ROSLYN_4_12_0_OR_GREATER using System.Collections.Immutable; using System.Linq; diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs index a4f067e5b..bbda90e1b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if ROSLYN_4_11_0_OR_GREATER +#if ROSLYN_4_12_0_OR_GREATER using System.Collections.Immutable; using System.Linq; diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs index f76cb4bc7..7e2aa7e57 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if ROSLYN_4_11_0_OR_GREATER +#if ROSLYN_4_12_0_OR_GREATER using System.Collections.Generic; using System.Collections.Immutable; diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs index 0601499fd..4fa8040ba 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if ROSLYN_4_11_0_OR_GREATER +#if ROSLYN_4_12_0_OR_GREATER using System.Collections.Immutable; using System.Linq; diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets index 1106146fa..a8fce2119 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets @@ -86,7 +86,7 @@ removes and removes all analyzers except the highest version that is supported. The fallback is just Roslyn 4.0. --> - roslyn4.11 + roslyn4.12 roslyn4.3 roslyn4.0 diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index 522b215a5..69a5befaf 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -68,9 +68,9 @@ - + - + @@ -122,10 +122,10 @@ --> - + - + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj similarity index 82% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj index a6b432501..eba477975 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj @@ -2,13 +2,13 @@ net472;net7.0;net8.0 - $(DefineConstants);ROSLYN_4_3_1_OR_GREATER;ROSLYN_4_11_0_OR_GREATER + $(DefineConstants);ROSLYN_4_3_1_OR_GREATER;ROSLYN_4_12_0_OR_GREATER - + @@ -16,8 +16,8 @@ - - + + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs similarity index 100% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs similarity index 100% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs similarity index 100% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs From 1019bc331875b48a208015b6282dbaa6ea2eab42 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 22 Nov 2024 14:39:11 -0800 Subject: [PATCH 102/146] Update analyzer tests for Roslyn 4.13 --- .../Test_SourceGeneratorsDiagnostics.cs | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index d1f55ddc1..0cadc6439 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -32,7 +32,7 @@ public partial class SampleViewModel : ObservableObject } [TestMethod] - public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_Partial_Warns() + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_CSharp12_Partial_Warns() { const string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -51,8 +51,32 @@ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp13, + // /0/Test0.cs(8,31): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. DiagnosticResult.CompilerError("CS9248").WithSpan(8, 31, 8, 35).WithArguments("MyApp.SampleViewModel.Name")); } From 8e75139fff5ae803d665aa6683f28de451cc144c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 26 Nov 2024 18:55:20 -0800 Subject: [PATCH 103/146] Update code fixer to support multiple attributes --- ...lPropertyForObservablePropertyCodeFixer.cs | 130 +++++++++++++----- 1 file changed, 99 insertions(+), 31 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 168f43b5b..a8067ace3 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -100,13 +100,6 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration && identifierName == fieldName) { - // We only support fields with up to one attribute per attribute list. - // This is so we can easily check one attribute when updating targets. - if (fieldDeclaration.AttributeLists.Any(static list => list.Attributes.Count > 1)) - { - return; - } - // Register the code fix to update the class declaration to inherit from ObservableObject instead context.RegisterCodeFix( CodeAction.Create( @@ -194,33 +187,108 @@ private static async Task ConvertToPartialProperty( continue; } - // Make sure we can retrieve the symbol for the attribute type. - // We are guaranteed to always find a single attribute in the list. - if (!semanticModel.GetSymbolInfo(attributeListSyntax.Attributes[0], cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol)) + if (attributeListSyntax.Attributes.Count == 1) { - return document; - } - - // Case 3 - if (toolkitTypeSymbols.ContainsValue(attributeSymbol)) - { - propertyAttributes[i] = attributeListSyntax.WithTarget(null); - - continue; + // Make sure we can retrieve the symbol for the attribute type + if (!semanticModel.GetSymbolInfo(attributeListSyntax.Attributes[0], cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol)) + { + return document; + } + + // Case 3 + if (toolkitTypeSymbols.ContainsValue(attributeSymbol)) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(null); + + continue; + } + + // Case 4 + if (annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol)) + { + continue; + } + + // Case 5 + if (attributeListSyntax.Target is null) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword))); + + continue; + } } - - // Case 4 - if (annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol)) - { - continue; - } - - // Case 5 - if (attributeListSyntax.Target is null) + else { - propertyAttributes[i] = attributeListSyntax.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword))); - - continue; + // If we have multiple attributes in the current list, we need additional logic here. + // We could have any number of attributes here, so we split them into three buckets: + // - MVVM Toolkit attributes: these should be moved over with no target + // - Data annotation or validation attributes: these should be moved over with the same target + // - Any other attributes: these should be moved over with the 'field' target + List mvvmToolkitAttributes = []; + List annotationOrValidationAttributes = []; + List fieldAttributes = []; + + foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) + { + // Like for the single attribute case, make sure we can get the symbol for the attribute + if (!semanticModel.GetSymbolInfo(attributeSyntax, cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol)) + { + return document; + } + + bool isAnnotationOrValidationAttribute = annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol); + + // Split the attributes into the buckets. Note that we have a special rule for annotation and validation + // attributes when no target is specified. In that case, we will merge them with the MVVM Toolkit items. + // This allows us to try to keep the attributes in the same attribute list, rather than splitting them. + if (toolkitTypeSymbols.ContainsValue(attributeSymbol) || (isAnnotationOrValidationAttribute && attributeListSyntax.Target is null)) + { + mvvmToolkitAttributes.Add(attributeSyntax); + } + else if (isAnnotationOrValidationAttribute) + { + annotationOrValidationAttributes.Add(attributeSyntax); + } + else + { + fieldAttributes.Add(attributeSyntax); + } + } + + // We need to start inserting the new lists right before the one we're currently + // processing. We'll be removing it when we're done, the buckets will replace it. + int insertionIndex = i; + + // Helper to process and insert the new synthesized attribute lists into the target collection + void InsertAttributeListIfNeeded(List attributes, AttributeTargetSpecifierSyntax? attributeTarget) + { + if (attributes is []) + { + return; + } + + AttributeListSyntax attributeList = AttributeList(SeparatedList(attributes)).WithTarget(attributeTarget); + + // Only if this is the first non empty list we're adding, carry over the original trivia + if (insertionIndex == i) + { + attributeList = attributeList.WithTriviaFrom(attributeListSyntax); + } + + // Finally, insert the new list into the final tree + propertyAttributes.Insert(insertionIndex++, attributeList); + } + + InsertAttributeListIfNeeded(mvvmToolkitAttributes, attributeTarget: null); + InsertAttributeListIfNeeded(annotationOrValidationAttributes, attributeTarget: attributeListSyntax.Target); + InsertAttributeListIfNeeded(fieldAttributes, attributeTarget: AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword))); + + // Remove the attribute list that we have just split into buckets + propertyAttributes.RemoveAt(insertionIndex); + + // Move the current loop iteration to the last inserted item. + // We decrement by 1 because the new loop iteration will add 1. + i = insertionIndex - 1; } } From 8997b7e6c6a4bdd47417414600dac2d0c8b52377 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 26 Nov 2024 18:55:32 -0800 Subject: [PATCH 104/146] Add unit tests for multiple attributes --- ...lPropertyForObservablePropertyCodeFixer.cs | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index 0ccc7032f..f72ca8261 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -767,4 +767,232 @@ public void M() await test.RunAsync(); } + + [TestMethod] + public async Task SimpleFields_WithMultipleAttributes_SingleProperty() + { + string original = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + [ObservableProperty, NotifyPropertyChangedFor("Age")] private string name = String.Empty; + } + """; + + string @fixed = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + [ObservableProperty, NotifyPropertyChangedFor("Age")] + public partial string Name { get; set; } = String.Empty; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,74): info MVVMTK0042: The field Class1.name 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 74, 6, 78).WithArguments("Class1", "name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,27): error CS8050: Only auto-implemented properties, or properties that use the 'field' keyword, can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(7, 27, 7, 31), + + // /0/Test0.cs(7,27): error CS9248: Partial property 'Class1.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 27, 7, 31).WithArguments("Class1.Name"), + }); + + await test.RunAsync(); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/1007 + [TestMethod] + public async Task SimpleFields_WithMultipleAttributes_WithNoBlankLines() + { + string original = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + [ObservableProperty, NotifyPropertyChangedFor("Age")] private string name = String.Empty; + [ObservableProperty] private int age; + } + """; + + string @fixed = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + [ObservableProperty, NotifyPropertyChangedFor("Age")] + public partial string Name { get; set; } = String.Empty; + + [ObservableProperty] + public partial int Age { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,74): info MVVMTK0042: The field Class1.name 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 74, 6, 78).WithArguments("Class1", "name"), + + // /0/Test0.cs(7,38): info MVVMTK0042: The field Class1.age 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 38, 7, 41).WithArguments("Class1", "age"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,27): error CS8050: Only auto-implemented properties, or properties that use the 'field' keyword, can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(7, 27, 7, 31), + + // /0/Test0.cs(7,27): error CS9248: Partial property 'Class1.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 27, 7, 31).WithArguments("Class1.Name"), + + // /0/Test0.cs(10,24): error CS9248: Partial property 'Class1.Age' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(10, 24, 10, 27).WithArguments("Class1.Age"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFields_WithMultipleAttributes_WithMixedBuckets_1() + { + string original = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + // Leading trivia + [ObservableProperty, NotifyPropertyChangedFor("A"), Display] + [NotifyPropertyChangedFor("B")] + private string _name; + } + """; + + string @fixed = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + // Leading trivia + [ObservableProperty, NotifyPropertyChangedFor("A"), Display] + [NotifyPropertyChangedFor("B")] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,20): info MVVMTK0042: The field Class1._name 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(10, 20, 10, 25).WithArguments("Class1", "_name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,27): error CS9248: Partial property 'Class1.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(10, 27, 10, 31).WithArguments("Class1.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFields_WithMultipleAttributes_WithMixedBuckets_2() + { + string original = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + // Leading trivia + [NotifyPropertyChangedFor("B")] + [ObservableProperty, NotifyPropertyChangedFor("A"), Display, Test] + [NotifyPropertyChangedFor("C")] + [property: UIHint("name"), Test] + private string name; + } + + public class TestAttribute : Attribute; + """; + + string @fixed = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + // Leading trivia + [NotifyPropertyChangedFor("B")] + [ObservableProperty, NotifyPropertyChangedFor("A"), Display] + [field: Test] + [NotifyPropertyChangedFor("C")] + [UIHint("name"), Test] + public partial string Name { get; set; } + } + + public class TestAttribute : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(12,20): info MVVMTK0042: The field Class1.name 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) + CSharpCodeFixVerifier.Diagnostic().WithSpan(12, 20, 12, 24).WithArguments("Class1", "name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(13,27): error CS9248: Partial property 'Class1.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(13, 27, 13, 31).WithArguments("Class1.Name"), + }); + + await test.RunAsync(); + } } From ef2233657a16c190228c1665ed9218705cc83b35 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 28 Nov 2024 01:28:03 -0800 Subject: [PATCH 105/146] Add unit test for consistent compilation diagnostic --- .../Test_SourceGeneratorsDiagnostics.cs | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 0cadc6439..fc18d3e2c 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -468,7 +468,7 @@ namespace MyApp { public partial class SampleViewModel : ObservableObject { - [{|MVVMTK0051:ObservableProperty|}] + [{|MVVMTK0051:ObservableProperty|}] private string {|MVVMTK0045:name|}; } } @@ -480,6 +480,58 @@ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + [TestMethod] public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_NoXaml_Level1_DoesNotWarn() { From 421ad363f3fe4e2cb7bf62aedd617fbf06b1a984 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 28 Nov 2024 10:58:35 -0800 Subject: [PATCH 106/146] Update analyzer to emit consistent locations --- ...pertyOnFieldsIsNotAotCompatibleAnalyzer.cs | 65 +++++++++++++++++-- .../Test_SourceGeneratorsDiagnostics.cs | 12 ++-- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs index 4fa8040ba..1a925e7a3 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs @@ -4,7 +4,9 @@ #if ROSLYN_4_12_0_OR_GREATER +using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -68,11 +70,11 @@ public override void Initialize(AnalysisContext context) fieldSymbol.ContainingType, fieldSymbol.Name)); - // Notify that we did produce at least one diagnostic. Note: callbacks can run in parallel, so the order - // is not guaranteed. As such, there's no point in using an interlocked compare exchange operation here, - // since we couldn't rely on the value being written actually being the "first" occurrence anyway. - // So we can just do a normal volatile read for better performance. - Volatile.Write(ref firstObservablePropertyAttribute, observablePropertyAttribute); + // Track the attribute data to use as target for the diagnostic. This method takes care of effectively + // sorting all incoming values, so that the final one is deterministic across runs. This ensures that + // the actual location will be the same across recompilations, instead of jumping around all over the + // place. This also makes it possible to more easily suppress it, since its location would not change. + SetOrUpdateAttributeDataBySourceLocation(ref firstObservablePropertyAttribute, observablePropertyAttribute); } }, SymbolKind.Field); @@ -95,6 +97,59 @@ public override void Initialize(AnalysisContext context) }); }); } + + /// + /// Sets or updates the instance to use for compilation diagnostics, sorting by source location. + /// + /// The location of the previous value to potentially overwrite. + /// Thew new instance. + private static void SetOrUpdateAttributeDataBySourceLocation([NotNull] ref AttributeData? oldAttributeDataLocation, AttributeData newAttributeData) + { + bool hasReplacedOriginalValue; + + do + { + AttributeData? oldAttributeData = Volatile.Read(ref oldAttributeDataLocation); + + // If the old attribute data is null, it means this is the first time we called this method with a new + // attribute candidate. In that case, there is nothing to check: we should always store the new instance. + if (oldAttributeData is not null) + { + // Sort by file paths, alphabetically + int filePathRelativeSortIndex = string.Compare( + newAttributeData.ApplicationSyntaxReference?.SyntaxTree.FilePath, + oldAttributeData.ApplicationSyntaxReference?.SyntaxTree.FilePath, + StringComparison.OrdinalIgnoreCase); + + // Also sort by location (this is a tie-breaker if two values are from the same file) + bool isTextSpanLowerSorted = + (newAttributeData.ApplicationSyntaxReference?.Span.Start ?? 0) < + (oldAttributeData.ApplicationSyntaxReference?.Span.Start ?? 0); + + // The new candidate can be dropped if it's from a file that's alphabetically sorted after + // the old value, or whether the location is after the previous one, within the same file. + if (filePathRelativeSortIndex == 1 || (filePathRelativeSortIndex == 0 && !isTextSpanLowerSorted)) + { + break; + } + } + + // Attempt to actually replace the old value, without taking locks + AttributeData? originalValue = Interlocked.CompareExchange( + location1: ref oldAttributeDataLocation, + value: newAttributeData, + comparand: oldAttributeData); + + // This call might have raced against other threads, since all symbol actions can run in parallel. + // If the original value is the old value we read at the start of the method, it means no other + // thread raced against this one, so we are done. If it's different, then we failed to write the + // new candidate. We can discard the work done in this iteration and simply try again. + hasReplacedOriginalValue = oldAttributeData == originalValue; + } + while (!hasReplacedOriginalValue); +#pragma warning disable CS8777 // The loop always ensures that 'oldAttributeDataLocation' is not null on exit + } +#pragma warning restore CS8777 } #endif diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index fc18d3e2c..186fe98c8 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -526,10 +526,14 @@ public partial class YetAnotherViewModel : ObservableObject } """; - await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( - source, - LanguageVersion.CSharp12, - editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + // This test is non deterministic, so run it 10 times to ensure the likelihood of it passing just by luck is almost 0 + for (int i = 0; i < 10; i++) + { + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } } [TestMethod] From e094757355af2fca4ccc8e79f1327fb2fa69f309 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 29 Nov 2024 12:13:10 -0800 Subject: [PATCH 107/146] 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 108/146] 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 109/146] 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 110/146] 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 111/146] 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 112/146] 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 113/146] 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 114/146] 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 115/146] 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 116/146] 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 117/146] 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 118/146] 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 119/146] 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 120/146] 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"); From fc975d10261722da26b2193476e67751c6e46f07 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 00:40:56 -0800 Subject: [PATCH 121/146] Generalize forwarding all property modifiers --- .../ComponentModel/Models/PropertyInfo.cs | 4 +- .../ObservablePropertyGenerator.Execute.cs | 53 ++++++++++++++++--- .../Extensions/SyntaxKindExtensions.cs | 26 +++++++++ 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index c2c8feed6..1fee5b622 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -15,6 +15,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// The type name for the generated property, including nullability annotations. /// The field name. /// The generated property name. +/// The list of additional modifiers for the property (they are values). /// The accessibility of the property. /// The accessibility of the accessor. /// The accessibility of the accessor. @@ -23,7 +24,6 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// The sequence of commands to notify. /// Whether or not the generated property also broadcasts changes. /// Whether or not the generated property also validates its value. -/// Whether or not the generated property should be marked as required. /// Whether the old property value is being directly referenced. /// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether to include nullability annotations on the setter. @@ -34,6 +34,7 @@ internal sealed record PropertyInfo( string TypeNameWithNullabilityAnnotations, string FieldName, string PropertyName, + EquatableArray PropertyModifers, Accessibility PropertyAccessibility, Accessibility GetterAccessibility, Accessibility SetterAccessibility, @@ -42,7 +43,6 @@ internal sealed record PropertyInfo( EquatableArray NotifiedCommandNames, bool NotifyPropertyChangedRecipients, bool NotifyDataErrorInfo, - bool IsRequired, bool IsOldPropertyValueDirectlyReferenced, bool IsReferenceTypeOrUnconstrainedTypeParameter, bool IncludeMemberNotNullOnSetAccessor, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index e127859d7..ed663cb42 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; @@ -362,6 +363,9 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + // Get all additional modifiers for the member + ImmutableArray propertyModifiers = GetPropertyModifiers(memberSyntax); + // Retrieve the accessibility values for all components if (!TryGetAccessibilityModifiers( memberSyntax, @@ -388,6 +392,7 @@ public static bool TryGetInfo( typeNameWithNullabilityAnnotations, fieldName, propertyName, + propertyModifiers.AsUnderlyingType(), propertyAccessibility, getterAccessibility, setterAccessibility, @@ -396,7 +401,6 @@ public static bool TryGetInfo( notifiedCommandNames.ToImmutable(), notifyRecipients, notifyDataErrorInfo, - isRequired, isOldPropertyValueDirectlyReferenced, isReferenceTypeOrUnconstrainedTypeParameter, includeMemberNotNullOnSetAccessor, @@ -970,6 +974,45 @@ private static void GatherLegacyForwardedAttributes( } } + /// + /// Gathers all allowed property modifiers that should be forwarded to the generated property. + /// + /// The instance to process. + /// The returned set of property modifiers, if any. + private static ImmutableArray GetPropertyModifiers(MemberDeclarationSyntax memberSyntax) + { + // Fields never need to carry additional modifiers along + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + return ImmutableArray.Empty; + } + + // We only allow a subset of all possible modifiers (aside from the accessibility modifiers) + ReadOnlySpan candidateKinds = + [ + SyntaxKind.NewKeyword, + SyntaxKind.VirtualKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.OverrideKeyword, +#if ROSLYN_4_3_1_OR_GREATER + SyntaxKind.RequiredKeyword +#endif + ]; + + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + + // Track all modifiers from the allowed set on the input property declaration + foreach (SyntaxKind kind in candidateKinds) + { + if (memberSyntax.Modifiers.Any(kind)) + { + builder.Add(kind); + } + } + + return builder.ToImmutable(); + } + /// /// Tries to get the accessibility of the property and accessors, if possible. /// If the target member is not a property, it will use the defaults. @@ -1395,13 +1438,11 @@ private static SyntaxTokenList GetPropertyModifiers(PropertyInfo propertyInfo) { SyntaxTokenList propertyModifiers = propertyInfo.PropertyAccessibility.ToSyntaxTokenList(); -#if ROSLYN_4_3_1_OR_GREATER - // Add the 'required' modifier if the original member also had it - if (propertyInfo.IsRequired) + // Add all gathered modifiers + foreach (SyntaxKind modifier in propertyInfo.PropertyModifers.AsImmutableArray().FromUnderlyingType()) { - propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.RequiredKeyword)); + propertyModifiers = propertyModifiers.Add(Token(modifier)); } -#endif // Add the 'partial' modifier if the original member is a partial property if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs index a178dcc82..da890724e 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -12,6 +14,30 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; /// internal static class SyntaxKindExtensions { + /// + /// Converts an of values to one of their underlying type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray AsUnderlyingType(this ImmutableArray array) + { + ushort[]? underlyingArray = (ushort[]?)(object?)Unsafe.As, SyntaxKind[]?>(ref array); + + return Unsafe.As>(ref underlyingArray); + } + + /// + /// Converts an of values to one of their real type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray FromUnderlyingType(this ImmutableArray array) + { + SyntaxKind[]? typedArray = (SyntaxKind[]?)(object?)Unsafe.As, ushort[]?>(ref array); + + return Unsafe.As>(ref typedArray); + } + /// /// Converts a value to either "field" or "property" based on the kind. /// From d5f3a0c8fd29f674b86fbb4ded5f2f0ee54928d4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 08:43:14 -0800 Subject: [PATCH 122/146] Add 'InvalidPointerTypeObservablePropertyAttributeAnalyzer' --- .../AnalyzerReleases.Shipped.md | 1 + ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 10 ++- ...TypeObservablePropertyAttributeAnalyzer.cs | 64 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 16 +++++ 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index b0bf76a5a..f11f523ea 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -96,3 +96,4 @@ MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0052 MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053 MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054 +MVVMTK0055 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0055 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index f02990ae1..3c3b58a31 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -41,6 +41,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index ed663cb42..7f020635a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -99,7 +99,7 @@ public static bool IsCandidateValidForCompilation(MemberDeclarationSyntax node, public static bool IsCandidateSymbolValid(ISymbol memberSymbol) { #if ROSLYN_4_12_0_OR_GREATER - // We only need additional checks for properties (Roslyn already validates things for fields in our scenarios) + // We only need these additional checks for properties (Roslyn already validates things for fields in our scenarios) if (memberSymbol is IPropertySymbol propertySymbol) { // Ensure that the property declaration is a partial definition with no implementation @@ -116,6 +116,14 @@ public static bool IsCandidateSymbolValid(ISymbol memberSymbol) } #endif + // Pointer types are never allowed in either case + if (memberSymbol is + IPropertySymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer } or + IFieldSymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer }) + { + return false; + } + // We assume all other cases are supported (other failure cases will be detected later) return true; } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..5ece6f03e --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used with pointer types. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPointerTypeObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidObservablePropertyDeclarationReturnsPointerLikeType); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the [ObservableProperty] symbol + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure that we have a valid target symbol to analyze + if (context.Symbol is not (IFieldSymbol or IPropertySymbol)) + { + return; + } + + // If the property is not using [ObservableProperty], there's nothing to do + if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Emit a diagnostic if the type is a pointer type + if (context.Symbol is + IPropertySymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer } or + IFieldSymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer }) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyDeclarationReturnsPointerLikeType, + observablePropertyAttribute.GetLocation(), + context.Symbol.ContainingType, + context.Symbol.Name)); + } + }, SymbolKind.Field, SymbolKind.Property); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 36835788a..2de37f901 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -907,4 +907,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "A property using [ObservableProperty] returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0054"); + + /// + /// Gets a for when [ObservableProperty] is used on a property that returns a pointer type. + /// + /// Format: "The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)". + /// + /// + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsPointerLikeType = new DiagnosticDescriptor( + id: "MVVMTK0055", + title: "Using [ObservableProperty] on a property that returns pointer-like", + messageFormat: """The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [ObservableProperty] returns a pointer-like value ([ObservableProperty] must be used on properties of a non pointer-like type).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0055"); } From a35944f359b635b206f2c68b6784f40133e9fa02 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 08:46:15 -0800 Subject: [PATCH 123/146] Remove leftover 'required' handling code --- .../ObservablePropertyGenerator.Execute.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 7f020635a..17a97d5bd 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -390,11 +390,6 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); - // Check whether the property should be required - bool isRequired = GetIsRequiredProperty(memberSymbol); - - token.ThrowIfCancellationRequested(); - propertyInfo = new PropertyInfo( memberSyntax.Kind(), typeNameWithNullabilityAnnotations, @@ -1094,20 +1089,6 @@ private static bool TryGetAccessibilityModifiers( return true; } - /// - /// Checks whether an input member is a required property. - /// - /// The input instance to process. - /// Whether is a required property. - private static bool GetIsRequiredProperty(ISymbol memberSymbol) - { -#if ROSLYN_4_3_1_OR_GREATER - return memberSymbol is IPropertySymbol { IsRequired: true }; -#else - return false; -#endif - } - /// /// Gets a instance with the cached args for property changing notifications. /// From 00fdbef4df0d308ab6c85b78094019a2d62f63cf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 08:59:47 -0800 Subject: [PATCH 124/146] Add unit tests for analyzer changes --- .../Test_SourceGeneratorsCodegen.cs | 245 +++++++++++++++++- .../Test_SourceGeneratorsDiagnostics.cs | 57 ++++ 2 files changed, 292 insertions(+), 10 deletions(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs index 9d7092888..978017735 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -21,7 +21,6 @@ partial class Test_SourceGeneratorsCodegen public void ObservablePropertyWithValueType_OnPartialProperty_WithNoModifiers_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -97,7 +96,6 @@ partial int Number public void ObservablePropertyWithValueType_OnPartialProperty_RequiredProperty_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -168,11 +166,245 @@ private set VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); } + // See https://github.com/CommunityToolkit/dotnet/issues/1013 + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_NewProperty_WorksCorrectly() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class BaseViewModel : ObservableObject + { + public int Number { get; private set; } + } + + partial class MyViewModel : BaseViewModel + { + [ObservableProperty] + public new partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public new partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_VirtualProperty_WorksCorrectly() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public virtual partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public virtual partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + [DataRow("override")] + [DataRow("sealed override")] + public void ObservablePropertyWithValueType_OnPartialProperty_OverrideProperty_WorksCorrectly(string modifiers) + { + string source = $$""" + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class BaseViewModel : ObservableObject + { + public virtual partial int Number { get; private set; } + } + + partial class MyViewModel : BaseViewModel + { + [ObservableProperty] + public {{modifiers}} partial int Number { get; private set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public {{modifiers}} partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + [TestMethod] public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly1() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -247,7 +479,6 @@ private set public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly2() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -397,7 +628,6 @@ private protected set public void ObservablePropertyWithReferenceType_NotNullable_OnPartialProperty_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable @@ -474,7 +704,6 @@ public partial string Name public void ObservablePropertyWithReferenceType_Nullable_OnPartialProperty_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; #nullable enable @@ -551,7 +780,6 @@ public partial string? Name public void ObservableProperty_OnPartialProperty_AlsoNotifyPropertyChange_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp; @@ -631,7 +859,6 @@ public partial string Name public void ObservableProperty_OnPartialProperty_AlsoNotifyCanExecuteChange_WorksCorrectly() { string source = """ - using System; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -711,7 +938,6 @@ public partial string Name public void ObservableProperty_OnPartialProperty_AlsoNotifyRecipients_WorksCorrectly() { string source = """ - using System; using System.Windows.Input; using CommunityToolkit.Mvvm.ComponentModel; @@ -790,7 +1016,6 @@ public partial string Name public void ObservableProperty_OnPartialProperty_AlsoNotifyDataErrorInfo_WorksCorrectly() { string source = """ - using System; using System.ComponentModel.DataAnnotations; using System.Windows.Input; using CommunityToolkit.Mvvm.ComponentModel; diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index a92b3d589..91c35db10 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -998,4 +998,61 @@ public partial class SampleViewModel : ObservableObject await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); } + + [TestMethod] + public async Task InvalidPointerTypeObservablePropertyAttributeAnalyzer_ReturnsValidType_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string {|CS9248:Name|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPointerTypeObservablePropertyAttributeAnalyzer_ReturnsPointerType_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public unsafe partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0055:ObservableProperty|}] + public partial int* {|CS9248:Name|} { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPointerTypeObservablePropertyAttributeAnalyzer_ReturnsFunctionPointerType_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public unsafe partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0055:ObservableProperty|}] + public partial delegate* unmanaged[Stdcall] {|CS9248:Name|} { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } } From a8aaeb57e9478a74bbca019a023f71597cdc38bf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 09:27:31 -0800 Subject: [PATCH 125/146] Update NuGet packages to latest stable --- .../CommunityToolkit.Diagnostics.csproj | 4 ++-- .../CommunityToolkit.HighPerformance.csproj | 10 +++++----- .../CommunityToolkit.Mvvm.CodeFixers.props | 3 --- .../CommunityToolkit.Mvvm.SourceGenerators.props | 3 --- src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj | 6 +++--- src/Directory.Build.props | 2 +- .../CommunityToolkit.Common.UnitTests.csproj | 6 +++--- .../CommunityToolkit.Diagnostics.UnitTests.csproj | 6 +++--- .../CommunityToolkit.HighPerformance.UnitTests.csproj | 6 +++--- ...vvm.DisableINotifyPropertyChanging.UnitTests.csproj | 6 +++--- .../CommunityToolkit.Mvvm.Internals.UnitTests.csproj | 6 +++--- .../CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj | 6 +++--- .../CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj | 6 +++--- .../CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj | 6 +++--- ...t.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj | 8 ++++---- ...t.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj | 8 ++++---- ...t.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj | 8 ++++---- 17 files changed, 47 insertions(+), 53 deletions(-) diff --git a/src/CommunityToolkit.Diagnostics/CommunityToolkit.Diagnostics.csproj b/src/CommunityToolkit.Diagnostics/CommunityToolkit.Diagnostics.csproj index 5f14958a6..cc9d42d39 100644 --- a/src/CommunityToolkit.Diagnostics/CommunityToolkit.Diagnostics.csproj +++ b/src/CommunityToolkit.Diagnostics/CommunityToolkit.Diagnostics.csproj @@ -19,14 +19,14 @@ - + - + diff --git a/src/CommunityToolkit.HighPerformance/CommunityToolkit.HighPerformance.csproj b/src/CommunityToolkit.HighPerformance/CommunityToolkit.HighPerformance.csproj index 1f17262a5..06972b24b 100644 --- a/src/CommunityToolkit.HighPerformance/CommunityToolkit.HighPerformance.csproj +++ b/src/CommunityToolkit.HighPerformance/CommunityToolkit.HighPerformance.csproj @@ -30,10 +30,10 @@ - - - - + + + + @@ -45,7 +45,7 @@ - + diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props index 114924f1c..391336f41 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props @@ -27,9 +27,6 @@ $(DefineConstants);ROSLYN_4_3_1_OR_GREATER $(DefineConstants);ROSLYN_4_12_0_OR_GREATER - - - 4.12.0-3.final diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props index 0e176df51..1517ae8cf 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props @@ -44,9 +44,6 @@ $(NoWarn);RS2003 - - - 4.12.0-3.final diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index 69a5befaf..dd7c98d01 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -44,14 +44,14 @@ - - + + - + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 486e75379..84e173b48 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ - + all build; analyzers diff --git a/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj b/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj index e47e06627..920b2cd7b 100644 --- a/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj +++ b/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj b/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj index 633cc0fa4..150c0d3f8 100644 --- a/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj +++ b/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj b/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj index 772b868c5..516a2e527 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj @@ -11,9 +11,9 @@ - - - + + + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj index 5853d5a5c..388b7f394 100644 --- a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj index acb777815..430fab996 100644 --- a/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj @@ -5,9 +5,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj index eb46db98f..17ecba754 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj index b70464cb4..f027142a5 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj index cfdec7b22..a1fb65d0c 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj index 3656cfdf1..eaa0f012c 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj @@ -7,10 +7,10 @@ - - - - + + + + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index 495ef1dca..6aa049262 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj @@ -12,10 +12,10 @@ - - - - + + + + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj index eba477975..7d0387bee 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj @@ -8,10 +8,10 @@ - - - - + + + + From 2c4be6618a03771f888af7e35e51bea2f50a14c9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 5 Dec 2024 09:27:41 -0800 Subject: [PATCH 126/146] Update recommended Windows SDK packages --- .../CommunityToolkit.Mvvm.WindowsSdk.targets | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets index 1655776c5..cf89f2def 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets @@ -28,8 +28,8 @@ - The recommended Windows SDK package for WindowsAppSDK projects (ie. without UWP XAML projections). --> <_MvvmToolkitWindowsSdkPackageMinBuild>38 - <_MvvmToolkitWindowsSdkPackageRecommendedUwpBuild>39 - <_MvvmToolkitWindowsSdkPackageRecommendedWindowsAppSDKBuild>41 + <_MvvmToolkitWindowsSdkPackageRecommendedUwpBuild>54 + <_MvvmToolkitWindowsSdkPackageRecommendedWindowsAppSDKBuild>53 <_MvvmToolkitWindowsSdkPackageRecommendedBuild>$(_MvvmToolkitWindowsSdkPackageMinBuild) From a6f517ea9e829b0f64924efed7cf0745c02aa974 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Dec 2024 18:55:40 -0800 Subject: [PATCH 127/146] Support explicit properties in [GBCP] attribute --- ...leCustomPropertyWithBasesMemberAnalyzer.cs | 36 +++++++++++------ ...indableCustomPropertyCompatibleAnalyzer.cs | 40 ++++++++++++++++++- .../Diagnostics/DiagnosticDescriptors.cs | 18 ++++----- 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs index 7e2aa7e57..0c4861ec4 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs @@ -60,25 +60,35 @@ public override void Initialize(AnalysisContext context) return; } - // Warn on all [ObservableProperty] fields + // Warn on all [ObservableProperty] fields that would be included foreach (IFieldSymbol fieldSymbol in FindObservablePropertyFields(typeSymbol, observablePropertySymbol)) { - context.ReportDiagnostic(Diagnostic.Create( - WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField, - typeSymbol.GetLocationFromAttributeDataOrDefault(generatedBindableCustomPropertyAttribute), - typeSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name)); + string propertyName = ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol); + + if (WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField, + typeSymbol.GetLocationFromAttributeDataOrDefault(generatedBindableCustomPropertyAttribute), + typeSymbol, + fieldSymbol.ContainingType, + fieldSymbol.Name)); + } } - // Warn on all [RelayCommand] methods + // Warn on all [RelayCommand] methods that would be included foreach (IMethodSymbol methodSymbol in FindRelayCommandMethods(typeSymbol, relayCommandSymbol)) { - context.ReportDiagnostic(Diagnostic.Create( - WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand, - typeSymbol.GetLocationFromAttributeDataOrDefault(generatedBindableCustomPropertyAttribute), - typeSymbol, - methodSymbol)); + (_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol); + + if (WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand, + typeSymbol.GetLocationFromAttributeDataOrDefault(generatedBindableCustomPropertyAttribute), + typeSymbol, + methodSymbol)); + } } }, SymbolKind.NamedType); }); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs index 7cbcf136a..10029b84b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs @@ -54,9 +54,17 @@ public override void Initialize(AnalysisContext context) return; } - // If the containing type is using [GeneratedBindableCustomProperty], emit a warning - if (typeSymbol.HasAttributeWithType(generatedBindableCustomPropertySymbol)) + // If the containing type is not using [GeneratedBindableCustomProperty], we can also skip it + if (!typeSymbol.TryGetAttributeWithType(generatedBindableCustomPropertySymbol, out AttributeData? generatedBindableCustomPropertyAttribute)) { + return; + } + + (_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol); + + if (DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName)) + { + // Actually warn if the generated command would've been included by the generator context.ReportDiagnostic(Diagnostic.Create( WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible, methodSymbol.GetLocationFromAttributeDataOrDefault(relayCommandAttribute), @@ -65,4 +73,32 @@ public override void Initialize(AnalysisContext context) }, SymbolKind.Method); }); } + + /// + /// Checks whether a generated property with a given name would be included by the [GeneratedBindableCustomProperty] generator. + /// + /// The input value for the [GeneratedBindableCustomProperty] attribute. + /// The target generated property name to check. + /// Whether would be included by the [GeneratedBindableCustomProperty] generator. + internal static bool DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(AttributeData attributeData, string propertyName) + { + // Make sure we have a valid list of property names to explicitly include. + // If that is not the case, we consider all properties as included by default. + if (attributeData.ConstructorArguments is not [{ IsNull: false, Kind: TypedConstantKind.Array, Type: IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_String }, Values: var names }, ..]) + { + return true; + } + + // Simply match the input collection of target property names + foreach (TypedConstant propertyValue in names) + { + if (propertyValue is { IsNull: false, Type.SpecialType: SpecialType.System_String, Value: string targetName } && targetName == propertyName) + { + return true; + } + } + + // No matches, we can consider the property as not included + return false; + } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 2de37f901..5196d5d89 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -766,49 +766,49 @@ internal static class DiagnosticDescriptors /// /// Gets a for when [RelayCommand] is used on a method in types where [GeneratedBindableCustomProperty] is used. /// - /// 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)". + /// Format: "The method {0} using [RelayCommand] within a type also using [GeneratedBindableCustomProperty] and including the generated property, 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 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)""", + messageFormat: """The method {0} using [RelayCommand] within a type also using [GeneratedBindableCustomProperty] and including the generated property, 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)""", category: typeof(RelayCommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Using [RelayCommand] on methods within a type also using [GeneratedBindableCustomProperty] 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).", + description: "Using [RelayCommand] on methods within a type also using [GeneratedBindableCustomProperty] and including the generated property 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).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0046"); /// /// Gets a for when [GeneratedBindableCustomProperty] is used on a type that also uses [ObservableProperty] on any declared or inherited fields. /// - /// 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)". + /// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}, and including the generated property: 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 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)""", + messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}, and including the generated property: 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)""", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Using [GeneratedBindableCustomProperty] on types that also use [ObservableProperty] on any declared (or inherited) fields 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).", + description: "Using [GeneratedBindableCustomProperty] on types that also use [ObservableProperty] on any declared (or inherited) fields and including the generated property 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).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0047"); /// /// Gets a for when [GeneratedBindableCustomProperty] is used on a type that also uses [RelayCommand] on any declared or inherited methods. /// - /// 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)". + /// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1} and including the generated property: 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 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)""", + messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1} and including the generated property: 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)""", category: typeof(RelayCommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Using [GeneratedBindableCustomProperty] on types that also use [RelayCommand] on any inherited methods 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).", + description: "Using [GeneratedBindableCustomProperty] on types that also use [RelayCommand] on any inherited methods and including the generated property 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).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0048"); /// From fb0facdc984377b1598e3395a59af3997812389d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Dec 2024 18:55:47 -0800 Subject: [PATCH 128/146] Add unit tests for new scenarios --- .../Test_SourceGeneratorsDiagnostics.cs | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 8030231aa..cb866377d 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -2049,6 +2049,111 @@ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_NotIncluded2_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["OtherCommand"], [])] + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_NotIncluded3_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["OtherCommand"], ["DoStuffCommand"])] + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + [TestMethod] public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_Bindable_Warns() { @@ -2084,6 +2189,76 @@ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_Bindable_Included2_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["Blah", "", "DoStuffCommand"], [])] + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void {|MVVMTK0046:DoStuff|}() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + [TestMethod] public async Task WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer_NotTargetingWindows_DoesNotWarn() { From 1bfc5a498b8ca2da8dc10b0c0e74cc84d6d73d62 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Dec 2024 19:41:11 -0800 Subject: [PATCH 129/146] Add unit tests for inheritance scenarios --- .../Test_SourceGeneratorsDiagnostics.cs | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 91c35db10..07ac71412 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -747,6 +747,119 @@ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Field_NotIncluded_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["OtherName"], [])] + public partial class SampleViewModel : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Method_NotIncluded_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["OtherMethod"], [])] + public partial class SampleViewModel : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + [TestMethod] public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Field_Warns() { @@ -781,6 +894,42 @@ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + [TestMethod] public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Method_Warns() { @@ -818,6 +967,45 @@ await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + [TestMethod] public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnValidProperty_DoesNotWarn() { From 78348ca09a3af61b63766c56c80e2232cb3e8465 Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Wed, 11 Dec 2024 04:47:24 +0100 Subject: [PATCH 130/146] Improve test coverage for SpanExtensions.IndexOf (#1023) --- .../Extensions/Test_SpanExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_SpanExtensions.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_SpanExtensions.cs index 4fa090c02..f3dd41f05 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_SpanExtensions.cs +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_SpanExtensions.cs @@ -69,6 +69,7 @@ static void Test() Test(); Test(); Test(); + Test(); Test>(); } @@ -90,6 +91,7 @@ static void Test() Test(); Test(); Test(); + Test(); Test>(); } @@ -127,6 +129,7 @@ static void Test() Test(); Test(); Test(); + Test(); Test>(); } From fc429245ff935a35bc7007cdbc4a06735733df7e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Dec 2024 21:47:27 -0800 Subject: [PATCH 131/146] Add 'UseObservablePropertyOnSemiAutoPropertyAnalyzer' --- .../AnalyzerReleases.Shipped.md | 8 + ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 256 ++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 21 ++ 4 files changed, 286 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index f11f523ea..345f27de7 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -97,3 +97,11 @@ MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053 MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054 MVVMTK0055 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0055 + +## Release 8.4.1 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MVVMTK0056 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0056 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 3c3b58a31..31ad7f8cd 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -39,6 +39,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs new file mode 100644 index 000000000..5695b2aa0 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -0,0 +1,256 @@ +// 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.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates a suggestion whenever [ObservableProperty] is used on a semi-auto property when a partial property could be used instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseObservablePropertyOnSemiAutoPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Using [ObservableProperty] on partial properties is only supported when using C# preview. + // As such, if that is not the case, return immediately, as no diagnostic should be produced. + if (!context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Get the symbol for [ObservableProperty] and ObservableObject + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject") is not INamedTypeSymbol observableObjectSymbol) + { + return; + } + + // Get the symbol for the SetProperty method as well + if (!TryGetSetPropertyMethodSymbol(observableObjectSymbol, out IMethodSymbol? setPropertySymbol)) + { + return; + } + + context.RegisterSymbolStartAction(context => + { + // We only care about types that could derive from ObservableObject + if (context.Symbol is not INamedTypeSymbol { IsStatic: false, IsReferenceType: true, BaseType.SpecialType: not SpecialType.System_Object } typeSymbol) + { + return; + } + + // If the type does not derive from ObservableObject, ignore it + if (!typeSymbol.InheritsFromType(observableObjectSymbol)) + { + return; + } + + Dictionary propertyMap = new(SymbolEqualityComparer.Default); + + // Crawl all members to discover properties that might be of interest + foreach (ISymbol memberSymbol in typeSymbol.GetMembers()) + { + // We're only looking for properties that might be valid candidates for conversion + if (memberSymbol is not IPropertySymbol + { + IsStatic: false, + IsPartialDefinition: false, + PartialDefinitionPart: null, + PartialImplementationPart: null, + ReturnsByRef: false, + ReturnsByRefReadonly: false, + Type.IsRefLikeType: false, + GetMethod: not null, + SetMethod.IsInitOnly: false + } propertySymbol) + { + continue; + } + + // We can safely ignore properties that already have [ObservableProperty] + if (typeSymbol.HasAttributeWithType(observablePropertySymbol)) + { + continue; + } + + // Track the property for later + propertyMap.Add(propertySymbol, new bool[2]); + } + + // We want to process both accessors, where we specifically need both the syntax + // and their semantic model to verify what they're doing. We can use a code callback. + context.RegisterOperationBlockAction(context => + { + // Make sure the current symbol is a property accessor + if (context.OwningSymbol is not IMethodSymbol { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet, AssociatedSymbol: IPropertySymbol propertySymbol }) + { + return; + } + + // If so, check that we are actually processing one of the properties we care about + if (!propertyMap.TryGetValue(propertySymbol, out bool[]? validFlags)) + { + return; + } + + // Handle the 'get' logic + if (SymbolEqualityComparer.Default.Equals(propertySymbol.GetMethod, context.OwningSymbol)) + { + // We expect a top-level block operation, that immediately returns an expression + if (context.OperationBlocks is not [IBlockOperation { Operations: [IReturnOperation returnOperation] }]) + { + return; + } + + // Next, we expect the return to produce a field reference + if (returnOperation is not { ReturnedValue: IFieldReferenceOperation fieldReferenceOperation }) + { + return; + } + + // The field has to be implicitly declared and not constant (and not static) + if (fieldReferenceOperation.Field is not { IsImplicitlyDeclared: true, IsStatic: false } fieldSymbol) + { + return; + } + + // Validate tha the field is indeed 'field' (it will be associated with the property) + if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.AssociatedSymbol, propertySymbol)) + { + return; + } + + // The 'get' accessor is valid + validFlags[0] = true; + } + else if (SymbolEqualityComparer.Default.Equals(propertySymbol.SetMethod, context.OwningSymbol)) + { + // We expect a top-level block operation, that immediately performs an invocation + if (context.OperationBlocks is not [IBlockOperation { Operations: [IExpressionStatementOperation { Operation: IInvocationOperation invocationOperation }] }]) + { + return; + } + + // Brief filtering of the target method, also get the original definition + if (invocationOperation.TargetMethod is not { Name: "SetProperty", IsGenericMethod: true, IsStatic: false } methodSymbol) + { + return; + } + + // First, check that we're calling 'ObservableObject.SetProperty' + if (!SymbolEqualityComparer.Default.Equals(methodSymbol.ConstructedFrom, setPropertySymbol)) + { + return; + } + + // We matched the method, now let's validate the arguments + if (invocationOperation.Arguments is not [{ } locationArgument, { } valueArgument, { } propertyNameArgument]) + { + return; + } + + // The field has to be implicitly declared and not constant (and not static) + if (locationArgument.Value is not IFieldReferenceOperation { Field: { IsImplicitlyDeclared: true, IsStatic: false } fieldSymbol }) + { + return; + } + + // Validate tha the field is indeed 'field' (it will be associated with the property) + if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.AssociatedSymbol, propertySymbol)) + { + return; + } + + // The value is just the 'value' keyword + if (valueArgument.Value is not IParameterReferenceOperation { Syntax: IdentifierNameSyntax { Identifier.Text: "value" } }) + { + return; + } + + // The property name should be the default value + if (propertyNameArgument is not { IsImplicit: true, ArgumentKind: ArgumentKind.DefaultValue }) + { + return; + } + + // The 'set' accessor is valid + validFlags[1] = true; + } + }); + + // Finally, we can consume this information when we finish processing the symbol + context.RegisterSymbolEndAction(context => + { + // Emit a diagnostic for each property that was a valid match + foreach (KeyValuePair pair in propertyMap) + { + if (pair.Value is [true, true]) + { + context.ReportDiagnostic(Diagnostic.Create( + UseObservablePropertyOnSemiAutoProperty, + pair.Key.Locations.FirstOrDefault(), + pair.Key)); + } + } + }); + }, SymbolKind.NamedType); + }); + } + + /// + /// Tries to get the symbol for the target SetProperty method this analyzer looks for. + /// + /// The symbol for ObservableObject. + /// The resulting method symbol, if found (this should always be the case). + /// Whether could be resolved correctly. + private static bool TryGetSetPropertyMethodSymbol(INamedTypeSymbol observableObjectSymbol, [NotNullWhen(true)] out IMethodSymbol? setPropertySymbol) + { + foreach (ISymbol symbol in observableObjectSymbol.GetMembers("SetProperty")) + { + // We're guaranteed to only match methods here + IMethodSymbol methodSymbol = (IMethodSymbol)symbol; + + // Match the exact signature we need (there's several overloads) + if (methodSymbol.Parameters is not + [ + { Kind: SymbolKind.TypeParameter, RefKind: RefKind.Ref }, + { Kind: SymbolKind.TypeParameter, RefKind: RefKind.None }, + { Type.SpecialType: SpecialType.System_String } + ]) + { + setPropertySymbol = methodSymbol; + + return true; + } + } + + setPropertySymbol = null; + + return false; + } +} + +#endif \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 5196d5d89..b62fcb7ae 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -44,6 +44,11 @@ internal static class DiagnosticDescriptors /// public const string WinRTObservablePropertyOnFieldsIsNotAotCompatibleId = "MVVMTK0045"; + /// + /// The diagnostic id for . + /// + public const string UseObservablePropertyOnSemiAutoPropertyId = "MVVMTK0056"; + /// /// Gets a indicating when a duplicate declaration of would happen. /// @@ -923,4 +928,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "A property using [ObservableProperty] returns a pointer-like value ([ObservableProperty] must be used on properties of a non pointer-like type).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0055"); + + /// + /// Gets a for when a semi-auto property can be converted to use [ObservableProperty] instead. + /// + /// Format: "The semi-auto property {0}.{1} can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)". + /// + /// + public static readonly DiagnosticDescriptor UseObservablePropertyOnSemiAutoProperty = new DiagnosticDescriptor( + id: UseObservablePropertyOnSemiAutoPropertyId, + title: "Prefer using [ObservableProperty] over semi-auto properties", + messageFormat: """The semi-auto property {0}.{1} can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Semi-auto properties should be converted to partial properties using [ObservableProperty] when possible, which is recommended (doing so makes the code less verbose and results in more optimized code).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0056"); } From 7c6eeb6f7fb07692fd343155029242cdf24f01f0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Dec 2024 22:01:01 -0800 Subject: [PATCH 132/146] Add unit tests for new analyzer --- .../Test_SourceGeneratorsDiagnostics.cs | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 07ac71412..f735ebc37 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1243,4 +1243,210 @@ public unsafe partial class SampleViewModel : ObservableObject await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_NormalProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_SimilarProperty_NotObservableObject_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : MyBaseViewModel + { + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + + public abstract class MyBaseViewModel + { + protected void SetProperty(ref T location, T value, string propertyName = null) + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_NoGetter_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_NoSetter_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_OtherLocation_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + set + { + string test = field; + + SetProperty(ref test, value); + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_OtherValue_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + set + { + string test = "Bob"; + + SetProperty(ref field, test); + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string {|MVVMTK0056:Name|} + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_WithModifiers_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public new string {|MVVMTK0056:Name|} + { + get => field; + private set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_WithBlocks_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public new string {|MVVMTK0056:Name|} + { + get + { + return field; + } + private set + { + SetProperty(ref field, value); + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } } From 162ae4f24c55eaee46a214d7a764f5ef3a2a3779 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Dec 2024 23:01:34 -0800 Subject: [PATCH 133/146] Add 'UsePartialPropertyForSemiAutoPropertyCodeFixer' --- ...CommunityToolkit.Mvvm.CodeFixers.projitems | 1 + ...lPropertyForObservablePropertyCodeFixer.cs | 2 +- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 113 ++++++++++++++++++ ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 3 +- 4 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems index 40339da45..0b0abb069 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems @@ -12,6 +12,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index a8067ace3..ba0c3bec6 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -100,7 +100,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration && identifierName == fieldName) { - // Register the code fix to update the class declaration to inherit from ObservableObject instead + // Register the code fix to convert the field declaration to a partial property context.RegisterCodeFix( CodeAction.Create( title: "Use a partial property", diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs new file mode 100644 index 000000000..ba132fccc --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -0,0 +1,113 @@ +// 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 System.Composition; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.CodeFixers; + +/// +/// A code fixer that converts semi-auto properties to partial properties using [ObservableProperty]. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UsePartialPropertyForSemiAutoPropertyCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoPropertyId); + + /// + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Get the property declaration from the target diagnostic + if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration) + { + // Register the code fix to update the semi-auto property to a partial property + context.RegisterCodeFix( + CodeAction.Create( + title: "Use a partial property", + createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration), + equivalenceKey: "Use a partial property"), + diagnostic); + } + } + + /// + /// Applies the code fix to a target identifier and returns an updated document. + /// + /// The original document being fixed. + /// The original tree root belonging to the current document. + /// The for the property being updated. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static async Task ConvertToPartialProperty(Document document, SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration) + { + await Task.CompletedTask; + + // Get a new property that is partial and with semicolon token accessors + PropertyDeclarationSyntax updatedPropertyDeclaration = + propertyDeclaration + .AddModifiers(Token(SyntaxKind.PartialKeyword)) + .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty"))))) + .WithAdditionalAnnotations(Formatter.Annotation) + .WithAccessorList(AccessorList(List( + [ + // Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only + propertyDeclaration.AccessorList!.Accessors[0] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithAdditionalAnnotations(Formatter.Annotation), + propertyDeclaration.AccessorList!.Accessors[1] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithAdditionalAnnotations(Formatter.Annotation) + ]))); + + // Create an editor to perform all mutations + SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services); + + editor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); + + // Find the parent type for the property + TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!; + + // Make sure it's partial + if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + TypeDeclarationSyntax updatedTypeDeclaration = typeDeclaration.AddModifiers(Token(SyntaxKind.PartialKeyword)); + + editor.ReplaceNode(typeDeclaration, updatedTypeDeclaration); + } + + return document.WithSyntaxRoot(editor.GetChangedRoot()); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs index 5695b2aa0..37778a157 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -212,7 +212,8 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create( UseObservablePropertyOnSemiAutoProperty, pair.Key.Locations.FirstOrDefault(), - pair.Key)); + pair.Key.ContainingType, + pair.Key.Name)); } } }); From 450fb9f9c565514beae522823fd90b908cfff4c5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 9 Dec 2024 23:04:24 -0800 Subject: [PATCH 134/146] Add unit test for code fixer --- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs new file mode 100644 index 000000000..02a539070 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -0,0 +1,76 @@ +// 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.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CSharpCodeFixTest = CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers.CSharpCodeFixWithLanguageVersionTest< + CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnSemiAutoPropertyAnalyzer, + CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForSemiAutoPropertyCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; +using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< + CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnSemiAutoPropertyAnalyzer, + CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForSemiAutoPropertyCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +[TestClass] +public class Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer +{ + [TestMethod] + public async Task SimpleProperty() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } +} From 71aeda87f67c305a1534008ee4f5997e0c2ad1dd Mon Sep 17 00:00:00 2001 From: Youssef1313 Date: Tue, 10 Dec 2024 08:51:44 +0100 Subject: [PATCH 135/146] Fix failing test --- .../UsePartialPropertyForSemiAutoPropertyCodeFixer.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index ba132fccc..f7e6f7edd 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -98,12 +98,11 @@ private static async Task ConvertToPartialProperty(Document document, // Find the parent type for the property TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!; - // Make sure it's partial + // Make sure it's partial (we create the updated node in the function to preserve the updated property declaration). + // If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property. if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) { - TypeDeclarationSyntax updatedTypeDeclaration = typeDeclaration.AddModifiers(Token(SyntaxKind.PartialKeyword)); - - editor.ReplaceNode(typeDeclaration, updatedTypeDeclaration); + editor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); } return document.WithSyntaxRoot(editor.GetChangedRoot()); From 46cd488bdf0c5471be5f5c277a0b3ef34bacf2e6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 00:29:38 -0800 Subject: [PATCH 136/146] Add more unit tests for code fixer --- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 442 +++++++++++++++++- 1 file changed, 440 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index 02a539070..18b4a281b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -67,8 +67,446 @@ public partial class SampleViewModel : ObservableObject test.FixedState.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. - DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithLeadingTrivia() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + /// + /// This is a property. + /// + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + /// + /// This is a property. + /// + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(10, 19, 10, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithLeadingTrivia_AndAttributes() + { + string original = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + /// + /// This is a property. + /// + [Test("Targeting property")] + [field: Test("Targeting field")] + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + + public class TestAttribute(string text) : Attribute; + """; + + string @fixed = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + /// + /// This is a property. + /// + [ObservableProperty] + [Test("Targeting property")] + [field: Test("Targeting field")] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(13,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(13, 19, 13, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_Multiple() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + public string FirstName + { + get => field; + set => SetProperty(ref field, value); + } + + public string LastName + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string FirstName { get; set; } + + [ObservableProperty] + public partial string LastName { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 28).WithArguments("MyApp.SampleViewModel", "FirstName"), + + // /0/Test0.cs(13,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(13, 19, 13, 27).WithArguments("MyApp.SampleViewModel", "LastName"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithinPartialType() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithinPartialType_Multiple() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string FirstName + { + get => field; + set => SetProperty(ref field, value); + } + + public string LastName + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string FirstName { get; set; } + + [ObservableProperty] + public partial string LastName { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 28).WithArguments("MyApp.SampleViewModel", "FirstName"), + + // /0/Test0.cs(13,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(13, 19, 13, 27).WithArguments("MyApp.SampleViewModel", "LastName"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithinPartialType_Multiple_MixedScenario() + { + string original = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [Test("This is an attribute")] + public string Prop1 + { + get => field; + set => SetProperty(ref field, value); + } + + // Single comment + public string Prop2 + { + get => field; + set => SetProperty(ref field, value); + } + + /// + /// This is a property. + /// + public string Prop3 + { + get => field; + set => SetProperty(ref field, value); + } + + /// + /// This is another property. + /// + [Test("Another attribute")] + public string Prop4 + { + get => field; + set => SetProperty(ref field, value); + } + + // Some other single comment + [Test("Yet another attribute")] + public string Prop5 + { + get => field; + set => SetProperty(ref field, value); + } + } + + public class TestAttribute(string text) : Attribute; + """; + + string @fixed = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableAttribute] + [Test("This is an attribute")] + public partial string Prop1 { get; set; } + + // Single comment + [ObservableAttribute] + public partial string Prop2 { get; set; } + + /// + /// This is a property. + /// + [ObservableAttribute] + public partial string Prop3 { get; set; } + + /// + /// This is another property. + /// + [ObservableAttribute] + [Test("Another attribute")] + public partial string Prop4 { get; set; } + + // Some other single comment + [ObservableAttribute] + [Test("Yet another attribute")] + public partial string Prop5 { get; set; } + } + + public class TestAttribute(string text) : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(9,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop1 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(9, 19, 9, 24).WithArguments("MyApp.SampleViewModel", "Prop1"), + + // /0/Test0.cs(16,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop2 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(16, 19, 16, 24).WithArguments("MyApp.SampleViewModel", "Prop2"), + + // /0/Test0.cs(25,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop3 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(25, 19, 25, 24).WithArguments("MyApp.SampleViewModel", "Prop3"), + + // /0/Test0.cs(35,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop4 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(35, 19, 35, 24).WithArguments("MyApp.SampleViewModel", "Prop4"), + + // /0/Test0.cs(43,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop5 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(43, 19, 43, 24).WithArguments("MyApp.SampleViewModel", "Prop5"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), }); await test.RunAsync(); From 5e3d65fedebef7c726ce95f66a7eb3e8cb153fb6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 00:46:43 -0800 Subject: [PATCH 137/146] Improve handling of attribute lists, fix tests --- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 31 +++++++++++++++- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 36 +++++++++++++------ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index f7e6f7edd..4c3cd3049 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -69,11 +69,40 @@ private static async Task ConvertToPartialProperty(Document document, { await Task.CompletedTask; + // Prepare the [ObservableProperty] attribute, which is always inserted first + AttributeListSyntax observablePropertyAttributeList = AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty")))); + + // Start setting up the updated attribute lists + SyntaxList attributeLists = propertyDeclaration.AttributeLists; + + if (attributeLists is [AttributeListSyntax firstAttributeListSyntax, ..]) + { + // Remove the trivia from the original first attribute + attributeLists = attributeLists.Replace( + nodeInList: firstAttributeListSyntax, + newNode: firstAttributeListSyntax.WithoutTrivia()); + + // If the property has at least an attribute list, move the trivia from it to the new attribute + observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax); + + // Insert the new attribute + attributeLists = attributeLists.Insert(0, observablePropertyAttributeList); + } + else + { + // Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list + observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(propertyDeclaration); + + // Save the new attribute list + attributeLists = attributeLists.Add(observablePropertyAttributeList); + } + // Get a new property that is partial and with semicolon token accessors PropertyDeclarationSyntax updatedPropertyDeclaration = propertyDeclaration .AddModifiers(Token(SyntaxKind.PartialKeyword)) - .AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty"))))) + .WithoutTrivia() + .WithAttributeLists(attributeLists) .WithAdditionalAnnotations(Formatter.Annotation) .WithAccessorList(AccessorList(List( [ diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index 18b4a281b..a106be081 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -126,8 +126,8 @@ public partial class SampleViewModel : ObservableObject test.FixedState.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. - DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 31).WithArguments("MyApp.SampleViewModel.Name"), }); await test.RunAsync(); @@ -175,6 +175,8 @@ public partial class SampleViewModel : ObservableObject [field: Test("Targeting field")] public partial string Name { get; set; } } + + public class TestAttribute(string text) : Attribute; """; CSharpCodeFixTest test = new(LanguageVersion.Preview) @@ -193,8 +195,8 @@ public partial class SampleViewModel : ObservableObject test.FixedState.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. - DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + // /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 31).WithArguments("MyApp.SampleViewModel.Name"), }); await test.RunAsync(); @@ -447,29 +449,29 @@ namespace MyApp; public partial class SampleViewModel : ObservableObject { - [ObservableAttribute] + [ObservableProperty] [Test("This is an attribute")] public partial string Prop1 { get; set; } // Single comment - [ObservableAttribute] + [ObservableProperty] public partial string Prop2 { get; set; } /// /// This is a property. /// - [ObservableAttribute] + [ObservableProperty] public partial string Prop3 { get; set; } /// /// This is another property. /// - [ObservableAttribute] + [ObservableProperty] [Test("Another attribute")] public partial string Prop4 { get; set; } // Some other single comment - [ObservableAttribute] + [ObservableProperty] [Test("Yet another attribute")] public partial string Prop5 { get; set; } } @@ -505,8 +507,20 @@ public class TestAttribute(string text) : Attribute; test.FixedState.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. - DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + // /0/Test0.cs(10,27): error CS9248: Partial property 'SampleViewModel.Prop1' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(10, 27, 10, 32).WithArguments("MyApp.SampleViewModel.Prop1"), + + // /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.Prop2' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 32).WithArguments("MyApp.SampleViewModel.Prop2"), + + // /0/Test0.cs(20,27): error CS9248: Partial property 'SampleViewModel.Prop3' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(20, 27, 20, 32).WithArguments("MyApp.SampleViewModel.Prop3"), + + // /0/Test0.cs(27,27): error CS9248: Partial property 'SampleViewModel.Prop4' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(27, 27, 27, 32).WithArguments("MyApp.SampleViewModel.Prop4"), + + // /0/Test0.cs(32,27): error CS9248: Partial property 'SampleViewModel.Prop5' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(32, 27, 32, 32).WithArguments("MyApp.SampleViewModel.Prop5"), }); await test.RunAsync(); From e3489068a95baa8695108b3dbee2960c29c02699 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 09:28:16 -0800 Subject: [PATCH 138/146] Only remove leading trivia, add tests --- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 2 +- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 95 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index 4c3cd3049..71ac68726 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -101,7 +101,7 @@ private static async Task ConvertToPartialProperty(Document document, PropertyDeclarationSyntax updatedPropertyDeclaration = propertyDeclaration .AddModifiers(Token(SyntaxKind.PartialKeyword)) - .WithoutTrivia() + .WithoutLeadingTrivia() .WithAttributeLists(attributeLists) .WithAdditionalAnnotations(Formatter.Annotation) .WithAccessorList(AccessorList(List( diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index a106be081..57351d88f 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -267,6 +267,75 @@ public partial class SampleViewModel : ObservableObject await test.RunAsync(); } + [TestMethod] + public async Task SimpleProperty_Multiple_OnlyTriggersOnSecondOne() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + private string _firstName; + + public string FirstName + { + get => _firstName; + set => SetProperty(ref _firstName, value); + } + + public string LastName + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + private string _firstName; + + public string FirstName + { + get => _firstName; + set => SetProperty(ref _firstName, value); + } + + [ObservableProperty] + public partial string LastName { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(15,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(15, 19, 15, 27).WithArguments("MyApp.SampleViewModel", "LastName"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(16,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(16, 27, 16, 35).WithArguments("MyApp.SampleViewModel.LastName"), + }); + + await test.RunAsync(); + } + [TestMethod] public async Task SimpleProperty_WithinPartialType() { @@ -436,6 +505,19 @@ public string Prop5 get => field; set => SetProperty(ref field, value); } + + [Test("Attribute without trivia")] + public string Prop6 + { + get => field; + set => SetProperty(ref field, value); + } + + public string Prop7 + { + get => field; + set => SetProperty(ref field, value); + } } public class TestAttribute(string text) : Attribute; @@ -474,6 +556,13 @@ public partial class SampleViewModel : ObservableObject [ObservableProperty] [Test("Yet another attribute")] public partial string Prop5 { get; set; } + + [ObservableProperty] + [Test("Attribute without trivia")] + public partial string Prop6 { get; set; } + + [ObservableProperty] + public partial string Prop7 { get; set; } } public class TestAttribute(string text) : Attribute; @@ -503,6 +592,12 @@ public class TestAttribute(string text) : Attribute; // /0/Test0.cs(43,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop5 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) CSharpCodeFixVerifier.Diagnostic().WithSpan(43, 19, 43, 24).WithArguments("MyApp.SampleViewModel", "Prop5"), + + // /0/Test0.cs(50,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop6 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(50, 19, 50, 24).WithArguments("MyApp.SampleViewModel", "Prop6"), + + // /0/Test0.cs(56,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop7 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(56, 19, 56, 24).WithArguments("MyApp.SampleViewModel", "Prop7"), }); test.FixedState.ExpectedDiagnostics.AddRange(new[] From da8901493f151c1218b4d0af47421dad02da277e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 09:45:54 -0800 Subject: [PATCH 139/146] Fix leading trivia in accessors, add tests --- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 3 +- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 89 ++++++++++++++++++- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index 71ac68726..9bcddb458 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -116,8 +116,9 @@ private static async Task ConvertToPartialProperty(Document document, .WithBody(null) .WithExpressionBody(null) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia()) .WithAdditionalAnnotations(Formatter.Annotation) - ]))); + ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); // Create an editor to perform all mutations SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index 57351d88f..7fd8ba723 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -260,8 +260,80 @@ public partial class SampleViewModel : ObservableObject test.FixedState.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. - DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"), + + // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_Multiple_OnlyTriggersOnFirstOne() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + public string FirstName + { + get => field; + set => SetProperty(ref field, value); + } + + private string _lastName; + + public string LastName + { + get => _lastName; + set => SetProperty(ref _lastName, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string FirstName { get; set; } + + private string _lastName; + + public string LastName + { + get => _lastName; + set => SetProperty(ref _lastName, value); + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 28).WithArguments("MyApp.SampleViewModel", "FirstName"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"), }); await test.RunAsync(); @@ -447,8 +519,11 @@ public partial class SampleViewModel : ObservableObject test.FixedState.ExpectedDiagnostics.AddRange(new[] { - // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. - DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"), + + // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"), }); await test.RunAsync(); @@ -616,6 +691,12 @@ public class TestAttribute(string text) : Attribute; // /0/Test0.cs(32,27): error CS9248: Partial property 'SampleViewModel.Prop5' must have an implementation part. DiagnosticResult.CompilerError("CS9248").WithSpan(32, 27, 32, 32).WithArguments("MyApp.SampleViewModel.Prop5"), + + // /0/Test0.cs(36,27): error CS9248: Partial property 'SampleViewModel.Prop6' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(36, 27, 36, 32).WithArguments("MyApp.SampleViewModel.Prop6"), + + // /0/Test0.cs(39,27): error CS9248: Partial property 'SampleViewModel.Prop7' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(39, 27, 39, 32).WithArguments("MyApp.SampleViewModel.Prop7"), }); await test.RunAsync(); From df714eaa6c194b5f2b8a46713541939d6321b0ce Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 18:50:40 -0800 Subject: [PATCH 140/146] Add support for 'get;' accessors too --- ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 30 +++++++++++ ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 53 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs index 37778a157..ecb6502a2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -10,6 +10,7 @@ using System.Linq; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; @@ -201,6 +202,35 @@ public override void Initialize(AnalysisContext context) } }); + // We also need to track getters which have no body, and we need syntax for that + context.RegisterSyntaxNodeAction(context => + { + // Let's just make sure we do have a property symbol + if (context.ContainingSymbol is not IPropertySymbol { GetMethod: not null } propertySymbol) + { + return; + } + + // Lookup the property to get its flags + if (!propertyMap.TryGetValue(propertySymbol, out bool[]? validFlags)) + { + return; + } + + // We expect two accessors, skip if otherwise (the setter will be validated by the other callback) + if (context.Node is not PropertyDeclarationSyntax { AccessorList.Accessors: [{ } firstAccessor, { } secondAccessor] }) + { + return; + } + + // Check that either of them is a semicolon token 'get;' accessor (it can be in either position) + if (firstAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && firstAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken) || + secondAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && secondAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken)) + { + validFlags[0] = true; + } + }, SyntaxKind.PropertyDeclaration); + // Finally, we can consume this information when we finish processing the symbol context.RegisterSymbolEndAction(context => { diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index 7fd8ba723..a41af842d 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -74,6 +74,59 @@ public partial class SampleViewModel : ObservableObject await test.RunAsync(); } + [TestMethod] + public async Task SimpleProperty_WithSemicolonTokenGetAccessor() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + public string Name + { + get; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + [TestMethod] public async Task SimpleProperty_WithLeadingTrivia() { From a91e7a8b48fb4a6fdd4ead99f1f3fd728a5ddb93 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 19:35:51 -0800 Subject: [PATCH 141/146] Handle adding using directives if needed --- ...ialPropertyForSemiAutoPropertyCodeFixer.cs | 121 ++++++++++++++-- ...ablePropertyOnSemiAutoPropertyCodeFixer.cs | 132 ++++++++++++++++++ 2 files changed, 241 insertions(+), 12 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs index 9bcddb458..9b4d7e63d 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Text; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -32,9 +33,9 @@ public sealed class UsePartialPropertyForSemiAutoPropertyCodeFixer : CodeFixProv public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoPropertyId); /// - public override FixAllProvider? GetFixAllProvider() + public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider() { - return WellKnownFixAllProviders.BatchFixer; + return new FixAllProvider(); } /// @@ -43,16 +44,31 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) Diagnostic diagnostic = context.Diagnostics[0]; TextSpan diagnosticSpan = context.Span; + // This code fixer needs the semantic model, so check that first + if (!context.Document.SupportsSemanticModel) + { + return; + } + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); // Get the property declaration from the target diagnostic if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration) { + // Get the semantic model, as we need to resolve symbols + SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!; + + // Make sure we can resolve the [ObservableProperty] attribute (as we want to add it in the fixed code) + if (semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + // Register the code fix to update the semi-auto property to a partial property context.RegisterCodeFix( CodeAction.Create( title: "Use a partial property", - createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration), + createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration, observablePropertySymbol), equivalenceKey: "Use a partial property"), diagnostic); } @@ -64,14 +80,47 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) /// The original document being fixed. /// The original tree root belonging to the current document. /// The for the property being updated. + /// The for [ObservableProperty]. /// An updated document with the applied code fix, and being replaced with a partial property. - private static async Task ConvertToPartialProperty(Document document, SyntaxNode root, PropertyDeclarationSyntax propertyDeclaration) + private static async Task ConvertToPartialProperty( + Document document, + SyntaxNode root, + PropertyDeclarationSyntax propertyDeclaration, + INamedTypeSymbol observablePropertySymbol) { await Task.CompletedTask; - // Prepare the [ObservableProperty] attribute, which is always inserted first - AttributeListSyntax observablePropertyAttributeList = AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ObservableProperty")))); + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Create the attribute syntax for the new [ObservableProperty] attribute. Also + // annotate it to automatically add using directives to the document, if needed. + SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(observablePropertySymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + AttributeListSyntax observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax); + // Create an editor to perform all mutations + SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services); + + ConvertToPartialProperty( + propertyDeclaration, + observablePropertyAttributeList, + syntaxEditor); + + // Create the new document with the single change + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } + + /// + /// Applies the code fix to a target identifier and returns an updated document. + /// + /// The for the property being updated. + /// The with the attribute to add. + /// The instance to use. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static void ConvertToPartialProperty( + PropertyDeclarationSyntax propertyDeclaration, + AttributeListSyntax observablePropertyAttributeList, + SyntaxEditor syntaxEditor) + { // Start setting up the updated attribute lists SyntaxList attributeLists = propertyDeclaration.AttributeLists; @@ -120,10 +169,7 @@ private static async Task ConvertToPartialProperty(Document document, .WithAdditionalAnnotations(Formatter.Annotation) ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); - // Create an editor to perform all mutations - SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services); - - editor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); + syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); // Find the parent type for the property TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!; @@ -132,10 +178,61 @@ private static async Task ConvertToPartialProperty(Document document, // If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property. if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) { - editor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); + syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); } + } - return document.WithSyntaxRoot(editor.GetChangedRoot()); + /// + /// A custom with the logic from . + /// + private sealed class FixAllProvider : DocumentBasedFixAllProvider + { + /// + protected override async Task FixAllAsync(FixAllContext fixAllContext, Document document, ImmutableArray diagnostics) + { + // Get the semantic model, as we need to resolve symbols + if (await document.GetSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SemanticModel semanticModel) + { + return document; + } + + // Make sure we can resolve the [ObservableProperty] attribute here as well + if (semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return document; + } + + // Get the document root (this should always succeed) + if (await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SyntaxNode root) + { + return document; + } + + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Create the attribute syntax for the new [ObservableProperty] attribute here too + SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(observablePropertySymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + AttributeListSyntax observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax); + + // Create an editor to perform all mutations (across all edits in the file) + SyntaxEditor syntaxEditor = new(root, fixAllContext.Solution.Services); + + foreach (Diagnostic diagnostic in diagnostics) + { + // Get the current property declaration for the diagnostic + if (root.FindNode(diagnostic.Location.SourceSpan) is not PropertyDeclarationSyntax propertyDeclaration) + { + continue; + } + + ConvertToPartialProperty( + propertyDeclaration, + observablePropertyAttributeList, + syntaxEditor); + } + + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs index a41af842d..e9cccaa91 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -127,6 +127,57 @@ public partial class SampleViewModel : ObservableObject await test.RunAsync(); } + [TestMethod] + public async Task SimpleProperty_WithMissingUsingDirective() + { + string original = """ + namespace MyApp; + + public class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject + { + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 19, 5, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + [TestMethod] public async Task SimpleProperty_WithLeadingTrivia() { @@ -582,6 +633,87 @@ public partial class SampleViewModel : ObservableObject await test.RunAsync(); } + [TestMethod] + public async Task SimpleProperty_Multiple_WithMissingUsingDirective() + { + string original = """ + namespace MyApp; + + public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject + { + public string FirstName + { + get => field; + set => SetProperty(ref field, value); + } + + public string LastName + { + get => field; + set => SetProperty(ref field, value); + } + + public string PhoneNumber + { + get; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject + { + [ObservableProperty] + public partial string FirstName { get; set; } + + [ObservableProperty] + public partial string LastName { get; set; } + + [ObservableProperty] + public partial string PhoneNumber { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 19, 5, 28).WithArguments("MyApp.SampleViewModel", "FirstName"), + + // /0/Test0.cs(11,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(11, 19, 11, 27).WithArguments("MyApp.SampleViewModel", "LastName"), + + // /0/Test0.cs(17,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.PhoneNumber can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(17, 19, 17, 30).WithArguments("MyApp.SampleViewModel", "PhoneNumber"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"), + + // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"), + + // /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.PhoneNumber' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 38).WithArguments("MyApp.SampleViewModel.PhoneNumber"), + }); + + await test.RunAsync(); + } + [TestMethod] public async Task SimpleProperty_WithinPartialType_Multiple_MixedScenario() { From 76c41aa32c1aa74135420b08a57ebc6bf974852c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 20:13:24 -0800 Subject: [PATCH 142/146] Move MVVMTK0056 to 8.4.0 release --- .../AnalyzerReleases.Shipped.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 345f27de7..486e14158 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -97,11 +97,4 @@ MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053 MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054 MVVMTK0055 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0055 - -## Release 8.4.1 - -### New Rules - -Rule ID | Category | Severity | Notes ---------|----------|----------|------- MVVMTK0056 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0056 From a1bfe3d7f7f3e4cfa77152a0a2583fb1cd5797de Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 21:48:08 -0800 Subject: [PATCH 143/146] Port 'ObjectPool' type from Roslyn --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../Helpers/ObjectPool{T}.cs | 164 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 31ad7f8cd..f875a91df 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -89,6 +89,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs new file mode 100644 index 000000000..18145afa0 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs @@ -0,0 +1,164 @@ +// 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. + +// Ported and adapted from https://github.com/dotnet/roslyn + +using System; +using System.Diagnostics; +using System.Threading; + +#pragma warning disable RS1035, IDE0290 + +namespace Microsoft.CodeAnalysis.PooledObjects; + +/// +/// Generic implementation of object pooling pattern with predefined pool size limit. The main +/// purpose is that limited number of frequently used objects can be kept in the pool for +/// further recycling. +/// +/// Notes: +/// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there +/// is no space in the pool, extra returned objects will be dropped. +/// +/// 2) it is implied that if object was obtained from a pool, the caller will return it back in +/// a relatively short time. Keeping checked out objects for long durations is ok, but +/// reduces usefulness of pooling. Just new up your own. +/// +/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. +/// Rationale: +/// If there is no intent for reusing the object, do not use pool - just use "new". +/// +internal sealed class ObjectPool + where T : class +{ + // Storage for the pool objects. The first item is stored in a dedicated field because we + // expect to be able to satisfy most requests from it. + private T? firstItem; + private readonly Element[] items; + + // The factory is stored for the lifetime of the pool. We will call this only when pool needs to + // expand. compared to "new T()", Func gives more flexibility to implementers and faster + // than "new T()". + private readonly Func factory; + + /// + /// Creates a new instance with a given factory. + /// + /// The factory to use to produce new objects. + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) + { + } + + /// + /// Creates a new instance with a given factory. + /// + /// The factory to use to produce new objects. + /// The size of the pool. + public ObjectPool(Func factory, int size) + { + this.factory = factory; + this.items = new Element[size - 1]; + } + + /// + /// Produces an instance. + /// + /// The instance to return to the pool later. + /// + /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. + /// Note that Free will try to store recycled objects close to the start thus statistically + /// reducing how far we will typically search. + /// + public T Allocate() + { + // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements. + // Note that the initial read is optimistically not synchronized. That is intentional. + // We will interlock only when we have a candidate. in a worst case we may miss some + // recently returned objects. Not a big deal. + T? instance = this.firstItem; + if (instance == null || instance != Interlocked.CompareExchange(ref this.firstItem, null, instance)) + { + instance = AllocateSlow(); + } + + return instance; + } + + /// + /// Slow path to produce a new instance. + /// + /// The instance to return to the pool later. + private T AllocateSlow() + { + Element[] items = this.items; + + for (int i = 0; i < items.Length; i++) + { + T? instance = items[i].Value; + + if (instance is not null) + { + if (instance == Interlocked.CompareExchange(ref items[i].Value, null, instance)) + { + return instance; + } + } + } + + return this.factory(); + } + + /// + /// Returns objects to the pool. + /// + /// The object to return to the pool. + /// + /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. + /// Note that Free will try to store recycled objects close to the start thus statistically + /// reducing how far we will typically search in Allocate. + /// + public void Free(T obj) + { + if (this.firstItem is null) + { + this.firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + /// + /// Slow path to return an object to the pool. + /// + /// The object to return to the pool. + private void FreeSlow(T obj) + { + Element[] items = this.items; + + for (int i = 0; i < items.Length; i++) + { + if (items[i].Value == null) + { + items[i].Value = obj; + + break; + } + } + } + + /// + /// Wrapper to avoid array covariance. + /// + [DebuggerDisplay("{Value,nq}")] + private struct Element + { + /// + /// The value for the current element. + /// + public T? Value; + } +} \ No newline at end of file From 576646598c300733405c4ceca7cf898aed46a719 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Dec 2024 22:17:15 -0800 Subject: [PATCH 144/146] Enable pooling for objects in new analyzer --- ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs index ecb6502a2..a36aad071 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -4,6 +4,7 @@ #if ROSLYN_4_12_0_OR_GREATER +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -14,6 +15,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.PooledObjects; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace CommunityToolkit.Mvvm.SourceGenerators; @@ -24,6 +26,22 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class UseObservablePropertyOnSemiAutoPropertyAnalyzer : DiagnosticAnalyzer { + /// + /// The number of pooled flags per stack (ie. how many properties we expect on average per type). + /// + private const int NumberOfPooledFlagsPerStack = 20; + + /// + /// Shared pool for instances. + /// + [SuppressMessage("MicrosoftCodeAnalysisPerformance", "RS1008", Justification = "This is a pool of (empty) dictionaries, it is not actually storing compilation data.")] + private static readonly ObjectPool> PropertyMapPool = new(static () => new Dictionary(SymbolEqualityComparer.Default)); + + /// + /// Shared pool for -s of flags, one per type being processed. + /// + private static readonly ObjectPool> PropertyFlagsStackPool = new(CreatePropertyFlagsStack); + /// public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoProperty); @@ -69,7 +87,8 @@ public override void Initialize(AnalysisContext context) return; } - Dictionary propertyMap = new(SymbolEqualityComparer.Default); + Dictionary propertyMap = PropertyMapPool.Allocate(); + Stack propertyFlagsStack = PropertyFlagsStackPool.Allocate(); // Crawl all members to discover properties that might be of interest foreach (ISymbol memberSymbol in typeSymbol.GetMembers()) @@ -97,8 +116,13 @@ public override void Initialize(AnalysisContext context) continue; } + // Take an array from the stack or create a new one otherwise + bool[] flags = propertyFlagsStack.Count > 0 + ? propertyFlagsStack.Pop() + : new bool[2]; + // Track the property for later - propertyMap.Add(propertySymbol, new bool[2]); + propertyMap.Add(propertySymbol, flags); } // We want to process both accessors, where we specifically need both the syntax @@ -246,6 +270,24 @@ public override void Initialize(AnalysisContext context) pair.Key.Name)); } } + + // Before clearing the dictionary, move back all values to the stack + foreach (bool[] propertyFlags in propertyMap.Values) + { + // Make sure the array is cleared before returning it + propertyFlags.AsSpan().Clear(); + + propertyFlagsStack.Push(propertyFlags); + } + + // We are now done processing the symbol, we can return the dictionary. + // Note that we must clear it before doing so to avoid leaks and issues. + propertyMap.Clear(); + + PropertyMapPool.Free(propertyMap); + + // Also do the same for the stack, except we don't need to clean it (since it roots no compilation objects) + PropertyFlagsStackPool.Free(propertyFlagsStack); }); }, SymbolKind.NamedType); }); @@ -282,6 +324,23 @@ private static bool TryGetSetPropertyMethodSymbol(INamedTypeSymbol observableObj return false; } + + /// + /// Produces a new instance to pool. + /// + /// The resulting instance to use. + private static Stack CreatePropertyFlagsStack() + { + static IEnumerable EnumerateFlags() + { + for (int i = 0; i < NumberOfPooledFlagsPerStack; i++) + { + yield return new bool[2]; + } + } + + return new(EnumerateFlags()); + } } #endif \ No newline at end of file From 6cf850fe383d256eab89c8754b479b1875e4a3d7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 16:14:28 -0800 Subject: [PATCH 145/146] Improve filtering in semi-auto property analyzer --- ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 5 +++-- .../Test_SourceGeneratorsDiagnostics.cs | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs index a36aad071..2f044b3ca 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -110,8 +110,9 @@ public override void Initialize(AnalysisContext context) continue; } - // We can safely ignore properties that already have [ObservableProperty] - if (typeSymbol.HasAttributeWithType(observablePropertySymbol)) + // We can safely ignore properties that already have [ObservableProperty]. + // This is because in that case, the other analyzer will already emit an error. + if (propertySymbol.HasAttributeWithType(observablePropertySymbol)) { continue; } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index f735ebc37..fc1798d0e 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1381,6 +1381,28 @@ public string Name await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); } + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_WithObservableProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + [TestMethod] public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_Warns() { From 3c720ddf7b096bf97b72064a4fbace11dd4ec8ed Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Dec 2024 22:33:27 -0800 Subject: [PATCH 146/146] Handle semi-auto properties with expression bodies --- ...vablePropertyOnSemiAutoPropertyAnalyzer.cs | 8 +++++-- .../Test_SourceGeneratorsDiagnostics.cs | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs index 2f044b3ca..18155958a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -249,8 +249,12 @@ public override void Initialize(AnalysisContext context) } // Check that either of them is a semicolon token 'get;' accessor (it can be in either position) - if (firstAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && firstAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken) || - secondAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && secondAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken)) + if (firstAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && + firstAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken) && + firstAccessor.ExpressionBody is null || + secondAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && + secondAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken) && + secondAccessor.ExpressionBody is null) { validFlags[0] = true; } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs index fc1798d0e..e6845bbe5 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1403,6 +1403,27 @@ public string Name await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); } + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_GetAccessorWithExpressionBody_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => "Hello world"; + set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + [TestMethod] public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_Warns() {