From 11d15c9dbd59e5ba6bbceb27dce39aeec3283e0a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:13:59 -0700 Subject: [PATCH 01/43] Add blank 'Roslyn4110' project --- dotnet Community Toolkit.sln | 23 +++++++++++++++++++ ...it.Mvvm.SourceGenerators.Roslyn4110.csproj | 6 +++++ 2 files changed, 29 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index 688e32367..f66684005 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -83,6 +83,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Exter EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj", "{FCC13AD5-CEB8-4CC1-8250-89B616D126F2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -457,6 +459,26 @@ Global {E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x64.Build.0 = Release|Any CPU {E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x86.ActiveCfg = Release|Any CPU {E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x86.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM64.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x64.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x86.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|Any CPU.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM64.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM64.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x64.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x64.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -495,6 +517,7 @@ Global tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{ecfe93aa-4b98-4292-b3fa-9430d513b4f9}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{f3799252-7a66-4533-89d8-b3c312052d95}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{fb59ce88-7732-4a63-b5bd-ac5681b7da1a}*SharedItemsImports = 13 + src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{fcc13ad5-ceb8-4cc1-8250-89b616d126f2}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{fe3ea695-ea0f-4e5f-9257-e059aaa23b10}*SharedItemsImports = 5 EndGlobalSection EndGlobal diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj new file mode 100644 index 000000000..3cea30b35 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj @@ -0,0 +1,6 @@ + + + + + + From e27359fbb080d89811e1bbda25e2f45c7f6e1d10 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:22:39 -0700 Subject: [PATCH 02/43] Fix naming conventions for all Roslyn projects --- dotnet Community Toolkit.sln | 16 ++++++++-------- .../CommunityToolkit.Mvvm.CodeFixers.csproj | 2 +- ...lkit.Mvvm.SourceGenerators.Roslyn4001.csproj} | 0 ...lkit.Mvvm.SourceGenerators.Roslyn4031.csproj} | 0 .../CommunityToolkit.Mvvm.SourceGenerators.props | 10 ++++++---- .../CommunityToolkit.Mvvm.csproj | 8 ++++---- ...sableINotifyPropertyChanging.UnitTests.csproj | 2 +- ...lkit.Mvvm.ExternalAssembly.Roslyn4001.csproj} | 2 +- ...lkit.Mvvm.ExternalAssembly.Roslyn4031.csproj} | 2 +- ...nityToolkit.Mvvm.Roslyn4001.UnitTests.csproj} | 4 ++-- ...nityToolkit.Mvvm.Roslyn4031.UnitTests.csproj} | 4 ++-- ...SourceGenerators.Roslyn4001.UnitTests.csproj} | 2 +- ...ncVoidReturningRelayCommandMethodCodeFixer.cs | 2 +- ...singAttributeInsteadOfInheritanceCodeFixer.cs | 2 +- ...ferenceForObservablePropertyFieldCodeFixer.cs | 2 +- ...SourceGenerators.Roslyn4031.UnitTests.csproj} | 2 +- 16 files changed, 31 insertions(+), 29 deletions(-) rename src/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.csproj} (100%) rename src/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn431/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.csproj} (100%) rename tests/{CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj => CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj} (81%) rename tests/{CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj => CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj} (77%) rename tests/{CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj => CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj} (80%) rename tests/{CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj => CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj} (80%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj} (92%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests}/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs (98%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests}/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs (99%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests}/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs (99%) rename tests/{CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj => CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj} (92%) diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index f66684005..651a17bce 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -36,7 +36,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{88C6FFBE-3 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Diagnostics", "src\CommunityToolkit.Diagnostics\CommunityToolkit.Diagnostics.csproj", "{76F89522-CA28-458D-801D-947AB033A758}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn401", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj", "{E24D1146-5AD8-498F-A518-4890D8BF4937}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.csproj", "{E24D1146-5AD8-498F-A518-4890D8BF4937}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Diagnostics.UnitTests", "tests\CommunityToolkit.Diagnostics.UnitTests\CommunityToolkit.Diagnostics.UnitTests.csproj", "{35E48D4D-6433-4B70-98A9-BA544921EE04}" EndProject @@ -61,25 +61,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Inter EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.SourceGenerators", "src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.shproj", "{5E7F1212-A54B-40CA-98C5-1FF5CD1A1638}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn431", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj", "{DF455C40-B18E-4890-8758-7CCCB5CA7052}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.csproj", "{DF455C40-B18E-4890-8758-7CCCB5CA7052}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.UnitTests", "tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.shproj", "{B8DCD82E-B53B-4249-AD4E-F9B99ACB9334}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn401.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn401.UnitTests\CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj", "{AD9C3223-8E37-4FD4-A0D4-A45119551D3A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn4001.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4001.UnitTests\CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj", "{AD9C3223-8E37-4FD4-A0D4-A45119551D3A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn431.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn431.UnitTests\CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj", "{5B44F7F1-DCA2-4776-924E-A266F7BBF753}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn4031.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4031.UnitTests\CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj", "{5B44F7F1-DCA2-4776-924E-A266F7BBF753}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.SourceGenerators.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.shproj", "{FB59CE88-7732-4A63-B5BD-AC5681B7DA1A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj", "{F3799252-7A66-4533-89D8-B3C312052D95}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj", "{F3799252-7A66-4533-89D8-B3C312052D95}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj", "{FE3EA695-EA0F-4E5F-9257-E059AAA23B10}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj", "{FE3EA695-EA0F-4E5F-9257-E059AAA23B10}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.ExternalAssembly", "tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.shproj", "{E827A9CD-405F-43E4-84C7-68CC7E845CDC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj", "{ECFE93AA-4B98-4292-B3FA-9430D513B4F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj", "{ECFE93AA-4B98-4292-B3FA-9430D513B4F9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj", "{4FCD501C-1BB5-465C-AD19-356DAB6600C6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj", "{4FCD501C-1BB5-465C-AD19-356DAB6600C6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" EndProject diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj index 498bfc4b3..01020c625 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.csproj similarity index 100% rename from src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj rename to src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.csproj diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.csproj similarity index 100% rename from src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj rename to src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.csproj diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props index 6c5a90f5e..0a2e05c33 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props @@ -16,20 +16,22 @@ --> - - $(MSBuildProjectName.Substring(0, $([MSBuild]::Subtract($(MSBuildProjectName.Length), 10)))) + + $(MSBuildProjectName.Substring(0, $([MSBuild]::Subtract($(MSBuildProjectName.Length), 11)))) - $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 3)), 1)) - $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 2)), 1)) + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 4)), 1)) + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 3)), 2)) $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 1)), 1)) $(MvvmToolkitSourceGeneratorRoslynMajorVersion).$(MvvmToolkitSourceGeneratorRoslynMinorVersion).$(MvvmToolkitSourceGeneratorRoslynPatchVersion) $(DefineConstants);ROSLYN_4_3_1_OR_GREATER + $(DefineConstants);ROSLYN_4_11_0_OR_GREATER diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index d3f647efd..bb1b40b8e 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -66,8 +66,8 @@ - - + + @@ -116,8 +116,8 @@ Even though the fixer only references the 4.0.1 generator target, both versions export the same APIs that the code fixer project needs, and Roslyn versions are also forward compatible. --> - - + + diff --git a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj index 76f768ef0..9a4086826 100644 --- a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj @@ -12,7 +12,7 @@ - + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj similarity index 81% rename from tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj rename to tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj index ee36d9f08..ae18e53f9 100644 --- a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj +++ b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj similarity index 77% rename from tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj rename to tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj index 164e42e50..4a58a1151 100644 --- a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj +++ b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj similarity index 80% rename from tests/CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj index 115f23003..2ce1d0992 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj @@ -15,9 +15,9 @@ - + - + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj similarity index 80% rename from tests/CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj index c1973e0eb..460adb7b6 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj @@ -15,9 +15,9 @@ - + - + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj similarity index 92% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj index c1717ef47..584c015e3 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj @@ -16,7 +16,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs similarity index 98% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs index 5062572d1..cff9df992 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.AsyncVoidReturningRelayCommandMethodCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; [TestClass] public class Test_AsyncVoidReturningRelayCommandMethodCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs similarity index 99% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs index 018c5fc63..a2d3e920d 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.ClassUsingAttributeInsteadOfInheritanceCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; [TestClass] public class ClassUsingAttributeInsteadOfInheritanceCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs similarity index 99% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs index 596a11b66..9771ccd08 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.FieldReferenceForObservablePropertyFieldCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; [TestClass] public class Test_FieldReferenceForObservablePropertyFieldCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj similarity index 92% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index 7323b4a69..cbea561a0 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj @@ -15,7 +15,7 @@ - + From a67a31dfb0ca08986d6c2c16e69c73218e6a6ff8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:37:24 -0700 Subject: [PATCH 03/43] Add 'global.json' file --- global.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 000000000..1880a952c --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.403", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} \ No newline at end of file From 9c9061d50469ea538cd6377de119d76b9a615cdb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:42:05 -0700 Subject: [PATCH 04/43] Update .targets for Roslyn setup --- .../CommunityToolkit.Mvvm.SourceGenerators.targets | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets index aa4b8966f..1106146fa 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets @@ -86,6 +86,7 @@ removes and removes all analyzers except the highest version that is supported. The fallback is just Roslyn 4.0. --> + roslyn4.11 roslyn4.3 roslyn4.0 From f4fdb276e1ff472594ad0a9b11f35336d6efa118 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:42:13 -0700 Subject: [PATCH 05/43] Pack new source generator in MVVM Toolkit --- src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index bb1b40b8e..311b435de 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -68,6 +68,7 @@ + @@ -118,8 +119,10 @@ --> + + \ No newline at end of file From c8e794beeefdf89c95770727f149da6a588d4f5d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 19:57:43 -0700 Subject: [PATCH 06/43] Add logic to match partial properties --- .../ObservablePropertyGenerator.Execute.cs | 114 ++++++++++++++++++ .../ObservablePropertyGenerator.cs | 2 +- .../Extensions/SyntaxNodeExtensions.cs | 28 +++++ 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index b127bc7a3..007de9e02 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -30,6 +30,66 @@ partial class ObservablePropertyGenerator /// internal static class Execute { + /// + /// Checks whether an input syntax node is a candidate property declaration for the generator. + /// + /// The input syntax node to check. + /// The used to cancel the operation, if needed. + /// Whether is a candidate property declaration. + public static bool IsCandidatePropertyDeclaration(SyntaxNode node, CancellationToken token) + { + // Matches a valid field declaration, for legacy support + static bool IsCandidateField(SyntaxNode node) + { + return node is VariableDeclaratorSyntax { + Parent: VariableDeclarationSyntax { + Parent: FieldDeclarationSyntax { + Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }; + } + +#if ROSLYN_4_11_0_OR_GREATER + // Matches a valid partial property declaration + static bool IsCandidateProperty(SyntaxNode node) + { + // The node must be a property declaration with two accessors + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors } property) + { + return false; + } + + // The property must be partial (we'll check that it's a declaration from its symbol) + if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + return false; + } + + // The accessors must be a get and a set (with any accessibility) + if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || + accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) + { + return false; + } + + return true; + } + + // We only support matching properties on Roslyn 4.11 and greater + if (!IsCandidateField(node) && !IsCandidateProperty(node)) + { + return false; + } +#else + // Otherwise, we only support matching fields + if (!IsCandidateField(node)) + { + return false; + } +#endif + + // The property must be in a type with a base type (as it must derive from ObservableObject) + return node.Parent?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; + } + /// /// Processes a given field. /// @@ -799,6 +859,60 @@ private static void GetNullabilityInfo( semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } + /// + /// Tries to get the accessibility of the property and accessors, if possible. + /// + /// The input node. + /// The input instance. + /// The accessibility of the property, if available. + /// The accessibility of the accessor, if available. + /// The accessibility of the accessor, if available. + /// Whether the property was valid and the accessibilities could be retrieved. + private static bool TryGetAccessibilityModifiers( + PropertyDeclarationSyntax node, + IPropertySymbol symbol, + out Accessibility declaredAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility) + { + declaredAccessibility = Accessibility.NotApplicable; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; + + // Ensure that we have a getter and a setter, and that the setter is not init-only + if (symbol is not { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + { + return false; + } + + // Track the property accessibility if explicitly set + if (node.Modifiers.Count > 0) + { + declaredAccessibility = symbol.DeclaredAccessibility; + } + + // Track the accessors accessibility, if explicitly set + foreach (AccessorDeclarationSyntax accessor in node.AccessorList?.Accessors ?? []) + { + if (accessor.Modifiers.Count == 0) + { + continue; + } + + switch (accessor.Kind()) + { + case SyntaxKind.GetAccessorDeclaration: + getterAccessibility = getMethod.DeclaredAccessibility; + break; + case SyntaxKind.SetAccessorDeclaration: + setterAccessibility = setMethod.DeclaredAccessibility; + break; + } + } + + return true; + } + /// /// Gets a instance with the cached args for property changing notifications. /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 68801d449..df24ac51a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -27,7 +27,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> propertyInfoWithErrors = context.ForAttributeWithMetadataNameAndOptions( "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute", - static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, + Execute.IsCandidatePropertyDeclaration, static (context, token) => { if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs index 52b7ccbfc..c7a08da9d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -30,4 +32,30 @@ public static bool IsFirstSyntaxDeclarationForSymbol(this SyntaxNode syntaxNode, syntaxReference.SyntaxTree == syntaxNode.SyntaxTree && syntaxReference.Span == syntaxNode.Span; } + + /// + /// Checks whether a given is a given type declaration with or potentially with any base types, using only syntax. + /// + /// The type of declaration to check for. + /// The input to check. + /// Whether is a given type declaration with or potentially with any base types. + public static bool IsTypeDeclarationWithOrPotentiallyWithBaseTypes(this SyntaxNode node) + where T : TypeDeclarationSyntax + { + // Immediately bail if the node is not a type declaration of the specified type + if (node is not T typeDeclaration) + { + return false; + } + + // If the base types list is not empty, the type can definitely has implemented interfaces + if (typeDeclaration.BaseList is { Types.Count: > 0 }) + { + return true; + } + + // If the base types list is empty, check if the type is partial. If it is, it means + // that there could be another partial declaration with a non-empty base types list. + return typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword); + } } From 76264f78759f70ae18fdbf146960674fd4935b8e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 21:27:49 -0700 Subject: [PATCH 07/43] Update the generator to also work on properties --- .../ObservablePropertyGenerator.Execute.cs | 337 +++++++++++------- .../ObservablePropertyGenerator.cs | 8 +- .../Extensions/CompilationExtensions.cs | 10 + 3 files changed, 232 insertions(+), 123 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 007de9e02..aac15ed5b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -39,27 +39,38 @@ internal static class Execute public static bool IsCandidatePropertyDeclaration(SyntaxNode node, CancellationToken token) { // Matches a valid field declaration, for legacy support - static bool IsCandidateField(SyntaxNode node) + static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) { - return node is VariableDeclaratorSyntax { - Parent: VariableDeclarationSyntax { - Parent: FieldDeclarationSyntax { - Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }; + // The node must represent a field declaration + if (node is not VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { AttributeLists.Count: > 0 } fieldNode } }) + { + containingTypeNode = null; + + return false; + } + + containingTypeNode = (TypeDeclarationSyntax?)fieldNode.Parent; + + return true; } #if ROSLYN_4_11_0_OR_GREATER // Matches a valid partial property declaration - static bool IsCandidateProperty(SyntaxNode node) + static bool IsCandidateProperty(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) { // The node must be a property declaration with two accessors - if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors } property) + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) { + containingTypeNode = null; + return false; } // The property must be partial (we'll check that it's a declaration from its symbol) if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) { + containingTypeNode = null; + return false; } @@ -67,43 +78,91 @@ static bool IsCandidateProperty(SyntaxNode node) if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) { + containingTypeNode = null; + return false; } + containingTypeNode = (TypeDeclarationSyntax?)property.Parent; + return true; } // We only support matching properties on Roslyn 4.11 and greater - if (!IsCandidateField(node) && !IsCandidateProperty(node)) + if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && !IsCandidateProperty(node, out parentNode)) { return false; } #else // Otherwise, we only support matching fields - if (!IsCandidateField(node)) + if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode)) { return false; } #endif - // The property must be in a type with a base type (as it must derive from ObservableObject) - return node.Parent?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; + // The candidate member must be in a type with a base type (as it must derive from ObservableObject) + return parentNode?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; } /// - /// Processes a given field. + /// Checks whether a given candidate node is valid given a compilation. /// - /// The instance to process. - /// The input instance to process. + /// The instance to process. + /// The instance for the current run. + /// Whether is valid. + public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel semanticModel) + { + // At least C# 8 is always required + if (!semanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) + { + return false; + } + + // If the target is a property, we only support using C# preview. + // This is because the generator is relying on the 'field' keyword. + if (node is PropertyDeclarationSyntax && !semanticModel.Compilation.IsLanguageVersionPreview()) + { + return false; + } + + // All other cases are supported, the syntax filter is already validating that + return true; + } + + /// + /// Gets the candidate after the initial filtering. + /// + /// The input syntax node to convert. + /// The resulting instance. + public static MemberDeclarationSyntax GetCandidateMemberDeclaration(SyntaxNode node) + { + // If the node is a property declaration, just return it directly. Note that we don't have + // to check whether we're using Roslyn 4.11 here, as if that's not the case all of these + // syntax nodes would already have pre-filtered well before this method could run at all. + if (node is PropertyDeclarationSyntax propertySyntax) + { + return propertySyntax; + } + + // Otherwise, assume all targets are field declarations + return (MemberDeclarationSyntax)node.Parent!.Parent!; + } + + /// + /// Processes a given field or property. + /// + /// The instance to process. + /// The input instance to process. /// The instance for the current run. /// The options in use for the generator. /// The cancellation token for the current operation. /// The resulting value, if successfully retrieved. /// The resulting diagnostics from the processing operation. - /// The resulting instance for , if successful. + /// The resulting instance for , if successful. public static bool TryGetInfo( - FieldDeclarationSyntax fieldSyntax, - IFieldSymbol fieldSymbol, + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, SemanticModel semanticModel, AnalyzerConfigOptions options, CancellationToken token, @@ -113,13 +172,13 @@ public static bool TryGetInfo( using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); // Validate the target type - if (!IsTargetTypeValid(fieldSymbol, out bool shouldInvokeOnPropertyChanging)) + if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging)) { builder.Add( InvalidContainingTypeForObservablePropertyFieldError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); propertyInfo = null; diagnostics = builder.ToImmutable(); @@ -135,18 +194,18 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Get the property type and name - string typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); - string fieldName = fieldSymbol.Name; - string propertyName = GetGeneratedPropertyName(fieldSymbol); + string typeNameWithNullabilityAnnotations = GetPropertyType(memberSymbol).GetFullyQualifiedNameWithNullabilityAnnotations(); + string fieldName = memberSymbol.Name; + string propertyName = GetGeneratedPropertyName(memberSymbol); // Check for name collisions if (fieldName == propertyName) { builder.Add( ObservablePropertyNameCollisionError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); propertyInfo = null; diagnostics = builder.ToImmutable(); @@ -160,13 +219,13 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Check for special cases that are explicitly not allowed - if (IsGeneratedPropertyInvalid(propertyName, fieldSymbol.Type)) + if (IsGeneratedPropertyInvalid(propertyName, GetPropertyType(memberSymbol))) { builder.Add( InvalidObservablePropertyError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); propertyInfo = null; diagnostics = builder.ToImmutable(); @@ -185,13 +244,13 @@ public static bool TryGetInfo( bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients = false; bool hasOrInheritsClassLevelNotifyDataErrorInfo = false; bool hasAnyValidationAttributes = false; - bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName); + bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(memberSymbol, propertyName); token.ThrowIfCancellationRequested(); // Get the nullability info for the property GetNullabilityInfo( - fieldSymbol, + memberSymbol, semanticModel, out bool isReferenceTypeOrUnconstraindTypeParameter, out bool includeMemberNotNullOnSetAccessor); @@ -202,7 +261,7 @@ public static bool TryGetInfo( propertyChangedNames.Add(propertyName); // Get the class-level [NotifyPropertyChangedRecipients] setting, if any - if (TryGetIsNotifyingRecipients(fieldSymbol, out bool isBroadcastTargetValid)) + if (TryGetIsNotifyingRecipients(memberSymbol, out bool isBroadcastTargetValid)) { notifyRecipients = isBroadcastTargetValid; hasOrInheritsClassLevelNotifyPropertyChangedRecipients = true; @@ -211,7 +270,7 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Get the class-level [NotifyDataErrorInfo] setting, if any - if (TryGetNotifyDataErrorInfo(fieldSymbol, out bool isValidationTargetValid)) + if (TryGetNotifyDataErrorInfo(memberSymbol, out bool isValidationTargetValid)) { notifyDataErrorInfo = isValidationTargetValid; hasOrInheritsClassLevelNotifyDataErrorInfo = true; @@ -220,19 +279,19 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Gather attributes info - foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) + foreach (AttributeData attributeData in memberSymbol.GetAttributes()) { token.ThrowIfCancellationRequested(); // Gather dependent property and command names - if (TryGatherDependentPropertyChangedNames(fieldSymbol, attributeData, in propertyChangedNames, in builder) || - TryGatherDependentCommandNames(fieldSymbol, attributeData, in notifiedCommandNames, in builder)) + if (TryGatherDependentPropertyChangedNames(memberSymbol, attributeData, in propertyChangedNames, in builder) || + TryGatherDependentCommandNames(memberSymbol, attributeData, in notifiedCommandNames, in builder)) { continue; } // Check whether the property should also notify recipients - if (TryGetIsNotifyingRecipients(fieldSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyPropertyChangedRecipients, out isBroadcastTargetValid)) + if (TryGetIsNotifyingRecipients(memberSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyPropertyChangedRecipients, out isBroadcastTargetValid)) { notifyRecipients = isBroadcastTargetValid; @@ -240,13 +299,20 @@ public static bool TryGetInfo( } // Check whether the property should also be validated - if (TryGetNotifyDataErrorInfo(fieldSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyDataErrorInfo, out isValidationTargetValid)) + if (TryGetNotifyDataErrorInfo(memberSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyDataErrorInfo, out isValidationTargetValid)) { notifyDataErrorInfo = isValidationTargetValid; continue; } + // The following checks only apply to fields, not properties. That is, attributes + // on partial properties are never forwarded, as they are already on the member. + if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) + { + continue; + } + // Track the current attribute for forwarding if it is a validation attribute if (attributeData.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) { @@ -274,8 +340,16 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Gather explicit forwarded attributes info - foreach (AttributeListSyntax attributeList in fieldSyntax.AttributeLists) + foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) { + // For properties, we never need to forward any attributes with explicit targets either, because + // they can already "just work" when used with 'field'. As for 'get' and 'set', they can just be + // added directly to the partial declarations of the property accessors. + if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) + { + continue; + } + // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will // normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic // suppressor that recognizes uses of this target specifically to support [ObservableProperty]. @@ -307,7 +381,7 @@ public static bool TryGetInfo( builder.Add( InvalidPropertyTargetedAttributeOnObservablePropertyField, attribute, - fieldSymbol, + memberSymbol, attribute.Name); continue; @@ -321,7 +395,7 @@ public static bool TryGetInfo( builder.Add( InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, attribute, - fieldSymbol, + memberSymbol, attribute.Name); continue; @@ -335,13 +409,13 @@ public static bool TryGetInfo( // Log the diagnostic for missing ObservableValidator, if needed if (hasAnyValidationAttributes && - !fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + !memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { builder.Add( MissingObservableValidatorInheritanceForValidationAttributeError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name, + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name, forwardedAttributes.Count); } @@ -350,9 +424,9 @@ public static bool TryGetInfo( { builder.Add( MissingValidationAttributesForNotifyDataErrorInfoError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); } token.ThrowIfCancellationRequested(); @@ -420,19 +494,19 @@ public static bool GetEnableINotifyPropertyChangingSupport(AnalyzerConfigOptions /// /// Validates the containing type for a given field being annotated. /// - /// The input instance to process. + /// The input instance to process. /// Whether or not property changing events should also be raised. - /// Whether or not the containing type for is valid. - private static bool IsTargetTypeValid(IFieldSymbol fieldSymbol, out bool shouldInvokeOnPropertyChanging) + /// Whether or not the containing type for is valid. + private static bool IsTargetTypeValid(ISymbol memberSymbol, out bool shouldInvokeOnPropertyChanging) { // The [ObservableProperty] attribute can only be used in types that are known to expose the necessary OnPropertyChanged and OnPropertyChanging methods. // That means that the containing type for the field needs to match one of the following conditions: // - It inherits from ObservableObject (in which case it also implements INotifyPropertyChanging). // - It has the [ObservableObject] attribute (on itself or any of its base types). // - It has the [INotifyPropertyChanged] attribute (on itself or any of its base types). - bool isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject"); - bool hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute"); - bool hasINotifyPropertyChangedAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"); + bool isObservableObject = memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject"); + bool hasObservableObjectAttribute = memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute"); + bool hasINotifyPropertyChangedAttribute = memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"); shouldInvokeOnPropertyChanging = isObservableObject || hasObservableObjectAttribute; @@ -465,13 +539,13 @@ private static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol /// /// Tries to gather dependent properties from the given attribute. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The target collection of dependent property names to populate. /// The current collection of gathered diagnostics. /// Whether or not was an attribute containing any dependent properties. private static bool TryGatherDependentPropertyChangedNames( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder propertyChangedNames, in ImmutableArrayBuilder diagnostics) @@ -479,16 +553,16 @@ private static bool TryGatherDependentPropertyChangedNames( // Validates a property name using existing properties bool IsPropertyNameValid(string propertyName) { - return fieldSymbol.ContainingType.GetAllMembers(propertyName).OfType().Any(); + return memberSymbol.ContainingType.GetAllMembers(propertyName).OfType().Any(); } // Validate a property name including generated properties too bool IsPropertyNameValidWithGeneratedMembers(string propertyName) { - foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers()) + foreach (ISymbol member in memberSymbol.ContainingType.GetAllMembers()) { if (member is IFieldSymbol otherFieldSymbol && - !SymbolEqualityComparer.Default.Equals(fieldSymbol, otherFieldSymbol) && + !SymbolEqualityComparer.Default.Equals(memberSymbol, otherFieldSymbol) && otherFieldSymbol.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") && propertyName == GetGeneratedPropertyName(otherFieldSymbol)) { @@ -515,9 +589,9 @@ bool IsPropertyNameValidWithGeneratedMembers(string propertyName) { diagnostics.Add( NotifyPropertyChangedForInvalidTargetError, - fieldSymbol, + memberSymbol, dependentPropertyName ?? "", - fieldSymbol.ContainingType); + memberSymbol.ContainingType); } } @@ -530,13 +604,13 @@ bool IsPropertyNameValidWithGeneratedMembers(string propertyName) /// /// Tries to gather dependent commands from the given attribute. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The target collection of dependent command names to populate. /// The current collection of gathered diagnostics. /// Whether or not was an attribute containing any dependent commands. private static bool TryGatherDependentCommandNames( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder notifiedCommandNames, in ImmutableArrayBuilder diagnostics) @@ -546,7 +620,7 @@ bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembe { // Each target must be a string matching the name of a property from the containing type of the annotated field, and the // property must be of type IRelayCommand, or any type that implements that interface (to avoid generating invalid code). - if (fieldSymbol.ContainingType.GetAllMembers(commandName).OfType().FirstOrDefault() is IPropertySymbol propertySymbol) + if (memberSymbol.ContainingType.GetAllMembers(commandName).OfType().FirstOrDefault() is IPropertySymbol propertySymbol) { // If there is a property member with the specified name, check that it's valid. If it isn't, the // target is definitely not valid, and the additional checks below can just be skipped. The property @@ -575,7 +649,7 @@ bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembe // Validate a command name including generated command too bool IsCommandNameValidWithGeneratedMembers(string commandName) { - foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers()) + foreach (ISymbol member in memberSymbol.ContainingType.GetAllMembers()) { if (member is IMethodSymbol methodSymbol && methodSymbol.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") && @@ -605,9 +679,9 @@ bool IsCommandNameValidWithGeneratedMembers(string commandName) { diagnostics.Add( NotifyCanExecuteChangedForInvalidTargetError, - fieldSymbol, + memberSymbol, commandName ?? "", - fieldSymbol.ContainingType); + memberSymbol.ContainingType); } } @@ -620,16 +694,16 @@ bool IsCommandNameValidWithGeneratedMembers(string commandName) /// /// Checks whether a given generated property should also notify recipients. /// - /// The input instance to process. + /// The input instance to process. /// Whether or not the the property is in a valid target that can notify recipients. - /// Whether or not the generated property for is in a type annotated with [NotifyPropertyChangedRecipients]. - private static bool TryGetIsNotifyingRecipients(IFieldSymbol fieldSymbol, out bool isBroadcastTargetValid) + /// Whether or not the generated property for is in a type annotated with [NotifyPropertyChangedRecipients]. + private static bool TryGetIsNotifyingRecipients(ISymbol memberSymbol, out bool isBroadcastTargetValid) { - if (fieldSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") == true) + if (memberSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") == true) { // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || - fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || + memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) { isBroadcastTargetValid = true; @@ -651,14 +725,14 @@ private static bool TryGetIsNotifyingRecipients(IFieldSymbol fieldSymbol, out bo /// /// Checks whether a given generated property should also notify recipients. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The current collection of gathered diagnostics. - /// Indicates wether the containing type of has or inherits [NotifyPropertyChangedRecipients]. + /// Indicates wether the containing type of has or inherits [NotifyPropertyChangedRecipients]. /// Whether or not the the property is in a valid target that can notify recipients. - /// Whether or not the generated property for used [NotifyPropertyChangedRecipients]. + /// Whether or not the generated property for used [NotifyPropertyChangedRecipients]. private static bool TryGetIsNotifyingRecipients( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder diagnostics, bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients, @@ -671,14 +745,14 @@ private static bool TryGetIsNotifyingRecipients( { diagnostics.Add( UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); } // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || - fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || + memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) { isBroadcastTargetValid = true; @@ -688,9 +762,9 @@ private static bool TryGetIsNotifyingRecipients( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); isBroadcastTargetValid = false; @@ -705,15 +779,15 @@ private static bool TryGetIsNotifyingRecipients( /// /// Checks whether a given generated property should also validate its value. /// - /// The input instance to process. + /// The input instance to process. /// Whether or not the the property is in a valid target that can validate values. - /// Whether or not the generated property for used [NotifyDataErrorInfo]. - private static bool TryGetNotifyDataErrorInfo(IFieldSymbol fieldSymbol, out bool isValidationTargetValid) + /// Whether or not the generated property for used [NotifyDataErrorInfo]. + private static bool TryGetNotifyDataErrorInfo(ISymbol memberSymbol, out bool isValidationTargetValid) { - if (fieldSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") == true) + if (memberSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") == true) { // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { isValidationTargetValid = true; @@ -734,14 +808,14 @@ private static bool TryGetNotifyDataErrorInfo(IFieldSymbol fieldSymbol, out bool /// /// Checks whether a given generated property should also validate its value. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The current collection of gathered diagnostics. - /// Indicates wether the containing type of has or inherits [NotifyDataErrorInfo]. + /// Indicates whether the containing type of has or inherits [NotifyDataErrorInfo]. /// Whether or not the the property is in a valid target that can validate values. - /// Whether or not the generated property for used [NotifyDataErrorInfo]. + /// Whether or not the generated property for used [NotifyDataErrorInfo]. private static bool TryGetNotifyDataErrorInfo( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder diagnostics, bool hasOrInheritsClassLevelNotifyDataErrorInfo, @@ -754,13 +828,13 @@ private static bool TryGetNotifyDataErrorInfo( { diagnostics.Add( UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); } // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { isValidationTargetValid = true; @@ -770,9 +844,9 @@ private static bool TryGetNotifyDataErrorInfo( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( MissingObservableValidatorInheritanceForNotifyDataErrorInfoError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); isValidationTargetValid = false; @@ -787,13 +861,13 @@ private static bool TryGetNotifyDataErrorInfo( /// /// Checks whether the generated code has to directly reference the old property value. /// - /// The input instance to process. + /// The input instance to process. /// The name of the property being generated. /// Whether the generated code needs direct access to the old property value. - private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbol, string propertyName) + private static bool IsOldPropertyValueDirectlyReferenced(ISymbol memberSymbol, string propertyName) { // Check OnChanging( oldValue, newValue) first - foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changing")) + foreach (ISymbol symbol in memberSymbol.ContainingType.GetMembers($"On{propertyName}Changing")) { // No need to be too specific as we're not expecting false positives (which also wouldn't really // cause any problems anyway, just produce slightly worse codegen). Just checking the number of @@ -805,7 +879,7 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo } // Do the same for OnChanged( oldValue, newValue) - foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changed")) + foreach (ISymbol symbol in memberSymbol.ContainingType.GetMembers($"On{propertyName}Changed")) { if (symbol is IMethodSymbol { Parameters.Length: 2 }) { @@ -819,13 +893,13 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo /// /// Gets the nullability info on the generated property /// - /// The input instance to process. + /// The input instance to process. /// The instance for the current run. /// Whether the property type supports nullability. /// Whether should be used on the setter. /// private static void GetNullabilityInfo( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, SemanticModel semanticModel, out bool isReferenceTypeOrUnconstraindTypeParameter, out bool includeMemberNotNullOnSetAccessor) @@ -833,7 +907,7 @@ private static void GetNullabilityInfo( // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. // This will cover both reference types as well T when the constraints are not struct or unmanaged. // If this is true, it means the field storage can potentially be in a null state (even if not annotated). - isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType; + isReferenceTypeOrUnconstraindTypeParameter = !GetPropertyType(memberSymbol).IsValueType; // This is used to avoid nullability warnings when setting the property from a constructor, in case the field // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. @@ -855,7 +929,7 @@ private static void GetNullabilityInfo( // Of course, this can only be the case if the field type is also of a type that could be in a null state. includeMemberNotNullOnSetAccessor = isReferenceTypeOrUnconstraindTypeParameter && - fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && + GetPropertyType(memberSymbol).NullableAnnotation != NullableAnnotation.Annotated && semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } @@ -1345,6 +1419,23 @@ public static ImmutableArray GetOnPropertyChangeMethods onPropertyChanged2Declaration); } + /// + /// Gets the for a given member symbol (it can be either a field or a property). + /// + /// The input instance to process. + /// The type of . + private static ITypeSymbol GetPropertyType(ISymbol memberSymbol) + { + // Check if the member is a property first + if (memberSymbol is IPropertySymbol propertySymbol) + { + return propertySymbol.Type; + } + + // Otherwise, the only possible case is a field symbol + return ((IFieldSymbol)memberSymbol).Type; + } + /// /// Gets the for the type of a given property, when it can possibly be . /// @@ -1479,13 +1570,19 @@ private static FieldDeclarationSyntax CreateFieldDeclaration(string fullyQualifi } /// - /// Get the generated property name for an input field. + /// Get the generated property name for an input field or property. /// - /// The input instance to process. - /// The generated property name for . - public static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol) + /// The input instance to process. + /// The generated property name for . + public static string GetGeneratedPropertyName(ISymbol memberSymbol) { - string propertyName = fieldSymbol.Name; + // If the input is a property, just always match the name exactly + if (memberSymbol is IPropertySymbol propertySymbol) + { + return propertySymbol.Name; + } + + string propertyName = memberSymbol.Name; if (propertyName.StartsWith("m_")) { diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index df24ac51a..4b650c716 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -30,12 +30,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Execute.IsCandidatePropertyDeclaration, static (context, token) => { - if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) + MemberDeclarationSyntax memberSyntax = Execute.GetCandidateMemberDeclaration(context.TargetNode); + + // Validate that the candidate is valid for the current compilation + if (!Execute.IsCandidateValidForCompilation(memberSyntax, context.SemanticModel)) { return default; } - FieldDeclarationSyntax fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; IFieldSymbol fieldSymbol = (IFieldSymbol)context.TargetSymbol; // Get the hierarchy info for the target symbol, and try to gather the property info @@ -44,7 +46,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); _ = Execute.TryGetInfo( - fieldDeclaration, + memberSyntax, fieldSymbol, context.SemanticModel, context.GlobalOptions, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs index ac05bdff6..8f3c06b93 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs @@ -27,6 +27,16 @@ public static bool HasLanguageVersionAtLeastEqualTo(this Compilation compilation return ((CSharpCompilation)compilation).LanguageVersion >= languageVersion; } + /// + /// Checks whether a given compilation (assumed to be for C#) is using the preview language version. + /// + /// The to consider for analysis. + /// Whether is using the preview language version. + public static bool IsLanguageVersionPreview(this Compilation compilation) + { + return ((CSharpCompilation)compilation).LanguageVersion == LanguageVersion.Preview; + } + /// /// /// Checks whether or not a type with a specified metadata name is accessible from a given instance. From 3df53f345436d14bef8b1e39346b4aa8890123d2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 21:39:53 -0700 Subject: [PATCH 08/43] Add 'RequiresCSharpLanguageVersionPreviewAnalyzer' --- .../AnalyzerReleases.Shipped.md | 8 +++ ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...resCSharpLanguageVersionPreviewAnalyzer.cs | 65 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 16 +++++ 4 files changed, 90 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 53e58bfea..ce431cea7 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -75,3 +75,11 @@ MVVMTK0039 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warn Rule ID | Category | Severity | Notes --------|----------|----------|------- MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0040 + +## Release 8.4.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 9dc77b7d1..19661727b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -46,6 +46,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs new file mode 100644 index 000000000..1b7a96114 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_11_0_OR_GREATER + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates errors when a property using [ObservableProperty] on a partial property is in a project with the C# language version not set to preview. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class RequiresCSharpLanguageVersionPreviewAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [CSharpLanguageVersionIsNotPreviewForObservableProperty]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // If the language version is set to preview, we'll never emit diagnostics + if (context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We only want to target partial property definitions (also include non-partial ones for diagnostics) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null }) + { + return; + } + + // If the property is using [ObservableProperty], emit the diagnostic + if (context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + context.ReportDiagnostic(Diagnostic.Create( + CSharpLanguageVersionIsNotPreviewForObservableProperty, + observablePropertyAttribute.GetLocation(), + context.Symbol)); + } + }, SymbolKind.Property); + }); + } +} + +#endif \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 431e5da40..caa733463 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -674,4 +674,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "The backing fields of auto-properties cannot be annotated with [ObservableProperty] (the attribute can only be used directly on fields, and the generator will then handle generating the corresponding property).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0040"); + + /// + /// Gets a for a CanvasEffect property with invalid accessors. + /// + /// Format: "Using [ObservableProperty] on partial properties requires the C# language version to be set to 'preview', as support for the 'field' keyword is needed by the source generators to emit valid code (add preview to your .csproj/.props file)". + /// + /// + public static readonly DiagnosticDescriptor CSharpLanguageVersionIsNotPreviewForObservableProperty = new( + id: "MVVMTK0041", + title: "C# language version is not 'preview'", + messageFormat: """Using [ObservableProperty] on partial properties requires the C# language version to be set to 'preview', as support for the 'field' keyword is needed by the source generators to emit valid code (add preview to your .csproj/.props file)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The C# language version must be set to 'preview' when using [ObservableProperty] on partial properties for the source generators to emit valid code (the preview option must be set in the .csproj/.props file).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0041"); } From fff5f91b627839471da02a409e134db6c9a6debc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 21:51:06 -0700 Subject: [PATCH 09/43] Suppress warnings about removed rules --- .../CommunityToolkit.Mvvm.SourceGenerators.props | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props index 0a2e05c33..c29a655e2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props @@ -32,6 +32,9 @@ $(DefineConstants);ROSLYN_4_3_1_OR_GREATER $(DefineConstants);ROSLYN_4_11_0_OR_GREATER + + + $(NoWarn);RS2003 From 41dc834719b92d1f565a412597caba12a756b28b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 22:54:28 -0700 Subject: [PATCH 10/43] Add blank test project for new generator --- dotnet Community Toolkit.sln | 24 +++++++++++++++++++ ...urceGenerators.Roslyn4110.UnitTests.csproj | 23 ++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index 651a17bce..8a94f3c65 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -85,6 +85,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeF EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.csproj", "{FCC13AD5-CEB8-4CC1-8250-89B616D126F2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj", "{C342302D-A263-42D6-B8EE-01DEF8192690}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -479,6 +481,26 @@ Global {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x64.Build.0 = Release|Any CPU {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.ActiveCfg = Release|Any CPU {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM64.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x64.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x64.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x86.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x86.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|Any CPU.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM64.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM64.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x64.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x64.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x86.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -501,6 +523,7 @@ Global {E827A9CD-405F-43E4-84C7-68CC7E845CDC} = {B30036C4-D514-4E5B-A323-587A061772CE} {ECFE93AA-4B98-4292-B3FA-9430D513B4F9} = {B30036C4-D514-4E5B-A323-587A061772CE} {4FCD501C-1BB5-465C-AD19-356DAB6600C6} = {B30036C4-D514-4E5B-A323-587A061772CE} + {C342302D-A263-42D6-B8EE-01DEF8192690} = {B30036C4-D514-4E5B-A323-587A061772CE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5403B0C4-F244-4F73-A35C-FE664D0F4345} @@ -511,6 +534,7 @@ Global src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{5e7f1212-a54b-40ca-98c5-1ff5cd1a1638}*SharedItemsImports = 13 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{ad9c3223-8e37-4fd4-a0d4-a45119551d3a}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{b8dcd82e-b53b-4249-ad4e-f9b99acb9334}*SharedItemsImports = 13 + tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{c342302d-a263-42d6-b8ee-01def8192690}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{df455c40-b18e-4890-8758-7cccb5ca7052}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{e24d1146-5ad8-498f-a518-4890d8bf4937}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{e827a9cd-405f-43e4-84c7-68cc7e845cdc}*SharedItemsImports = 13 diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj new file mode 100644 index 000000000..73bec0bc0 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -0,0 +1,23 @@ + + + + net472;net6.0;net7.0;net8.0 + $(DefineConstants);ROSLYN_4_11_0_OR_GREATER + + + + + + + + + + + + + + + + + + \ No newline at end of file From 34a7c6f6080f092fac60df604f2eb79e7cae9fff Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 23:54:07 -0700 Subject: [PATCH 11/43] Update '[ObservableProperty]' attribute --- .../Attributes/ObservablePropertyAttribute.cs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs index 0e765267a..c6ae78763 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs @@ -8,7 +8,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// -/// An attribute that indicates that a given field should be wrapped by a generated observable property. +/// An attribute that indicates that a given partial property should be implemented by the source generator. /// In order to use this attribute, the containing type has to inherit from , or it /// must be using or . /// If the containing type also implements the (that is, if it either inherits from @@ -20,10 +20,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// partial class MyViewModel : ObservableObject /// { /// [ObservableProperty] -/// private string name; +/// public partial string name { get; set; } /// /// [ObservableProperty] -/// private bool isEnabled; +/// public partial bool IsEnabled { get; set; } /// } /// /// @@ -31,27 +31,43 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Name +/// public partial string Name /// { -/// get => name; -/// set => SetProperty(ref name, value); +/// get => field; +/// set => SetProperty(ref field, value); /// } /// -/// public bool IsEnabled +/// public partial bool IsEnabled /// { -/// get => isEnabled; -/// set => SetProperty(ref isEnabled, value); +/// get => field; +/// set => SetProperty(ref field, value); /// } /// } /// /// /// +/// +/// In order to use this attribute on partial properties, the .NET 9 SDK is required, and C# preview must +/// be used. If that is not available, this attribute can be used to annotate fields instead, like so: +/// +/// partial class MyViewModel : ObservableObject +/// { +/// [ObservableProperty] +/// private string name; +/// +/// [ObservableProperty] +/// private bool isEnabled; +/// } +/// +/// +/// /// The generated properties will automatically use the UpperCamelCase format for their names, /// which will be derived from the field names. The generator can also recognize fields using either /// the _lowerCamel or m_lowerCamel naming scheme. Otherwise, the first character in the /// source field name will be converted to uppercase (eg. isEnabled to IsEnabled). +/// /// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class ObservablePropertyAttribute : Attribute { } From bbd59ec247975817a9f9621ec24e83b2dc35a560 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 22 Oct 2024 23:55:53 -0700 Subject: [PATCH 12/43] Add unit tests for new analyzer --- ...urceGenerators.Roslyn4110.UnitTests.csproj | 2 +- .../Test_SourceGeneratorsDiagnostics.cs | 50 +++++++++++++++++++ .../Test_SourceGeneratorsDiagnostics.cs | 2 +- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj index 73bec0bc0..394a10614 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -2,7 +2,7 @@ net472;net6.0;net7.0;net8.0 - $(DefineConstants);ROSLYN_4_11_0_OR_GREATER + $(DefineConstants);ROSLYN_4_3_1_OR_GREATER;ROSLYN_4_11_0_OR_GREATER diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs new file mode 100644 index 000000000..03a3f323d --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +partial class Test_SourceGeneratorsDiagnostics +{ + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0041:ObservableProperty|}] + public string Bar { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp12); + } + + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsPreview_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public string Bar { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, languageVersion: LanguageVersion.Preview); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index fc767e712..0ab363974 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -19,7 +19,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] -public class Test_SourceGeneratorsDiagnostics +public partial class Test_SourceGeneratorsDiagnostics { [TestMethod] public void DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError_Explicit() From f970ba6c0b72bb44d06e666b578a4386d3bb806f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 19:23:39 -0700 Subject: [PATCH 13/43] Move forwarded attributes gathering to helper --- .../ObservablePropertyGenerator.Execute.cs | 158 +++++++++++------- 1 file changed, 93 insertions(+), 65 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index aac15ed5b..82415d7b1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -339,71 +339,15 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); - // Gather explicit forwarded attributes info - foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) - { - // For properties, we never need to forward any attributes with explicit targets either, because - // they can already "just work" when used with 'field'. As for 'get' and 'set', they can just be - // added directly to the partial declarations of the property accessors. - if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) - { - continue; - } - - // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will - // normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic - // suppressor that recognizes uses of this target specifically to support [ObservableProperty]. - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.GetKeyword or SyntaxKind.SetKeyword) targetIdentifier) - { - continue; - } - - token.ThrowIfCancellationRequested(); - - foreach (AttributeSyntax attribute in attributeList.Attributes) - { - // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. - // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: - // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not - // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. - // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. - // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. - // - We then go over each attribute argument expression and get the operation for it. This will still be available even - // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all - // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) - // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. - // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. - // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the - // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the - // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) - { - builder.Add( - InvalidPropertyTargetedAttributeOnObservablePropertyField, - attribute, - memberSymbol, - attribute.Name); - - continue; - } - - IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) - { - builder.Add( - InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, - attribute, - memberSymbol, - attribute.Name); - - continue; - } - - forwardedAttributes.Add(attributeInfo); - } - } + // Also gather any forwarded attributes on the annotated member, if it is a field. + // This method will not do anything for properties, as those don't support this. + GatherLegacyForwardedAttributes( + memberSyntax, + memberSymbol, + semanticModel, + in forwardedAttributes, + in builder, + token); token.ThrowIfCancellationRequested(); @@ -933,6 +877,90 @@ private static void GetNullabilityInfo( semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } + /// + /// Gathers all forwarded attributes from the given member syntax. + /// + /// The instance to process. + /// The input instance to process. + /// The instance for the current run. + /// The collection of forwarded attributes to add new ones to. + /// The current collection of gathered diagnostics. + /// The cancellation token for the current operation. + private static void GatherLegacyForwardedAttributes( + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, + SemanticModel semanticModel, + in ImmutableArrayBuilder forwardedAttributes, + in ImmutableArrayBuilder diagnostics, + CancellationToken token) + { + // For properties, we never need to forward any attributes with explicit targets either, because + // they can already "just work" when used with 'field'. As for 'get' and 'set', they can just be + // added directly to the partial declarations of the property accessors. + if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) + { + return; + } + + // Gather explicit forwarded attributes info + foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) + { + // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will + // normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic + // suppressor that recognizes uses of this target specifically to support [ObservableProperty]. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.GetKeyword or SyntaxKind.SetKeyword) targetIdentifier) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. + // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: + // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not + // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. + // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. + // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. + // - We then go over each attribute argument expression and get the operation for it. This will still be available even + // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all + // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) + // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. + // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. + // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the + // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the + // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) + { + diagnostics.Add( + InvalidPropertyTargetedAttributeOnObservablePropertyField, + attribute, + memberSymbol, + attribute.Name); + + continue; + } + + IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); + + // Try to extract the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) + { + diagnostics.Add( + InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, + attribute, + memberSymbol, + attribute.Name); + + continue; + } + + forwardedAttributes.Add(attributeInfo); + } + } + } + /// /// Tries to get the accessibility of the property and accessors, if possible. /// From e5b28029500302baa4a43edf41c75d81247f6d99 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 19:35:36 -0700 Subject: [PATCH 14/43] Gather accessibility information from nodes --- .../ComponentModel/Models/PropertyInfo.cs | 7 +++ .../ObservablePropertyGenerator.Execute.cs | 59 +++++++++++++++---- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 8c3bca4da..20e98ba13 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Mvvm.SourceGenerators.Helpers; +using Microsoft.CodeAnalysis; namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; @@ -12,6 +13,9 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// The type name for the generated property, including nullability annotations. /// The field name. /// The generated property name. +/// The accessibility of the property. +/// The accessibility of the accessor. +/// The accessibility of the accessor. /// The sequence of property changing properties to notify. /// The sequence of property changed properties to notify. /// The sequence of commands to notify. @@ -26,6 +30,9 @@ internal sealed record PropertyInfo( string TypeNameWithNullabilityAnnotations, string FieldName, string PropertyName, + Accessibility PropertyAccessibility, + Accessibility GetterAccessibility, + Accessibility SetterAccessibility, EquatableArray PropertyChangingNames, EquatableArray PropertyChangedNames, EquatableArray NotifiedCommandNames, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 82415d7b1..412b14349 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -394,10 +394,29 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + // Retrieve the accessibility values for all components + if (!TryGetAccessibilityModifiers( + memberSyntax, + memberSymbol, + out Accessibility propertyAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility)) + { + propertyInfo = null; + diagnostics = builder.ToImmutable(); + + return false; + } + + token.ThrowIfCancellationRequested(); + propertyInfo = new PropertyInfo( typeNameWithNullabilityAnnotations, fieldName, propertyName, + propertyAccessibility, + getterAccessibility, + setterAccessibility, effectivePropertyChangingNames, effectivePropertyChangedNames, notifiedCommandNames.ToImmutable(), @@ -963,38 +982,56 @@ private static void GatherLegacyForwardedAttributes( /// /// Tries to get the accessibility of the property and accessors, if possible. + /// If the target member is not a property, it will use the defaults. /// - /// The input node. - /// The input instance. - /// The accessibility of the property, if available. + /// The instance to process. + /// The input instance to process. + /// The accessibility of the property, if available. /// The accessibility of the accessor, if available. /// The accessibility of the accessor, if available. /// Whether the property was valid and the accessibilities could be retrieved. private static bool TryGetAccessibilityModifiers( - PropertyDeclarationSyntax node, - IPropertySymbol symbol, - out Accessibility declaredAccessibility, + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, + out Accessibility propertyAccessibility, out Accessibility getterAccessibility, out Accessibility setterAccessibility) { - declaredAccessibility = Accessibility.NotApplicable; + // For legacy support for fields, all accessibilities are public. + // To customize the accessibility, partial properties should be used. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + propertyAccessibility = Accessibility.Public; + getterAccessibility = Accessibility.Public; + setterAccessibility = Accessibility.Public; + + return true; + } + + propertyAccessibility = Accessibility.NotApplicable; getterAccessibility = Accessibility.NotApplicable; setterAccessibility = Accessibility.NotApplicable; // Ensure that we have a getter and a setter, and that the setter is not init-only - if (symbol is not { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + if (memberSymbol is not IPropertySymbol { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + { + return false; + } + + // At this point the node is definitely a property, just do a sanity check + if (memberSyntax is not PropertyDeclarationSyntax propertySyntax) { return false; } // Track the property accessibility if explicitly set - if (node.Modifiers.Count > 0) + if (propertySyntax.Modifiers.Count > 0) { - declaredAccessibility = symbol.DeclaredAccessibility; + propertyAccessibility = memberSymbol.DeclaredAccessibility; } // Track the accessors accessibility, if explicitly set - foreach (AccessorDeclarationSyntax accessor in node.AccessorList?.Accessors ?? []) + foreach (AccessorDeclarationSyntax accessor in propertySyntax.AccessorList?.Accessors ?? []) { if (accessor.Modifiers.Count == 0) { From 17263fe7ce8559bcc0766423de936772841fe03a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 21:11:29 -0700 Subject: [PATCH 15/43] Generalizing the generated accessibility modifiers --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 23 +++++++----- .../Extensions/AccessibilityExtensions.cs | 35 +++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 19661727b..3ab9f89d6 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -52,6 +52,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 412b14349..40f749c17 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -997,13 +997,14 @@ private static bool TryGetAccessibilityModifiers( out Accessibility getterAccessibility, out Accessibility setterAccessibility) { - // For legacy support for fields, all accessibilities are public. - // To customize the accessibility, partial properties should be used. + // For legacy support for fields, the property that is generated is public, and neither + // accessors will have any accessibility modifiers. To customize the accessibility, + // partial properties should be used instead. if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) { propertyAccessibility = Accessibility.Public; - getterAccessibility = Accessibility.Public; - setterAccessibility = Accessibility.Public; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; return true; } @@ -1297,11 +1298,14 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // Prepare the setter for the generated property: // - // set + // set // { // // } - AccessorDeclarationSyntax setAccessor = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithBody(Block(setterIfStatement)); + AccessorDeclarationSyntax setAccessor = + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithModifiers(propertyInfo.SetterAccessibility.ToSyntaxTokenList()) + .WithBody(Block(setterIfStatement)); // Add the [MemberNotNull] attribute if needed: // @@ -1338,10 +1342,10 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // - // public + // // { // - // get => ; + // get => ; // // } return @@ -1355,9 +1359,10 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .AddAttributeLists(forwardedPropertyAttributes) - .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .WithModifiers(propertyInfo.PropertyAccessibility.ToSyntaxTokenList()) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithModifiers(propertyInfo.GetterAccessibility.ToSyntaxTokenList()) .WithExpressionBody(ArrowExpressionClause(getterFieldExpression)) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) .AddAttributeLists(forwardedGetAccessorAttributes), diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs new file mode 100644 index 000000000..09ef24907 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AccessibilityExtensions +{ + /// + /// Converts a given value to the equivalent ."/> + /// + /// The input value to convert. + /// The representing the modifiers for . + public static SyntaxTokenList ToSyntaxTokenList(this Accessibility accessibility) + { + return accessibility switch + { + Accessibility.NotApplicable => TokenList(), + Accessibility.Private => TokenList(Token(SyntaxKind.PrivateKeyword)), + Accessibility.ProtectedAndInternal => TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.InternalKeyword)), + Accessibility.Protected => TokenList(Token(SyntaxKind.ProtectedKeyword)), + Accessibility.Internal => TokenList(Token(SyntaxKind.InternalKeyword)), + Accessibility.ProtectedOrInternal => TokenList(Token(SyntaxKind.ProtectedKeyword), Token(SyntaxKind.InternalKeyword)), + Accessibility.Public => TokenList(Token(SyntaxKind.PublicKeyword)), + _ => TokenList() + }; + } +} From 5f2ffcf4c0e43ef48f68dda23f708985a79478d5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 21:15:43 -0700 Subject: [PATCH 16/43] Don't emit an error for collisions for properties --- .../ComponentModel/Models/PropertyInfo.cs | 4 ++-- .../ObservablePropertyGenerator.Execute.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 20e98ba13..21f4f5e2d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -22,7 +22,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// Whether or not the generated property also broadcasts changes. /// Whether or not the generated property also validates its value. /// Whether the old property value is being directly referenced. -/// Indicates whether the property is of a reference type or an unconstrained type parameter. +/// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether to include nullability annotations on the setter. /// Indicates whether to annotate the setter as requiring unreferenced code. /// The sequence of forwarded attributes for the generated property. @@ -39,7 +39,7 @@ internal sealed record PropertyInfo( bool NotifyPropertyChangedRecipients, bool NotifyDataErrorInfo, bool IsOldPropertyValueDirectlyReferenced, - bool IsReferenceTypeOrUnconstraindTypeParameter, + bool IsReferenceTypeOrUnconstrainedTypeParameter, bool IncludeMemberNotNullOnSetAccessor, bool IncludeRequiresUnreferencedCodeOnSetAccessor, EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 40f749c17..1eb438977 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -198,8 +198,8 @@ public static bool TryGetInfo( string fieldName = memberSymbol.Name; string propertyName = GetGeneratedPropertyName(memberSymbol); - // Check for name collisions - if (fieldName == propertyName) + // Check for name collisions (only for fields) + if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) { builder.Add( ObservablePropertyNameCollisionError, @@ -252,7 +252,7 @@ public static bool TryGetInfo( GetNullabilityInfo( memberSymbol, semanticModel, - out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool isReferenceTypeOrUnconstrainedTypeParameter, out bool includeMemberNotNullOnSetAccessor); token.ThrowIfCancellationRequested(); @@ -423,7 +423,7 @@ public static bool TryGetInfo( notifyRecipients, notifyDataErrorInfo, isOldPropertyValueDirectlyReferenced, - isReferenceTypeOrUnconstraindTypeParameter, + isReferenceTypeOrUnconstrainedTypeParameter, includeMemberNotNullOnSetAccessor, includeRequiresUnreferencedCodeOnSetAccessor, forwardedAttributes.ToImmutable()); @@ -1518,7 +1518,7 @@ private static TypeSyntax GetMaybeNullPropertyType(PropertyInfo propertyInfo) // happen when the property is first set to some value that is not null (but the backing field would still be so). // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. - return propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch + return propertyInfo.IsReferenceTypeOrUnconstrainedTypeParameter switch { true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"), From 46d03e83b439decc7903c8d4f8f4b5bbd654a6aa Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 21:27:57 -0700 Subject: [PATCH 17/43] Update generation to account for properties --- .../ComponentModel/Models/PropertyInfo.cs | 3 ++ .../ObservablePropertyGenerator.Execute.cs | 35 ++++++++++++++----- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 21f4f5e2d..367d9a751 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -4,12 +4,14 @@ using CommunityToolkit.Mvvm.SourceGenerators.Helpers; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// /// A model representing an generated property /// +/// The syntax kind of the annotated member that triggered this property generation. /// The type name for the generated property, including nullability annotations. /// The field name. /// The generated property name. @@ -27,6 +29,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// Indicates whether to annotate the setter as requiring unreferenced code. /// The sequence of forwarded attributes for the generated property. internal sealed record PropertyInfo( + SyntaxKind AnnotatedMemberKind, string TypeNameWithNullabilityAnnotations, string FieldName, string PropertyName, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 1eb438977..f9838da46 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -411,6 +411,7 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); propertyInfo = new PropertyInfo( + memberSyntax.Kind(), typeNameWithNullabilityAnnotations, fieldName, propertyName, @@ -1092,11 +1093,17 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf ExpressionSyntax getterFieldExpression; ExpressionSyntax setterFieldExpression; - // In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments - // with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter. - if (propertyInfo.FieldName == "value") + // If the annotated member is a partial property, we always use the 'field' keyword + if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) { - // We only need to add "this." when referencing the field in the setter (getter and XML docs are not ambiguous) + getterFieldIdentifierName = "field"; + getterFieldExpression = setterFieldExpression = IdentifierName(getterFieldIdentifierName); + } + else if (propertyInfo.FieldName == "value") + { + // In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments + // with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter. We only need + // to add "this." when referencing the field in the setter (getter and XML docs are not ambiguous) getterFieldIdentifierName = "value"; getterFieldExpression = IdentifierName(getterFieldIdentifierName); setterFieldExpression = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), (IdentifierNameSyntax)getterFieldExpression); @@ -1116,6 +1123,13 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf getterFieldExpression = setterFieldExpression = IdentifierName(getterFieldIdentifierName); } + // Prepare the XML docs: + // - For partial properties, always just inherit from the partial declaration + // - For fields, inherit from them + string xmlSummary = propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration + ? "/// " + : $"/// "; + if (propertyInfo.NotifyPropertyChangedRecipients || propertyInfo.IsOldPropertyValueDirectlyReferenced) { // Store the old value for later. This code generates a statement as follows: @@ -1336,13 +1350,18 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // Also add any forwarded attributes setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes); + // Prepare the modifiers for the property + SyntaxTokenList propertyModifiers = propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration + ? propertyInfo.PropertyAccessibility.ToSyntaxTokenList().Add(Token(SyntaxKind.PartialKeyword)) + : propertyInfo.PropertyAccessibility.ToSyntaxTokenList(); + // Construct the generated property as follows: // - // /// + // // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // - // + // // { // // get => ; @@ -1356,10 +1375,10 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), + .WithOpenBracketToken(Token(TriviaList(Comment(xmlSummary)), SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .AddAttributeLists(forwardedPropertyAttributes) - .WithModifiers(propertyInfo.PropertyAccessibility.ToSyntaxTokenList()) + .WithModifiers(propertyModifiers) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithModifiers(propertyInfo.GetterAccessibility.ToSyntaxTokenList()) From 52ad254adef21c290e0a43d4bacd415a9095d5ce Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 22:00:03 -0700 Subject: [PATCH 18/43] Fix a generator crash when used on properties --- .../ComponentModel/ObservablePropertyGenerator.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 4b650c716..9957f3bbe 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -38,16 +38,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return default; } - IFieldSymbol fieldSymbol = (IFieldSymbol)context.TargetSymbol; - // Get the hierarchy info for the target symbol, and try to gather the property info - HierarchyInfo hierarchy = HierarchyInfo.From(fieldSymbol.ContainingType); + HierarchyInfo hierarchy = HierarchyInfo.From(context.TargetSymbol.ContainingType); token.ThrowIfCancellationRequested(); _ = Execute.TryGetInfo( memberSyntax, - fieldSymbol, + context.TargetSymbol, context.SemanticModel, context.GlobalOptions, token, From 8ca2f3c8cfb6507313bcc874df06f9dc61347da0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 22:00:28 -0700 Subject: [PATCH 19/43] Omit implicit accessibility modifiers --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 4 +-- .../Extensions/SyntaxTokenListExtensions.cs | 32 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 3ab9f89d6..5a44bf69d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -66,6 +66,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index f9838da46..63402cca2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -1027,7 +1027,7 @@ private static bool TryGetAccessibilityModifiers( } // Track the property accessibility if explicitly set - if (propertySyntax.Modifiers.Count > 0) + if (propertySyntax.Modifiers.ContainsAnyAccessibilityModifiers()) { propertyAccessibility = memberSymbol.DeclaredAccessibility; } @@ -1035,7 +1035,7 @@ private static bool TryGetAccessibilityModifiers( // Track the accessors accessibility, if explicitly set foreach (AccessorDeclarationSyntax accessor in propertySyntax.AccessorList?.Accessors ?? []) { - if (accessor.Modifiers.Count == 0) + if (!accessor.Modifiers.ContainsAnyAccessibilityModifiers()) { continue; } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs new file mode 100644 index 000000000..99c1ecb85 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxTokenListExtensions +{ + /// + /// Checks whether a given value contains any accessibility modifiers. + /// + /// The input value to check. + /// Whether contains any accessibility modifiers. + public static bool ContainsAnyAccessibilityModifiers(this SyntaxTokenList syntaxList) + { + foreach (SyntaxToken token in syntaxList) + { + if (SyntaxFacts.IsAccessibilityModifier(token.Kind())) + { + return true; + } + } + + return false; + } +} From db221edcf872dc9273a9d9ac71240e13f9ab40da Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 22:00:52 -0700 Subject: [PATCH 20/43] Update the targets of additional attributes --- .../NotifyCanExecuteChangedForAttribute.cs | 17 +++++++----- .../NotifyDataErrorInfoAttribute.cs | 22 +++++++++------ .../NotifyPropertyChangedForAttribute.cs | 27 ++++++++++--------- 3 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyCanExecuteChangedForAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyCanExecuteChangedForAttribute.cs index 748281015..33f0b3fb2 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyCanExecuteChangedForAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyCanExecuteChangedForAttribute.cs @@ -12,8 +12,8 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// An attribute that can be used to support properties in generated properties. When this attribute is /// used, the generated property setter will also call for the properties specified /// in the attribute data, causing the validation logic for the command to be executed again. This can be useful to keep the code compact -/// when there are one or more dependent commands that should also be notified when a property is updated. If this attribute is used in -/// a field without , it is ignored (just like ). +/// when there are one or more dependent commands that should also be notified when a property is updated. If this attribute is used on +/// a property without , it is ignored (just like ). /// /// In order to use this attribute, the target property has to implement the interface. /// @@ -24,7 +24,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// { /// [ObservableProperty] /// [NotifyCanExecuteChangedFor(nameof(GreetUserCommand))] -/// private string name; +/// public partial string Name { get; set; } /// /// public IRelayCommand GreetUserCommand { get; } /// } @@ -34,12 +34,12 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Name +/// public partial string Name /// { -/// get => name; +/// get => field; /// set /// { -/// if (SetProperty(ref name, value)) +/// if (SetProperty(ref field, value)) /// { /// GreetUserCommand.NotifyCanExecuteChanged(); /// } @@ -48,7 +48,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// } /// /// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)] +/// +/// Just like , this attribute can also be used on fields as well. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] public sealed class NotifyCanExecuteChangedForAttribute : Attribute { /// diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyDataErrorInfoAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyDataErrorInfoAttribute.cs index 2aa7b1159..6a741d601 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyDataErrorInfoAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyDataErrorInfoAttribute.cs @@ -8,7 +8,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// An attribute that can be used to support in generated properties, when applied to -/// fields contained in a type that is inheriting from and using any validation attributes. +/// partial properties contained in a type that is inheriting from and using any validation attributes. /// When this attribute is used, the generated property setter will also call . /// This allows generated properties to opt-in into validation behavior without having to fallback into a full explicit observable property. /// @@ -20,7 +20,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// [NotifyDataErrorInfo] /// [Required] /// [MinLength(2)] -/// private string username; +/// public partial string Username { get; set; } /// } /// /// @@ -28,17 +28,23 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// [Required] -/// [MinLength(2)] -/// public string Username +/// public partial string Username /// { -/// get => username; -/// set => SetProperty(ref username, value, validate: true); +/// get => field; +/// set => SetProperty(ref field, value, validate: true); /// } /// } /// /// -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +/// +/// +/// This attribute can also be used on a class, which will enable the validation on all generated properties contained in it. +/// +/// +/// Just like , this attribute can also be used on fields as well. +/// +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class NotifyDataErrorInfoAttribute : Attribute { } diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs index 09178f9f5..d963ed72c 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs @@ -13,7 +13,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// used, the generated property setter will also call (or the equivalent /// method in the target class) for the properties specified in the attribute data. This can be useful to keep the code compact when /// there are one or more dependent properties that should also be reported as updated when the value of the annotated observable -/// property is changed. If this attribute is used in a field without , it is ignored. +/// property is changed. If this attribute is used on a property without , it is ignored. /// /// In order to use this attribute, the containing type has to implement the interface /// and expose a method with the same signature as . If the containing @@ -27,11 +27,11 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// { /// [ObservableProperty] /// [NotifyPropertyChangedFor(nameof(FullName))] -/// private string name; +/// public partial string Name { get; set; } /// /// [ObservableProperty] /// [NotifyPropertyChangedFor(nameof(FullName))] -/// private string surname; +/// public partial string Surname { get; set; } /// /// public string FullName => $"{Name} {Surname}"; /// } @@ -41,17 +41,17 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Name +/// public partial string Name /// { -/// get => name; +/// get => field; /// set /// { -/// if (!EqualityComparer<string>.Default.Equals(name, value)) +/// if (!EqualityComparer<string>.Default.Equals(field, value)) /// { /// OnPropertyChanging(nameof(Name)); /// OnPropertyChanged(nameof(FullName)); /// -/// name = value; +/// field = value; /// /// OnPropertyChanged(nameof(Name)); /// OnPropertyChanged(nameof(FullName)); @@ -59,17 +59,17 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// } /// } /// -/// public string Surname +/// public partial string Surname /// { -/// get => surname; +/// get => field; /// set /// { -/// if (!EqualityComparer<string>.Default.Equals(name, value)) +/// if (!EqualityComparer<string>.Default.Equals(field, value)) /// { /// OnPropertyChanging(nameof(Surname)); /// OnPropertyChanged(nameof(FullName)); /// -/// surname = value; +/// field = value; /// /// OnPropertyChanged(nameof(Surname)); /// OnPropertyChanged(nameof(FullName)); @@ -79,7 +79,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// } /// /// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)] +/// +/// Just like , this attribute can also be used on fields as well. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] public sealed class NotifyPropertyChangedForAttribute : Attribute { /// From f9f77719a39b24e1c19e500571543d312070471f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 23 Oct 2024 23:17:30 -0700 Subject: [PATCH 21/43] Fix handling notify data error info --- .../ObservablePropertyGenerator.Execute.cs | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 63402cca2..91d805706 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -306,34 +306,17 @@ public static bool TryGetInfo( continue; } - // The following checks only apply to fields, not properties. That is, attributes - // on partial properties are never forwarded, as they are already on the member. - if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) - { - continue; - } - // Track the current attribute for forwarding if it is a validation attribute if (attributeData.AttributeClass?.InheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") == true) { hasAnyValidationAttributes = true; - forwardedAttributes.Add(AttributeInfo.Create(attributeData)); - } - - // Also track the current attribute for forwarding if it is of any of the following types: - // - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute) - // - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute) - // - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute) - // - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute) - // - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute) - if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || - attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + // Only forward the attribute if the target is a field. + // Otherwise, the attribute is already applied correctly. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + } } } @@ -922,6 +905,27 @@ private static void GatherLegacyForwardedAttributes( return; } + // Also track the current attribute for forwarding if it is of any of the following types: + // - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute) + // - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute) + // - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute) + // - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute) + // - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute) + // + // All of these have special handling and are always forwarded when a field is being targeted. + // That is because these attributes really only mean anything when used on generated properties. + foreach (AttributeData attributeData in memberSymbol.GetAttributes()) + { + if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || + attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true) + { + forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + } + } + // Gather explicit forwarded attributes info foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) { From 5d37b7322fc117ca46bb11e77869a1a4adb64626 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 00:32:21 -0700 Subject: [PATCH 22/43] Fix nullability for generated partial properties --- .../ObservablePropertyGenerator.Execute.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 91d805706..813dcd977 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -856,6 +856,18 @@ private static void GetNullabilityInfo( // If this is true, it means the field storage can potentially be in a null state (even if not annotated). isReferenceTypeOrUnconstraindTypeParameter = !GetPropertyType(memberSymbol).IsValueType; + // Special case if the target member is a partial property. In this case, the type should always match the + // declared type of the property declaration, and there is no need for the attribute on the setter. This + // is because assigning the property in the constructor will directly assign to the backing field, and not + // doing so from the constructor will cause Roslyn to emit a warning. Additionally, Roslyn can always see + // that the backing field is being assigned from the setter, so the attribute is just never needed here. + if (memberSymbol.Kind is SymbolKind.Property) + { + includeMemberNotNullOnSetAccessor = false; + + return; + } + // This is used to avoid nullability warnings when setting the property from a constructor, in case the field // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. // Consider this example: @@ -1141,7 +1153,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // __oldValue = ; setterStatements.Add( LocalDeclarationStatement( - VariableDeclaration(GetMaybeNullPropertyType(propertyInfo)) + VariableDeclaration(GetPropertyTypeForOldValue(propertyInfo)) .AddVariables( VariableDeclarator(Identifier("__oldValue")) .WithInitializer(EqualsValueClause(setterFieldExpression))))); @@ -1426,7 +1438,7 @@ public static ImmutableArray GetOnPropertyChangeMethods .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); // Get the type for the 'oldValue' parameter (which can be null on first invocation) - TypeSyntax oldValueTypeSyntax = GetMaybeNullPropertyType(propertyInfo); + TypeSyntax oldValueTypeSyntax = GetPropertyTypeForOldValue(propertyInfo); // Construct the generated method as follows: // @@ -1534,8 +1546,15 @@ private static ITypeSymbol GetPropertyType(ISymbol memberSymbol) /// /// The input instance to process. /// The type of a given property, when it can possibly be - private static TypeSyntax GetMaybeNullPropertyType(PropertyInfo propertyInfo) + private static TypeSyntax GetPropertyTypeForOldValue(PropertyInfo propertyInfo) { + // For partial properties, the old value always matches the exact property type. + // See additional notes for this in the 'GetNullabilityInfo' method above. + if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) + { + return IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); + } + // Prepare the nullable type for the previous property value. This is needed because if the type is a reference // type, the previous value might be null even if the property type is not nullable, as the first invocation would // happen when the property is first set to some value that is not null (but the backing field would still be so). From aabcd1596022545a8e3493036cb0c0d82f7a7fe4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 00:32:34 -0700 Subject: [PATCH 23/43] Add initial codegen tests for partial properties --- .../Test_SourceGeneratorsCodegen.cs | 718 ++++++++++++++++++ .../Test_SourceGeneratorsCodegen.cs | 2 +- 2 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs new file mode 100644 index 000000000..62539c403 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -0,0 +1,718 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +partial class Test_SourceGeneratorsCodegen +{ + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithNoModifiers_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + partial int Number { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + partial int Number + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly1() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly2() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + internal partial int Number { private get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal partial int Number + { + private get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithReferenceType_NotNullable_OnPartialProperty_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithReferenceType_Nullable_OnPartialProperty_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public partial string? Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyPropertyChange_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + public partial string Name { get; set; } + + public string FullName => ""; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FullName); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyCanExecuteChange_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + + namespace MyApp; + + partial class MyViewModel : ObservableRecipient + { + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(TestCommand))] + public partial string Name { get; set; } + + public IRelayCommand TestCommand => null; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + TestCommand.NotifyCanExecuteChanged(); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyRecipients_WorksCorrectly() + { + string source = """ + using System; + using System.Windows.Input; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableRecipient + { + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial string Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + string __oldValue = field; + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + Broadcast(__oldValue, value, "Name"); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyDataErrorInfo_WorksCorrectly() + { + string source = """ + using System; + using System.ComponentModel.DataAnnotations; + using System.Windows.Input; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableValidator + { + [ObservableProperty] + [NotifyDataErrorInfo] + [Required] + public partial string Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index 6997cfaa1..d3d44ec5b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -16,7 +16,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] -public class Test_SourceGeneratorsCodegen +public partial class Test_SourceGeneratorsCodegen { [TestMethod] public void ObservablePropertyWithNonNullableReferenceType_EmitsMemberNotNullAttribute() From 62c06f11131d59f0fb3863663c91fec45ddb5390 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 11:44:32 -0700 Subject: [PATCH 24/43] Add 'UseObservablePropertyOnPartialPropertyAnalyzer' --- .../AnalyzerReleases.Shipped.md | 1 + ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...resCSharpLanguageVersionPreviewAnalyzer.cs | 2 +- ...rvablePropertyOnPartialPropertyAnalyzer.cs | 70 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 21 ++++++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index ce431cea7..d77a3a005 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -83,3 +83,4 @@ MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator Rule ID | Category | Severity | Notes --------|----------|----------|------- MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 +MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 5a44bf69d..5df7ef0e0 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -47,6 +47,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs index 1b7a96114..aa96c9bef 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs @@ -13,7 +13,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// -/// A diagnostic analyzer that generates errors when a property using [ObservableProperty] on a partial property is in a project with the C# language version not set to preview. +/// A diagnostic analyzer that generates errors when a property using [ObservableProperty] on a partial property is in a project with the C# language version not set to preview. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class RequiresCSharpLanguageVersionPreviewAnalyzer : DiagnosticAnalyzer diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs new file mode 100644 index 000000000..a45b0bde3 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_11_0_OR_GREATER + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates a suggestion whenever [ObservableProperty] is used on a field when a partial property could be used instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseObservablePropertyOnPartialPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UseObservablePropertyOnPartialProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Using [ObservableProperty] on partial properties is only supported when using C# preview. + // As such, if that is not the case, return immediately, as no diagnostic should be produced. + if (!context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We're intentionally only looking for fields here + if (context.Symbol is not IFieldSymbol fieldSymbol) + { + return; + } + + // Check that we are in fact using [ObservableProperty] + if (!fieldSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Emit the diagnostic for this field to suggest changing to a partial property instead + context.ReportDiagnostic(Diagnostic.Create( + UseObservablePropertyOnPartialProperty, + observablePropertyAttribute.GetLocation(), + fieldSymbol.ContainingType, + fieldSymbol)); + }, SymbolKind.Field); + }); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index caa733463..f68a7ed72 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -34,6 +34,11 @@ internal static class DiagnosticDescriptors /// public const string AsyncVoidReturningRelayCommandMethodId = "MVVMTK0039"; + /// + /// The diagnostic id for . + /// + public const string UseObservablePropertyOnPartialPropertyId = "MVVMTK0042"; + /// /// Gets a indicating when a duplicate declaration of would happen. /// @@ -690,4 +695,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "The C# language version must be set to 'preview' when using [ObservableProperty] on partial properties for the source generators to emit valid code (the preview option must be set in the .csproj/.props file).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0041"); + + /// + /// Gets a for a CanvasEffect property with invalid accessors. + /// + /// Format: "The field {0}.{1} using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well)". + /// + /// + public static readonly DiagnosticDescriptor UseObservablePropertyOnPartialProperty = new( + id: UseObservablePropertyOnPartialPropertyId, + title: "Prefer using [ObservableProperty] on partial properties", + messageFormat: """The field {0}.{1} using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Fields using [ObservableProperty] can be converted to partial properties instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0042"); } From 6342b83d7a710ac571f80891b0e89fa223c87328 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 11:48:22 -0700 Subject: [PATCH 25/43] Add unit tests for new analyzer --- .../Test_SourceGeneratorsDiagnostics.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 03a3f323d..718e566b1 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -47,4 +47,61 @@ public partial class SampleViewModel : ObservableObject await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, languageVersion: LanguageVersion.Preview); } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsNotPreview_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp12); + } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsPreview_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0042:ObservableProperty|}] + private string name; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsPreview_OnPartialProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public string Bar { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } } From efa90a3347fef8bdf80bae7dab0d3a84caa0eb9a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 13:34:34 -0700 Subject: [PATCH 26/43] Add 'InvalidPropertyLevelObservablePropertyAttributeAnalyzer' --- .../AnalyzerReleases.Shipped.md | 3 +- ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + .../ObservablePropertyGenerator.Execute.cs | 36 +----- ...evelObservablePropertyAttributeAnalyzer.cs | 122 ++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 16 +++ 5 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index d77a3a005..044a3783c 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -83,4 +83,5 @@ MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator Rule ID | Category | Severity | Notes --------|----------|----------|------- MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 -MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 +MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0042 +MVVMTK0043| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 5df7ef0e0..045c0ab14 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -47,6 +47,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 813dcd977..c4a08b831 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -55,41 +55,9 @@ static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? contain } #if ROSLYN_4_11_0_OR_GREATER - // Matches a valid partial property declaration - static bool IsCandidateProperty(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) - { - // The node must be a property declaration with two accessors - if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) - { - containingTypeNode = null; - - return false; - } - - // The property must be partial (we'll check that it's a declaration from its symbol) - if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) - { - containingTypeNode = null; - - return false; - } - - // The accessors must be a get and a set (with any accessibility) - if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || - accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) - { - containingTypeNode = null; - - return false; - } - - containingTypeNode = (TypeDeclarationSyntax?)property.Parent; - - return true; - } - // We only support matching properties on Roslyn 4.11 and greater - if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && !IsCandidateProperty(node, out parentNode)) + if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && + !InvalidPropertyLevelObservablePropertyAttributeAnalyzer.IsValidCandidateProperty(node, out parentNode)) { return false; } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..8560dc1d1 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +#if ROSLYN_4_11_0_OR_GREATER +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +#endif +using Microsoft.CodeAnalysis; +#if ROSLYN_4_11_0_OR_GREATER +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +#endif +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used on an invalid property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyLevelObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidPropertyDeclarationForObservableProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + +#if ROSLYN_4_11_0_OR_GREATER + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Don't analyze generated symbols, we only want to warn those + // that users have actually written on their own in source code. + if (context.IsGeneratedCode) + { + return; + } + + // We're intentionally only looking for properties here + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property isn't using [ObservableProperty], there's nothing to do + if (!propertySymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Check that the property has valid syntax + foreach (SyntaxReference propertyReference in propertySymbol.DeclaringSyntaxReferences) + { + SyntaxNode propertyNode = propertyReference.GetSyntax(context.CancellationToken); + + if (!IsValidCandidateProperty(propertyNode, out _)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationForObservableProperty, + observablePropertyAttribute.GetLocation(), + propertySymbol.ContainingType, + propertySymbol)); + } + } + }, SymbolKind.Property); + }); +#endif + } + +#if ROSLYN_4_11_0_OR_GREATER + /// + /// Checks whether a given property declaration has valid syntax. + /// + /// The input node to validate. + /// The resulting node for the containing type of the property, if valid. + /// Whether is a valid property. + internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) + { + // The node must be a property declaration with two accessors + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) + { + containingTypeNode = null; + + return false; + } + + // The property must be partial (we'll check that it's a declaration from its symbol) + if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + containingTypeNode = null; + + return false; + } + + // The accessors must be a get and a set (with any accessibility) + if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || + accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) + { + containingTypeNode = null; + + return false; + } + + containingTypeNode = (TypeDeclarationSyntax?)property.Parent; + + return true; + } +#endif +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index f68a7ed72..b04ba72b8 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -711,4 +711,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Fields using [ObservableProperty] can be converted to partial properties instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0042"); + + /// + /// Gets a indicating when [ObservableProperty] is applied to a property with an invalid declaration. + /// + /// Format: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be a partial property with a getter and a setter that is not init-only)". + /// + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationForObservableProperty = new DiagnosticDescriptor( + id: "MVVMTK0043", + title: "Invalid property declaration for [ObservableProperty]", + messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be a partial property with a getter and a setter that is not init-only)", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [ObservableProperty] must be partial properties with a getter and a setter that is not init-only.", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0043"); } From 767c05b2552f0abb8ddc1541c266f6469b7fd106 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 13:35:04 -0700 Subject: [PATCH 27/43] Add 'UnsupportedRoslynVersionForPartialPropertyAnalyzer' --- .../AnalyzerReleases.Shipped.md | 1 + ...ityToolkit.Mvvm.SourceGenerators.projitems | 1 + ...RoslynVersionForPartialPropertyAnalyzer.cs | 60 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 16 +++++ 4 files changed, 78 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 044a3783c..cb19f6246 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -85,3 +85,4 @@ Rule ID | Category | Severity | Notes MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0042 MVVMTK0043| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 +MVVMTK0044| CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 045c0ab14..9e2e2d555 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -46,6 +46,7 @@ + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs new file mode 100644 index 000000000..9a9c53f02 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !ROSLYN_4_11_0_OR_GREATER + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used on a property, if the Roslyn version in use is not high enough. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UnsupportedRoslynVersionForPartialPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UnsupportedRoslynVersionForObservablePartialPropertySupport); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We're intentionally only looking for properties here + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property has [ObservableProperty], emit an error in all cases + if (propertySymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + context.ReportDiagnostic(Diagnostic.Create( + UnsupportedRoslynVersionForObservablePartialPropertySupport, + observablePropertyAttribute.GetLocation(), + propertySymbol.ContainingType, + propertySymbol)); + } + }, SymbolKind.Property); + }); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index b04ba72b8..5f7fb3883 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -727,4 +727,20 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: "Properties annotated with [ObservableProperty] must be partial properties with a getter and a setter that is not init-only.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0043"); + + /// + /// Gets a indicating when [ObservableProperty] is applied to a property when an unsupported version of Roslyn is used. + /// + /// Format: "The property {0}.{1} cannot be used to generate an observable property, as the current Roslyn version being used is not high enough (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK)". + /// + /// + public static readonly DiagnosticDescriptor UnsupportedRoslynVersionForObservablePartialPropertySupport = new DiagnosticDescriptor( + id: "MVVMTK0044", + title: "Unsupported Roslyn version for using [ObservableProperty] on partial properties", + messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as the current Roslyn version being used is not high enough (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK)", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Using [ObservableProperty] with (partial) properties requires a higher version of Roslyn (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0044"); } From eea59fd474db2c88948fc4c3a0f080fa4a7b4969 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 14:42:00 -0700 Subject: [PATCH 28/43] Fix handling of 'private protected' accessors --- .../Extensions/AccessibilityExtensions.cs | 2 +- .../Test_SourceGeneratorsCodegen.cs | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs index 09ef24907..cd7a8226b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs @@ -24,7 +24,7 @@ public static SyntaxTokenList ToSyntaxTokenList(this Accessibility accessibility { Accessibility.NotApplicable => TokenList(), Accessibility.Private => TokenList(Token(SyntaxKind.PrivateKeyword)), - Accessibility.ProtectedAndInternal => TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.InternalKeyword)), + Accessibility.ProtectedAndInternal => TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.ProtectedKeyword)), Accessibility.Protected => TokenList(Token(SyntaxKind.ProtectedKeyword)), Accessibility.Internal => TokenList(Token(SyntaxKind.InternalKeyword)), Accessibility.ProtectedOrInternal => TokenList(Token(SyntaxKind.ProtectedKeyword), Token(SyntaxKind.InternalKeyword)), diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs index 62539c403..dce57de5c 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -243,6 +243,81 @@ internal partial int Number } [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly3() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + protected internal partial string Name { get; private protected set; } + } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + protected internal partial string Name + { + get => field; + private protected set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + +[TestMethod] public void ObservablePropertyWithReferenceType_NotNullable_OnPartialProperty_WorksCorrectly() { string source = """ From 162bab5991c3015cfb6422fbab4f79125285163c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 14:42:24 -0700 Subject: [PATCH 29/43] Add unit tests for invalid property declarations --- ...evelObservablePropertyAttributeAnalyzer.cs | 12 +- ...RoslynVersionForPartialPropertyAnalyzer.cs | 32 ++++ ...urceGenerators.Roslyn4031.UnitTests.csproj | 4 + .../Test_SourceGeneratorsDiagnostics.cs | 139 +++++++++++++++++- .../Test_SourceGeneratorsDiagnostics.cs | 32 +++- 5 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs index 8560dc1d1..fdb349086 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -2,15 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Immutable; #if ROSLYN_4_11_0_OR_GREATER + +using System.Collections.Immutable; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; -#endif using Microsoft.CodeAnalysis; -#if ROSLYN_4_11_0_OR_GREATER using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -#endif using Microsoft.CodeAnalysis.Diagnostics; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; @@ -31,7 +29,6 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); -#if ROSLYN_4_11_0_OR_GREATER context.RegisterCompilationStartAction(static context => { // Get the symbol for [ObservableProperty] @@ -77,10 +74,8 @@ public override void Initialize(AnalysisContext context) } }, SymbolKind.Property); }); -#endif } -#if ROSLYN_4_11_0_OR_GREATER /// /// Checks whether a given property declaration has valid syntax. /// @@ -118,5 +113,6 @@ internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarati return true; } -#endif } + +#endif diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs new file mode 100644 index 000000000..b5cadd896 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +[TestClass] +public class Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer +{ + [TestMethod] + public async Task UnsupportedRoslynVersionForPartialPropertyAnalyzer_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0044:ObservableProperty|}] + public string Bar { get; set; } + } + } + """; + + await Test_SourceGeneratorsDiagnostics.VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index cbea561a0..96233ae85 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj @@ -5,6 +5,10 @@ $(DefineConstants);ROSLYN_4_3_1_OR_GREATER + + + + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 718e566b1..1bb0b461c 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -21,7 +21,7 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [{|MVVMTK0041:ObservableProperty|}] - public string Bar { get; set; } + public string Name { get; set; } } } """; @@ -40,7 +40,7 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [ObservableProperty] - public string Bar { get; set; } + public string Name { get; set; } } } """; @@ -97,11 +97,144 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [ObservableProperty] - public string Bar { get; set; } + public string Name { get; set; } } } """; await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnValidPropertyDeclaration_DoesNotWarn1() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string {|CS9248:Name|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0103", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnValidPropertyDeclaration_DoesNotWarn2() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + internal partial string {|CS9248:Name|} { get; private set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0103", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnValidPropertyDeclaration_DoesNotWarn3() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + protected internal partial string {|CS9248:Name|} { get; private protected set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0103", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnNonPartialProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnReadOnlyProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { get; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnWriteOnlyProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnInitOnlyProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { get; init; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 0ab363974..6883440df 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1886,7 +1886,21 @@ internal static class IsExternalInit /// The type of the analyzer to test. /// The input source to process with diagnostic annotations. /// The language version to use to parse code and run tests. - private static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion) + internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion) + where TAnalyzer : DiagnosticAnalyzer, new() + { + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(markdownSource, languageVersion, [], []); + } + + /// + /// Verifies the diagnostic errors for a given analyzer, and that all available source generators can run successfully with the input source (including subsequent compilation). + /// + /// The type of the analyzer to test. + /// The input source to process with diagnostic annotations. + /// The language version to use to parse code and run tests. + /// The diagnostic ids to expect for the input source code. + /// The list of diagnostic ids to ignore in the final compilation. + internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion, string[] generatorDiagnosticsIds, string[] ignoredDiagnosticIds) where TAnalyzer : DiagnosticAnalyzer, new() { await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(markdownSource, languageVersion); @@ -1905,7 +1919,7 @@ private static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration "Foo()") string source = Regex.Replace(markdownSource, @"{\|((?:,?\w+)+):(.+)\|}", m => m.Groups[2].Value); - VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion)), generators, Array.Empty()); + VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion)), generators, generatorDiagnosticsIds, ignoredDiagnosticIds); } /// @@ -1914,12 +1928,12 @@ private static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGenerationThe generator type to use. /// The input source to process. /// The diagnostic ids to expect for the input source code. - private static void VerifyGeneratedDiagnostics(string source, params string[] diagnosticsIds) + internal static void VerifyGeneratedDiagnostics(string source, params string[] diagnosticsIds) where TGenerator : class, IIncrementalGenerator, new() { IIncrementalGenerator generator = new TGenerator(); - VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp8)), new[] { generator }, diagnosticsIds); + VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp8)), new[] { generator }, diagnosticsIds, []); } /// @@ -1928,7 +1942,8 @@ private static void VerifyGeneratedDiagnostics(string source, params /// The input source tree to process. /// The generators to apply to the input syntax tree. /// The diagnostic ids to expect for the input source code. - private static void VerifyGeneratedDiagnostics(SyntaxTree syntaxTree, IIncrementalGenerator[] generators, string[] generatorDiagnosticsIds) + /// The list of diagnostic ids to ignore in the final compilation. + internal static void VerifyGeneratedDiagnostics(SyntaxTree syntaxTree, IIncrementalGenerator[] generators, string[] generatorDiagnosticsIds, string[] ignoredDiagnosticIds) { // Ensure CommunityToolkit.Mvvm and System.ComponentModel.DataAnnotations are loaded Type observableObjectType = typeof(ObservableObject); @@ -1944,7 +1959,7 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() // Create a syntax tree with the input source CSharpCompilation compilation = CSharpCompilation.Create( "original", - new SyntaxTree[] { syntaxTree }, + [syntaxTree], references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); @@ -1963,7 +1978,10 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() // Compute diagnostics for the final compiled output (just include errors) List outputCompilationDiagnostics = outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToList(); - Assert.IsTrue(outputCompilationDiagnostics.Count == 0, $"resultingIds: {string.Join(", ", outputCompilationDiagnostics)}"); + // Filtered diagnostics + List filteredDiagnostics = outputCompilationDiagnostics.Where(diagnostic => !ignoredDiagnosticIds.Contains(diagnostic.Id)).ToList(); + + Assert.IsTrue(filteredDiagnostics.Count == 0, $"resultingIds: {string.Join(", ", filteredDiagnostics)}"); } GC.KeepAlive(observableObjectType); From 5e02b9acb25bf83264454157e306b2188f62dbd1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 17:09:22 -0700 Subject: [PATCH 30/43] Add 'UsePartialPropertyForObservablePropertyCodeFixer' --- ...lPropertyForObservablePropertyCodeFixer.cs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs new file mode 100644 index 000000000..0072e1aec --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.SourceGenerators; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.CodeFixers; + +/// +/// A code fixer that converts fields using [ObservableProperty] to partial properties. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UsePartialPropertyForObservablePropertyCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnPartialPropertyId); + + /// + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + // Retrieve the properties passed by the analyzer + if (diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey] is not string fieldName || + diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey] is not string propertyName) + { + return; + } + + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Get the field declaration from the target diagnostic (we only support individual fields, with a single declaration) + if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration && + identifierName == fieldName) + { + // Register the code fix to update the class declaration to inherit from ObservableObject instead + context.RegisterCodeFix( + CodeAction.Create( + title: "Use a partial property", + createChangedDocument: token => ConvertToPartialProperty(context.Document, root, fieldDeclaration, fieldName, propertyName, context.CancellationToken), + equivalenceKey: "Use a partial property"), + diagnostic); + } + } + + /// + /// Applies the code fix to a target identifier and returns an updated document. + /// + /// The original document being fixed. + /// The original tree root belonging to the current document. + /// The for the field being updated. + /// The name of the annotated field. + /// The name of the generated property. + /// The cancellation token for the operation. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static async Task ConvertToPartialProperty( + Document document, + SyntaxNode root, + FieldDeclarationSyntax fieldDeclaration, + string fieldName, + string propertyName, + CancellationToken cancellationToken) + { + // Get all attributes that were on the field. Here we only include those targeting either + // the field, or the property. Those targeting the accessors will be moved there directly. + AttributeListSyntax[] propertyAttributes = + fieldDeclaration + .AttributeLists + .Where(list => list.Target is null || list.Target.Kind() is SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) + .ToArray(); + + // Separately, also get all attributes for the property getters + AttributeListSyntax[] getterAttributes = + fieldDeclaration + .AttributeLists + .Where(list => list.Target?.Kind() is SyntaxKind.GetKeyword) + .ToArray(); + + // Also do the same for the setters + AttributeListSyntax[] setterAttributes = + fieldDeclaration + .AttributeLists + .Where(list => list.Target?.Kind() is SyntaxKind.SetKeyword) + .ToArray(); + + // Create the following property declaration: + // + // + // public partial + // { + // + // get; + // + // + // set; + // } + PropertyDeclarationSyntax propertyDeclaration = + PropertyDeclaration(fieldDeclaration.Declaration.Type, Identifier(propertyName)) + .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.PartialKeyword)) + .AddAttributeLists(propertyAttributes) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .AddAttributeLists(getterAttributes), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .AddAttributeLists(setterAttributes)); + + SyntaxTree updatedTree = root.ReplaceNode(fieldDeclaration, propertyDeclaration).SyntaxTree; + + return document.WithSyntaxRoot(await updatedTree.GetRootAsync(cancellationToken).ConfigureAwait(false)); + } +} From 9ba8f27919167d96846d29799e59a878c4793722 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 17:09:45 -0700 Subject: [PATCH 31/43] Add new helper for testing code fixers --- ...rsionTest{TAnalyzer,TCodeFix,TVerifier}.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs new file mode 100644 index 000000000..55127c5c8 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; + +/// +/// A custom that uses a specific C# language version to parse code. +/// +/// The type of the analyzer to produce diagnostics. +/// The type of code fix to test. +/// The type of verifier to use to validate the code fixer. +internal sealed class CSharpCodeFixWithLanguageVersionTest : CSharpCodeFixTest + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + where TVerifier : IVerifier, new() +{ + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion languageVersion; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The C# language version to use to parse code. + public CSharpCodeFixWithLanguageVersionTest(LanguageVersion languageVersion) + { + this.languageVersion = languageVersion; + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(this.languageVersion, DocumentationMode.Diagnose); + } +} From b98a6586bcff21b735e5d597d6c00bfb8ce3aa2e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 17:10:04 -0700 Subject: [PATCH 32/43] Add unit test for new code fixer --- ...rvablePropertyOnPartialPropertyAnalyzer.cs | 3 + ...oidReturningRelayCommandMethodCodeFixer.cs | 2 +- ...gAttributeInsteadOfInheritanceCodeFixer.cs | 2 +- ...enceForObservablePropertyFieldCodeFixer.cs | 2 +- ...urceGenerators.Roslyn4031.UnitTests.csproj | 1 + ...urceGenerators.Roslyn4110.UnitTests.csproj | 2 + ...lPropertyForObservablePropertyCodeFixer.cs | 69 +++++++++++++++++++ ....Mvvm.SourceGenerators.UnitTests.projitems | 1 + ...lyzerWithLanguageVersionTest{TAnalyzer}.cs | 2 +- 9 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs index a45b0bde3..16a42bc1c 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -60,6 +60,9 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create( UseObservablePropertyOnPartialProperty, observablePropertyAttribute.GetLocation(), + ImmutableDictionary.Create() + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name) + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)), fieldSymbol.ContainingType, fieldSymbol)); }, SymbolKind.Field); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs index cff9df992..c77c38b7b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.AsyncVoidReturningRelayCommandMethodCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class Test_AsyncVoidReturningRelayCommandMethodCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs index a2d3e920d..2e63a49f9 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.ClassUsingAttributeInsteadOfInheritanceCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class ClassUsingAttributeInsteadOfInheritanceCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs index 9771ccd08..665761405 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.FieldReferenceForObservablePropertyFieldCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class Test_FieldReferenceForObservablePropertyFieldCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index 96233ae85..9dbd39d92 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj index 394a10614..15077e2aa 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests.csproj @@ -7,6 +7,7 @@ + @@ -15,6 +16,7 @@ + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs new file mode 100644 index 000000000..10b9f710d --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CSharpCodeFixTest = CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers.CSharpCodeFixWithLanguageVersionTest< + CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnPartialPropertyAnalyzer, + CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForObservablePropertyCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; +using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< + CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnPartialPropertyAnalyzer, + CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForObservablePropertyCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +[TestClass] +public class Test_UsePartialPropertyForObservablePropertyCodeFixer +{ + [TestMethod] + public async Task SimpleFieldWithNoReferences() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I") + }); + + await test.RunAsync(); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems index 881fdb625..8f141adb6 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems @@ -9,6 +9,7 @@ CommunityToolkit.Mvvm.SourceGenerators.UnitTests + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs index d76d8f356..8e41258ab 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs @@ -29,7 +29,7 @@ internal sealed class CSharpAnalyzerWithLanguageVersionTest : CSharpA private readonly LanguageVersion languageVersion; /// - /// Creates a new instance with the specified paramaters. + /// Creates a new instance with the specified parameters. /// /// The C# language version to use to parse code. private CSharpAnalyzerWithLanguageVersionTest(LanguageVersion languageVersion) From 8cdebe0a9c131830baa2cee377081e8900daa1ca Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 18:55:37 -0700 Subject: [PATCH 33/43] Fix attributes handling, add unit tests --- ...lPropertyForObservablePropertyCodeFixer.cs | 132 ++++++++++- .../Extensions/CompilationExtensions.cs | 3 + .../Extensions/SymbolInfoExtensions.cs | 2 +- ...lPropertyForObservablePropertyCodeFixer.cs | 223 +++++++++++++++++- 4 files changed, 352 insertions(+), 8 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 0072e1aec..8814e9451 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -2,12 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.SourceGenerators; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -26,6 +28,30 @@ namespace CommunityToolkit.Mvvm.CodeFixers; [Shared] public sealed class UsePartialPropertyForObservablePropertyCodeFixer : CodeFixProvider { + /// + /// The mapping of well-known MVVM Toolkit attributes. + /// + private static readonly ImmutableDictionary MvvmToolkitAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("NotifyCanExecuteChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyCanExecuteChangedForAttribute"), + new KeyValuePair("NotifyDataErrorInfoAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute"), + new KeyValuePair("NotifyPropertyChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedForAttribute"), + new KeyValuePair("NotifyPropertyChangedRecipientsAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute"), + new KeyValuePair("ObservablePropertyAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") + }); + + /// + /// The mapping of well-known data annotation attributes. + /// + private static readonly ImmutableDictionary DataAnnotationsAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("UIHintAttribute", "System.ComponentModel.DataAnnotations.UIHintAttribute"), + new KeyValuePair("ScaffoldColumnAttribute", "System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute"), + new KeyValuePair("DisplayAttribute", "System.ComponentModel.DataAnnotations.DisplayAttribute"), + new KeyValuePair("EditableAttribute", "System.ComponentModel.DataAnnotations.EditableAttribute"), + new KeyValuePair("KeyAttribute", "System.ComponentModel.DataAnnotations.KeyAttribute") + }); + /// public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnPartialPropertyId); @@ -41,6 +67,12 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) Diagnostic diagnostic = context.Diagnostics[0]; TextSpan diagnosticSpan = context.Span; + // This code fixer needs the semantic model, so check that first + if (!context.Document.SupportsSemanticModel) + { + return; + } + // Retrieve the properties passed by the analyzer if (diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey] is not string fieldName || diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey] is not string propertyName) @@ -54,6 +86,13 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration && identifierName == fieldName) { + // We only support fields with up to one attribute per attribute list. + // This is so we can easily check one attribute when updating targets. + if (fieldDeclaration.AttributeLists.Any(static list => list.Attributes.Count > 1)) + { + return; + } + // Register the code fix to update the class declaration to inherit from ObservableObject instead context.RegisterCodeFix( CodeAction.Create( @@ -82,26 +121,107 @@ private static async Task ConvertToPartialProperty( string propertyName, CancellationToken cancellationToken) { + SemanticModel semanticModel = (await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false))!; + + // Try to get all necessary type symbols to process the attributes + if (!semanticModel.Compilation.TryBuildNamedTypeSymbolMap(MvvmToolkitAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary? toolkitTypeSymbols) || + !semanticModel.Compilation.TryBuildNamedTypeSymbolMap(DataAnnotationsAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary? annotationTypeSymbols)) + { + return document; + } + + // Also query [ValidationAttribute] + if (semanticModel.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") is not INamedTypeSymbol validationAttributeSymbol) + { + return document; + } + // Get all attributes that were on the field. Here we only include those targeting either // the field, or the property. Those targeting the accessors will be moved there directly. - AttributeListSyntax[] propertyAttributes = + List propertyAttributes = fieldDeclaration .AttributeLists - .Where(list => list.Target is null || list.Target.Kind() is SyntaxKind.FieldKeyword or SyntaxKind.PropertyKeyword) - .ToArray(); + .Where(list => list.Target is null || list.Target.Identifier.Kind() is not (SyntaxKind.GetKeyword or SyntaxKind.SetKeyword)) + .ToList(); + + // Fixup attribute lists as following: + // 1) If they have the 'field:' target, keep it (it's no longer the default) + // 2) If they have the 'property:' target, remove it (it's not needed anymore) + // 3) If they are from the MVVM Toolkit, remove the target (they'll apply to the property) + // 4) If they have no target and they are either a validation attribute, or any of the well-known + // data annotation attributes (which are automatically forwarded), leave them without a target. + // 5) If they have no target, add 'field:' to preserve the original behavior + // 5) Otherwise, leave them without changes (this will carry over invalid targets as-is) + for (int i = 0; i < propertyAttributes.Count; i++) + { + AttributeListSyntax attributeListSyntax = propertyAttributes[i]; + + // Special case: the list has no attributes. Just remove it entirely. + if (attributeListSyntax.Attributes is []) + { + propertyAttributes.RemoveAt(i--); + + continue; + } + + // Case 1 + if (attributeListSyntax.Target?.Identifier.IsKind(SyntaxKind.FieldKeyword) is true) + { + continue; + } + + // Case 2 + if (attributeListSyntax.Target?.Identifier.IsKind(SyntaxKind.PropertyKeyword) is true) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(null); + + continue; + } + + // Make sure we can retrieve the symbol for the attribute type. + // We are guaranteed to always find a single attribute in the list. + if (!semanticModel.GetSymbolInfo(attributeListSyntax.Attributes[0], cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol)) + { + return document; + } + + // Case 3 + if (toolkitTypeSymbols.ContainsValue(attributeSymbol)) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(null); + + continue; + } + + // Case 4 + if (annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol)) + { + continue; + } + + // Case 5 + if (attributeListSyntax.Target is null) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword))); + + continue; + } + } // Separately, also get all attributes for the property getters AttributeListSyntax[] getterAttributes = fieldDeclaration .AttributeLists - .Where(list => list.Target?.Kind() is SyntaxKind.GetKeyword) + .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.GetKeyword) + .Select(list => list.WithTarget(null)) .ToArray(); // Also do the same for the setters AttributeListSyntax[] setterAttributes = fieldDeclaration .AttributeLists - .Where(list => list.Target?.Kind() is SyntaxKind.SetKeyword) + .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.SetKeyword) + .Select(list => list.WithTarget(null)) .ToArray(); // Create the following property declaration: @@ -118,7 +238,7 @@ private static async Task ConvertToPartialProperty( PropertyDeclarationSyntax propertyDeclaration = PropertyDeclaration(fieldDeclaration.Declaration.Type, Identifier(propertyName)) .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.PartialKeyword)) - .AddAttributeLists(propertyAttributes) + .AddAttributeLists(propertyAttributes.ToArray()) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs index 8f3c06b93..6a584bbfb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs @@ -118,6 +118,9 @@ public static bool TryBuildNamedTypeSymbolMap( { ImmutableDictionary.Builder builder = ImmutableDictionary.CreateBuilder(); + // Ensure we always use the right comparer for values, when needed + builder.ValueComparer = SymbolEqualityComparer.Default; + foreach (KeyValuePair pair in typeNames) { if (compilation.GetTypeByMetadataName(pair.Value) is not INamedTypeSymbol attributeSymbol) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs index c87f9a8fb..4a81b6bee 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs @@ -19,7 +19,7 @@ internal static class SymbolInfoExtensions /// The resulting attribute type symbol, if correctly resolved. /// Whether is resolved to a symbol. /// - /// This can be used to ensure users haven't eg. spelled names incorrecty or missed a using directive. Normally, code would just + /// This can be used to ensure users haven't eg. spelled names incorrectly or missed a using directive. Normally, code would just /// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore /// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own. /// diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index 10b9f710d..7e23f160d 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -61,7 +61,228 @@ partial class C : ObservableObject test.FixedState.ExpectedDiagnostics.AddRange(new[] { // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. - DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I") + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithAdditionalAttributes1() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello")] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello")] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 24, 7, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithAdditionalAttributes2() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello1")] + [NotifyCanExecuteChangedFor("hello2")] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello1")] + [NotifyCanExecuteChangedFor("hello2")] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 24, 8, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithAdditionalAttributes3() + { + string original = """ + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [field: MinLength(1)] + [property: MinLength(2)] + private int i; + } + """; + + string @fixed = """ + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [field: MinLength(1)] + [MinLength(2)] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(9,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(9, 24, 9, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithAdditionalAttributes4() + { + string original = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [Test("This is on the field")] + [field: Test("This is also on a the field, but using 'field:'")] + [property: Test("This is on the property")] + [get: Test("This is on the getter")] + [set: Test("This is also on the setter")] + [set: Test("This is a second one on the setter")] + [ignored: Test("This should be ignored, but still carried over")] + private int i; + } + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class TestAttribute(string text) : Attribute; + """; + + string @fixed = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [field: Test("This is on the field")] + [field: Test("This is also on a the field, but using 'field:'")] + [Test("This is on the property")] + [ignored: Test("This should be ignored, but still carried over")] + public partial int I { [Test("This is on the getter")] + get; [Test("This is also on the setter")] + [Test("This is a second one on the setter")] + set; + } + } + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true)] + public class TestAttribute(string text) : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 6, 7, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(12,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(12, 24, 12, 25).WithArguments("C.I"), }); await test.RunAsync(); From e6a5c091f8951eb76590882612618f22733d110e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 19:18:09 -0700 Subject: [PATCH 34/43] Add unit tests for comments in code fixer --- ...lPropertyForObservablePropertyCodeFixer.cs | 12 +- ...lPropertyForObservablePropertyCodeFixer.cs | 152 ++++++++++++++++++ 2 files changed, 160 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 8814e9451..4dfb27ba3 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -213,7 +214,7 @@ private static async Task ConvertToPartialProperty( fieldDeclaration .AttributeLists .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.GetKeyword) - .Select(list => list.WithTarget(null)) + .Select(list => list.WithTarget(null).WithAdditionalAnnotations(Formatter.Annotation)) .ToArray(); // Also do the same for the setters @@ -221,7 +222,7 @@ private static async Task ConvertToPartialProperty( fieldDeclaration .AttributeLists .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.SetKeyword) - .Select(list => list.WithTarget(null)) + .Select(list => list.WithTarget(null).WithAdditionalAnnotations(Formatter.Annotation)) .ToArray(); // Create the following property declaration: @@ -239,13 +240,16 @@ private static async Task ConvertToPartialProperty( PropertyDeclaration(fieldDeclaration.Declaration.Type, Identifier(propertyName)) .AddModifiers(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.PartialKeyword)) .AddAttributeLists(propertyAttributes.ToArray()) + .WithAdditionalAnnotations(Formatter.Annotation) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) - .AddAttributeLists(getterAttributes), + .AddAttributeLists(getterAttributes) + .WithAdditionalAnnotations(Formatter.Annotation), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) - .AddAttributeLists(setterAttributes)); + .AddAttributeLists(setterAttributes) + .WithAdditionalAnnotations(Formatter.Annotation)); SyntaxTree updatedTree = root.ReplaceNode(fieldDeclaration, propertyDeclaration).SyntaxTree; diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index 7e23f160d..5520aaae5 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -287,4 +287,156 @@ public class TestAttribute(string text) : Attribute; await test.RunAsync(); } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithSimpleComment() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment + [ObservableProperty] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment + [ObservableProperty] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 24, 7, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithTwoLineComment() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment. + // This is more comment. + [ObservableProperty] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment. + // This is more comment. + [ObservableProperty] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 6, 7, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 24, 8, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithXmlComment() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + /// + /// Blah blah blah. + /// + /// Blah blah blah. + [ObservableProperty] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + /// + /// Blah blah blah. + /// + /// Blah blah blah. + [ObservableProperty] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(9,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(9, 6, 9, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(10, 24, 10, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } } From 00ebe428cd52f0aa611808c869cca7f16cff6915 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 19:43:22 -0700 Subject: [PATCH 35/43] Support updating field references as well --- ...lPropertyForObservablePropertyCodeFixer.cs | 34 ++++++++++- ...lPropertyForObservablePropertyCodeFixer.cs | 60 +++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index 4dfb27ba3..ebeee4cc7 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; @@ -251,8 +252,37 @@ private static async Task ConvertToPartialProperty( .AddAttributeLists(setterAttributes) .WithAdditionalAnnotations(Formatter.Annotation)); - SyntaxTree updatedTree = root.ReplaceNode(fieldDeclaration, propertyDeclaration).SyntaxTree; + // Create an editor to perform all mutations. This allows to keep track of multiple + // replacements for nodes on the same original tree, which otherwise wouldn't work. + SyntaxEditor editor = new(root, document.Project.Solution.Workspace); - return document.WithSyntaxRoot(await updatedTree.GetRootAsync(cancellationToken).ConfigureAwait(false)); + editor.ReplaceNode(fieldDeclaration, propertyDeclaration); + + // Get the field declaration from the target diagnostic (we only support individual fields, with a single declaration) + foreach (SyntaxNode descendantNode in root.DescendantNodes()) + { + // We only care about identifier nodes + if (descendantNode is not IdentifierNameSyntax identifierSyntax) + { + continue; + } + + // Pre-filter to only match the field name we just replaced + if (identifierSyntax.Identifier.Text != fieldName) + { + continue; + } + + // Make sure the identifier actually refers to the field being replaced + if (semanticModel.GetSymbolInfo(identifierSyntax, cancellationToken).Symbol is not IFieldSymbol fieldSymbol) + { + continue; + } + + // Replace the field reference with a reference to the new property + editor.ReplaceNode(identifierSyntax, IdentifierName(propertyName)); + } + + return document.WithSyntaxRoot(editor.GetChangedRoot()); } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index 5520aaae5..f574a0836 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -439,4 +439,64 @@ partial class C : ObservableObject await test.RunAsync(); } + + [TestMethod] + public async Task SimpleFieldWithSomeReferences() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i; + + public void M() + { + i = 42; + } + + public int N() => i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } + + public void M() + { + I = 42; + } + + public int N() => I; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } } From 34538245d6774d0b7752090c203e4f07486c2f70 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 19:57:37 -0700 Subject: [PATCH 36/43] Improve messages in diagnostics when emitted on properties --- ...ityToolkit.Mvvm.SourceGenerators.projitems | 2 + .../ObservablePropertyGenerator.Execute.cs | 14 +++- ...entObservablePropertyAttributesAnalyzer.cs | 13 +++- .../Diagnostics/DiagnosticDescriptors.cs | 76 +++++++++---------- .../Extensions/SymbolKindExtensions.cs | 30 ++++++++ .../Extensions/SyntaxKindExtensions.cs | 30 ++++++++ 6 files changed, 119 insertions(+), 46 deletions(-) create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs create mode 100644 src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 9e2e2d555..988d87a15 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -69,6 +69,8 @@ + + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index c4a08b831..cf90fa8a5 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -143,8 +143,9 @@ public static bool TryGetInfo( if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging)) { builder.Add( - InvalidContainingTypeForObservablePropertyFieldError, + InvalidContainingTypeForObservablePropertyMemberError, memberSymbol, + memberSyntax.Kind().ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); @@ -309,6 +310,7 @@ public static bool TryGetInfo( builder.Add( MissingObservableValidatorInheritanceForValidationAttributeError, memberSymbol, + memberSyntax.Kind().ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name, forwardedAttributes.Count); @@ -320,6 +322,7 @@ public static bool TryGetInfo( builder.Add( MissingValidationAttributesForNotifyDataErrorInfoError, memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); } @@ -659,8 +662,9 @@ private static bool TryGetIsNotifyingRecipients( if (hasOrInheritsClassLevelNotifyPropertyChangedRecipients) { diagnostics.Add( - UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning, + UnnecessaryNotifyPropertyChangedRecipientsAttributeOnMemberWarning, memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); } @@ -676,7 +680,7 @@ private static bool TryGetIsNotifyingRecipients( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( - InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError, + InvalidContainingTypeForNotifyPropertyChangedRecipientsMemberError, memberSymbol, memberSymbol.ContainingType, memberSymbol.Name); @@ -742,8 +746,9 @@ private static bool TryGetNotifyDataErrorInfo( if (hasOrInheritsClassLevelNotifyDataErrorInfo) { diagnostics.Add( - UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning, + UnnecessaryNotifyDataErrorInfoAttributeOnMemberWarning, memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); } @@ -760,6 +765,7 @@ private static bool TryGetNotifyDataErrorInfo( diagnostics.Add( MissingObservableValidatorInheritanceForNotifyDataErrorInfoError, memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), memberSymbol.ContainingType, memberSymbol.Name); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs index 1505b6865..064223911 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs @@ -40,7 +40,7 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); // Defer the registration so it can be skipped if C# 8.0 or more is not available. - // That is because in that case source generators are not supported at all anyaway. + // That is because in that case source generators are not supported at all anyway. context.RegisterCompilationStartAction(static context => { if (!context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) @@ -72,7 +72,7 @@ public override void Initialize(AnalysisContext context) foreach (AttributeData dependentAttribute in attributes) { - // Go over each attribute on the target symbol, anche check if any of them matches one of the trigger attributes. + // Go over each attribute on the target symbol, and check if any of them matches one of the trigger attributes. // The logic here is the same as the one in UnsupportedCSharpLanguageVersionAnalyzer. if (dependentAttribute.AttributeClass is { Name: string attributeName } dependentAttributeClass && typeSymbols.TryGetValue(attributeName, out INamedTypeSymbol? dependentAttributeSymbol) && @@ -89,13 +89,18 @@ public override void Initialize(AnalysisContext context) } } - context.ReportDiagnostic(Diagnostic.Create(FieldWithOrphanedDependentObservablePropertyAttributesError, context.Symbol.Locations.FirstOrDefault(), context.Symbol.ContainingType, context.Symbol.Name)); + context.ReportDiagnostic(Diagnostic.Create( + FieldWithOrphanedDependentObservablePropertyAttributesError, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol.Kind.ToFieldOrPropertyKeyword(), + context.Symbol.ContainingType, + context.Symbol.Name)); // Just like in UnsupportedCSharpLanguageVersionAnalyzer, stop if a diagnostic has been emitted for the current symbol return; } } - }, SymbolKind.Field); + }, SymbolKind.Field, SymbolKind.Property); }); } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 5f7fb3883..74986d3ce 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -122,17 +122,17 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when the target type doesn't inherit from the ObservableValidator class. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator". + /// Format: "The {0} {1}.{2} cannot be used to generate an observable property, as it has {3} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator". /// /// public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceForValidationAttributeError = new DiagnosticDescriptor( id: "MVVMTK0006", title: "Missing ObservableValidator inheritance", - messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator", + messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as it has {3} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Cannot apply [ObservableProperty] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", + description: "Cannot apply [ObservableProperty] to fields or properties with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0006"); /// @@ -325,35 +325,35 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0018"); /// - /// Gets a indicating when [ObservableProperty] is applied to a field in an invalid type. + /// Gets a indicating when [ObservableProperty] is applied to a field or property in an invalid type. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]". + /// Format: "The {0} {1}.{2} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]". /// /// - public static readonly DiagnosticDescriptor InvalidContainingTypeForObservablePropertyFieldError = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor InvalidContainingTypeForObservablePropertyMemberError = new DiagnosticDescriptor( id: "MVVMTK0019", - title: "Invalid containing type for [ObservableProperty] field", - messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]", + title: "Invalid containing type for [ObservableProperty] field or property", + messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Fields annotated with [ObservableProperty] must be contained in a type that inherits from ObservableObject or that is annotated with [ObservableObject] or [INotifyPropertyChanged] (including base types).", + description: "Fields and properties annotated with [ObservableProperty] must be contained in a type that inherits from ObservableObject or that is annotated with [ObservableObject] or [INotifyPropertyChanged] (including base types).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0019"); /// - /// Gets a indicating when [ObservableProperty] is applied to a field in an invalid type. + /// Gets a indicating when [ObservableProperty] is applied to a field or property in an invalid type. /// - /// Format: "The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]". + /// Format: "The {0} {1}.{2} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]". /// /// public static readonly DiagnosticDescriptor FieldWithOrphanedDependentObservablePropertyAttributesError = new DiagnosticDescriptor( id: "MVVMTK0020", title: "Invalid use of attributes dependent on [ObservableProperty]", - messageFormat: "The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]", + messageFormat: "The {0} {1}.{2} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Fields not annotated with [ObservableProperty] cannot use [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo].", + description: "Fields and properties not annotated with [ObservableProperty] cannot use [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo].", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0020"); /// @@ -373,19 +373,19 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0021"); /// - /// Gets a indicating when [NotifyPropertyChangedRecipients] is applied to a field in an invalid type. + /// Gets a indicating when [NotifyPropertyChangedRecipients] is applied to a field or property in an invalid type. /// - /// Format: "The field {0}.{1} cannot be annotated with [NotifyPropertyChangedRecipients], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]". + /// Format: "The {0} {1}.{2} cannot be annotated with [NotifyPropertyChangedRecipients], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]". /// /// - public static readonly DiagnosticDescriptor InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor InvalidContainingTypeForNotifyPropertyChangedRecipientsMemberError = new DiagnosticDescriptor( id: "MVVMTK0022", - title: "Invalid containing type for [ObservableProperty] field", - messageFormat: "The field {0}.{1} cannot be annotated with [NotifyPropertyChangedRecipients], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]", + title: "Invalid containing type for [ObservableProperty] field or property", + messageFormat: "The {0} {1}.{2} cannot be annotated with [NotifyPropertyChangedRecipients], as its containing type doesn't inherit from ObservableRecipient, nor does it use [ObservableRecipient]", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Fields annotated with [NotifyPropertyChangedRecipients] must be contained in a type that inherits from ObservableRecipient or that is annotated with [ObservableRecipient] (including base types).", + description: "Fields and properties annotated with [NotifyPropertyChangedRecipients] must be contained in a type that inherits from ObservableRecipient or that is annotated with [ObservableRecipient] (including base types).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0022"); /// @@ -423,33 +423,33 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when the target type doesn't inherit from the ObservableValidator class. /// - /// Format: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator". + /// Format: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator". /// /// public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceForNotifyDataErrorInfoError = new DiagnosticDescriptor( id: "MVVMTK0025", title: "Missing ObservableValidator inheritance", - messageFormat: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator", + messageFormat: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Cannot apply [NotifyDataErrorInfo] to fields that are declared in a type that doesn't inherit from ObservableValidator.", + description: "Cannot apply [NotifyDataErrorInfo] to fields and properties that are declared in a type that doesn't inherit from ObservableValidator.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0025"); /// - /// Gets a indicating when the target field uses [NotifyDataErrorInfo] but has no validation attributes. + /// Gets a indicating when the target field or property uses [NotifyDataErrorInfo] but has no validation attributes. /// - /// Format: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation". + /// Format: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation". /// /// public static readonly DiagnosticDescriptor MissingValidationAttributesForNotifyDataErrorInfoError = new DiagnosticDescriptor( id: "MVVMTK0026", title: "Missing validation attributes", - messageFormat: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation", + messageFormat: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Cannot apply [NotifyDataErrorInfo] to fields that don't have any validation attributes to use during validation.", + description: "Cannot apply [NotifyDataErrorInfo] to fields and properties that don't have any validation attributes to use during validation.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0026"); /// @@ -485,35 +485,35 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0028"); /// - /// Gets a indicating when [NotifyPropertyChangedRecipients] is applied to a field in a class with [NotifyPropertyChangedRecipients] used at the class-level. + /// Gets a indicating when [NotifyPropertyChangedRecipients] is applied to a field or property in a class with [NotifyPropertyChangedRecipients] used at the class-level. /// - /// Format: "The field {0}.{1} is annotated with [NotifyPropertyChangedRecipients], but that is not needed since its containing type already uses or inherits [NotifyPropertyChangedRecipients] at the class-level". + /// Format: "The {0} {1}.{2} is annotated with [NotifyPropertyChangedRecipients], but that is not needed since its containing type already uses or inherits [NotifyPropertyChangedRecipients] at the class-level". /// /// - public static readonly DiagnosticDescriptor UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor UnnecessaryNotifyPropertyChangedRecipientsAttributeOnMemberWarning = new DiagnosticDescriptor( id: "MVVMTK0029", - title: "Unnecessary [NotifyPropertyChangedRecipients] field annotation", - messageFormat: "The field {0}.{1} is annotated with [NotifyPropertyChangedRecipients], but that is not needed since its containing type already uses or inherits [NotifyPropertyChangedRecipients] at the class-level", + title: "Unnecessary [NotifyPropertyChangedRecipients] field or property annotation", + messageFormat: "The {0} {1}.{2} is annotated with [NotifyPropertyChangedRecipients], but that is not needed since its containing type already uses or inherits [NotifyPropertyChangedRecipients] at the class-level", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Annotating a field with [NotifyPropertyChangedRecipients] is not necessary if the containing type has or inherits [NotifyPropertyChangedRecipients] at the class-level.", + description: "Annotating a field or property with [NotifyPropertyChangedRecipients] is not necessary if the containing type has or inherits [NotifyPropertyChangedRecipients] at the class-level.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0029"); /// - /// Gets a indicating when [NotifyDataErrorInfo] is applied to a field in a class with [NotifyDataErrorInfo] used at the class-level. + /// Gets a indicating when [NotifyDataErrorInfo] is applied to a field or property in a class with [NotifyDataErrorInfo] used at the class-level. /// - /// Format: "The field {0}.{1} is annotated with [NotifyDataErrorInfo], but that is not needed since its containing type already uses or inherits [NotifyDataErrorInfo] at the class-level". + /// Format: "The {0} {1}.{2} is annotated with [NotifyDataErrorInfo], but that is not needed since its containing type already uses or inherits [NotifyDataErrorInfo] at the class-level". /// /// - public static readonly DiagnosticDescriptor UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor UnnecessaryNotifyDataErrorInfoAttributeOnMemberWarning = new DiagnosticDescriptor( id: "MVVMTK0030", - title: "Unnecessary [NotifyDataErrorInfo] field annotation", - messageFormat: "The field {0}.{1} is annotated with [NotifyDataErrorInfo], but that is not needed since its containing type already uses or inherits [NotifyDataErrorInfo] at the class-level", + title: "Unnecessary [NotifyDataErrorInfo] field or property annotation", + messageFormat: "The {0} {1}.{2} is annotated with [NotifyDataErrorInfo], but that is not needed since its containing type already uses or inherits [NotifyDataErrorInfo] at the class-level", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, - description: "Annotating a field with [NotifyDataErrorInfo] is not necessary if the containing type has or inherits [NotifyDataErrorInfo] at the class-level.", + description: "Annotating a field or property with [NotifyDataErrorInfo] is not necessary if the containing type has or inherits [NotifyDataErrorInfo] at the class-level.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0030"); /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs new file mode 100644 index 000000000..398b17bd5 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SymbolKindExtensions +{ + /// + /// Converts a value to either "field" or "property" based on the kind. + /// + /// The input value. + /// Either "field" or "property" based on . + /// Thrown if is neither nor . + public static string ToFieldOrPropertyKeyword(this SymbolKind kind) + { + return kind switch + { + SymbolKind.Field => "field", + SymbolKind.Property => "property", + _ => throw new ArgumentException($"Unsupported symbol kind '{kind}' for field or property keyword conversion."), + }; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs new file mode 100644 index 000000000..a178dcc82 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxKindExtensions +{ + /// + /// Converts a value to either "field" or "property" based on the kind. + /// + /// The input value. + /// Either "field" or "property" based on . + /// Thrown if is neither nor . + public static string ToFieldOrPropertyKeyword(this SyntaxKind kind) + { + return kind switch + { + SyntaxKind.FieldDeclaration => "field", + SyntaxKind.PropertyDeclaration => "property", + _ => throw new ArgumentException($"Unsupported syntax kind '{kind}' for field or property keyword conversion."), + }; + } +} From 7b402b65966c8aa148cad8163217a9e256e0a6e7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 22:45:52 -0700 Subject: [PATCH 37/43] Add workaround for older Roslyn versions --- .../ObservablePropertyGenerator.Execute.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index cf90fa8a5..0cecdc6d5 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -137,6 +137,17 @@ public static bool TryGetInfo( [NotNullWhen(true)] out PropertyInfo? propertyInfo, out ImmutableArray diagnostics) { + // Special case for downlevel: if a field has the 'partial' modifier, ignore it. + // This is because older compilers might parse a partial property as a field. + // In that case, we ignore it here and rely on Roslyn producing a build error. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration) && memberSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + propertyInfo = null; + diagnostics = ImmutableArray.Empty; + + return false; + } + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); // Validate the target type From ca5d9ddd9a79baa0aeb39daffd70e49bf0006ebf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 22:46:16 -0700 Subject: [PATCH 38/43] Fix a unit test on .NET Framework --- .../Test_SourceGeneratorsCodegen.cs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs index dce57de5c..658d20aeb 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -730,6 +730,7 @@ partial class MyViewModel : ObservableValidator } """; +#if NET6_0_OR_GREATER string result = """ // #pragma warning disable @@ -787,6 +788,64 @@ public partial string Name } } """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; +#endif VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); } From 648788ca0deb5e5c24a44bc236f25145256f6670 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 22:59:33 -0700 Subject: [PATCH 39/43] Fix last remaining unit test, and test regex --- .../Test_SourceGeneratorsDiagnostics.cs | 17 ++++++++++++++++- .../Test_SourceGeneratorsDiagnostics.cs | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 1bb0b461c..18da79da2 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -222,6 +222,7 @@ public partial class SampleViewModel : ObservableObject [TestMethod] public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnInitOnlyProperty_Warns() { +#if NET6_0_OR_GREATER const string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -234,7 +235,21 @@ public partial class SampleViewModel : ObservableObject } } """; +#else + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { get; {|CS0518:init|}; } + } + } + """; +#endif - await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0518", "CS9248"]); } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 6883440df..605651520 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -1917,7 +1917,7 @@ internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration "Foo()") - string source = Regex.Replace(markdownSource, @"{\|((?:,?\w+)+):(.+)\|}", m => m.Groups[2].Value); + string source = Regex.Replace(markdownSource, @"{\|((?:,?\w+)+):(.+?)\|}", m => m.Groups[2].Value); VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion)), generators, generatorDiagnosticsIds, ignoredDiagnosticIds); } From 9c9ff44c196fb595f05162d0acc93195d0cf1dd4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 23:00:55 -0700 Subject: [PATCH 40/43] Set version.json to 8.4.0 --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 72763352d..b2cb92bde 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "8.3.0-build.{height}", + "version": "8.4.0-build.{height}", "publicReleaseRefSpec": [ "^refs/heads/main$", // we release out of main "^refs/heads/dev$", // we release out of dev From f75edabda8e4735534dd512cf1cbab08f66ecc7d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 24 Oct 2024 23:49:04 -0700 Subject: [PATCH 41/43] Don't suggest partial properties for static fields --- ...rvablePropertyOnPartialPropertyAnalyzer.cs | 9 +++++++++ .../Test_SourceGeneratorsDiagnostics.cs | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs index 16a42bc1c..8fbe4c978 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -56,6 +56,15 @@ public override void Initialize(AnalysisContext context) return; } + // It's not really meant to be used this way, but technically speaking the generator also supports + // static fields. So for those users leveraging that (for whatever reason), make sure to skip those. + // Partial properties using [ObservableProperty] cannot be static, and we never want the code fixer + // to prompt the user, run, and then result in code that will fail to compile. + if (fieldSymbol.IsStatic) + { + return; + } + // Emit the diagnostic for this field to suggest changing to a partial property instead context.ReportDiagnostic(Diagnostic.Create( UseObservablePropertyOnPartialProperty, diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs index 18da79da2..e38182eaf 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -252,4 +252,23 @@ public partial class SampleViewModel : ObservableObject await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0518", "CS9248"]); } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsPreview_OnStaticField_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private static string name; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } } From 0a81f27b98cad335aebebc294fdf82a0b455681c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 25 Oct 2024 00:05:22 -0700 Subject: [PATCH 42/43] Support field initializers in code fixer --- ...lPropertyForObservablePropertyCodeFixer.cs | 6 + ...lPropertyForObservablePropertyCodeFixer.cs | 151 ++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs index ebeee4cc7..f688fa8f8 100644 --- a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -252,6 +252,12 @@ private static async Task ConvertToPartialProperty( .AddAttributeLists(setterAttributes) .WithAdditionalAnnotations(Formatter.Annotation)); + // If the field has an initializer, preserve that on the property + if (fieldDeclaration.Declaration.Variables[0].Initializer is { } fieldInitializer) + { + propertyDeclaration = propertyDeclaration.WithInitializer(fieldInitializer).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + } + // Create an editor to perform all mutations. This allows to keep track of multiple // replacements for nodes on the same original tree, which otherwise wouldn't work. SyntaxEditor editor = new(root, document.Project.Solution.Workspace); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs index f574a0836..6ecdabb23 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4110.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -499,4 +499,155 @@ public void M() await test.RunAsync(); } + + [TestMethod] + public async Task SimpleField_WithInitializer1() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i = 42; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } = 42; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,6): info MVVMTK0042: The field C.C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 6, 5, 24).WithArguments("C", "C.i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + + // /0/Test0.cs(6,24): error CS8050: Only auto-implemented properties can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(6, 24, 6, 25), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleField_WithInitializer2() + { + string original = """ + using System.Collections.Generic; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private ICollection items = ["A", "B", "C"]; + } + """; + + string @fixed = """ + using System.Collections.Generic; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial ICollection Items { get; set; } = ["A", "B", "C"]; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.items using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.items"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,40): error CS8050: Only auto-implemented properties can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(7, 40, 7, 45), + + // /0/Test0.cs(7,40): error CS9248: Partial property 'C.Items' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 40, 7, 45).WithArguments("C.Items"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleField_WithInitializer3() + { + string original = """ + using System.Collections.Generic; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private ICollection items = new List { "A", "B", "C" }; + } + """; + + string @fixed = """ + using System.Collections.Generic; + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial ICollection Items { get; set; } = new List { "A", "B", "C" }; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,6): info MVVMTK0042: The field C.C.items using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 6, 6, 24).WithArguments("C", "C.items"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,40): error CS8050: Only auto-implemented properties can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(7, 40, 7, 45), + + // /0/Test0.cs(7,40): error CS9248: Partial property 'C.Items' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 40, 7, 45).WithArguments("C.Items"), + }); + + await test.RunAsync(); + } } From a7840b2c75e9bc23f26321e8bf00f60d2ad7b379 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 26 Oct 2024 11:08:34 -0700 Subject: [PATCH 43/43] Don't run analyzers on generated code --- .../ObservablePropertyGenerator.Execute.cs | 10 +--------- .../AsyncVoidReturningRelayCommandMethodAnalyzer.cs | 2 +- ...ldTargetedObservablePropertyAttributeAnalyzer.cs | 2 +- ...assUsingAttributeInsteadOfInheritanceAnalyzer.cs | 2 +- ...ldReferenceForObservablePropertyFieldAnalyzer.cs | 2 +- ...DependentObservablePropertyAttributesAnalyzer.cs | 2 +- ...lassLevelNotifyDataErrorInfoAttributeAnalyzer.cs | 2 +- ...ifyPropertyChangedRecipientsAttributeAnalyzer.cs | 2 +- ...pertyLevelObservablePropertyAttributeAnalyzer.cs | 13 +------------ .../RequiresCSharpLanguageVersionPreviewAnalyzer.cs | 2 +- .../UnsupportedCSharpLanguageVersionAnalyzer.cs | 2 +- ...portedRoslynVersionForPartialPropertyAnalyzer.cs | 2 +- ...seObservablePropertyOnPartialPropertyAnalyzer.cs | 2 +- 13 files changed, 13 insertions(+), 32 deletions(-) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 0cecdc6d5..6dc483e6b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -54,20 +54,12 @@ static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? contain return true; } -#if ROSLYN_4_11_0_OR_GREATER - // We only support matching properties on Roslyn 4.11 and greater + // Check that the target is a valid field or partial property if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && !InvalidPropertyLevelObservablePropertyAttributeAnalyzer.IsValidCandidateProperty(node, out parentNode)) { return false; } -#else - // Otherwise, we only support matching fields - if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode)) - { - return false; - } -#endif // The candidate member must be in a type with a base type (as it must derive from ObservableObject) return parentNode?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs index 8b8688d1e..164618ddf 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class AsyncVoidReturningRelayCommandMethodAnalyzer : DiagnosticAna /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs index 8740fe206..79390067b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs @@ -22,7 +22,7 @@ public sealed class AutoPropertyWithFieldTargetedObservablePropertyAttributeAnal /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs index 39419d2b0..13da28dd2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs @@ -54,7 +54,7 @@ public sealed class ClassUsingAttributeInsteadOfInheritanceAnalyzer : Diagnostic /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs index 599fa7cdb..df9052d36 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs @@ -32,7 +32,7 @@ public sealed class FieldReferenceForObservablePropertyFieldAnalyzer : Diagnosti /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs index 064223911..cc4796a23 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs @@ -36,7 +36,7 @@ public sealed class FieldWithOrphanedDependentObservablePropertyAttributesAnalyz /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); // Defer the registration so it can be skipped if C# 8.0 or more is not available. diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs index f4232ada0..bfdf087f1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer : Diag /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs index c9525ed3d..69d4cf0f8 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAna /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs index fdb349086..0b65550d0 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#if ROSLYN_4_11_0_OR_GREATER - using System.Collections.Immutable; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; @@ -26,7 +24,7 @@ public sealed class InvalidPropertyLevelObservablePropertyAttributeAnalyzer : Di /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => @@ -39,13 +37,6 @@ public override void Initialize(AnalysisContext context) context.RegisterSymbolAction(context => { - // Don't analyze generated symbols, we only want to warn those - // that users have actually written on their own in source code. - if (context.IsGeneratedCode) - { - return; - } - // We're intentionally only looking for properties here if (context.Symbol is not IPropertySymbol propertySymbol) { @@ -114,5 +105,3 @@ internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarati return true; } } - -#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs index aa96c9bef..301b0e3f4 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs @@ -24,7 +24,7 @@ public sealed class RequiresCSharpLanguageVersionPreviewAnalyzer : DiagnosticAna /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs index af81bcac4..42312904b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs @@ -41,7 +41,7 @@ public sealed class UnsupportedCSharpLanguageVersionAnalyzer : DiagnosticAnalyze /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); // Defer the callback registration to when the compilation starts, so we can execute more diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs index 9a9c53f02..dc0512312 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs @@ -24,7 +24,7 @@ public sealed class UnsupportedRoslynVersionForPartialPropertyAnalyzer : Diagnos /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs index 8fbe4c978..68a77d6d1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -24,7 +24,7 @@ public sealed class UseObservablePropertyOnPartialPropertyAnalyzer : DiagnosticA /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context =>