diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln
index 688e32367..8a94f3c65 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,28 +61,32 @@ 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
+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
@@ -457,6 +461,46 @@ 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
+ {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
@@ -479,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}
@@ -489,12 +534,14 @@ 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
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/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
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.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs
new file mode 100644
index 000000000..f688fa8f8
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs
@@ -0,0 +1,294 @@
+// 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.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;
+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 fields using [ObservableProperty] to partial properties.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp)]
+[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);
+
+ ///
+ public override FixAllProvider? GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ ///
+ 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)
+ {
+ 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)
+ {
+ // 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(
+ 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)
+ {
+ 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.
+ List propertyAttributes =
+ fieldDeclaration
+ .AttributeLists
+ .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?.Identifier.Kind() is SyntaxKind.GetKeyword)
+ .Select(list => list.WithTarget(null).WithAdditionalAnnotations(Formatter.Annotation))
+ .ToArray();
+
+ // Also do the same for the setters
+ AttributeListSyntax[] setterAttributes =
+ fieldDeclaration
+ .AttributeLists
+ .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.SetKeyword)
+ .Select(list => list.WithTarget(null).WithAdditionalAnnotations(Formatter.Annotation))
+ .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.ToArray())
+ .WithAdditionalAnnotations(Formatter.Annotation)
+ .AddAccessorListAccessors(
+ AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
+ .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
+ .AddAttributeLists(getterAttributes)
+ .WithAdditionalAnnotations(Formatter.Annotation),
+ AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
+ .WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
+ .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);
+
+ 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/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.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 @@
+
+
+
+
+
+
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
index 53e58bfea..cb19f6246 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md
@@ -75,3 +75,14 @@ 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
+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 9dc77b7d1..988d87a15 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems
@@ -46,11 +46,16 @@
+
+
+
+
+
@@ -64,6 +69,9 @@
+
+
+
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props
index 6c5a90f5e..c29a655e2 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props
@@ -16,20 +16,25 @@
-->
-
- $(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
+
+
+ $(NoWarn);RS2003
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs
index 8c3bca4da..367d9a751 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs
@@ -3,36 +3,46 @@
// See the LICENSE file in the project root for more information.
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.
+/// 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.
/// 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.
internal sealed record PropertyInfo(
+ SyntaxKind AnnotatedMemberKind,
string TypeNameWithNullabilityAnnotations,
string FieldName,
string PropertyName,
+ Accessibility PropertyAccessibility,
+ Accessibility GetterAccessibility,
+ Accessibility SetterAccessibility,
EquatableArray PropertyChangingNames,
EquatableArray PropertyChangedNames,
EquatableArray NotifiedCommandNames,
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 b127bc7a3..6dc483e6b 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs
@@ -31,35 +31,126 @@ partial class ObservablePropertyGenerator
internal static class Execute
{
///
- /// Processes a given field.
+ /// Checks whether an input syntax node is a candidate property declaration for the generator.
///
- /// The instance to process.
- /// The input instance to process.
+ /// 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, out TypeDeclarationSyntax? containingTypeNode)
+ {
+ // 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;
+ }
+
+ // Check that the target is a valid field or partial property
+ if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) &&
+ !InvalidPropertyLevelObservablePropertyAttributeAnalyzer.IsValidCandidateProperty(node, out parentNode))
+ {
+ return false;
+ }
+
+ // The candidate member must be in a type with a base type (as it must derive from ObservableObject)
+ return parentNode?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true;
+ }
+
+ ///
+ /// Checks whether a given candidate node is valid given a compilation.
+ ///
+ /// 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,
[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
- if (!IsTargetTypeValid(fieldSymbol, out bool shouldInvokeOnPropertyChanging))
+ if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging))
{
builder.Add(
- InvalidContainingTypeForObservablePropertyFieldError,
- fieldSymbol,
- fieldSymbol.ContainingType,
- fieldSymbol.Name);
+ InvalidContainingTypeForObservablePropertyMemberError,
+ memberSymbol,
+ memberSyntax.Kind().ToFieldOrPropertyKeyword(),
+ memberSymbol.ContainingType,
+ memberSymbol.Name);
propertyInfo = null;
diagnostics = builder.ToImmutable();
@@ -75,18 +166,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)
+ // Check for name collisions (only for fields)
+ if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration))
{
builder.Add(
ObservablePropertyNameCollisionError,
- fieldSymbol,
- fieldSymbol.ContainingType,
- fieldSymbol.Name);
+ memberSymbol,
+ memberSymbol.ContainingType,
+ memberSymbol.Name);
propertyInfo = null;
diagnostics = builder.ToImmutable();
@@ -100,13 +191,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();
@@ -125,15 +216,15 @@ 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 isReferenceTypeOrUnconstrainedTypeParameter,
out bool includeMemberNotNullOnSetAccessor);
token.ThrowIfCancellationRequested();
@@ -142,7 +233,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;
@@ -151,7 +242,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;
@@ -160,19 +251,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;
@@ -180,7 +271,7 @@ 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;
@@ -192,96 +283,39 @@ public static bool TryGetInfo(
{
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));
+ }
}
}
token.ThrowIfCancellationRequested();
- // Gather explicit forwarded attributes info
- foreach (AttributeListSyntax attributeList in fieldSyntax.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))
- {
- builder.Add(
- InvalidPropertyTargetedAttributeOnObservablePropertyField,
- attribute,
- fieldSymbol,
- 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,
- fieldSymbol,
- 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();
// 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,
+ memberSyntax.Kind().ToFieldOrPropertyKeyword(),
+ memberSymbol.ContainingType,
+ memberSymbol.Name,
forwardedAttributes.Count);
}
@@ -290,9 +324,10 @@ public static bool TryGetInfo(
{
builder.Add(
MissingValidationAttributesForNotifyDataErrorInfoError,
- fieldSymbol,
- fieldSymbol.ContainingType,
- fieldSymbol.Name);
+ memberSymbol,
+ memberSymbol.Kind.ToFieldOrPropertyKeyword(),
+ memberSymbol.ContainingType,
+ memberSymbol.Name);
}
token.ThrowIfCancellationRequested();
@@ -316,17 +351,37 @@ 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(
+ memberSyntax.Kind(),
typeNameWithNullabilityAnnotations,
fieldName,
propertyName,
+ propertyAccessibility,
+ getterAccessibility,
+ setterAccessibility,
effectivePropertyChangingNames,
effectivePropertyChangedNames,
notifiedCommandNames.ToImmutable(),
notifyRecipients,
notifyDataErrorInfo,
isOldPropertyValueDirectlyReferenced,
- isReferenceTypeOrUnconstraindTypeParameter,
+ isReferenceTypeOrUnconstrainedTypeParameter,
includeMemberNotNullOnSetAccessor,
includeRequiresUnreferencedCodeOnSetAccessor,
forwardedAttributes.ToImmutable());
@@ -360,19 +415,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;
@@ -405,13 +460,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)
@@ -419,16 +474,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))
{
@@ -455,9 +510,9 @@ bool IsPropertyNameValidWithGeneratedMembers(string propertyName)
{
diagnostics.Add(
NotifyPropertyChangedForInvalidTargetError,
- fieldSymbol,
+ memberSymbol,
dependentPropertyName ?? "",
- fieldSymbol.ContainingType);
+ memberSymbol.ContainingType);
}
}
@@ -470,13 +525,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)
@@ -486,7 +541,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
@@ -515,7 +570,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") &&
@@ -545,9 +600,9 @@ bool IsCommandNameValidWithGeneratedMembers(string commandName)
{
diagnostics.Add(
NotifyCanExecuteChangedForInvalidTargetError,
- fieldSymbol,
+ memberSymbol,
commandName ?? "",
- fieldSymbol.ContainingType);
+ memberSymbol.ContainingType);
}
}
@@ -560,16 +615,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;
@@ -591,14 +646,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,
@@ -610,15 +665,16 @@ private static bool TryGetIsNotifyingRecipients(
if (hasOrInheritsClassLevelNotifyPropertyChangedRecipients)
{
diagnostics.Add(
- UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning,
- fieldSymbol,
- fieldSymbol.ContainingType,
- fieldSymbol.Name);
+ UnnecessaryNotifyPropertyChangedRecipientsAttributeOnMemberWarning,
+ memberSymbol,
+ memberSymbol.Kind.ToFieldOrPropertyKeyword(),
+ 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;
@@ -627,10 +683,10 @@ private static bool TryGetIsNotifyingRecipients(
// Otherwise just emit the diagnostic and then ignore the attribute
diagnostics.Add(
- InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError,
- fieldSymbol,
- fieldSymbol.ContainingType,
- fieldSymbol.Name);
+ InvalidContainingTypeForNotifyPropertyChangedRecipientsMemberError,
+ memberSymbol,
+ memberSymbol.ContainingType,
+ memberSymbol.Name);
isBroadcastTargetValid = false;
@@ -645,15 +701,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;
@@ -674,14 +730,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,
@@ -693,14 +749,15 @@ private static bool TryGetNotifyDataErrorInfo(
if (hasOrInheritsClassLevelNotifyDataErrorInfo)
{
diagnostics.Add(
- UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning,
- fieldSymbol,
- fieldSymbol.ContainingType,
- fieldSymbol.Name);
+ UnnecessaryNotifyDataErrorInfoAttributeOnMemberWarning,
+ memberSymbol,
+ memberSymbol.Kind.ToFieldOrPropertyKeyword(),
+ 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;
@@ -710,9 +767,10 @@ private static bool TryGetNotifyDataErrorInfo(
// Otherwise just emit the diagnostic and then ignore the attribute
diagnostics.Add(
MissingObservableValidatorInheritanceForNotifyDataErrorInfoError,
- fieldSymbol,
- fieldSymbol.ContainingType,
- fieldSymbol.Name);
+ memberSymbol,
+ memberSymbol.Kind.ToFieldOrPropertyKeyword(),
+ memberSymbol.ContainingType,
+ memberSymbol.Name);
isValidationTargetValid = false;
@@ -727,13 +785,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
@@ -745,7 +803,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 })
{
@@ -759,13 +817,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)
@@ -773,7 +831,19 @@ 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;
+
+ // 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.
@@ -795,10 +865,188 @@ 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");
}
+ ///
+ /// 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;
+ }
+
+ // 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)
+ {
+ // 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.
+ /// If the target member is not a property, it will use the defaults.
+ ///
+ /// 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(
+ MemberDeclarationSyntax memberSyntax,
+ ISymbol memberSymbol,
+ out Accessibility propertyAccessibility,
+ out Accessibility getterAccessibility,
+ out Accessibility setterAccessibility)
+ {
+ // 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.NotApplicable;
+ setterAccessibility = Accessibility.NotApplicable;
+
+ 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 (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 (propertySyntax.Modifiers.ContainsAnyAccessibilityModifiers())
+ {
+ propertyAccessibility = memberSymbol.DeclaredAccessibility;
+ }
+
+ // Track the accessors accessibility, if explicitly set
+ foreach (AccessorDeclarationSyntax accessor in propertySyntax.AccessorList?.Accessors ?? [])
+ {
+ if (!accessor.Modifiers.ContainsAnyAccessibilityModifiers())
+ {
+ 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.
///
@@ -838,11 +1086,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)
+ {
+ getterFieldIdentifierName = "field";
+ getterFieldExpression = setterFieldExpression = IdentifierName(getterFieldIdentifierName);
+ }
+ else if (propertyInfo.FieldName == "value")
{
- // We only need to add "this." when referencing the field in the setter (getter and XML docs are not ambiguous)
+ // 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);
@@ -862,6 +1116,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:
@@ -869,7 +1130,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf
// __oldValue = ;
setterStatements.Add(
LocalDeclarationStatement(
- VariableDeclaration(GetMaybeNullPropertyType(propertyInfo))
+ VariableDeclaration(GetPropertyTypeForOldValue(propertyInfo))
.AddVariables(
VariableDeclarator(Identifier("__oldValue"))
.WithInitializer(EqualsValueClause(setterFieldExpression)))));
@@ -1044,11 +1305,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:
//
@@ -1079,16 +1343,21 @@ 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]
//
- // public
+ //
// {
//
- // get => ;
+ // get => ;
//
// }
return
@@ -1099,12 +1368,13 @@ 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)
- .AddModifiers(Token(SyntaxKind.PublicKeyword))
+ .WithModifiers(propertyModifiers)
.AddAccessorListAccessors(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
+ .WithModifiers(propertyInfo.GetterAccessibility.ToSyntaxTokenList())
.WithExpressionBody(ArrowExpressionClause(getterFieldExpression))
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))
.AddAttributeLists(forwardedGetAccessorAttributes),
@@ -1145,7 +1415,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:
//
@@ -1231,19 +1501,43 @@ 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 .
///
/// 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).
// 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}?"),
@@ -1365,13 +1659,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 68801d449..9957f3bbe 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs
@@ -27,25 +27,25 @@ 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))
+ 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
- HierarchyInfo hierarchy = HierarchyInfo.From(fieldSymbol.ContainingType);
+ HierarchyInfo hierarchy = HierarchyInfo.From(context.TargetSymbol.ContainingType);
token.ThrowIfCancellationRequested();
_ = Execute.TryGetInfo(
- fieldDeclaration,
- fieldSymbol,
+ memberSyntax,
+ context.TargetSymbol,
context.SemanticModel,
context.GlobalOptions,
token,
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 1505b6865..cc4796a23 100644
--- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs
@@ -36,11 +36,11 @@ 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.
- // 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/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
new file mode 100644
index 000000000..0b65550d0
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs
@@ -0,0 +1,107 @@
+// 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.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+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.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 =>
+ {
+ // 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);
+ });
+ }
+
+ ///
+ /// 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;
+ }
+}
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..301b0e3f4
--- /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.None);
+ 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/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
new file mode 100644
index 000000000..dc0512312
--- /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.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 =>
+ {
+ // 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/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs
new file mode 100644
index 000000000..68a77d6d1
--- /dev/null
+++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs
@@ -0,0 +1,82 @@
+// 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.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]
+ 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;
+ }
+
+ // 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,
+ observablePropertyAttribute.GetLocation(),
+ ImmutableDictionary.Create()
+ .Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name)
+ .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)),
+ 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 431e5da40..74986d3ce 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.
///
@@ -117,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");
///
@@ -320,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");
///
@@ -368,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");
///
@@ -418,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");
///
@@ -480,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");
///
@@ -674,4 +679,68 @@ 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");
+
+ ///
+ /// 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");
+
+ ///
+ /// 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");
+
+ ///
+ /// 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");
}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs
new file mode 100644
index 000000000..cd7a8226b
--- /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.ProtectedKeyword)),
+ 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()
+ };
+ }
+}
diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs
index ac05bdff6..6a584bbfb 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.
@@ -108,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/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."),
+ };
+ }
+}
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);
+ }
}
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;
+ }
+}
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
diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj
index d3f647efd..311b435de 100644
--- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj
+++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj
@@ -66,8 +66,9 @@
-
-
+
+
+
@@ -116,10 +117,12 @@
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.
-->
-
-
+
+
+
+
\ No newline at end of file
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
{
///
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
{
}
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..c77c38b7b 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.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..2e63a49f9 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.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..665761405 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.UnitTests;
[TestClass]
public class Test_FieldReferenceForObservablePropertyFieldCodeFixer
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.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj
similarity index 70%
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..9dbd39d92 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
@@ -5,8 +5,13 @@
$(DefineConstants);ROSLYN_4_3_1_OR_GREATER
+
+
+
+
+
@@ -15,7 +20,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
new file mode 100644
index 000000000..15077e2aa
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net472;net6.0;net7.0;net8.0
+ $(DefineConstants);ROSLYN_4_3_1_OR_GREATER;ROSLYN_4_11_0_OR_GREATER
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
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..658d20aeb
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs
@@ -0,0 +1,852 @@
+// 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 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 = """
+ 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; }
+ }
+ """;
+
+#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 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);
+ }
+ }
+ """;
+#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));
+ }
+}
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..e38182eaf
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs
@@ -0,0 +1,274 @@
+// 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 Name { 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 Name { get; set; }
+ }
+ }
+ """;
+
+ 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 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()
+ {
+#if NET6_0_OR_GREATER
+ const string source = """
+ using CommunityToolkit.Mvvm.ComponentModel;
+
+ namespace MyApp
+ {
+ public partial class SampleViewModel : ObservableObject
+ {
+ [{|MVVMTK0043:ObservableProperty|}]
+ public partial string {|CS9248:Name|} { get; init; }
+ }
+ }
+ """;
+#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, [], ["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);
+ }
+}
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..6ecdabb23
--- /dev/null
+++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs
@@ -0,0 +1,653 @@
+// 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();
+ }
+
+ [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();
+ }
+
+ [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();
+ }
+
+ [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();
+ }
+
+ [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();
+ }
+}
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)
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);
+ }
+}
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()
diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs
index fc767e712..605651520 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()
@@ -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);
@@ -1903,9 +1917,9 @@ private 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, 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);
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