diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1478f9285..9543006cc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -31,7 +31,6 @@ body: required: true attributes: label: Steps to reproduce - render: text description: Provide steps to reproduce the issue, or let us know why it can't be reproduced (e.g. more complex setup, environment, dependencies, etc...). If you want to insert a code snippet, make sure to properly format it (add 3 backticks ` at the start and end of your code, followed by the language in use, eg. "csharp") and to remove leading whitespace in each line (if you're pasting code from another IDE where it was indented). placeholder: | Example repro steps: diff --git a/Directory.Build.props b/Directory.Build.props index 48a18c427..6510902b7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -25,7 +25,7 @@ - + @@ -39,7 +39,7 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 713592b83..50ebd69aa 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,14 +34,14 @@ jobs: # Test solution # + # Run .NET 8 unit tests + - script: dotnet test --no-build -c $(Build.Configuration) -f net8.0 -l "trx;LogFileName=VSTestResults_net8.0.trx" + displayName: Run .NET 8 unit tests + # Run .NET 7 unit tests - script: dotnet test --no-build -c $(Build.Configuration) -f net7.0 -l "trx;LogFileName=VSTestResults_net7.0.trx" displayName: Run .NET 7 unit tests - # Run .NET 6 unit tests - - script: dotnet test --no-build -c $(Build.Configuration) -f net6.0 -l "trx;LogFileName=VSTestResults_net6.0.trx" - displayName: Run .NET 6 unit tests - # Run .NET Framework 4.7.2 unit tests - script: dotnet test --no-build -c $(Build.Configuration) -f net472 -l "trx;LogFileName=VSTestResults_net472.trx" displayName: Run .NET Framework 4.7.2 unit tests diff --git a/build/Community.Toolkit.Common.props b/build/Community.Toolkit.Common.props index a43ae645d..4d0ec4379 100644 --- a/build/Community.Toolkit.Common.props +++ b/build/Community.Toolkit.Common.props @@ -20,7 +20,7 @@ true - 11.0 + 12.0 enable $(Product) Asset diff --git a/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index e8d004357..7d7065f8b 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 @@ -53,7 +53,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CD16E790 build\Update-Headers.ps1 = build\Update-Headers.ps1 EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{6640D447-C28D-4DBB-91F4-3ADCE0CA64AD}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FeatureSwitches", "FeatureSwitches", "{6640D447-C28D-4DBB-91F4-3ADCE0CA64AD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests", "tests\CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests\CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj", "{9E09DA49-4389-4ECE-8B68-EBDB1221DA90}" EndProject @@ -61,27 +61,37 @@ 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeFixers.Roslyn4001", "src\CommunityToolkit.Mvvm.CodeFixers.Roslyn4001\CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.csproj", "{FCC13AD5-CEB8-4CC1-8250-89B616D126F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj", "{C342302D-A263-42D6-B8EE-01DEF8192690}" +EndProject +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.shproj", "{A2EBDA90-B720-430D-83F5-C6BCC355232C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.CodeFixers.Roslyn4120", "src\CommunityToolkit.Mvvm.CodeFixers.Roslyn4120\CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj", "{98572004-D29A-486E-8053-6D409557CE44}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Mvvm.Roslyn4120.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4120.UnitTests\CommunityToolkit.Mvvm.Roslyn4120.UnitTests.csproj", "{87BF1537-935A-414D-8318-458F61A6E562}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -457,6 +467,86 @@ Global {E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x64.Build.0 = Release|Any CPU {E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x86.ActiveCfg = Release|Any CPU {E79DCA2A-4C59-499F-85BD-F45215ED6B72}.Release|x86.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|ARM64.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x64.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x64.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x86.ActiveCfg = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Debug|x86.Build.0 = Debug|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|Any CPU.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM64.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|ARM64.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x64.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x64.Build.0 = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.ActiveCfg = Release|Any CPU + {FCC13AD5-CEB8-4CC1-8250-89B616D126F2}.Release|x86.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|ARM64.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x64.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x64.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x86.ActiveCfg = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Debug|x86.Build.0 = Debug|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|Any CPU.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM64.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|ARM64.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x64.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x64.Build.0 = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x86.ActiveCfg = Release|Any CPU + {C342302D-A263-42D6-B8EE-01DEF8192690}.Release|x86.Build.0 = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|ARM.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|ARM.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|ARM64.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|x64.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|x64.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|x86.ActiveCfg = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Debug|x86.Build.0 = Debug|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|Any CPU.Build.0 = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|ARM.ActiveCfg = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|ARM.Build.0 = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|ARM64.ActiveCfg = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|ARM64.Build.0 = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|x64.ActiveCfg = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|x64.Build.0 = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|x86.ActiveCfg = Release|Any CPU + {98572004-D29A-486E-8053-6D409557CE44}.Release|x86.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|ARM64.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x64.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x64.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x86.ActiveCfg = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Debug|x86.Build.0 = Debug|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|Any CPU.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM64.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|ARM64.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|x64.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|x64.Build.0 = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|x86.ActiveCfg = Release|Any CPU + {87BF1537-935A-414D-8318-458F61A6E562}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -479,6 +569,8 @@ 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} + {87BF1537-935A-414D-8318-458F61A6E562} = {B30036C4-D514-4E5B-A323-587A061772CE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5403B0C4-F244-4F73-A35C-FE664D0F4345} @@ -487,14 +579,20 @@ Global tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{4fcd501c-1bb5-465c-ad19-356dab6600c6}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{5b44f7f1-dca2-4776-924e-a266f7bbf753}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{5e7f1212-a54b-40ca-98c5-1ff5cd1a1638}*SharedItemsImports = 13 + tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{87bf1537-935a-414d-8318-458f61a6e562}*SharedItemsImports = 5 + src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{98572004-d29a-486e-8053-6d409557ce44}*SharedItemsImports = 5 + src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{a2ebda90-b720-430d-83f5-c6bcc355232c}*SharedItemsImports = 13 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{ad9c3223-8e37-4fd4-a0d4-a45119551d3a}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{b8dcd82e-b53b-4249-ad4e-f9b99acb9334}*SharedItemsImports = 13 + tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{c342302d-a263-42d6-b8ee-01def8192690}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{df455c40-b18e-4890-8758-7cccb5ca7052}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{e24d1146-5ad8-498f-a518-4890d8bf4937}*SharedItemsImports = 5 + src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{e79dca2a-4c59-499f-85bd-f45215ed6b72}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{e827a9cd-405f-43e4-84c7-68cc7e845cdc}*SharedItemsImports = 13 tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{ecfe93aa-4b98-4292-b3fa-9430d513b4f9}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{f3799252-7a66-4533-89d8-b3c312052d95}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{fb59ce88-7732-4a63-b5bd-ac5681b7da1a}*SharedItemsImports = 13 + src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{fcc13ad5-ceb8-4cc1-8250-89b616d126f2}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems*{fe3ea695-ea0f-4e5f-9257-e059aaa23b10}*SharedItemsImports = 5 EndGlobalSection EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 000000000..00b67caef --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Common/CommunityToolkit.Common.csproj b/src/CommunityToolkit.Common/CommunityToolkit.Common.csproj index f2d7cb70b..a7d4a430b 100644 --- a/src/CommunityToolkit.Common/CommunityToolkit.Common.csproj +++ b/src/CommunityToolkit.Common/CommunityToolkit.Common.csproj @@ -1,7 +1,7 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;netstandard2.1;net8.0 diff --git a/src/CommunityToolkit.Common/Deferred/EventDeferral.cs b/src/CommunityToolkit.Common/Deferred/EventDeferral.cs index d5a36b7cb..65660ff03 100644 --- a/src/CommunityToolkit.Common/Deferred/EventDeferral.cs +++ b/src/CommunityToolkit.Common/Deferred/EventDeferral.cs @@ -4,6 +4,9 @@ using System; using System.ComponentModel; +#if NET8_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif using System.Threading; using System.Threading.Tasks; @@ -16,8 +19,11 @@ namespace CommunityToolkit.Common.Deferred; /// public class EventDeferral : IDisposable { - // TODO: If/when .NET 6 is base, we can upgrade to non-generic version +#if NET8_0_OR_GREATER + private readonly TaskCompletionSource taskCompletionSource = new(); +#else private readonly TaskCompletionSource taskCompletionSource = new(); +#endif internal EventDeferral() { @@ -26,7 +32,14 @@ internal EventDeferral() /// /// Call when finished with the Deferral. /// - public void Complete() => this.taskCompletionSource.TrySetResult(null); + public void Complete() + { +#if NET8_0_OR_GREATER + this.taskCompletionSource.TrySetResult(); +#else + this.taskCompletionSource.TrySetResult(null); +#endif + } /// /// Waits for the to be completed by the event handler. @@ -38,9 +51,19 @@ internal EventDeferral() [Obsolete("This is an internal only method to be used by EventHandler extension classes, public callers should call GetDeferral() instead on the DeferredEventArgs.")] public async Task WaitForCompletion(CancellationToken cancellationToken) { - using (cancellationToken.Register(() => this.taskCompletionSource.TrySetCanceled())) + using (cancellationToken.Register( +#if NET8_0_OR_GREATER + callback: static obj => Unsafe.As(obj!).taskCompletionSource.TrySetCanceled(), +#else + callback: static obj => ((EventDeferral)obj).taskCompletionSource.TrySetCanceled(), +#endif + state: this)) { +#if NET8_0_OR_GREATER + await this.taskCompletionSource.Task; +#else _ = await this.taskCompletionSource.Task; +#endif } } diff --git a/src/CommunityToolkit.Diagnostics/CommunityToolkit.Diagnostics.csproj b/src/CommunityToolkit.Diagnostics/CommunityToolkit.Diagnostics.csproj index 6cd510f16..cc9d42d39 100644 --- a/src/CommunityToolkit.Diagnostics/CommunityToolkit.Diagnostics.csproj +++ b/src/CommunityToolkit.Diagnostics/CommunityToolkit.Diagnostics.csproj @@ -1,7 +1,7 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;netstandard2.1;net8.0 @@ -19,14 +19,14 @@ - + - + diff --git a/src/CommunityToolkit.Diagnostics/Internals/Guard.ThrowHelper.cs b/src/CommunityToolkit.Diagnostics/Internals/Guard.ThrowHelper.cs index c76fd642d..6d28cfae4 100644 --- a/src/CommunityToolkit.Diagnostics/Internals/Guard.ThrowHelper.cs +++ b/src/CommunityToolkit.Diagnostics/Internals/Guard.ThrowHelper.cs @@ -60,7 +60,7 @@ public static void ThrowArgumentExceptionForIsNull(T? value, string name) [DoesNotReturn] public static void ThrowArgumentNullExceptionForIsNotNull(string name) { - throw new ArgumentNullException(name, $"Parameter {AssertString(name)} ({typeof(T).ToTypeString()}) must be not null)."); + throw new ArgumentNullException(name, $"Parameter {AssertString(name)} ({typeof(T).ToTypeString()}) must be not null."); } /// diff --git a/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs b/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs index 3cb8f12e1..b1ef81e37 100644 --- a/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs +++ b/src/CommunityToolkit.HighPerformance/Buffers/StringPool.cs @@ -85,7 +85,7 @@ static void FindFactors(int size, int factor, out uint x, out uint y) // that we consider acceptable, and pick the best results produced. // The ratio between maps influences the number of objects being allocated, // as well as the multithreading performance when locking on maps. - // We still want to contraint this number to avoid situations where we + // We still want to constraint this number to avoid situations where we // have a way too high number of maps compared to total size. FindFactors(minimumSize, 2, out uint x2, out uint y2); FindFactors(minimumSize, 3, out uint x3, out uint y3); @@ -113,7 +113,7 @@ static void FindFactors(int size, int factor, out uint x, out uint y) // We preallocate the maps in advance, since each bucket only contains the // array field, which is not preinitialized, so the allocations are minimal. - // This lets us lock on each individual maps when retrieving a string instance. + // This lets us lock on each individual map when retrieving a string instance. foreach (ref FixedSizePriorityMap map in span) { map = new FixedSizePriorityMap((int)y2); diff --git a/src/CommunityToolkit.HighPerformance/CommunityToolkit.HighPerformance.csproj b/src/CommunityToolkit.HighPerformance/CommunityToolkit.HighPerformance.csproj index bfc0e6bbe..06972b24b 100644 --- a/src/CommunityToolkit.HighPerformance/CommunityToolkit.HighPerformance.csproj +++ b/src/CommunityToolkit.HighPerformance/CommunityToolkit.HighPerformance.csproj @@ -1,7 +1,7 @@ - netstandard2.0;netstandard2.1;net6.0;net7.0 + netstandard2.0;netstandard2.1;net7.0;net8.0 @@ -30,10 +30,10 @@ - - - - + + + + @@ -45,7 +45,7 @@ - + diff --git a/src/CommunityToolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs b/src/CommunityToolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs index df06445ca..d2d7c33a6 100644 --- a/src/CommunityToolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs +++ b/src/CommunityToolkit.HighPerformance/Enumerables/SpanTokenizer{T}.cs @@ -73,7 +73,10 @@ public bool MoveNext() { this.start = newEnd; - int index = this.span.Slice(newEnd).IndexOf(this.separator); + // Here we're inside the 'CommunityToolkit.HighPerformance.Enumerables' namespace, so the + // 'MemoryExtensions' type from the .NET Community Toolkit would be bound instead. Because + // want the one from the BCL (to search by value), we can use its fully qualified name. + int index = System.MemoryExtensions.IndexOf(this.span.Slice(newEnd), this.separator); // Extract the current subsequence if (index >= 0) diff --git a/src/CommunityToolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs index 6d659db42..4bdfcc8d0 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/IBufferWriterExtensions.cs @@ -48,10 +48,9 @@ public static Stream AsStream(this IBufferWriter writer) public static unsafe void Write(this IBufferWriter writer, T value) where T : unmanaged { - int length = sizeof(T); - Span span = writer.GetSpan(1); + Span span = writer.GetSpan(sizeof(T)); - if (span.Length < length) + if (span.Length < sizeof(T)) { ThrowArgumentExceptionForEndOfBuffer(); } @@ -60,7 +59,7 @@ public static unsafe void Write(this IBufferWriter writer, T value) Unsafe.WriteUnaligned(ref r0, value); - writer.Advance(length); + writer.Advance(sizeof(T)); } /// diff --git a/src/CommunityToolkit.HighPerformance/Extensions/NullableExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/NullableExtensions.cs index 98fe1f38b..f4a6cac3a 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/NullableExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/NullableExtensions.cs @@ -48,7 +48,7 @@ public static ref T DangerousGetValueOrDefaultReference(this ref T? value) /// The type of the underlying value. /// The . /// A reference to the value of the input instance, or a reference. - /// The returned reference can be tested for using . + /// The returned reference can be tested for using . [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe ref T DangerousGetValueOrNullReference(ref this T? value) where T : struct diff --git a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySequenceExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySequenceExtensions.cs new file mode 100644 index 000000000..e4ca8f766 --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySequenceExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.HighPerformance.Streams; +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.HighPerformance; + +/// +/// Helpers for working with the type. +/// +public static class ReadOnlySequenceExtensions +{ + /// + /// Returns a wrapping the contents of the given of instance. + /// + /// The input of instance. + /// A wrapping the data within . + /// + /// Since this method only receives a instance, which does not track + /// the lifetime of its underlying buffer, it is responsibility of the caller to manage that. + /// In particular, the caller must ensure that the target buffer is not disposed as long + /// as the returned is in use, to avoid unexpected issues. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Stream AsStream(this ReadOnlySequence sequence) + { + return ReadOnlySequenceStream.Create(sequence); + } +} diff --git a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs index 0e78fc341..e7e64c788 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -210,10 +210,10 @@ public static ReadOnlySpan2D AsSpan2D(this ReadOnlySpan span, int offse /// The reference to the target item to get the index for. /// The index of within , or -1. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int IndexOf(this ReadOnlySpan span, in T value) + public static unsafe int IndexOf(this ReadOnlySpan span, ref readonly T value) { ref T r0 = ref MemoryMarshal.GetReference(span); - ref T r1 = ref Unsafe.AsRef(value); + ref T r1 = ref Unsafe.AsRef(in value); IntPtr byteOffset = Unsafe.ByteOffset(ref r0, ref r1); nint elementOffset = byteOffset / (nint)(uint)sizeof(T); diff --git a/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs index 7fb71727a..21f0725e7 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -148,10 +148,15 @@ public static Span Cast(this Span span) /// The reference to the target item to get the index for. /// The index of within , or -1. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int IndexOf(this Span span, ref T value) + public static unsafe int IndexOf(this Span span, ref readonly T value) { ref T r0 = ref MemoryMarshal.GetReference(span); - IntPtr byteOffset = Unsafe.ByteOffset(ref r0, ref value); + IntPtr byteOffset = +#if NET8_0_OR_GREATER + Unsafe.ByteOffset(ref r0, in value); +#else + Unsafe.ByteOffset(ref r0, ref Unsafe.AsRef(in value)); +#endif nint elementOffset = byteOffset / (nint)(uint)sizeof(T); diff --git a/src/CommunityToolkit.HighPerformance/Extensions/StreamExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/StreamExtensions.cs index a4808e15b..0cee5f4c9 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/StreamExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/StreamExtensions.cs @@ -169,7 +169,7 @@ public static int Read(this Stream stream, Span buffer) /// The destination to write data to. /// The source to read data from. #if NETSTANDARD2_1_OR_GREATER - [Obsolete("This API is only available for binary compatibility, but Stream.Read should be used instead.")] + [Obsolete("This API is only available for binary compatibility, but Stream.Write should be used instead.")] [EditorBrowsable(EditorBrowsableState.Never)] #endif public static void Write(this Stream stream, ReadOnlySpan buffer) @@ -275,7 +275,7 @@ public static unsafe void Write(this Stream stream, in T value) where T : unmanaged { #if NETSTANDARD2_1_OR_GREATER - ref T r0 = ref Unsafe.AsRef(value); + ref T r0 = ref Unsafe.AsRef(in value); ref byte r1 = ref Unsafe.As(ref r0); int length = sizeof(T); diff --git a/src/CommunityToolkit.HighPerformance/Extensions/StringExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/StringExtensions.cs index a3260c5bd..b2875637c 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/StringExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/StringExtensions.cs @@ -27,7 +27,7 @@ public static class StringExtensions public static ref char DangerousGetReference(this string text) { #if NET6_0_OR_GREATER - return ref Unsafe.AsRef(text.GetPinnableReference()); + return ref Unsafe.AsRef(in text.GetPinnableReference()); #else return ref MemoryMarshal.GetReference(text.AsSpan()); #endif @@ -44,7 +44,7 @@ public static ref char DangerousGetReference(this string text) public static ref char DangerousGetReferenceAt(this string text, int i) { #if NET6_0_OR_GREATER - ref char r0 = ref Unsafe.AsRef(text.GetPinnableReference()); + ref char r0 = ref Unsafe.AsRef(in text.GetPinnableReference()); #else ref char r0 = ref MemoryMarshal.GetReference(text.AsSpan()); #endif diff --git a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs index 5a8fe49d5..4ea95a236 100644 --- a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs +++ b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.For.IAction.cs @@ -172,7 +172,7 @@ public static void For(int start, int end, in TAction action, int minim { for (int i = start; i < end; i++) { - Unsafe.AsRef(action).Invoke(i); + Unsafe.AsRef(in action).Invoke(i); } return; @@ -225,7 +225,7 @@ public void Invoke(int i) for (int j = low; j < stop; j++) { - Unsafe.AsRef(this.action).Invoke(j); + Unsafe.AsRef(in this.action).Invoke(j); } } } diff --git a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs index 6a16c8ca1..66009ea7b 100644 --- a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs +++ b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.For.IAction2D.cs @@ -257,7 +257,7 @@ public static void For2D(int top, int bottom, int left, int right, in T { for (int x = left; x < right; x++) { - Unsafe.AsRef(action).Invoke(y, x); + Unsafe.AsRef(in action).Invoke(y, x); } } @@ -319,7 +319,7 @@ public void Invoke(int i) { for (int x = this.startX; x < this.endX; x++) { - Unsafe.AsRef(this.action).Invoke(y, x); + Unsafe.AsRef(in this.action).Invoke(y, x); } } } diff --git a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs index 7d4ae7a7c..f0e4790af 100644 --- a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs +++ b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction.cs @@ -91,7 +91,7 @@ public static void ForEach(ReadOnlyMemory memory, in TAct { foreach (TItem? item in memory.Span) { - Unsafe.AsRef(action).Invoke(item); + Unsafe.AsRef(in action).Invoke(item); } return; @@ -144,7 +144,7 @@ public void Invoke(int i) while (Unsafe.IsAddressLessThan(ref rStart, ref rEnd)) { - Unsafe.AsRef(this.action).Invoke(in rStart); + Unsafe.AsRef(in this.action).Invoke(in rStart); rStart = ref Unsafe.Add(ref rStart, 1); } diff --git a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs index e10361c16..a600c529c 100644 --- a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs +++ b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IInAction2D.cs @@ -144,7 +144,7 @@ public void Invoke(int i) while (Unsafe.IsAddressLessThan(ref rStart, ref rEnd)) { - Unsafe.AsRef(this.action).Invoke(in rStart); + Unsafe.AsRef(in this.action).Invoke(in rStart); rStart = ref Unsafe.Add(ref rStart, 1); } diff --git a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs index ca990f4fc..86ed6290f 100644 --- a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs +++ b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction.cs @@ -91,7 +91,7 @@ public static void ForEach(Memory memory, in TAction acti { foreach (ref TItem item in memory.Span) { - Unsafe.AsRef(action).Invoke(ref item); + Unsafe.AsRef(in action).Invoke(ref item); } return; @@ -144,7 +144,7 @@ public void Invoke(int i) while (Unsafe.IsAddressLessThan(ref rStart, ref rEnd)) { - Unsafe.AsRef(this.action).Invoke(ref rStart); + Unsafe.AsRef(in this.action).Invoke(ref rStart); rStart = ref Unsafe.Add(ref rStart, 1); } diff --git a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs index 4a6bfc28c..799f26d69 100644 --- a/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs +++ b/src/CommunityToolkit.HighPerformance/Helpers/ParallelHelper.ForEach.IRefAction2D.cs @@ -151,7 +151,7 @@ public void Invoke(int i) while (Unsafe.IsAddressLessThan(ref rStart, ref rEnd)) { - Unsafe.AsRef(this.action).Invoke(ref rStart); + Unsafe.AsRef(in this.action).Invoke(ref rStart); rStart = ref Unsafe.Add(ref rStart, 1); } diff --git a/src/CommunityToolkit.HighPerformance/Memory/Memory2D{T}.cs b/src/CommunityToolkit.HighPerformance/Memory/Memory2D{T}.cs index 2b7b09b40..848499cf8 100644 --- a/src/CommunityToolkit.HighPerformance/Memory/Memory2D{T}.cs +++ b/src/CommunityToolkit.HighPerformance/Memory/Memory2D{T}.cs @@ -338,7 +338,7 @@ public Memory2D(MemoryManager memoryManager, int height, int width) /// /// Thrown when the requested area is outside of bounds for . /// - public Memory2D(MemoryManager memoryManager, int offset, int height, int width, int pitch) + public unsafe Memory2D(MemoryManager memoryManager, int offset, int height, int width, int pitch) { int length = memoryManager.GetSpan().Length; @@ -378,7 +378,7 @@ public Memory2D(MemoryManager memoryManager, int offset, int height, int widt } this.instance = memoryManager; - this.offset = (nint)(uint)offset; + this.offset = (nint)(uint)offset * (nint)(uint)sizeof(T); this.height = height; this.width = width; this.pitch = pitch; @@ -413,7 +413,7 @@ internal Memory2D(Memory memory, int height, int width) /// /// Thrown when the requested area is outside of bounds for . /// - internal Memory2D(Memory memory, int offset, int height, int width, int pitch) + internal unsafe Memory2D(Memory memory, int offset, int height, int width, int pitch) { if ((uint)offset > (uint)memory.Length) { @@ -477,7 +477,7 @@ internal Memory2D(Memory memory, int offset, int height, int width, int pitch else if (MemoryMarshal.TryGetMemoryManager>(memory, out MemoryManager? memoryManager, out int memoryManagerStart, out _)) { this.instance = memoryManager; - this.offset = (nint)(uint)(memoryManagerStart + offset); + this.offset = (nint)(uint)(memoryManagerStart + offset) * (nint)(uint)sizeof(T); } else { diff --git a/src/CommunityToolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs b/src/CommunityToolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs index 7c57e620c..2786c8799 100644 --- a/src/CommunityToolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs +++ b/src/CommunityToolkit.HighPerformance/Memory/ReadOnlyMemory2D{T}.cs @@ -358,7 +358,7 @@ public ReadOnlyMemory2D(MemoryManager memoryManager, int height, int width) /// /// Thrown when the requested area is outside of bounds for . /// - public ReadOnlyMemory2D(MemoryManager memoryManager, int offset, int height, int width, int pitch) + public unsafe ReadOnlyMemory2D(MemoryManager memoryManager, int offset, int height, int width, int pitch) { int length = memoryManager.GetSpan().Length; @@ -398,7 +398,7 @@ public ReadOnlyMemory2D(MemoryManager memoryManager, int offset, int height, } this.instance = memoryManager; - this.offset = (nint)(uint)offset; + this.offset = (nint)(uint)offset * (nint)(uint)sizeof(T); this.height = height; this.width = width; this.pitch = pitch; @@ -433,7 +433,7 @@ internal ReadOnlyMemory2D(ReadOnlyMemory memory, int height, int width) /// /// Thrown when the requested area is outside of bounds for . /// - internal ReadOnlyMemory2D(ReadOnlyMemory memory, int offset, int height, int width, int pitch) + internal unsafe ReadOnlyMemory2D(ReadOnlyMemory memory, int offset, int height, int width, int pitch) { if ((uint)offset > (uint)memory.Length) { @@ -489,7 +489,7 @@ internal ReadOnlyMemory2D(ReadOnlyMemory memory, int offset, int height, int else if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager? memoryManager, out int memoryManagerStart, out _)) { this.instance = memoryManager; - this.offset = (nint)(uint)(memoryManagerStart + offset); + this.offset = (nint)(uint)(memoryManagerStart + offset) * (nint)(uint)sizeof(T); } else { diff --git a/src/CommunityToolkit.HighPerformance/Streams/MemoryStream.Validate.cs b/src/CommunityToolkit.HighPerformance/Streams/MemoryStream.Validate.cs index e7dae1359..126d90a44 100644 --- a/src/CommunityToolkit.HighPerformance/Streams/MemoryStream.Validate.cs +++ b/src/CommunityToolkit.HighPerformance/Streams/MemoryStream.Validate.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; @@ -24,6 +25,20 @@ public static void ValidatePosition(long position, int length) } } + /// + /// Validates the argument (it needs to be in the [0, length]) range. + /// + /// The new value being set. + /// The maximum length of the target . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ValidatePosition(long position, long length) + { + if ((ulong)position > (ulong)length) + { + ThrowArgumentOutOfRangeExceptionForPosition(); + } + } + /// /// Validates the or arguments. /// @@ -31,7 +46,7 @@ public static void ValidatePosition(long position, int length) /// The offset within . /// The number of elements to process within . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidateBuffer(byte[]? buffer, int offset, int count) + public static void ValidateBuffer([NotNull] byte[]? buffer, int offset, int count) { if (buffer is null) { diff --git a/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs b/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs new file mode 100644 index 000000000..2e76f6a3f --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Streams/ReadOnlySequenceStream.cs @@ -0,0 +1,304 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + +namespace CommunityToolkit.HighPerformance.Streams; + +/// +/// A implementation wrapping a of instance. +/// +internal sealed partial class ReadOnlySequenceStream : Stream +{ + /// + /// The instance currently in use. + /// + private readonly ReadOnlySequence source; + + /// + /// The current position within . + /// + private long position; + + /// + /// Indicates whether or not the current instance has been disposed + /// + private bool disposed; + + /// + /// Initializes a new instance of the class with the specified source. + /// + /// The source. + public ReadOnlySequenceStream(ReadOnlySequence source) + { + this.source = source; + } + + /// + public sealed override bool CanRead + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !this.disposed; + } + + /// + public sealed override bool CanSeek + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => !this.disposed; + } + + /// + public sealed override bool CanWrite + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => false; + } + + /// + public sealed override long Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + MemoryStream.ValidateDisposed(this.disposed); + + return this.source.Length; + } + } + + /// + public sealed override long Position + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + MemoryStream.ValidateDisposed(this.disposed); + + return this.position; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set + { + MemoryStream.ValidateDisposed(this.disposed); + MemoryStream.ValidatePosition(value, this.source.Length); + + this.position = value; + } + } + + /// + /// Creates a new from the input of instance. + /// + /// The input instance. + /// A wrapping the underlying data for . + public static Stream Create(ReadOnlySequence sequence) + { + return new ReadOnlySequenceStream(sequence); + } + + /// + public sealed override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + MemoryStream.ValidateDisposed(this.disposed); + + if (this.position >= this.source.Length) + { + return Task.CompletedTask; + } + + if (this.source.IsSingleSegment) + { + ReadOnlyMemory buffer = this.source.First.Slice(unchecked((int)this.position)); + + this.position = this.source.Length; + + return destination.WriteAsync(buffer, cancellationToken).AsTask(); + } + + async Task CoreCopyToAsync(Stream destination, CancellationToken cancellationToken) + { + ReadOnlySequence sequence = this.source.Slice(this.position); + + this.position = this.source.Length; + + foreach (ReadOnlyMemory segment in sequence) + { + await destination.WriteAsync(segment, cancellationToken).ConfigureAwait(false); + } + } + + return CoreCopyToAsync(destination, cancellationToken); + } + catch (OperationCanceledException e) + { + return Task.FromCanceled(e.CancellationToken); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + /// + public sealed override void Flush() + { + } + + /// + public sealed override Task FlushAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.CompletedTask; + } + + /// + public sealed override Task ReadAsync(byte[]? buffer, int offset, int count, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + int result = Read(buffer, offset, count); + + return Task.FromResult(result); + } + catch (OperationCanceledException e) + { + return Task.FromCanceled(e.CancellationToken); + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public sealed override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public sealed override long Seek(long offset, SeekOrigin origin) + { + MemoryStream.ValidateDisposed(this.disposed); + + long index = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => this.position + offset, + SeekOrigin.End => this.source.Length + offset, + _ => MemoryStream.ThrowArgumentExceptionForSeekOrigin() + }; + + MemoryStream.ValidatePosition(index, this.source.Length); + + this.position = index; + + return index; + } + + /// + public sealed override void SetLength(long value) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public sealed override int Read(byte[]? buffer, int offset, int count) + { + MemoryStream.ValidateDisposed(this.disposed); + MemoryStream.ValidateBuffer(buffer, offset, count); + + if (this.position >= this.source.Length) + { + return 0; + } + + ReadOnlySequence sequence = this.source.Slice(this.position); + Span destination = buffer.AsSpan(offset, count); + int bytesCopied = 0; + + foreach (ReadOnlyMemory segment in sequence) + { + int bytesToCopy = Math.Min(segment.Length, destination.Length); + + segment.Span.Slice(0, bytesToCopy).CopyTo(destination); + + destination = destination.Slice(bytesToCopy); + + bytesCopied += bytesToCopy; + + this.position += bytesToCopy; + + if (destination.Length == 0) + { + break; + } + } + + return bytesCopied; + } + + /// + public sealed override int ReadByte() + { + MemoryStream.ValidateDisposed(this.disposed); + + if (this.position == this.source.Length) + { + return -1; + } + + ReadOnlySequence sequence = this.source.Slice(this.position); + + this.position++; + + return sequence.First.Span[0]; + } + + /// + public sealed override void Write(byte[]? buffer, int offset, int count) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + public sealed override void WriteByte(byte value) + { + throw MemoryStream.GetNotSupportedException(); + } + + /// + protected override void Dispose(bool disposing) + { + this.disposed = true; + } +} diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj new file mode 100644 index 000000000..9f443712d --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001/CommunityToolkit.Mvvm.CodeFixers.Roslyn4001.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj new file mode 100644 index 000000000..9f443712d --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120/CommunityToolkit.Mvvm.CodeFixers.Roslyn4120.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj deleted file mode 100644 index 498bfc4b3..000000000 --- a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - netstandard2.0 - false - true - - - - - - - - - - - diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems new file mode 100644 index 000000000..0b0abb069 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems @@ -0,0 +1,21 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + a2ebda90-b720-430d-83f5-c6bcc355232c + + + CommunityToolkit.Mvvm.CodeFixers + + + + + + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props new file mode 100644 index 000000000..391336f41 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props @@ -0,0 +1,40 @@ + + + + netstandard2.0 + false + true + + + embedded + + + + + + + $(MSBuildProjectName.Substring(0, $([MSBuild]::Subtract($(MSBuildProjectName.Length), 11)))) + + + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 10)))) + + + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 4)), 1)) + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 3)), 2)) + $(MSBuildProjectName.Substring($([MSBuild]::Subtract($(MSBuildProjectName.Length), 1)), 1)) + $(MvvmToolkitSourceGeneratorRoslynMajorVersion).$(MvvmToolkitSourceGeneratorRoslynMinorVersion).$(MvvmToolkitSourceGeneratorRoslynPatchVersion) + + + $(DefineConstants);ROSLYN_4_3_1_OR_GREATER + $(DefineConstants);ROSLYN_4_12_0_OR_GREATER + + + + + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.shproj b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.shproj new file mode 100644 index 000000000..f36e53832 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.shproj @@ -0,0 +1,13 @@ + + + + a2ebda90-b720-430d-83f5-c6bcc355232c + 14.0 + + + + + + + + diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs new file mode 100644 index 000000000..ba0c3bec6 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -0,0 +1,405 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_12_0_OR_GREATER + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.SourceGenerators; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.CodeFixers; + +/// +/// A code fixer that converts fields using [ObservableProperty] to partial properties. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UsePartialPropertyForObservablePropertyCodeFixer : CodeFixProvider +{ + /// + /// The mapping of well-known MVVM Toolkit attributes. + /// + private static readonly ImmutableDictionary MvvmToolkitAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("NotifyCanExecuteChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyCanExecuteChangedForAttribute"), + new KeyValuePair("NotifyDataErrorInfoAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute"), + new KeyValuePair("NotifyPropertyChangedForAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedForAttribute"), + new KeyValuePair("NotifyPropertyChangedRecipientsAttribute", "CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute"), + new KeyValuePair("ObservablePropertyAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") + }); + + /// + /// The mapping of well-known data annotation attributes. + /// + private static readonly ImmutableDictionary DataAnnotationsAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("UIHintAttribute", "System.ComponentModel.DataAnnotations.UIHintAttribute"), + new KeyValuePair("ScaffoldColumnAttribute", "System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute"), + new KeyValuePair("DisplayAttribute", "System.ComponentModel.DataAnnotations.DisplayAttribute"), + new KeyValuePair("EditableAttribute", "System.ComponentModel.DataAnnotations.EditableAttribute"), + new KeyValuePair("KeyAttribute", "System.ComponentModel.DataAnnotations.KeyAttribute") + }); + + /// + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + UseObservablePropertyOnPartialPropertyId, + WinRTObservablePropertyOnFieldsIsNotAotCompatibleId); + + /// + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + // This code fixer needs the semantic model, so check that first + if (!context.Document.SupportsSemanticModel) + { + return; + } + + SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!; + + // If the language is not preview, we cannot apply this code fix (as it would generate invalid C# code) + if (!semanticModel.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Retrieve the properties passed by the analyzer + if (diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey] is not string fieldName || + diagnostic.Properties[FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey] is not string propertyName) + { + 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 convert the field declaration to a partial property + context.RegisterCodeFix( + CodeAction.Create( + title: "Use a partial property", + createChangedDocument: token => ConvertToPartialProperty(context.Document, root, fieldDeclaration, semanticModel, 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 semantic model for . + /// 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, + SemanticModel semanticModel, + string fieldName, + string propertyName, + CancellationToken cancellationToken) + { + await Task.CompletedTask; + + // Try to get all necessary type symbols to process the attributes + if (!semanticModel.Compilation.TryBuildNamedTypeSymbolMap(MvvmToolkitAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary? toolkitTypeSymbols) || + !semanticModel.Compilation.TryBuildNamedTypeSymbolMap(DataAnnotationsAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary? annotationTypeSymbols)) + { + return document; + } + + // Also query [ValidationAttribute] + if (semanticModel.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute") is not INamedTypeSymbol validationAttributeSymbol) + { + return document; + } + + // Get all attributes that were on the field. Here we only include those targeting either + // the field, or the property. Those targeting the accessors will be moved there directly. + List propertyAttributes = + fieldDeclaration + .AttributeLists + .Where(list => list.Target is null || list.Target.Identifier.Kind() is not (SyntaxKind.GetKeyword or SyntaxKind.SetKeyword)) + .ToList(); + + // Fixup attribute lists as following: + // 1) If they have the 'field:' target, keep it (it's no longer the default) + // 2) If they have the 'property:' target, remove it (it's not needed anymore) + // 3) If they are from the MVVM Toolkit, remove the target (they'll apply to the property) + // 4) If they have no target and they are either a validation attribute, or any of the well-known + // data annotation attributes (which are automatically forwarded), leave them without a target. + // 5) If they have no target, add 'field:' to preserve the original behavior + // 5) Otherwise, leave them without changes (this will carry over invalid targets as-is) + for (int i = 0; i < propertyAttributes.Count; i++) + { + AttributeListSyntax attributeListSyntax = propertyAttributes[i]; + + // Special case: the list has no attributes. Just remove it entirely. + if (attributeListSyntax.Attributes is []) + { + propertyAttributes.RemoveAt(i--); + + continue; + } + + // Case 1 + if (attributeListSyntax.Target?.Identifier.IsKind(SyntaxKind.FieldKeyword) is true) + { + continue; + } + + // Case 2 + if (attributeListSyntax.Target?.Identifier.IsKind(SyntaxKind.PropertyKeyword) is true) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(null); + + continue; + } + + if (attributeListSyntax.Attributes.Count == 1) + { + // Make sure we can retrieve the symbol for the attribute type + if (!semanticModel.GetSymbolInfo(attributeListSyntax.Attributes[0], cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol)) + { + return document; + } + + // Case 3 + if (toolkitTypeSymbols.ContainsValue(attributeSymbol)) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(null); + + continue; + } + + // Case 4 + if (annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol)) + { + continue; + } + + // Case 5 + if (attributeListSyntax.Target is null) + { + propertyAttributes[i] = attributeListSyntax.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword))); + + continue; + } + } + else + { + // If we have multiple attributes in the current list, we need additional logic here. + // We could have any number of attributes here, so we split them into three buckets: + // - MVVM Toolkit attributes: these should be moved over with no target + // - Data annotation or validation attributes: these should be moved over with the same target + // - Any other attributes: these should be moved over with the 'field' target + List mvvmToolkitAttributes = []; + List annotationOrValidationAttributes = []; + List fieldAttributes = []; + + foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) + { + // Like for the single attribute case, make sure we can get the symbol for the attribute + if (!semanticModel.GetSymbolInfo(attributeSyntax, cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol)) + { + return document; + } + + bool isAnnotationOrValidationAttribute = annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol); + + // Split the attributes into the buckets. Note that we have a special rule for annotation and validation + // attributes when no target is specified. In that case, we will merge them with the MVVM Toolkit items. + // This allows us to try to keep the attributes in the same attribute list, rather than splitting them. + if (toolkitTypeSymbols.ContainsValue(attributeSymbol) || (isAnnotationOrValidationAttribute && attributeListSyntax.Target is null)) + { + mvvmToolkitAttributes.Add(attributeSyntax); + } + else if (isAnnotationOrValidationAttribute) + { + annotationOrValidationAttributes.Add(attributeSyntax); + } + else + { + fieldAttributes.Add(attributeSyntax); + } + } + + // We need to start inserting the new lists right before the one we're currently + // processing. We'll be removing it when we're done, the buckets will replace it. + int insertionIndex = i; + + // Helper to process and insert the new synthesized attribute lists into the target collection + void InsertAttributeListIfNeeded(List attributes, AttributeTargetSpecifierSyntax? attributeTarget) + { + if (attributes is []) + { + return; + } + + AttributeListSyntax attributeList = AttributeList(SeparatedList(attributes)).WithTarget(attributeTarget); + + // Only if this is the first non empty list we're adding, carry over the original trivia + if (insertionIndex == i) + { + attributeList = attributeList.WithTriviaFrom(attributeListSyntax); + } + + // Finally, insert the new list into the final tree + propertyAttributes.Insert(insertionIndex++, attributeList); + } + + InsertAttributeListIfNeeded(mvvmToolkitAttributes, attributeTarget: null); + InsertAttributeListIfNeeded(annotationOrValidationAttributes, attributeTarget: attributeListSyntax.Target); + InsertAttributeListIfNeeded(fieldAttributes, attributeTarget: AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword))); + + // Remove the attribute list that we have just split into buckets + propertyAttributes.RemoveAt(insertionIndex); + + // Move the current loop iteration to the last inserted item. + // We decrement by 1 because the new loop iteration will add 1. + i = insertionIndex - 1; + } + } + + // Separately, also get all attributes for the property getters + AttributeListSyntax[] getterAttributes = + fieldDeclaration + .AttributeLists + .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.GetKeyword) + .Select(list => list.WithTarget(null).WithAdditionalAnnotations(Formatter.Annotation)) + .ToArray(); + + // Also do the same for the setters + AttributeListSyntax[] setterAttributes = + fieldDeclaration + .AttributeLists + .Where(list => list.Target?.Identifier.Kind() is SyntaxKind.SetKeyword) + .Select(list => list.WithTarget(null).WithAdditionalAnnotations(Formatter.Annotation)) + .ToArray(); + + // Create the following property declaration: + // + // + // + // { + // + // get; + // + // + // set; + // } + PropertyDeclarationSyntax propertyDeclaration = + PropertyDeclaration(fieldDeclaration.Declaration.Type, Identifier(propertyName)) + .WithModifiers(GetPropertyModifiers(fieldDeclaration)) + .AddAttributeLists(propertyAttributes.ToArray()) + .WithAdditionalAnnotations(Formatter.Annotation) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .AddAttributeLists(getterAttributes) + .WithAdditionalAnnotations(Formatter.Annotation), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .AddAttributeLists(setterAttributes) + .WithAdditionalAnnotations(Formatter.Annotation)); + + // If the field has an initializer, preserve that on the property + if (fieldDeclaration.Declaration.Variables[0].Initializer is { } fieldInitializer) + { + propertyDeclaration = propertyDeclaration.WithInitializer(fieldInitializer).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + } + + // Create an editor to perform all mutations. This allows to keep track of multiple + // replacements for nodes on the same original tree, which otherwise wouldn't work. + SyntaxEditor editor = new(root, document.Project.Solution.Workspace.Services); + + 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; + } + + // Special case for 'this.' accesses: we want to drop the 'this.' prefix + if (identifierSyntax.Parent is MemberAccessExpressionSyntax { Expression: ThisExpressionSyntax } thisExpressionSyntax) + { + editor.ReplaceNode(thisExpressionSyntax, IdentifierName(propertyName)); + } + else + { + // Replace the field reference with a reference to the new property + editor.ReplaceNode(identifierSyntax, IdentifierName(propertyName)); + } + } + + return document.WithSyntaxRoot(editor.GetChangedRoot()); + } + + /// + /// Gets all modifiers that need to be added to a generated property. + /// + /// The for the field being updated. + /// The list of necessary modifiers for . + private static SyntaxTokenList GetPropertyModifiers(FieldDeclarationSyntax fieldDeclaration) + { + SyntaxTokenList propertyModifiers = TokenList(Token(SyntaxKind.PublicKeyword)); + + // Add the 'required' modifier if the field also had it + if (fieldDeclaration.Modifiers.Any(SyntaxKind.RequiredKeyword)) + { + propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.RequiredKeyword)); + } + + // Always add 'partial' last + return propertyModifiers.Add(Token(SyntaxKind.PartialKeyword)); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs new file mode 100644 index 000000000..9b4d7e63d --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForSemiAutoPropertyCodeFixer.cs @@ -0,0 +1,239 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_12_0_OR_GREATER + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; +using Microsoft.CodeAnalysis.Text; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.CodeFixers; + +/// +/// A code fixer that converts semi-auto properties to partial properties using [ObservableProperty]. +/// +[ExportCodeFixProvider(LanguageNames.CSharp)] +[Shared] +public sealed class UsePartialPropertyForSemiAutoPropertyCodeFixer : CodeFixProvider +{ + /// + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoPropertyId); + + /// + public override Microsoft.CodeAnalysis.CodeFixes.FixAllProvider? GetFixAllProvider() + { + return new FixAllProvider(); + } + + /// + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + Diagnostic diagnostic = context.Diagnostics[0]; + TextSpan diagnosticSpan = context.Span; + + // This code fixer needs the semantic model, so check that first + if (!context.Document.SupportsSemanticModel) + { + return; + } + + SyntaxNode? root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + + // Get the property declaration from the target diagnostic + if (root!.FindNode(diagnosticSpan) is PropertyDeclarationSyntax propertyDeclaration) + { + // Get the semantic model, as we need to resolve symbols + SemanticModel semanticModel = (await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false))!; + + // Make sure we can resolve the [ObservableProperty] attribute (as we want to add it in the fixed code) + if (semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + // Register the code fix to update the semi-auto property to a partial property + context.RegisterCodeFix( + CodeAction.Create( + title: "Use a partial property", + createChangedDocument: token => ConvertToPartialProperty(context.Document, root, propertyDeclaration, observablePropertySymbol), + equivalenceKey: "Use a partial property"), + diagnostic); + } + } + + /// + /// Applies the code fix to a target identifier and returns an updated document. + /// + /// The original document being fixed. + /// The original tree root belonging to the current document. + /// The for the property being updated. + /// The for [ObservableProperty]. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static async Task ConvertToPartialProperty( + Document document, + SyntaxNode root, + PropertyDeclarationSyntax propertyDeclaration, + INamedTypeSymbol observablePropertySymbol) + { + await Task.CompletedTask; + + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Create the attribute syntax for the new [ObservableProperty] attribute. Also + // annotate it to automatically add using directives to the document, if needed. + SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(observablePropertySymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + AttributeListSyntax observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax); + + // Create an editor to perform all mutations + SyntaxEditor syntaxEditor = new(root, document.Project.Solution.Workspace.Services); + + ConvertToPartialProperty( + propertyDeclaration, + observablePropertyAttributeList, + syntaxEditor); + + // Create the new document with the single change + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } + + /// + /// Applies the code fix to a target identifier and returns an updated document. + /// + /// The for the property being updated. + /// The with the attribute to add. + /// The instance to use. + /// An updated document with the applied code fix, and being replaced with a partial property. + private static void ConvertToPartialProperty( + PropertyDeclarationSyntax propertyDeclaration, + AttributeListSyntax observablePropertyAttributeList, + SyntaxEditor syntaxEditor) + { + // Start setting up the updated attribute lists + SyntaxList attributeLists = propertyDeclaration.AttributeLists; + + if (attributeLists is [AttributeListSyntax firstAttributeListSyntax, ..]) + { + // Remove the trivia from the original first attribute + attributeLists = attributeLists.Replace( + nodeInList: firstAttributeListSyntax, + newNode: firstAttributeListSyntax.WithoutTrivia()); + + // If the property has at least an attribute list, move the trivia from it to the new attribute + observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(firstAttributeListSyntax); + + // Insert the new attribute + attributeLists = attributeLists.Insert(0, observablePropertyAttributeList); + } + else + { + // Otherwise (there are no attribute lists), transfer the trivia to the new (only) attribute list + observablePropertyAttributeList = observablePropertyAttributeList.WithTriviaFrom(propertyDeclaration); + + // Save the new attribute list + attributeLists = attributeLists.Add(observablePropertyAttributeList); + } + + // Get a new property that is partial and with semicolon token accessors + PropertyDeclarationSyntax updatedPropertyDeclaration = + propertyDeclaration + .AddModifiers(Token(SyntaxKind.PartialKeyword)) + .WithoutLeadingTrivia() + .WithAttributeLists(attributeLists) + .WithAdditionalAnnotations(Formatter.Annotation) + .WithAccessorList(AccessorList(List( + [ + // Keep the accessors (so we can easily keep all trivia, modifiers, attributes, etc.) but make them semicolon only + propertyDeclaration.AccessorList!.Accessors[0] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithAdditionalAnnotations(Formatter.Annotation), + propertyDeclaration.AccessorList!.Accessors[1] + .WithBody(null) + .WithExpressionBody(null) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithTrailingTrivia(propertyDeclaration.AccessorList.Accessors[1].GetTrailingTrivia()) + .WithAdditionalAnnotations(Formatter.Annotation) + ])).WithTrailingTrivia(propertyDeclaration.AccessorList.GetTrailingTrivia())); + + syntaxEditor.ReplaceNode(propertyDeclaration, updatedPropertyDeclaration); + + // Find the parent type for the property + TypeDeclarationSyntax typeDeclaration = propertyDeclaration.FirstAncestorOrSelf()!; + + // Make sure it's partial (we create the updated node in the function to preserve the updated property declaration). + // If we created it separately and replaced it, the whole tree would also be replaced, and we'd lose the new property. + if (!typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + syntaxEditor.ReplaceNode(typeDeclaration, static (node, generator) => generator.WithModifiers(node, generator.GetModifiers(node).WithPartial(true))); + } + } + + /// + /// A custom with the logic from . + /// + private sealed class FixAllProvider : DocumentBasedFixAllProvider + { + /// + protected override async Task FixAllAsync(FixAllContext fixAllContext, Document document, ImmutableArray diagnostics) + { + // Get the semantic model, as we need to resolve symbols + if (await document.GetSemanticModelAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SemanticModel semanticModel) + { + return document; + } + + // Make sure we can resolve the [ObservableProperty] attribute here as well + if (semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return document; + } + + // Get the document root (this should always succeed) + if (await document.GetSyntaxRootAsync(fixAllContext.CancellationToken).ConfigureAwait(false) is not SyntaxNode root) + { + return document; + } + + SyntaxGenerator syntaxGenerator = SyntaxGenerator.GetGenerator(document); + + // Create the attribute syntax for the new [ObservableProperty] attribute here too + SyntaxNode attributeTypeSyntax = syntaxGenerator.TypeExpression(observablePropertySymbol).WithAdditionalAnnotations(Simplifier.AddImportsAnnotation); + AttributeListSyntax observablePropertyAttributeList = (AttributeListSyntax)syntaxGenerator.Attribute(attributeTypeSyntax); + + // Create an editor to perform all mutations (across all edits in the file) + SyntaxEditor syntaxEditor = new(root, fixAllContext.Solution.Services); + + foreach (Diagnostic diagnostic in diagnostics) + { + // Get the current property declaration for the diagnostic + if (root.FindNode(diagnostic.Location.SourceSpan) is not PropertyDeclarationSyntax propertyDeclaration) + { + continue; + } + + ConvertToPartialProperty( + propertyDeclaration, + observablePropertyAttributeList, + syntaxEditor); + } + + return document.WithSyntaxRoot(syntaxEditor.GetChangedRoot()); + } + } +} + +#endif 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.Roslyn4120/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.csproj b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.csproj new file mode 100644 index 000000000..3cea30b35 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index 53e58bfea..486e14158 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -75,3 +75,26 @@ MVVMTK0039 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warn Rule ID | Category | Severity | Notes --------|----------|----------|------- MVVMTK0040 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0040 + +## Release 8.4.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MVVMTK0041 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0041 +MVVMTK0042 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0042 +MVVMTK0043 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0043 +MVVMTK0044 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0044 +MVVMTK0045 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0045 +MVVMTK0046 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0046 +MVVMTK0047 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0047 +MVVMTK0048 | CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0048 +MVVMTK0049 | CommunityToolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0049 +MVVMTK0050 | CommunityToolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Warning | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0050 +MVVMTK0051 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0051 +MVVMTK0052 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0052 +MVVMTK0053 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0053 +MVVMTK0054 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0054 +MVVMTK0055 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0055 +MVVMTK0056 | CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Info | See https://aka.ms/mvvmtoolkit/errors/mvvmtk0056 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index 5bc1b1792..f875a91df 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -39,22 +39,39 @@ + + + + + + + + + + + + + + - + + + + @@ -63,12 +80,16 @@ + + + + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props index 6c5a90f5e..1517ae8cf 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props @@ -4,6 +4,15 @@ netstandard2.0 false true + + + embedded - - $(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_12_0_OR_GREATER + + + $(NoWarn);RS2003 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs index 1009cd4c1..74b456063 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/AttributeInfo.cs @@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using CommunityToolkit.Mvvm.SourceGenerators.Helpers; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -17,10 +18,12 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// /// A model representing an attribute declaration. /// +/// Indicates the target of the attribute. /// The type name of the attribute. /// The values for all constructor arguments for the attribute. /// The values for all named arguments for the attribute. internal sealed record AttributeInfo( + SyntaxKind AttributeTarget, string TypeName, EquatableArray ConstructorArgumentInfo, EquatableArray<(string Name, TypedConstantInfo Value)> NamedArgumentInfo) @@ -50,6 +53,7 @@ public static AttributeInfo Create(AttributeData attributeData) } return new( + SyntaxKind.PropertyKeyword, typeName, constructorArguments.ToImmutable(), namedArguments.ToImmutable()); @@ -61,6 +65,7 @@ public static AttributeInfo Create(AttributeData attributeData) /// The symbol for the attribute type. /// The instance for the current run. /// The sequence of instances to process. + /// The kind of target for the attribute. /// The cancellation token for the current operation. /// The resulting instance, if available /// Whether a resulting instance could be created. @@ -68,6 +73,7 @@ public static bool TryCreate( INamedTypeSymbol typeSymbol, SemanticModel semanticModel, IEnumerable arguments, + SyntaxKind syntaxKind, CancellationToken token, [NotNullWhen(true)] out AttributeInfo? info) { @@ -105,6 +111,7 @@ public static bool TryCreate( } info = new AttributeInfo( + syntaxKind, typeName, constructorArguments.ToImmutable(), namedArguments.ToImmutable()); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs index 2bf62d0de..1fee5b622 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -3,34 +3,48 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Mvvm.SourceGenerators.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models; /// /// A model representing an generated property /// +/// The syntax kind of the annotated member that triggered this property generation. /// The type name for the generated property, including nullability annotations. /// The field name. /// The generated property name. +/// The list of additional modifiers for the property (they are values). +/// The accessibility of the property. +/// The accessibility of the accessor. +/// The accessibility of the accessor. /// The sequence of property changing properties to notify. /// The sequence of property changed properties to notify. /// The sequence of commands to notify. /// Whether or not the generated property also broadcasts changes. /// Whether or not the generated property also validates its value. /// Whether the old property value is being directly referenced. -/// Indicates whether the property is of a reference type or an unconstrained type parameter. +/// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether to include nullability annotations on the setter. +/// Indicates whether to annotate the setter as requiring unreferenced code. /// The sequence of forwarded attributes for the generated property. internal sealed record PropertyInfo( + SyntaxKind AnnotatedMemberKind, string TypeNameWithNullabilityAnnotations, string FieldName, string PropertyName, + EquatableArray PropertyModifers, + Accessibility PropertyAccessibility, + Accessibility GetterAccessibility, + Accessibility SetterAccessibility, EquatableArray PropertyChangingNames, EquatableArray PropertyChangedNames, EquatableArray NotifiedCommandNames, bool NotifyPropertyChangedRecipients, bool NotifyDataErrorInfo, bool IsOldPropertyValueDirectlyReferenced, - bool IsReferenceTypeOrUnconstraindTypeParameter, + bool IsReferenceTypeOrUnconstrainedTypeParameter, bool IncludeMemberNotNullOnSetAccessor, + bool IncludeRequiresUnreferencedCodeOnSetAccessor, EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs index 855dce1fb..17a97d5bd 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; @@ -16,6 +17,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -30,58 +32,179 @@ partial class ObservablePropertyGenerator internal static class Execute { /// - /// Processes a given field. + /// Checks whether an input syntax node is a candidate property declaration for the generator. /// - /// The instance to process. - /// The input instance to process. + /// The input syntax node to check. + /// The used to cancel the operation, if needed. + /// Whether is a candidate property declaration. + public static bool IsCandidatePropertyDeclaration(SyntaxNode node, CancellationToken token) + { + // Matches a valid field declaration, for legacy support + static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) + { + // The node must represent a field declaration + if (node is not VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { AttributeLists.Count: > 0 } fieldNode } }) + { + containingTypeNode = null; + + return false; + } + + containingTypeNode = (TypeDeclarationSyntax?)fieldNode.Parent; + + return true; + } + + // Check that the target is a valid field or partial property + if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && + !InvalidPropertyLevelObservablePropertyAttributeAnalyzer.IsValidCandidateProperty(node, out parentNode)) + { + return false; + } + + // The candidate member must be in a type with a base type (as it must derive from ObservableObject) + return parentNode?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; + } + + /// + /// Checks whether a given candidate node is valid given a compilation. + /// + /// The instance to process. + /// The instance for the current run. + /// Whether is valid. + public static bool IsCandidateValidForCompilation(MemberDeclarationSyntax 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; + } + + /// + /// Performs additional checks before running the core generation logic. + /// + /// The input instance to process. + /// Whether is valid. + public static bool IsCandidateSymbolValid(ISymbol memberSymbol) + { +#if ROSLYN_4_12_0_OR_GREATER + // We only need these additional checks for properties (Roslyn already validates things for fields in our scenarios) + if (memberSymbol is IPropertySymbol propertySymbol) + { + // Ensure that the property declaration is a partial definition with no implementation + if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null }) + { + return false; + } + + // Also ignore all properties that have an invalid declaration + if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly || propertySymbol.Type.IsRefLikeType) + { + return false; + } + } +#endif + + // Pointer types are never allowed in either case + if (memberSymbol is + IPropertySymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer } or + IFieldSymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer }) + { + return false; + } + + // We assume all other cases are supported (other failure cases will be detected later) + return true; + } + + /// + /// 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.12 here, as if that's not the case all of these + // syntax nodes would already have pre-filtered well before this method could run at all. + if (node is PropertyDeclarationSyntax propertySyntax) + { + return propertySyntax; + } + + // Otherwise, assume all targets are field declarations + return (MemberDeclarationSyntax)node.Parent!.Parent!; + } + + /// + /// Processes a given field or property. + /// + /// The instance to process. + /// The input instance to process. /// The instance for the current run. + /// The options in use for the generator. /// The cancellation token for the current operation. /// The resulting value, if successfully retrieved. /// The resulting diagnostics from the processing operation. - /// The resulting instance for , if successful. + /// The resulting instance for , if successful. public static bool TryGetInfo( - FieldDeclarationSyntax fieldSyntax, - IFieldSymbol fieldSymbol, + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, SemanticModel semanticModel, + AnalyzerConfigOptions options, CancellationToken token, [NotNullWhen(true)] out PropertyInfo? propertyInfo, out ImmutableArray diagnostics) { - using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + // 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; + } // Validate the target type - if (!IsTargetTypeValid(fieldSymbol, out bool shouldInvokeOnPropertyChanging)) + if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging)) { - builder.Add( - InvalidContainingTypeForObservablePropertyFieldError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); - propertyInfo = null; - diagnostics = builder.ToImmutable(); + diagnostics = ImmutableArray.Empty; return false; } token.ThrowIfCancellationRequested(); + // Override the property changing support if explicitly disabled. + // This setting is enabled by default, for backwards compatibility. + shouldInvokeOnPropertyChanging &= options.GetMSBuildBooleanPropertyValue("MvvmToolkitEnableINotifyPropertyChangingSupport", defaultValue: true); + + token.ThrowIfCancellationRequested(); + // Get the property type and name - string typeNameWithNullabilityAnnotations = fieldSymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(); - string fieldName = fieldSymbol.Name; - string propertyName = GetGeneratedPropertyName(fieldSymbol); + string typeNameWithNullabilityAnnotations = GetPropertyType(memberSymbol).GetFullyQualifiedNameWithNullabilityAnnotations(); + string fieldName = memberSymbol.Name; + string propertyName = GetGeneratedPropertyName(memberSymbol); - // Check for name collisions - if (fieldName == propertyName) + // Check for name collisions (only for fields) + if (fieldName == propertyName && memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) { - builder.Add( - ObservablePropertyNameCollisionError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); - propertyInfo = null; - diagnostics = builder.ToImmutable(); + diagnostics = ImmutableArray.Empty; // If the generated property would collide, skip generating it entirely. This makes sure that // users only get the helpful diagnostic about the collision, and not the normal compiler error @@ -92,16 +215,10 @@ 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); - propertyInfo = null; - diagnostics = builder.ToImmutable(); + diagnostics = ImmutableArray.Empty; return false; } @@ -109,7 +226,6 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); using ImmutableArrayBuilder propertyChangedNames = ImmutableArrayBuilder.Rent(); - using ImmutableArrayBuilder propertyChangingNames = ImmutableArrayBuilder.Rent(); using ImmutableArrayBuilder notifiedCommandNames = ImmutableArrayBuilder.Rent(); using ImmutableArrayBuilder forwardedAttributes = ImmutableArrayBuilder.Rent(); @@ -118,30 +234,24 @@ public static bool TryGetInfo( bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients = false; bool hasOrInheritsClassLevelNotifyDataErrorInfo = false; bool hasAnyValidationAttributes = false; - bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName); + bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(memberSymbol, propertyName); token.ThrowIfCancellationRequested(); // Get the nullability info for the property GetNullabilityInfo( - fieldSymbol, + memberSymbol, semanticModel, - out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool isReferenceTypeOrUnconstrainedTypeParameter, out bool includeMemberNotNullOnSetAccessor); token.ThrowIfCancellationRequested(); - // Track the property changing event for the property, if the type supports it - if (shouldInvokeOnPropertyChanging) - { - propertyChangingNames.Add(propertyName); - } - // The current property is always notified 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; @@ -150,7 +260,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; @@ -158,20 +268,22 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + // 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; @@ -179,7 +291,7 @@ public static bool TryGetInfo( } // Check whether the property should also be validated - if (TryGetNotifyDataErrorInfo(fieldSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyDataErrorInfo, out isValidationTargetValid)) + if (TryGetNotifyDataErrorInfo(memberSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyDataErrorInfo, out isValidationTargetValid)) { notifyDataErrorInfo = isValidationTargetValid; @@ -191,96 +303,39 @@ public static bool TryGetInfo( { hasAnyValidationAttributes = true; - forwardedAttributes.Add(AttributeInfo.Create(attributeData)); - } - - // Also track the current attribute for forwarding if it is of any of the following types: - // - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute) - // - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute) - // - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute) - // - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute) - // - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute) - if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || - attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || - attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true) - { - forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + // Only forward the attribute if the target is a field. + // Otherwise, the attribute is already applied correctly. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + } } } token.ThrowIfCancellationRequested(); - // Gather explicit forwarded attributes info - foreach (AttributeListSyntax attributeList in fieldSyntax.AttributeLists) - { - // Only look for attribute lists explicitly targeting the (generated) property. 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)) - { - continue; - } - - token.ThrowIfCancellationRequested(); - - foreach (AttributeSyntax attribute in attributeList.Attributes) - { - // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. - // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: - // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not - // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. - // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. - // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. - // - We then go over each attribute argument expression and get the operation for it. This will still be available even - // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all - // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) - // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. - // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. - // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the - // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the - // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. - if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) - { - builder.Add( - InvalidPropertyTargetedAttributeOnObservablePropertyField, - attribute, - fieldSymbol, - attribute.Name); - - continue; - } - - IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); - - // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo)) - { - builder.Add( - InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, - attribute, - fieldSymbol, - attribute.Name); - - continue; - } - - forwardedAttributes.Add(attributeInfo); - } - } + // Also gather any forwarded attributes on the annotated member, if it is a field. + // This method will not do anything for properties, as those don't support this. + GatherLegacyForwardedAttributes( + memberSyntax, + memberSymbol, + semanticModel, + in forwardedAttributes, + in builder, + token); token.ThrowIfCancellationRequested(); // Log the diagnostic for missing ObservableValidator, if needed if (hasAnyValidationAttributes && - !fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + !memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { builder.Add( MissingObservableValidatorInheritanceForValidationAttributeError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name, + memberSymbol, + memberSyntax.Kind().ToFieldOrPropertyKeyword(), + memberSymbol.ContainingType, + memberSymbol.Name, forwardedAttributes.Count); } @@ -289,25 +344,70 @@ public static bool TryGetInfo( { builder.Add( MissingValidationAttributesForNotifyDataErrorInfoError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), + memberSymbol.ContainingType, + memberSymbol.Name); + } + + token.ThrowIfCancellationRequested(); + + // We should generate [RequiresUnreferencedCode] on the setter if [NotifyDataErrorInfo] was used and the attribute is available + bool includeRequiresUnreferencedCodeOnSetAccessor = + notifyDataErrorInfo && + semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute"); + + token.ThrowIfCancellationRequested(); + + // Prepare the effective property changing/changed names. For the property changing names, + // there are two possible cases: if the mode is disabled, then there are no names to report + // at all. If the mode is enabled, then the list is just the same as for property changed. + ImmutableArray effectivePropertyChangedNames = propertyChangedNames.ToImmutable(); + ImmutableArray effectivePropertyChangingNames = shouldInvokeOnPropertyChanging switch + { + true => effectivePropertyChangedNames, + false => ImmutableArray.Empty + }; + + token.ThrowIfCancellationRequested(); + + // Get all additional modifiers for the member + ImmutableArray propertyModifiers = GetPropertyModifiers(memberSyntax); + + // Retrieve the accessibility values for all components + if (!TryGetAccessibilityModifiers( + memberSyntax, + memberSymbol, + out Accessibility propertyAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility)) + { + propertyInfo = null; + diagnostics = builder.ToImmutable(); + + return false; } token.ThrowIfCancellationRequested(); propertyInfo = new PropertyInfo( + memberSyntax.Kind(), typeNameWithNullabilityAnnotations, fieldName, propertyName, - propertyChangingNames.ToImmutable(), - propertyChangedNames.ToImmutable(), + propertyModifiers.AsUnderlyingType(), + propertyAccessibility, + getterAccessibility, + setterAccessibility, + effectivePropertyChangingNames, + effectivePropertyChangedNames, notifiedCommandNames.ToImmutable(), notifyRecipients, notifyDataErrorInfo, isOldPropertyValueDirectlyReferenced, - isReferenceTypeOrUnconstraindTypeParameter, + isReferenceTypeOrUnconstrainedTypeParameter, includeMemberNotNullOnSetAccessor, + includeRequiresUnreferencedCodeOnSetAccessor, forwardedAttributes.ToImmutable()); diagnostics = builder.ToImmutable(); @@ -318,19 +418,19 @@ public static bool TryGetInfo( /// /// 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; @@ -343,7 +443,7 @@ private static bool IsTargetTypeValid(IFieldSymbol fieldSymbol, out bool shouldI /// The property name. /// The property type. /// Whether the generated property is invalid. - private static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType) + public static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol propertyType) { // If the generated property name is called "Property" and the type is either object or it is PropertyChangedEventArgs or // PropertyChangingEventArgs (or a type derived from either of those two types), consider it invalid. This is needed because @@ -363,13 +463,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) @@ -377,16 +477,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)) { @@ -413,9 +513,9 @@ bool IsPropertyNameValidWithGeneratedMembers(string propertyName) { diagnostics.Add( NotifyPropertyChangedForInvalidTargetError, - fieldSymbol, + memberSymbol, dependentPropertyName ?? "", - fieldSymbol.ContainingType); + memberSymbol.ContainingType); } } @@ -428,13 +528,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) @@ -444,7 +544,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 @@ -473,7 +573,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") && @@ -503,9 +603,9 @@ bool IsCommandNameValidWithGeneratedMembers(string commandName) { diagnostics.Add( NotifyCanExecuteChangedForInvalidTargetError, - fieldSymbol, + memberSymbol, commandName ?? "", - fieldSymbol.ContainingType); + memberSymbol.ContainingType); } } @@ -518,16 +618,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; @@ -549,14 +649,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, @@ -568,15 +668,16 @@ private static bool TryGetIsNotifyingRecipients( if (hasOrInheritsClassLevelNotifyPropertyChangedRecipients) { diagnostics.Add( - UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + UnnecessaryNotifyPropertyChangedRecipientsAttributeOnMemberWarning, + memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), + memberSymbol.ContainingType, + memberSymbol.Name); } // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || - fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || + memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) { isBroadcastTargetValid = true; @@ -585,10 +686,10 @@ private static bool TryGetIsNotifyingRecipients( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( - InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + InvalidContainingTypeForNotifyPropertyChangedRecipientsMemberError, + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); isBroadcastTargetValid = false; @@ -603,15 +704,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; @@ -632,14 +733,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, @@ -651,14 +752,15 @@ private static bool TryGetNotifyDataErrorInfo( if (hasOrInheritsClassLevelNotifyDataErrorInfo) { diagnostics.Add( - UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + UnnecessaryNotifyDataErrorInfoAttributeOnMemberWarning, + memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), + memberSymbol.ContainingType, + memberSymbol.Name); } // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { isValidationTargetValid = true; @@ -668,9 +770,10 @@ private static bool TryGetNotifyDataErrorInfo( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( MissingObservableValidatorInheritanceForNotifyDataErrorInfoError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), + memberSymbol.ContainingType, + memberSymbol.Name); isValidationTargetValid = false; @@ -685,13 +788,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 @@ -703,7 +806,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 }) { @@ -717,13 +820,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) @@ -731,7 +834,19 @@ private static void GetNullabilityInfo( // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. // This will cover both reference types as well T when the constraints are not struct or unmanaged. // If this is true, it means the field storage can potentially be in a null state (even if not annotated). - isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType; + isReferenceTypeOrUnconstraindTypeParameter = !GetPropertyType(memberSymbol).IsValueType; + + // Special case if the target member is a partial property. In this case, the type should always match the + // declared type of the property declaration, and there is no need for the attribute on the setter. This + // is because assigning the property in the constructor will directly assign to the backing field, and not + // doing so from the constructor will cause Roslyn to emit a warning. Additionally, Roslyn can always see + // that the backing field is being assigned from the setter, so the attribute is just never needed here. + if (memberSymbol.Kind is SymbolKind.Property) + { + includeMemberNotNullOnSetAccessor = false; + + return; + } // This is used to avoid nullability warnings when setting the property from a constructor, in case the field // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. @@ -753,10 +868,227 @@ private static void GetNullabilityInfo( // Of course, this can only be the case if the field type is also of a type that could be in a null state. includeMemberNotNullOnSetAccessor = isReferenceTypeOrUnconstraindTypeParameter && - fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && + GetPropertyType(memberSymbol).NullableAnnotation != NullableAnnotation.Annotated && semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } + /// + /// Gathers all forwarded attributes from the given member syntax. + /// + /// The instance to process. + /// The input instance to process. + /// The instance for the current run. + /// The collection of forwarded attributes to add new ones to. + /// The current collection of gathered diagnostics. + /// The cancellation token for the current operation. + private static void GatherLegacyForwardedAttributes( + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, + SemanticModel semanticModel, + in ImmutableArrayBuilder forwardedAttributes, + in ImmutableArrayBuilder diagnostics, + CancellationToken token) + { + // For properties, we never need to forward any attributes with explicit targets either, because + // they can already "just work" when used with 'field'. As for 'get' and 'set', they can just be + // added directly to the partial declarations of the property accessors. + if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) + { + return; + } + + // Also track the current attribute for forwarding if it is of any of the following types: + // - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute) + // - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute) + // - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute) + // - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute) + // - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute) + // + // All of these have special handling and are always forwarded when a field is being targeted. + // That is because these attributes really only mean anything when used on generated properties. + foreach (AttributeData attributeData in memberSymbol.GetAttributes()) + { + if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || + attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true) + { + forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + } + } + + // Gather explicit forwarded attributes info + foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) + { + // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will + // normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic + // suppressor that recognizes uses of this target specifically to support [ObservableProperty]. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.GetKeyword or SyntaxKind.SetKeyword) targetIdentifier) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. + // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: + // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not + // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. + // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. + // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. + // - We then go over each attribute argument expression and get the operation for it. This will still be available even + // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all + // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) + // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. + // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. + // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the + // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the + // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) + { + diagnostics.Add( + InvalidPropertyTargetedAttributeOnObservablePropertyField, + attribute, + memberSymbol, + attribute.Name); + + continue; + } + + IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); + + // Try to extract the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) + { + diagnostics.Add( + InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, + attribute, + memberSymbol, + attribute.Name); + + continue; + } + + forwardedAttributes.Add(attributeInfo); + } + } + } + + /// + /// Gathers all allowed property modifiers that should be forwarded to the generated property. + /// + /// The instance to process. + /// The returned set of property modifiers, if any. + private static ImmutableArray GetPropertyModifiers(MemberDeclarationSyntax memberSyntax) + { + // Fields never need to carry additional modifiers along + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + return ImmutableArray.Empty; + } + + // We only allow a subset of all possible modifiers (aside from the accessibility modifiers) + ReadOnlySpan candidateKinds = + [ + SyntaxKind.NewKeyword, + SyntaxKind.VirtualKeyword, + SyntaxKind.SealedKeyword, + SyntaxKind.OverrideKeyword, +#if ROSLYN_4_3_1_OR_GREATER + SyntaxKind.RequiredKeyword +#endif + ]; + + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + + // Track all modifiers from the allowed set on the input property declaration + foreach (SyntaxKind kind in candidateKinds) + { + if (memberSyntax.Modifiers.Any(kind)) + { + builder.Add(kind); + } + } + + return builder.ToImmutable(); + } + + /// + /// Tries to get the accessibility of the property and accessors, if possible. + /// If the target member is not a property, it will use the defaults. + /// + /// The instance to process. + /// The input instance to process. + /// The accessibility of the property, if available. + /// The accessibility of the accessor, if available. + /// The accessibility of the accessor, if available. + /// Whether the property was valid and the accessibilities could be retrieved. + private static bool TryGetAccessibilityModifiers( + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, + out Accessibility propertyAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility) + { + // For legacy support for fields, the property that is generated is public, and neither + // accessors will have any accessibility modifiers. To customize the accessibility, + // partial properties should be used instead. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + propertyAccessibility = Accessibility.Public; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; + + return true; + } + + propertyAccessibility = Accessibility.NotApplicable; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; + + // Ensure that we have a getter and a setter, and that the setter is not init-only + if (memberSymbol is not IPropertySymbol { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + { + return false; + } + + // At this point the node is definitely a property, just do a sanity check + if (memberSyntax is not PropertyDeclarationSyntax propertySyntax) + { + return false; + } + + // Track the property accessibility if explicitly set + if (propertySyntax.Modifiers.ContainsAnyAccessibilityModifiers()) + { + propertyAccessibility = memberSymbol.DeclaredAccessibility; + } + + // Track the accessors accessibility, if explicitly set + foreach (AccessorDeclarationSyntax accessor in propertySyntax.AccessorList?.Accessors ?? []) + { + if (!accessor.Modifiers.ContainsAnyAccessibilityModifiers()) + { + continue; + } + + switch (accessor.Kind()) + { + case SyntaxKind.GetAccessorDeclaration: + getterAccessibility = getMethod.DeclaredAccessibility; + break; + case SyntaxKind.SetAccessorDeclaration: + setterAccessibility = setMethod.DeclaredAccessibility; + break; + } + } + + return true; + } + /// /// Gets a instance with the cached args for property changing notifications. /// @@ -792,18 +1124,21 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf { using ImmutableArrayBuilder setterStatements = ImmutableArrayBuilder.Rent(); - // Get the property type syntax - TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); - string getterFieldIdentifierName; ExpressionSyntax getterFieldExpression; ExpressionSyntax setterFieldExpression; - // In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments - // with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter. - if (propertyInfo.FieldName == "value") + // If the annotated member is a partial property, we always use the 'field' keyword + if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) + { + getterFieldIdentifierName = "field"; + getterFieldExpression = setterFieldExpression = IdentifierName(getterFieldIdentifierName); + } + else if (propertyInfo.FieldName == "value") { - // We only need to add "this." when referencing the field in the setter (getter and XML docs are not ambiguous) + // In case the backing field is exactly named "value", we need to add the "this." prefix to ensure that comparisons and assignments + // with it in the generated setter body are executed correctly and without conflicts with the implicit value parameter. We only need + // to add "this." when referencing the field in the setter (getter and XML docs are not ambiguous) getterFieldIdentifierName = "value"; getterFieldExpression = IdentifierName(getterFieldIdentifierName); setterFieldExpression = MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), (IdentifierNameSyntax)getterFieldExpression); @@ -823,6 +1158,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: @@ -830,7 +1172,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // __oldValue = ; setterStatements.Add( LocalDeclarationStatement( - VariableDeclaration(propertyType) + VariableDeclaration(GetPropertyTypeForOldValue(propertyInfo)) .AddVariables( VariableDeclarator(Identifier("__oldValue")) .WithInitializer(EqualsValueClause(setterFieldExpression))))); @@ -959,6 +1301,9 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.PropertyName)))))); } + // Get the property type syntax + TypeSyntax propertyType = IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); + // Generate the inner setter block as follows: // // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) @@ -983,19 +1328,33 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf Argument(IdentifierName("value")))), Block(setterStatements.AsEnumerable())); - // Prepare the forwarded attributes, if any - ImmutableArray forwardedAttributes = + // Prepare the forwarded attributes, if any, for all targets + AttributeListSyntax[] forwardedPropertyAttributes = + propertyInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.PropertyKeyword) + .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) + .ToArray(); + AttributeListSyntax[] forwardedGetAccessorAttributes = + propertyInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.GetKeyword) + .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) + .ToArray(); + AttributeListSyntax[] forwardedSetAccessorAttributes = propertyInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.SetKeyword) .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); + .ToArray(); // 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: // @@ -1010,15 +1369,32 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyInfo.FieldName))))))); } + // Add the [RequiresUnreferencedCode] attribute if needed: + // + // [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + // + if (propertyInfo.IncludeRequiresUnreferencedCodeOnSetAccessor) + { + setAccessor = setAccessor.AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("The type of the current instance cannot be statically discovered."))))))); + } + + // Also add any forwarded attributes + setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes); + // Construct the generated property as follows: // - // /// + // // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // - // public + // // { - // get => ; + // + // get => ; // // } return @@ -1029,17 +1405,43 @@ 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(forwardedAttributes.ToArray()) - .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .AddAttributeLists(forwardedPropertyAttributes) + .WithModifiers(GetPropertyModifiers(propertyInfo)) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithModifiers(propertyInfo.GetterAccessibility.ToSyntaxTokenList()) .WithExpressionBody(ArrowExpressionClause(getterFieldExpression)) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .AddAttributeLists(forwardedGetAccessorAttributes), setAccessor); } + /// + /// Gets all modifiers that need to be added to a generated property. + /// + /// The input instance to process. + /// The list of necessary modifiers for . + private static SyntaxTokenList GetPropertyModifiers(PropertyInfo propertyInfo) + { + SyntaxTokenList propertyModifiers = propertyInfo.PropertyAccessibility.ToSyntaxTokenList(); + + // Add all gathered modifiers + foreach (SyntaxKind modifier in propertyInfo.PropertyModifers.AsImmutableArray().FromUnderlyingType()) + { + propertyModifiers = propertyModifiers.Add(Token(modifier)); + } + + // Add the 'partial' modifier if the original member is a partial property + if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) + { + propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.PartialKeyword)); + } + + return propertyModifiers; + } + /// /// Gets the instances for the OnPropertyChanging and OnPropertyChanged methods for the input field. /// @@ -1073,17 +1475,8 @@ public static ImmutableArray GetOnPropertyChangeMethods Comment($"/// This method is invoked right before the value of is changed.")), SyntaxKind.OpenBracketToken, TriviaList()))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); - // Prepare the nullable type for the previous property value. This is needed because if the type is a reference - // type, the previous value might be null even if the property type is not nullable, as the first invocation would - // happen when the property is first set to some value that is not null (but the backing field would still be so). - // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability - // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. - TypeSyntax oldValueTypeSyntax = propertyInfo.IsReferenceTypeOrUnconstraindTypeParameter switch - { - true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") - => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"), - _ => parameterType - }; + // Get the type for the 'oldValue' parameter (which can be null on first invocation) + TypeSyntax oldValueTypeSyntax = GetPropertyTypeForOldValue(propertyInfo); // Construct the generated method as follows: // @@ -1169,6 +1562,50 @@ 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 . + public static ITypeSymbol GetPropertyType(ISymbol memberSymbol) + { + // Check if the member is a property first + if (memberSymbol is IPropertySymbol propertySymbol) + { + return propertySymbol.Type; + } + + // Otherwise, the only possible case is a field symbol + return ((IFieldSymbol)memberSymbol).Type; + } + + /// + /// Gets the for the type of a given property, when it can possibly be . + /// + /// The input instance to process. + /// The type of a given property, when it can possibly be + private static TypeSyntax GetPropertyTypeForOldValue(PropertyInfo propertyInfo) + { + // For partial properties, the old value always matches the exact property type. + // See additional notes for this in the 'GetNullabilityInfo' method above. + if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) + { + return IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations); + } + + // Prepare the nullable type for the previous property value. This is needed because if the type is a reference + // type, the previous value might be null even if the property type is not nullable, as the first invocation would + // happen when the property is first set to some value that is not null (but the backing field would still be so). + // As a cheap way to check whether we need to add nullable, we can simply check whether the type name with nullability + // annotations ends with a '?'. If it doesn't and the type is a reference type, we add it. Otherwise, we keep it. + return propertyInfo.IsReferenceTypeOrUnconstrainedTypeParameter switch + { + true when !propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?") + => IdentifierName($"{propertyInfo.TypeNameWithNullabilityAnnotations}?"), + _ => IdentifierName(propertyInfo.TypeNameWithNullabilityAnnotations) + }; + } + /// /// Gets a instance with the cached args of a specified type. /// @@ -1283,13 +1720,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 ad7e5fe03..05bee8c83 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -9,7 +9,6 @@ using CommunityToolkit.Mvvm.SourceGenerators.Helpers; using CommunityToolkit.Mvvm.SourceGenerators.Models; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CommunityToolkit.Mvvm.SourceGenerators; @@ -25,26 +24,40 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { // Gather info for all annotated fields IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> propertyInfoWithErrors = - context.SyntaxProvider - .ForAttributeWithMetadataName( + context.ForAttributeWithMetadataNameAndOptions( "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute", - static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, + Execute.IsCandidatePropertyDeclaration, static (context, token) => { - if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) + MemberDeclarationSyntax memberSyntax = Execute.GetCandidateMemberDeclaration(context.TargetNode); + + // Validate that the candidate is valid for the current compilation + if (!Execute.IsCandidateValidForCompilation(memberSyntax, context.SemanticModel)) + { + return default; + } + + // Validate the symbol as well before doing any work + if (!Execute.IsCandidateSymbolValid(context.TargetSymbol)) { return default; } - FieldDeclarationSyntax fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; - IFieldSymbol fieldSymbol = (IFieldSymbol)context.TargetSymbol; + token.ThrowIfCancellationRequested(); // Get the hierarchy info for the target symbol, and try to gather the property info - HierarchyInfo hierarchy = HierarchyInfo.From(fieldSymbol.ContainingType); + HierarchyInfo hierarchy = HierarchyInfo.From(context.TargetSymbol.ContainingType); token.ThrowIfCancellationRequested(); - _ = Execute.TryGetInfo(fieldDeclaration, fieldSymbol, context.SemanticModel, token, out PropertyInfo? propertyInfo, out ImmutableArray diagnostics); + _ = Execute.TryGetInfo( + memberSyntax, + context.TargetSymbol, + context.SemanticModel, + context.GlobalOptions, + token, + out PropertyInfo? propertyInfo, + out ImmutableArray diagnostics); token.ThrowIfCancellationRequested(); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs index 684430db7..0b82211ba 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.Execute.cs @@ -181,7 +181,7 @@ public static CompilationUnitSyntax GetSyntax(ValidationInfo validationInfo) // full runtime type safety (as a safe cast is used to validate the input argument), and with less reflection needed. // Note that we're deliberately creating a new delegate instance here and not using code that could see the C# compiler // create a static class to cache a reusable delegate, because each generated method will only be called at most once, - // as the returned delegate will be cached by the MVVM Toolkit itself. So this ensures the the produced code is minimal, + // as the returned delegate will be cached by the MVVM Toolkit itself. So this ensures the produced code is minimal, // and that there will be no unnecessary static fields and objects being created and possibly never collected. // This code will produce a syntax tree as follows: // diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index e3a00ae0a..4ec9bc6f0 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -12,7 +12,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// -/// A source generator for message registration without relying on compiled LINQ expressions. +/// A source generator for property validation without relying on compiled LINQ expressions. /// [Generator(LanguageNames.CSharp)] public sealed partial class ObservableValidatorValidateAllPropertiesGenerator : IIncrementalGenerator diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs index 8b8688d1e..164618ddf 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AsyncVoidReturningRelayCommandMethodAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class AsyncVoidReturningRelayCommandMethodAnalyzer : DiagnosticAna /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs index 8740fe206..79390067b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/AutoPropertyWithFieldTargetedObservablePropertyAttributeAnalyzer.cs @@ -22,7 +22,7 @@ public sealed class AutoPropertyWithFieldTargetedObservablePropertyAttributeAnal /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs index 39419d2b0..13da28dd2 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/ClassUsingAttributeInsteadOfInheritanceAnalyzer.cs @@ -54,7 +54,7 @@ public sealed class ClassUsingAttributeInsteadOfInheritanceAnalyzer : Diagnostic /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs index 599fa7cdb..df9052d36 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldReferenceForObservablePropertyFieldAnalyzer.cs @@ -32,7 +32,7 @@ public sealed class FieldReferenceForObservablePropertyFieldAnalyzer : Diagnosti /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs index 1505b6865..cc4796a23 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/FieldWithOrphanedDependentObservablePropertyAttributesAnalyzer.cs @@ -36,11 +36,11 @@ public sealed class FieldWithOrphanedDependentObservablePropertyAttributesAnalyz /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); // Defer the registration so it can be skipped if C# 8.0 or more is not available. - // That is because in that case source generators are not supported at all anyaway. + // That is because in that case source generators are not supported at all anyway. context.RegisterCompilationStartAction(static context => { if (!context.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) @@ -72,7 +72,7 @@ public override void Initialize(AnalysisContext context) foreach (AttributeData dependentAttribute in attributes) { - // Go over each attribute on the target symbol, anche check if any of them matches one of the trigger attributes. + // Go over each attribute on the target symbol, and check if any of them matches one of the trigger attributes. // The logic here is the same as the one in UnsupportedCSharpLanguageVersionAnalyzer. if (dependentAttribute.AttributeClass is { Name: string attributeName } dependentAttributeClass && typeSymbols.TryGetValue(attributeName, out INamedTypeSymbol? dependentAttributeSymbol) && @@ -89,13 +89,18 @@ public override void Initialize(AnalysisContext context) } } - context.ReportDiagnostic(Diagnostic.Create(FieldWithOrphanedDependentObservablePropertyAttributesError, context.Symbol.Locations.FirstOrDefault(), context.Symbol.ContainingType, context.Symbol.Name)); + context.ReportDiagnostic(Diagnostic.Create( + FieldWithOrphanedDependentObservablePropertyAttributesError, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol.Kind.ToFieldOrPropertyKeyword(), + context.Symbol.ContainingType, + context.Symbol.Name)); // Just like in UnsupportedCSharpLanguageVersionAnalyzer, stop if a diagnostic has been emitted for the current symbol return; } } - }, SymbolKind.Field); + }, SymbolKind.Field, SymbolKind.Property); }); } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs index f4232ada0..bfdf087f1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class InvalidClassLevelNotifyDataErrorInfoAttributeAnalyzer : Diag /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs index c9525ed3d..69d4cf0f8 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAnalyzer.cs @@ -23,7 +23,7 @@ public sealed class InvalidClassLevelNotifyPropertyChangedRecipientsAttributeAna /// public override void Initialize(AnalysisContext context) { - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterCompilationStartAction(static context => diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..e960cf47e --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error when a field or property with [ObservableProperty] is not valid (special cases) +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidGeneratedPropertyObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidObservablePropertyError); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] and the event args we need + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangedEventArgs") is not INamedTypeSymbol propertyChangedEventArgsSymbol || + context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangingEventArgs") is not INamedTypeSymbol propertyChangingEventArgsSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Validate that we do have a field or a property + if (context.Symbol is not (IFieldSymbol or IPropertySymbol)) + { + return; + } + + // Ensure we do have the [ObservableProperty] attribute + if (!context.Symbol.HasAttributeWithType(observablePropertySymbol)) + { + return; + } + + ITypeSymbol propertyType = ObservablePropertyGenerator.Execute.GetPropertyType(context.Symbol); + string propertyName = ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(context.Symbol); + + // Same logic as 'IsGeneratedPropertyInvalid' in the generator + if (propertyName == "Property") + { + // Check for collisions with the generated helpers and the property, only happens with these 3 types + if (propertyType.SpecialType == SpecialType.System_Object || + propertyType.HasOrInheritsFromType(propertyChangedEventArgsSymbol) || + propertyType.HasOrInheritsFromType(propertyChangingEventArgsSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyError, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol.Kind.ToFieldOrPropertyKeyword(), + context.Symbol.ContainingType, + context.Symbol.Name)); + } + } + }, SymbolKind.Field, SymbolKind.Property); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..8b144049e --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_12_0_OR_GREATER + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used on an invalid partial property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition, + InvalidObservablePropertyDeclarationReturnsByRef, + InvalidObservablePropertyDeclarationReturnsRefLikeType); + + /// + public override void Initialize(AnalysisContext context) + { + // This generator is intentionally also analyzing generated code, because Roslyn will interpret properties + // that have '[GeneratedCode]' on them as being generated (and the same will apply to all partial parts). + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the [ObservableProperty] and [GeneratedCode] symbols + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute") is not { } generatedCodeAttributeSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure that we have some target property to analyze (also skip implementation parts) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } propertySymbol) + { + return; + } + + // If the property is not using [ObservableProperty], there's nothing to do + if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Emit an error if the property is not a partial definition with no implementation... + if (propertySymbol is not { IsPartialDefinition: true, PartialImplementationPart: null }) + { + // ...But only if it wasn't actually generated by the [ObservableProperty] generator. + bool isImplementationAllowed = + propertySymbol is { IsPartialDefinition: true, PartialImplementationPart: IPropertySymbol implementationPartSymbol } && + implementationPartSymbol.TryGetAttributeWithType(generatedCodeAttributeSymbol, out AttributeData? generatedCodeAttributeData) && + generatedCodeAttributeData.TryGetConstructorArgument(0, out string? toolName) && + toolName == typeof(ObservablePropertyGenerator).FullName; + + // Emit the diagnostic only for cases that were not valid generator outputs + if (!isImplementationAllowed) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition, + observablePropertyAttribute.GetLocation(), + propertySymbol.ContainingType, + propertySymbol.Name)); + } + } + + // Emit an error if the property returns a value by ref + if (propertySymbol.ReturnsByRef || propertySymbol.ReturnsByRefReadonly) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyDeclarationReturnsByRef, + observablePropertyAttribute.GetLocation(), + propertySymbol.ContainingType, + propertySymbol.Name)); + } + + // Emit an error if the property type is a ref struct + if (propertySymbol.Type.IsRefLikeType) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyDeclarationReturnsRefLikeType, + observablePropertyAttribute.GetLocation(), + propertySymbol.ContainingType, + propertySymbol.Name)); + } + }, SymbolKind.Property); + }); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..5ece6f03e --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPointerTypeObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used with pointer types. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPointerTypeObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidObservablePropertyDeclarationReturnsPointerLikeType); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the [ObservableProperty] symbol + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure that we have a valid target symbol to analyze + if (context.Symbol is not (IFieldSymbol or IPropertySymbol)) + { + return; + } + + // If the property is not using [ObservableProperty], there's nothing to do + if (!context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Emit a diagnostic if the type is a pointer type + if (context.Symbol is + IPropertySymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer } or + IFieldSymbol { Type.TypeKind: TypeKind.Pointer or TypeKind.FunctionPointer }) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidObservablePropertyDeclarationReturnsPointerLikeType, + observablePropertyAttribute.GetLocation(), + context.Symbol.ContainingType, + context.Symbol.Name)); + } + }, SymbolKind.Field, SymbolKind.Property); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..e304690fa --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used on an invalid property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyLevelObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidPropertyDeclarationForObservableProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We're intentionally only looking for properties here + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property isn't using [ObservableProperty], there's nothing to do + if (!propertySymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Check that the property has valid syntax + foreach (SyntaxReference propertyReference in propertySymbol.DeclaringSyntaxReferences) + { + SyntaxNode propertyNode = propertyReference.GetSyntax(context.CancellationToken); + + if (!IsValidCandidateProperty(propertyNode, out _)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationForObservableProperty, + observablePropertyAttribute.GetLocation(), + propertySymbol.ContainingType, + propertySymbol.Name)); + + return; + } + } + }, SymbolKind.Property); + }); + } + + /// + /// Checks whether a given property declaration has valid syntax. + /// + /// The input node to validate. + /// The resulting node for the containing type of the property, if valid. + /// Whether is a valid property. + internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) + { + // The node must be a property declaration with two accessors + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) + { + containingTypeNode = null; + + return false; + } + + // The property must be partial (we'll check that it's a declaration from its symbol) + if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + containingTypeNode = null; + + return false; + } + + // Static properties are not supported + if (property.Modifiers.Any(SyntaxKind.StaticKeyword)) + { + containingTypeNode = null; + + return false; + } + + // The accessors must be a get and a set (with any accessibility) + if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || + accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) + { + containingTypeNode = null; + + return false; + } + + containingTypeNode = (TypeDeclarationSyntax?)property.Parent; + + return true; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..70c645227 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidTargetObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error when a field or property with [ObservableProperty] is not a valid target. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidTargetObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidContainingTypeForObservablePropertyMemberError); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the required symbols for the analyzer + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject") is not INamedTypeSymbol observableObjectSymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute") is not INamedTypeSymbol observableObjectAttributeSymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute") is not INamedTypeSymbol notifyPropertyChangedAttributeSymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Validate that we do have a field or a property + if (context.Symbol is not (IFieldSymbol or IPropertySymbol)) + { + return; + } + + // Ensure we do have the [ObservableProperty] attribute + if (!context.Symbol.HasAttributeWithType(observablePropertySymbol)) + { + return; + } + + // Same logic as in 'IsTargetTypeValid' in the generator + bool isObservableObject = context.Symbol.ContainingType.InheritsFromType(observableObjectSymbol); + bool hasObservableObjectAttribute = context.Symbol.ContainingType.HasOrInheritsAttributeWithType(observableObjectAttributeSymbol); + bool hasINotifyPropertyChangedAttribute = context.Symbol.ContainingType.HasOrInheritsAttributeWithType(notifyPropertyChangedAttributeSymbol); + + // Emit the diagnostic if the target is not valid + if (!isObservableObject && !hasObservableObjectAttribute && !hasINotifyPropertyChangedAttribute) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidContainingTypeForObservablePropertyMemberError, + context.Symbol.Locations.FirstOrDefault(), + context.Symbol.Kind.ToFieldOrPropertyKeyword(), + context.Symbol.ContainingType, + context.Symbol.Name)); + } + }, SymbolKind.Field, SymbolKind.Property); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..06894ac25 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/PropertyNameCollisionObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error when a generated property from [ObservableProperty] would collide with the field name. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PropertyNameCollisionObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(ObservablePropertyNameCollisionError); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure we do have a valid field + if (context.Symbol is not IFieldSymbol fieldSymbol) + { + return; + } + + // We only care if the field has [ObservableProperty] + if (!fieldSymbol.HasAttributeWithType(observablePropertySymbol)) + { + return; + } + + // Emit the diagnostic if there is a name collision + if (fieldSymbol.Name == ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + ObservablePropertyNameCollisionError, + fieldSymbol.Locations.FirstOrDefault(), + fieldSymbol.ContainingType, + fieldSymbol.Name)); + } + }, SymbolKind.Field); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs new file mode 100644 index 000000000..7cb7cd233 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/RequiresCSharpLanguageVersionPreviewAnalyzer.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_12_0_OR_GREATER + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates errors when a property using [ObservableProperty] on a partial property is in a project with the C# language version not set to preview. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class RequiresCSharpLanguageVersionPreviewAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [CSharpLanguageVersionIsNotPreviewForObservableProperty]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // If the language version is set to preview, we'll never emit diagnostics + if (context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We only want to target partial property definitions (also include non-partial ones for diagnostics) + if (context.Symbol is not IPropertySymbol { PartialDefinitionPart: null } partialProperty) + { + return; + } + + // Make sure to skip the warning if the property is not actually partial + if (partialProperty.DeclaringSyntaxReferences is [var syntaxReference]) + { + // Make sure we can find the syntax node, and that it's a property declaration + if (syntaxReference.GetSyntax(context.CancellationToken) is PropertyDeclarationSyntax propertyDeclarationSyntax) + { + // If the property is not partial, ignore it, as we'll already have a warning from the other analyzer here + if (!propertyDeclarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + return; + } + } + } + + // If the property is using [ObservableProperty], emit the diagnostic + if (context.Symbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + context.ReportDiagnostic(Diagnostic.Create( + CSharpLanguageVersionIsNotPreviewForObservableProperty, + observablePropertyAttribute.GetLocation(), + context.Symbol)); + } + }, SymbolKind.Property); + }); + } +} + +#endif \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs index af81bcac4..cbfa6ecd1 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedCSharpLanguageVersionAnalyzer.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using CommunityToolkit.Mvvm.SourceGenerators.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -41,7 +40,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 @@ -76,7 +75,7 @@ public override void Initialize(AnalysisContext context) typeSymbols.TryGetValue(attributeName, out INamedTypeSymbol? attributeSymbol) && SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbol)) { - context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, context.Symbol.Locations.FirstOrDefault())); + context.ReportDiagnostic(Diagnostic.Create(UnsupportedCSharpLanguageVersionError, context.Symbol.GetLocationFromAttributeDataOrDefault(attribute))); // If we created a diagnostic for this symbol, we can stop. Even if there's multiple attributes, no need for repeated errors return; 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..2760ab023 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if !ROSLYN_4_12_0_OR_GREATER + +using System.Collections.Immutable; +using System.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used on a property, if the Roslyn version in use is not high enough. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UnsupportedRoslynVersionForPartialPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UnsupportedRoslynVersionForObservablePartialPropertySupport); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We're intentionally only looking for properties here + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property has [ObservableProperty], emit an error in all cases + if (propertySymbol.HasAttributeWithType(observablePropertySymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + UnsupportedRoslynVersionForObservablePartialPropertySupport, + propertySymbol.Locations.FirstOrDefault(), + propertySymbol.ContainingType, + propertySymbol.Name)); + } + }, SymbolKind.Property); + }); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs new file mode 100644 index 000000000..bbda90e1b --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnPartialPropertyAnalyzer.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_12_0_OR_GREATER + +using System.Collections.Immutable; +using System.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates a suggestion whenever [ObservableProperty] is used on a field when a partial property could be used instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseObservablePropertyOnPartialPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UseObservablePropertyOnPartialProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Using [ObservableProperty] on partial properties is only supported when using C# preview. + // As such, if that is not the case, return immediately, as no diagnostic should be produced. + if (!context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // If CsWinRT is in AOT-optimization mode, disable this analyzer, as the WinRT one will produce a warning instead + if (context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsCsWinRTAotOptimizerEnabled(context.Compilation)) + { + return; + } + + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + 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.HasAttributeWithType(observablePropertySymbol)) + { + 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, + fieldSymbol.Locations.FirstOrDefault(), + ImmutableDictionary.Create() + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name) + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)), + fieldSymbol.ContainingType, + fieldSymbol.Name)); + }, SymbolKind.Field); + }); + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs new file mode 100644 index 000000000..18155958a --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/UseObservablePropertyOnSemiAutoPropertyAnalyzer.cs @@ -0,0 +1,351 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_12_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.PooledObjects; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates a suggestion whenever [ObservableProperty] is used on a semi-auto property when a partial property could be used instead. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseObservablePropertyOnSemiAutoPropertyAnalyzer : DiagnosticAnalyzer +{ + /// + /// The number of pooled flags per stack (ie. how many properties we expect on average per type). + /// + private const int NumberOfPooledFlagsPerStack = 20; + + /// + /// Shared pool for instances. + /// + [SuppressMessage("MicrosoftCodeAnalysisPerformance", "RS1008", Justification = "This is a pool of (empty) dictionaries, it is not actually storing compilation data.")] + private static readonly ObjectPool> PropertyMapPool = new(static () => new Dictionary(SymbolEqualityComparer.Default)); + + /// + /// Shared pool for -s of flags, one per type being processed. + /// + private static readonly ObjectPool> PropertyFlagsStackPool = new(CreatePropertyFlagsStack); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(UseObservablePropertyOnSemiAutoProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Using [ObservableProperty] on partial properties is only supported when using C# preview. + // As such, if that is not the case, return immediately, as no diagnostic should be produced. + if (!context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + // Get the symbol for [ObservableProperty] and ObservableObject + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableObject") is not INamedTypeSymbol observableObjectSymbol) + { + return; + } + + // Get the symbol for the SetProperty method as well + if (!TryGetSetPropertyMethodSymbol(observableObjectSymbol, out IMethodSymbol? setPropertySymbol)) + { + return; + } + + context.RegisterSymbolStartAction(context => + { + // We only care about types that could derive from ObservableObject + if (context.Symbol is not INamedTypeSymbol { IsStatic: false, IsReferenceType: true, BaseType.SpecialType: not SpecialType.System_Object } typeSymbol) + { + return; + } + + // If the type does not derive from ObservableObject, ignore it + if (!typeSymbol.InheritsFromType(observableObjectSymbol)) + { + return; + } + + Dictionary propertyMap = PropertyMapPool.Allocate(); + Stack propertyFlagsStack = PropertyFlagsStackPool.Allocate(); + + // Crawl all members to discover properties that might be of interest + foreach (ISymbol memberSymbol in typeSymbol.GetMembers()) + { + // We're only looking for properties that might be valid candidates for conversion + if (memberSymbol is not IPropertySymbol + { + IsStatic: false, + IsPartialDefinition: false, + PartialDefinitionPart: null, + PartialImplementationPart: null, + ReturnsByRef: false, + ReturnsByRefReadonly: false, + Type.IsRefLikeType: false, + GetMethod: not null, + SetMethod.IsInitOnly: false + } propertySymbol) + { + continue; + } + + // We can safely ignore properties that already have [ObservableProperty]. + // This is because in that case, the other analyzer will already emit an error. + if (propertySymbol.HasAttributeWithType(observablePropertySymbol)) + { + continue; + } + + // Take an array from the stack or create a new one otherwise + bool[] flags = propertyFlagsStack.Count > 0 + ? propertyFlagsStack.Pop() + : new bool[2]; + + // Track the property for later + propertyMap.Add(propertySymbol, flags); + } + + // We want to process both accessors, where we specifically need both the syntax + // and their semantic model to verify what they're doing. We can use a code callback. + context.RegisterOperationBlockAction(context => + { + // Make sure the current symbol is a property accessor + if (context.OwningSymbol is not IMethodSymbol { MethodKind: MethodKind.PropertyGet or MethodKind.PropertySet, AssociatedSymbol: IPropertySymbol propertySymbol }) + { + return; + } + + // If so, check that we are actually processing one of the properties we care about + if (!propertyMap.TryGetValue(propertySymbol, out bool[]? validFlags)) + { + return; + } + + // Handle the 'get' logic + if (SymbolEqualityComparer.Default.Equals(propertySymbol.GetMethod, context.OwningSymbol)) + { + // We expect a top-level block operation, that immediately returns an expression + if (context.OperationBlocks is not [IBlockOperation { Operations: [IReturnOperation returnOperation] }]) + { + return; + } + + // Next, we expect the return to produce a field reference + if (returnOperation is not { ReturnedValue: IFieldReferenceOperation fieldReferenceOperation }) + { + return; + } + + // The field has to be implicitly declared and not constant (and not static) + if (fieldReferenceOperation.Field is not { IsImplicitlyDeclared: true, IsStatic: false } fieldSymbol) + { + return; + } + + // Validate tha the field is indeed 'field' (it will be associated with the property) + if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.AssociatedSymbol, propertySymbol)) + { + return; + } + + // The 'get' accessor is valid + validFlags[0] = true; + } + else if (SymbolEqualityComparer.Default.Equals(propertySymbol.SetMethod, context.OwningSymbol)) + { + // We expect a top-level block operation, that immediately performs an invocation + if (context.OperationBlocks is not [IBlockOperation { Operations: [IExpressionStatementOperation { Operation: IInvocationOperation invocationOperation }] }]) + { + return; + } + + // Brief filtering of the target method, also get the original definition + if (invocationOperation.TargetMethod is not { Name: "SetProperty", IsGenericMethod: true, IsStatic: false } methodSymbol) + { + return; + } + + // First, check that we're calling 'ObservableObject.SetProperty' + if (!SymbolEqualityComparer.Default.Equals(methodSymbol.ConstructedFrom, setPropertySymbol)) + { + return; + } + + // We matched the method, now let's validate the arguments + if (invocationOperation.Arguments is not [{ } locationArgument, { } valueArgument, { } propertyNameArgument]) + { + return; + } + + // The field has to be implicitly declared and not constant (and not static) + if (locationArgument.Value is not IFieldReferenceOperation { Field: { IsImplicitlyDeclared: true, IsStatic: false } fieldSymbol }) + { + return; + } + + // Validate tha the field is indeed 'field' (it will be associated with the property) + if (!SymbolEqualityComparer.Default.Equals(fieldSymbol.AssociatedSymbol, propertySymbol)) + { + return; + } + + // The value is just the 'value' keyword + if (valueArgument.Value is not IParameterReferenceOperation { Syntax: IdentifierNameSyntax { Identifier.Text: "value" } }) + { + return; + } + + // The property name should be the default value + if (propertyNameArgument is not { IsImplicit: true, ArgumentKind: ArgumentKind.DefaultValue }) + { + return; + } + + // The 'set' accessor is valid + validFlags[1] = true; + } + }); + + // We also need to track getters which have no body, and we need syntax for that + context.RegisterSyntaxNodeAction(context => + { + // Let's just make sure we do have a property symbol + if (context.ContainingSymbol is not IPropertySymbol { GetMethod: not null } propertySymbol) + { + return; + } + + // Lookup the property to get its flags + if (!propertyMap.TryGetValue(propertySymbol, out bool[]? validFlags)) + { + return; + } + + // We expect two accessors, skip if otherwise (the setter will be validated by the other callback) + if (context.Node is not PropertyDeclarationSyntax { AccessorList.Accessors: [{ } firstAccessor, { } secondAccessor] }) + { + return; + } + + // Check that either of them is a semicolon token 'get;' accessor (it can be in either position) + if (firstAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && + firstAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken) && + firstAccessor.ExpressionBody is null || + secondAccessor.IsKind(SyntaxKind.GetAccessorDeclaration) && + secondAccessor.SemicolonToken.IsKind(SyntaxKind.SemicolonToken) && + secondAccessor.ExpressionBody is null) + { + validFlags[0] = true; + } + }, SyntaxKind.PropertyDeclaration); + + // Finally, we can consume this information when we finish processing the symbol + context.RegisterSymbolEndAction(context => + { + // Emit a diagnostic for each property that was a valid match + foreach (KeyValuePair pair in propertyMap) + { + if (pair.Value is [true, true]) + { + context.ReportDiagnostic(Diagnostic.Create( + UseObservablePropertyOnSemiAutoProperty, + pair.Key.Locations.FirstOrDefault(), + pair.Key.ContainingType, + pair.Key.Name)); + } + } + + // Before clearing the dictionary, move back all values to the stack + foreach (bool[] propertyFlags in propertyMap.Values) + { + // Make sure the array is cleared before returning it + propertyFlags.AsSpan().Clear(); + + propertyFlagsStack.Push(propertyFlags); + } + + // We are now done processing the symbol, we can return the dictionary. + // Note that we must clear it before doing so to avoid leaks and issues. + propertyMap.Clear(); + + PropertyMapPool.Free(propertyMap); + + // Also do the same for the stack, except we don't need to clean it (since it roots no compilation objects) + PropertyFlagsStackPool.Free(propertyFlagsStack); + }); + }, SymbolKind.NamedType); + }); + } + + /// + /// Tries to get the symbol for the target SetProperty method this analyzer looks for. + /// + /// The symbol for ObservableObject. + /// The resulting method symbol, if found (this should always be the case). + /// Whether could be resolved correctly. + private static bool TryGetSetPropertyMethodSymbol(INamedTypeSymbol observableObjectSymbol, [NotNullWhen(true)] out IMethodSymbol? setPropertySymbol) + { + foreach (ISymbol symbol in observableObjectSymbol.GetMembers("SetProperty")) + { + // We're guaranteed to only match methods here + IMethodSymbol methodSymbol = (IMethodSymbol)symbol; + + // Match the exact signature we need (there's several overloads) + if (methodSymbol.Parameters is not + [ + { Kind: SymbolKind.TypeParameter, RefKind: RefKind.Ref }, + { Kind: SymbolKind.TypeParameter, RefKind: RefKind.None }, + { Type.SpecialType: SpecialType.System_String } + ]) + { + setPropertySymbol = methodSymbol; + + return true; + } + } + + setPropertySymbol = null; + + return false; + } + + /// + /// Produces a new instance to pool. + /// + /// The resulting instance to use. + private static Stack CreatePropertyFlagsStack() + { + static IEnumerable EnumerateFlags() + { + for (int i = 0; i < NumberOfPooledFlagsPerStack; i++) + { + yield return new bool[2]; + } + } + + return new(EnumerateFlags()); + } +} + +#endif \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer.cs new file mode 100644 index 000000000..2b0621dcb --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using 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 warning when [ObservableObject] and [INotifyPropertyChanged] are used on a class in WinRT scenarios. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer : DiagnosticAnalyzer +{ + /// + /// The mapping of target attributes that will trigger the analyzer. + /// + private static readonly ImmutableDictionary GeneratorAttributeNamesToFullyQualifiedNamesMap = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("ObservableObjectAttribute", "CommunityToolkit.Mvvm.ComponentModel.ObservableObjectAttribute"), + new KeyValuePair("INotifyPropertyChangedAttribute", "CommunityToolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute"), + }); + + /// + /// The mapping of diagnostics for each target attribute. + /// + private static readonly ImmutableDictionary GeneratorAttributeNamesToDiagnosticsMap = ImmutableDictionary.CreateRange(new[] + { + new KeyValuePair("ObservableObjectAttribute", WinRTUsingObservableObjectAttribute), + new KeyValuePair("INotifyPropertyChangedAttribute", WinRTUsingINotifyPropertyChangedAttribute), + }); + + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + WinRTUsingObservableObjectAttribute, + WinRTUsingINotifyPropertyChangedAttribute); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // This analyzer is only enabled when CsWinRT is in AOT mode + if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsCsWinRTAotOptimizerEnabled(context.Compilation)) + { + return; + } + + // Try to get all necessary type symbols + if (!context.Compilation.TryBuildNamedTypeSymbolMap(GeneratorAttributeNamesToFullyQualifiedNamesMap, out ImmutableDictionary? typeSymbols)) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We're looking for class declarations that don't have any base type (same as the other analyzer for non-WinRT scenarios), but inverted for base types. + // That is, we only want to warn in cases where the other analyzer would not warn. Otherwise, warnings from that one are already more than sufficient. + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsRecord: false, IsStatic: false, IsImplicitlyDeclared: false, BaseType.SpecialType: not SpecialType.System_Object } classSymbol) + { + return; + } + + foreach (AttributeData attribute in context.Symbol.GetAttributes()) + { + // Warn if either attribute is used, as it's not compatible with AOT + if (attribute.AttributeClass is { Name: string attributeName } attributeClass && + typeSymbols.TryGetValue(attributeName, out INamedTypeSymbol? attributeSymbol) && + SymbolEqualityComparer.Default.Equals(attributeClass, attributeSymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + GeneratorAttributeNamesToDiagnosticsMap[attributeClass.Name], + context.Symbol.GetLocationFromAttributeDataOrDefault(attribute), + context.Symbol)); + } + } + }, SymbolKind.NamedType); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs new file mode 100644 index 000000000..0c4861ec4 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs @@ -0,0 +1,147 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_12_0_OR_GREATER + +using System.Collections.Generic; +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error when [GeneratedBindableCustomProperty] is used on types with invalid generated base members. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField, + WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // This analyzer is only enabled when CsWinRT is also used + if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsUsingWindowsRuntimePack()) + { + return; + } + + // Get the symbols for [ObservableProperty], [RelayCommand] and [GeneratedBindableCustomProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol || + context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") is not INamedTypeSymbol relayCommandSymbol || + context.Compilation.GetTypeByMetadataName("WinRT.GeneratedBindableCustomPropertyAttribute") is not INamedTypeSymbol generatedBindableCustomPropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure we do have a valid type + if (context.Symbol is not INamedTypeSymbol typeSymbol) + { + return; + } + + // We only care about it if it's using [GeneratedBindableCustomProperty] + if (!typeSymbol.TryGetAttributeWithType(generatedBindableCustomPropertySymbol, out AttributeData? generatedBindableCustomPropertyAttribute)) + { + return; + } + + // Warn on all [ObservableProperty] fields that would be included + foreach (IFieldSymbol fieldSymbol in FindObservablePropertyFields(typeSymbol, observablePropertySymbol)) + { + string propertyName = ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol); + + if (WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField, + typeSymbol.GetLocationFromAttributeDataOrDefault(generatedBindableCustomPropertyAttribute), + typeSymbol, + fieldSymbol.ContainingType, + fieldSymbol.Name)); + } + } + + // Warn on all [RelayCommand] methods that would be included + foreach (IMethodSymbol methodSymbol in FindRelayCommandMethods(typeSymbol, relayCommandSymbol)) + { + (_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol); + + if (WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand, + typeSymbol.GetLocationFromAttributeDataOrDefault(generatedBindableCustomPropertyAttribute), + typeSymbol, + methodSymbol)); + } + } + }, SymbolKind.NamedType); + }); + } + + /// + /// Finds all methods in the base types that have the [RelayCommand] attribute. + /// + /// The instance to inspect. + /// The symbol for the [RelayCommand] + /// All instances for matching members. + private static IEnumerable FindRelayCommandMethods(INamedTypeSymbol typeSymbol, INamedTypeSymbol relayCommandSymbol) + { + // Check whether the base type (if any) is from the same assembly, and stop if it isn't. We do not + // want to include methods from the same type, as those will already be caught by another analyzer. + if (!SymbolEqualityComparer.Default.Equals(typeSymbol.ContainingAssembly, typeSymbol.BaseType?.ContainingAssembly)) + { + yield break; + } + + foreach (ISymbol memberSymbol in typeSymbol.BaseType.GetAllMembersFromSameAssembly()) + { + if (memberSymbol is IMethodSymbol methodSymbol && + methodSymbol.HasAttributeWithType(relayCommandSymbol)) + { + yield return methodSymbol; + } + } + } + + /// + /// Finds all fields in the base types that have the [ObservableProperty] attribute. + /// + /// The instance to inspect. + /// The symbol for the [ObservableProperty] + /// All instances for matching members. + private static IEnumerable FindObservablePropertyFields(INamedTypeSymbol typeSymbol, INamedTypeSymbol observablePropertySymbol) + { + // Skip the base type if not from the same assembly, same as above + if (!SymbolEqualityComparer.Default.Equals(typeSymbol.ContainingAssembly, typeSymbol.BaseType?.ContainingAssembly)) + { + yield break; + } + + foreach (ISymbol memberSymbol in typeSymbol.BaseType.GetAllMembersFromSameAssembly()) + { + if (memberSymbol is IFieldSymbol fieldSymbol && + fieldSymbol.HasAttributeWithType(observablePropertySymbol)) + { + yield return fieldSymbol; + } + } + } +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs new file mode 100644 index 000000000..1a925e7a3 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if ROSLYN_4_12_0_OR_GREATER + +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error when [ObservableProperty] is used on a field in a scenario where it wouldn't be AOT compatible. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + WinRTObservablePropertyOnFieldsIsNotAotCompatible, + WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // This analyzer is only enabled in cases where CsWinRT is producing AOT-compatible code + if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsCsWinRTAotOptimizerEnabled(context.Compilation)) + { + return; + } + + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + // Track whether we produced any diagnostics, for the compilation end scenario + AttributeData? firstObservablePropertyAttribute = null; + + context.RegisterSymbolAction(context => + { + // Ensure we do have a valid field + if (context.Symbol is not IFieldSymbol fieldSymbol) + { + return; + } + + // Emit a diagnostic if the field is using the [ObservableProperty] attribute + if (fieldSymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTObservablePropertyOnFieldsIsNotAotCompatible, + fieldSymbol.Locations.FirstOrDefault(), + ImmutableDictionary.Create() + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.FieldNameKey, fieldSymbol.Name) + .Add(FieldReferenceForObservablePropertyFieldAnalyzer.PropertyNameKey, ObservablePropertyGenerator.Execute.GetGeneratedPropertyName(fieldSymbol)), + fieldSymbol.ContainingType, + fieldSymbol.Name)); + + // Track the attribute data to use as target for the diagnostic. This method takes care of effectively + // sorting all incoming values, so that the final one is deterministic across runs. This ensures that + // the actual location will be the same across recompilations, instead of jumping around all over the + // place. This also makes it possible to more easily suppress it, since its location would not change. + SetOrUpdateAttributeDataBySourceLocation(ref firstObservablePropertyAttribute, observablePropertyAttribute); + } + }, SymbolKind.Field); + + // If C# preview is already in use, we can stop here. The last diagnostic is only needed when partial properties + // cannot be used, to inform developers that they'll need to bump the language version to enable the code fixer. + if (context.Compilation.IsLanguageVersionPreview()) + { + return; + } + + context.RegisterCompilationEndAction(context => + { + // If we have produced at least one diagnostic, also emit the info message + if (Volatile.Read(ref firstObservablePropertyAttribute) is { } observablePropertyAttribute) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo, + observablePropertyAttribute.GetLocation())); + } + }); + }); + } + + /// + /// Sets or updates the instance to use for compilation diagnostics, sorting by source location. + /// + /// The location of the previous value to potentially overwrite. + /// Thew new instance. + private static void SetOrUpdateAttributeDataBySourceLocation([NotNull] ref AttributeData? oldAttributeDataLocation, AttributeData newAttributeData) + { + bool hasReplacedOriginalValue; + + do + { + AttributeData? oldAttributeData = Volatile.Read(ref oldAttributeDataLocation); + + // If the old attribute data is null, it means this is the first time we called this method with a new + // attribute candidate. In that case, there is nothing to check: we should always store the new instance. + if (oldAttributeData is not null) + { + // Sort by file paths, alphabetically + int filePathRelativeSortIndex = string.Compare( + newAttributeData.ApplicationSyntaxReference?.SyntaxTree.FilePath, + oldAttributeData.ApplicationSyntaxReference?.SyntaxTree.FilePath, + StringComparison.OrdinalIgnoreCase); + + // Also sort by location (this is a tie-breaker if two values are from the same file) + bool isTextSpanLowerSorted = + (newAttributeData.ApplicationSyntaxReference?.Span.Start ?? 0) < + (oldAttributeData.ApplicationSyntaxReference?.Span.Start ?? 0); + + // The new candidate can be dropped if it's from a file that's alphabetically sorted after + // the old value, or whether the location is after the previous one, within the same file. + if (filePathRelativeSortIndex == 1 || (filePathRelativeSortIndex == 0 && !isTextSpanLowerSorted)) + { + break; + } + } + + // Attempt to actually replace the old value, without taking locks + AttributeData? originalValue = Interlocked.CompareExchange( + location1: ref oldAttributeDataLocation, + value: newAttributeData, + comparand: oldAttributeData); + + // This call might have raced against other threads, since all symbol actions can run in parallel. + // If the original value is the old value we read at the start of the method, it means no other + // thread raced against this one, so we are done. If it's different, then we failed to write the + // new candidate. We can discard the work done in this iteration and simply try again. + hasReplacedOriginalValue = oldAttributeData == originalValue; + } + while (!hasReplacedOriginalValue); +#pragma warning disable CS8777 // The loop always ensures that 'oldAttributeDataLocation' is not null on exit + } +#pragma warning restore CS8777 +} + +#endif diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs new file mode 100644 index 000000000..10029b84b --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error when [RelayCommand] is used on a method inside a type with [GeneratedBindableCustomProperty]. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // This analyzer is only enabled when CsWinRT is also used + if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsUsingWindowsRuntimePack()) + { + return; + } + + // Get the symbols for [RelayCommand] and [GeneratedBindableCustomProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") is not INamedTypeSymbol relayCommandSymbol || + context.Compilation.GetTypeByMetadataName("WinRT.GeneratedBindableCustomPropertyAttribute") is not INamedTypeSymbol generatedBindableCustomPropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Ensure we do have a valid method with a containing type we can reference + if (context.Symbol is not IMethodSymbol { ContainingType: INamedTypeSymbol typeSymbol } methodSymbol) + { + return; + } + + // If the method is not using [RelayCommand], we can skip it + if (!methodSymbol.TryGetAttributeWithType(relayCommandSymbol, out AttributeData? relayCommandAttribute)) + { + return; + } + + // If the containing type is not using [GeneratedBindableCustomProperty], we can also skip it + if (!typeSymbol.TryGetAttributeWithType(generatedBindableCustomPropertySymbol, out AttributeData? generatedBindableCustomPropertyAttribute)) + { + return; + } + + (_, string propertyName) = RelayCommandGenerator.Execute.GetGeneratedFieldAndPropertyNames(methodSymbol); + + if (DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(generatedBindableCustomPropertyAttribute, propertyName)) + { + // Actually warn if the generated command would've been included by the generator + context.ReportDiagnostic(Diagnostic.Create( + WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible, + methodSymbol.GetLocationFromAttributeDataOrDefault(relayCommandAttribute), + methodSymbol)); + } + }, SymbolKind.Method); + }); + } + + /// + /// Checks whether a generated property with a given name would be included by the [GeneratedBindableCustomProperty] generator. + /// + /// The input value for the [GeneratedBindableCustomProperty] attribute. + /// The target generated property name to check. + /// Whether would be included by the [GeneratedBindableCustomProperty] generator. + internal static bool DoesGeneratedBindableCustomPropertyAttributeIncludePropertyName(AttributeData attributeData, string propertyName) + { + // Make sure we have a valid list of property names to explicitly include. + // If that is not the case, we consider all properties as included by default. + if (attributeData.ConstructorArguments is not [{ IsNull: false, Kind: TypedConstantKind.Array, Type: IArrayTypeSymbol { ElementType.SpecialType: SpecialType.System_String }, Values: var names }, ..]) + { + return true; + } + + // Simply match the input collection of target property names + foreach (TypedConstant propertyValue in names) + { + if (propertyValue is { IsNull: false, Type.SpecialType: SpecialType.System_String, Value: string targetName } && targetName == propertyName) + { + return true; + } + } + + // No matches, we can consider the property as not included + return false; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 431e5da40..b62fcb7ae 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -34,6 +34,21 @@ internal static class DiagnosticDescriptors /// public const string AsyncVoidReturningRelayCommandMethodId = "MVVMTK0039"; + /// + /// The diagnostic id for . + /// + public const string UseObservablePropertyOnPartialPropertyId = "MVVMTK0042"; + + /// + /// The diagnostic id for . + /// + public const string WinRTObservablePropertyOnFieldsIsNotAotCompatibleId = "MVVMTK0045"; + + /// + /// The diagnostic id for . + /// + public const string UseObservablePropertyOnSemiAutoPropertyId = "MVVMTK0056"; + /// /// Gets a indicating when a duplicate declaration of would happen. /// @@ -117,17 +132,17 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when the target type doesn't inherit from the ObservableValidator class. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator". + /// Format: "The {0} {1}.{2} cannot be used to generate an observable property, as it has {3} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator". /// /// public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceForValidationAttributeError = new DiagnosticDescriptor( id: "MVVMTK0006", title: "Missing ObservableValidator inheritance", - messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator", + messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as it has {3} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Cannot apply [ObservableProperty] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", + description: "Cannot apply [ObservableProperty] to fields or properties with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0006"); /// @@ -320,35 +335,35 @@ internal static class DiagnosticDescriptors helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0018"); /// - /// Gets a indicating when [ObservableProperty] is applied to a field in an invalid type. + /// Gets a indicating when [ObservableProperty] is applied to a field or property in an invalid type. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]". + /// Format: "The {0} {1}.{2} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]". /// /// - public static readonly DiagnosticDescriptor InvalidContainingTypeForObservablePropertyFieldError = new DiagnosticDescriptor( + public static readonly DiagnosticDescriptor InvalidContainingTypeForObservablePropertyMemberError = new DiagnosticDescriptor( id: "MVVMTK0019", - title: "Invalid containing type for [ObservableProperty] field", - messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]", + title: "Invalid containing type for [ObservableProperty] field or property", + messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as its containing type doesn't inherit from ObservableObject, nor does it use [ObservableObject] or [INotifyPropertyChanged]", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Fields annotated with [ObservableProperty] must be contained in a type that inherits from ObservableObject or that is annotated with [ObservableObject] or [INotifyPropertyChanged] (including base types).", + description: "Fields and properties annotated with [ObservableProperty] must be contained in a type that inherits from ObservableObject or that is annotated with [ObservableObject] or [INotifyPropertyChanged] (including base types).", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0019"); /// - /// Gets a indicating when [ObservableProperty] is applied to a field in an invalid type. + /// Gets a indicating when [ObservableProperty] is applied to a field or property in an invalid type. /// - /// Format: "The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]". + /// Format: "The {0} {1}.{2} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]". /// /// public static readonly DiagnosticDescriptor FieldWithOrphanedDependentObservablePropertyAttributesError = new DiagnosticDescriptor( id: "MVVMTK0020", title: "Invalid use of attributes dependent on [ObservableProperty]", - messageFormat: "The field {0}.{1} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]", + messageFormat: "The {0} {1}.{2} needs to be annotated with [ObservableProperty] in order to enable using [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo]", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Fields not annotated with [ObservableProperty] cannot use [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo].", + description: "Fields and properties not annotated with [ObservableProperty] cannot use [NotifyPropertyChangedFor], [NotifyCanExecuteChangedFor], [NotifyPropertyChangedRecipients] and [NotifyDataErrorInfo].", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0020"); /// @@ -368,19 +383,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"); /// @@ -402,49 +417,49 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when a generated property created with [ObservableProperty] would cause conflicts with other generated members. /// - /// Format: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members". + /// Format: "The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members". /// /// public static readonly DiagnosticDescriptor InvalidObservablePropertyError = new DiagnosticDescriptor( id: "MVVMTK0024", title: "Invalid generated property declaration", - messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members", + messageFormat: "The {0} {1}.{2} cannot be used to generate an observable property, as its name or type would cause conflicts with other generated members", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "The fields annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.", + description: "The fields and properties annotated with [ObservableProperty] cannot result in a property name or have a type that would cause conflicts with other generated members.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0024"); /// /// Gets a indicating when the target type doesn't inherit from the ObservableValidator class. /// - /// Format: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator". + /// Format: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator". /// /// public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceForNotifyDataErrorInfoError = new DiagnosticDescriptor( id: "MVVMTK0025", title: "Missing ObservableValidator inheritance", - messageFormat: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator", + messageFormat: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it is declared in a type that doesn't inherit from ObservableValidator", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Cannot apply [NotifyDataErrorInfo] to fields that are declared in a type that doesn't inherit from ObservableValidator.", + description: "Cannot apply [NotifyDataErrorInfo] to fields and properties that are declared in a type that doesn't inherit from ObservableValidator.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0025"); /// - /// Gets a indicating when the target field uses [NotifyDataErrorInfo] but has no validation attributes. + /// Gets a indicating when the target field or property uses [NotifyDataErrorInfo] but has no validation attributes. /// - /// Format: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation". + /// Format: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation". /// /// public static readonly DiagnosticDescriptor MissingValidationAttributesForNotifyDataErrorInfoError = new DiagnosticDescriptor( id: "MVVMTK0026", title: "Missing validation attributes", - messageFormat: "The field {0}.{1} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation", + messageFormat: "The {0} {1}.{2} cannot be annotated with [NotifyDataErrorInfo], as it doesn't have any validation attributes to use during validation", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "Cannot apply [NotifyDataErrorInfo] to fields that don't have any validation attributes to use during validation.", + description: "Cannot apply [NotifyDataErrorInfo] to fields and properties that don't have any validation attributes to use during validation.", helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0026"); /// @@ -480,35 +495,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"); /// @@ -624,8 +639,7 @@ internal static class DiagnosticDescriptors category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must be using valid expressions.", - helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0037"); + description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must be using valid expressions."); /// /// Gets a indicating when a method with [RelayCommand] is using an invalid attribute targeting the field or property. @@ -640,8 +654,7 @@ internal static class DiagnosticDescriptors category: typeof(RelayCommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must be using valid expressions.", - helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0038"); + description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must be using valid expressions."); /// /// Gets a indicating when a method with [RelayCommand] is async void. @@ -674,4 +687,261 @@ 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 the C# language version not being sufficient for [ObservableProperty] on partial properties. + /// + /// Format: "Using [ObservableProperty] on partial properties requires the C# language version to be set to 'preview', as support for the 'field' keyword is needed by the source generators to emit valid code (add preview to your .csproj/.props file)". + /// + /// + public static readonly DiagnosticDescriptor CSharpLanguageVersionIsNotPreviewForObservableProperty = new DiagnosticDescriptor( + id: "MVVMTK0041", + title: "C# language version is not 'preview'", + messageFormat: """Using [ObservableProperty] on partial properties requires the C# language version to be set to 'preview', as support for the 'field' keyword is needed by the source generators to emit valid code (add preview to your .csproj/.props file)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "The C# language version must be set to 'preview' when using [ObservableProperty] on partial properties for the source generators to emit valid code (the preview option must be set in the .csproj/.props file).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0041"); + + /// + /// Gets a for when [ObservableProperty] on a field should be converted to a partial property. + /// + /// Format: "The field {0}.{1} using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well)". + /// + /// + public static readonly DiagnosticDescriptor UseObservablePropertyOnPartialProperty = new DiagnosticDescriptor( + id: UseObservablePropertyOnPartialPropertyId, + title: "Prefer using [ObservableProperty] on partial properties", + messageFormat: """The field {0}.{1} using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Fields using [ObservableProperty] can be converted to partial properties instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0042"); + + /// + /// Gets a indicating when [ObservableProperty] is applied to a property with an invalid declaration. + /// + /// Format: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be an instance (non static) partial property with a getter and a setter that is not init-only)". + /// + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationForObservableProperty = new DiagnosticDescriptor( + id: "MVVMTK0043", + title: "Invalid property declaration for [ObservableProperty]", + messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be an instance (non static) partial property with a getter and a setter that is not init-only)", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [ObservableProperty] must be instance (non static) 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"); + + /// + /// Gets a for when [ObservableProperty] is used on a field in WinRT scenarios. + /// + /// Format: "The field {0}.{1} using [ObservableProperty] will generate code that is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and a partial property should be used instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)". + /// + /// + public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatible = new DiagnosticDescriptor( + id: WinRTObservablePropertyOnFieldsIsNotAotCompatibleId, + title: "Using [ObservableProperty] on fields is not AOT compatible for WinRT", + messageFormat: """The field {0}.{1} using [ObservableProperty] will generate code that is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and a partial property should be used instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Fields using [ObservableProperty] will generate code that is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and partial properties should be used instead (as they allow the CsWinRT generators to correctly produce the necessary WinRT marshalling code).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0045"); + + /// + /// Gets a for when [RelayCommand] is used on a method in types where [GeneratedBindableCustomProperty] is used. + /// + /// Format: "The method {0} using [RelayCommand] within a type also using [GeneratedBindableCustomProperty] and including the generated property, which is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator)". + /// + /// + public static readonly DiagnosticDescriptor WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible = new DiagnosticDescriptor( + id: "MVVMTK0046", + title: "Using [RelayCommand] is not compatible with [GeneratedBindableCustomProperty]", + messageFormat: """The method {0} using [RelayCommand] within a type also using [GeneratedBindableCustomProperty] and including the generated property, which is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator)""", + category: typeof(RelayCommandGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using [RelayCommand] on methods within a type also using [GeneratedBindableCustomProperty] and including the generated property is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0046"); + + /// + /// Gets a for when [GeneratedBindableCustomProperty] is used on a type that also uses [ObservableProperty] on any declared or inherited fields. + /// + /// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}, and including the generated property: combining the two generators is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)". + /// + /// + public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField = new DiagnosticDescriptor( + id: "MVVMTK0047", + title: "Using [GeneratedBindableCustomProperty] is not compatible with [ObservableProperty] on fields", + messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}, and including the generated property: combining the two generators is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using [GeneratedBindableCustomProperty] on types that also use [ObservableProperty] on any declared (or inherited) fields and including the generated property is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0047"); + + /// + /// Gets a for when [GeneratedBindableCustomProperty] is used on a type that also uses [RelayCommand] on any declared or inherited methods. + /// + /// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1} and including the generated property: combining the two generators is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)". + /// + /// + public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand = new DiagnosticDescriptor( + id: "MVVMTK0048", + title: "Using [GeneratedBindableCustomProperty] is not compatible with [RelayCommand]", + messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1} and including the generated property: combining the two generators is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)""", + category: typeof(RelayCommandGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using [GeneratedBindableCustomProperty] on types that also use [RelayCommand] on any inherited methods and including the generated property is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0048"); + + /// + /// Gets a indicating when [INotifyPropertyChanged] is used on a type in WinRT scenarios. + /// + /// Format: "The type {0} is using the [INotifyPropertyChanged] attribute, with is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and it should derive from ObservableObject or manually implement INotifyPropertyChanged instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)". + /// + /// + public static readonly DiagnosticDescriptor WinRTUsingINotifyPropertyChangedAttribute = new DiagnosticDescriptor( + id: "MVVMTK0049", + title: "Using [INotifyPropertyChanged] is not AOT compatible for WinRT", + messageFormat: "The type {0} is using the [INotifyPropertyChanged] attribute, with is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and it should derive from ObservableObject or manually implement INotifyPropertyChanged instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)", + category: typeof(INotifyPropertyChangedGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using the [INotifyPropertyChanged] attribute on types is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and they should derive from ObservableObject or manually implement INotifyPropertyChanged instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0049"); + + /// + /// Gets a indicating when [ObservableObject] is used on a type in WinRT scenarios. + /// + /// Format: "The type {0} is using the [ObservableObject] attribute, with is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and it should derive from ObservableObject instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)". + /// + /// + public static readonly DiagnosticDescriptor WinRTUsingObservableObjectAttribute = new DiagnosticDescriptor( + id: "MVVMTK0050", + title: "Using [ObservableObject] is not AOT compatible for WinRT", + messageFormat: "The type {0} is using the [ObservableObject] attribute, with is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and it should derive from ObservableObject instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code)", + category: typeof(ObservableObjectGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using the [ObservableObject] attribute on types is not AOT compatible in WinRT scenarios (such as UWP XAML and WinUI 3 apps), and they should derive from ObservableObject instead (as it allows the CsWinRT generators to correctly produce the necessary WinRT marshalling code).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0050"); + + /// + /// Gets a for when [ObservableProperty] is used on a field in WinRT scenarios. + /// + /// Format: "This project produced one or more 'MVVMTK0045' warnings due to [ObservableProperty] being used on fields, which is not AOT compatible in WinRT scenarios, but it can't enable partial properties and the associated code fixer because 'LangVersion' is not set to 'preview' (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings)". + /// + /// + public static readonly DiagnosticDescriptor WinRTObservablePropertyOnFieldsIsNotAotCompatibleCompilationEndInfo = new DiagnosticDescriptor( + id: "MVVMTK0051", + title: "Using [ObservableProperty] with WinRT and AOT requires 'LangVersion=preview'", + messageFormat: """This project produced one or more 'MVVMTK0045' warnings due to [ObservableProperty] being used on fields, which is not AOT compatible in WinRT scenarios, but it can't enable partial properties and the associated code fixer because 'LangVersion' is not set to 'preview' (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "This project producing one or more 'MVVMTK0045' warnings due to [ObservableProperty] being used on fields, which is not AOT compatible in WinRT scenarios, should set 'LangVersion' to 'preview' to enable partial properties and the associated code fixer (setting 'LangVersion=preview' is required to use [ObservableProperty] on partial properties and address these warnings).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0051", + customTags: WellKnownDiagnosticTags.CompilationEnd); + + /// + /// Gets a for when [ObservableProperty] is used on a property that is not an incomplete partial definition. + /// + /// Format: "The property {0}.{1} is not an incomplete partial definition ([ObservableProperty] must be used on partial property definitions with no implementation part)". + /// + /// + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationIsNotIncompletePartialDefinition = new DiagnosticDescriptor( + id: "MVVMTK0052", + title: "Using [ObservableProperty] on an invalid property declaration (not incomplete partial definition)", + messageFormat: """The property {0}.{1} is not an incomplete partial definition ([ObservableProperty] must be used on partial property definitions with no implementation part)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [ObservableProperty] is not an incomplete partial definition part ([ObservableProperty] must be used on partial property definitions with no implementation part).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0052"); + + /// + /// Gets a for when [ObservableProperty] is used on a property that returns a ref value. + /// + /// Format: "The property {0}.{1} returns a ref value ([ObservableProperty] must be used on properties returning a type by value)". + /// + /// + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsByRef = new DiagnosticDescriptor( + id: "MVVMTK0053", + title: "Using [ObservableProperty] on a property that returns byref", + messageFormat: """The property {0}.{1} returns a ref value ([ObservableProperty] must be used on properties returning a type by value)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [ObservableProperty] returns a value by reference ([ObservableProperty] must be used on properties returning a type by value).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0053"); + + /// + /// Gets a for when [ObservableProperty] is used on a property that returns a byref-like value. + /// + /// Format: "The property {0}.{1} returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type)". + /// + /// + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsRefLikeType = new DiagnosticDescriptor( + id: "MVVMTK0054", + title: "Using [ObservableProperty] on a property that returns byref-like", + messageFormat: """The property {0}.{1} returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [ObservableProperty] returns a byref-like value ([ObservableProperty] must be used on properties of a non byref-like type).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0054"); + + /// + /// Gets a for when [ObservableProperty] is used on a property that returns a pointer type. + /// + /// Format: "The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)". + /// + /// + public static readonly DiagnosticDescriptor InvalidObservablePropertyDeclarationReturnsPointerLikeType = new DiagnosticDescriptor( + id: "MVVMTK0055", + title: "Using [ObservableProperty] on a property that returns pointer-like", + messageFormat: """The property {0}.{1} returns a pointer or function pointer value ([ObservableProperty] must be used on properties of a non pointer-like type)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "A property using [ObservableProperty] returns a pointer-like value ([ObservableProperty] must be used on properties of a non pointer-like type).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0055"); + + /// + /// Gets a for when a semi-auto property can be converted to use [ObservableProperty] instead. + /// + /// Format: "The semi-auto property {0}.{1} can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)". + /// + /// + public static readonly DiagnosticDescriptor UseObservablePropertyOnSemiAutoProperty = new DiagnosticDescriptor( + id: UseObservablePropertyOnSemiAutoPropertyId, + title: "Prefer using [ObservableProperty] over semi-auto properties", + messageFormat: """The semi-auto property {0}.{1} can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Semi-auto properties should be converted to partial properties using [ObservableProperty] when possible, which is recommended (doing so makes the code less verbose and results in more optimized code).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0056"); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs index 4a6c803a9..774024c9f 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -4,6 +4,8 @@ using Microsoft.CodeAnalysis; +#pragma warning disable IDE0090 // Use 'new SuppressionDescriptor(...)' + namespace CommunityToolkit.Mvvm.SourceGenerators.Diagnostics; /// @@ -14,15 +16,23 @@ internal static class SuppressionDescriptors /// /// Gets a for a field using [ObservableProperty] with an attribute list targeting a property. /// - public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new( + public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new SuppressionDescriptor( id: "MVVMTKSPR0001", suppressedDiagnosticId: "CS0657", - justification: "Fields using [ObservableProperty] can use [property:] attribute lists to forward attributes to the generated properties"); + justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); + + /// + /// Gets a for a field using [ObservableProperty] with an attribute list targeting a get or set accessor. + /// + public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyFieldAccessors = new SuppressionDescriptor( + id: "MVVMTKSPR0001", + suppressedDiagnosticId: "CS0658", + justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); /// /// Gets a for a method using [RelayCommand] with an attribute list targeting a field or property. /// - public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new( + public static readonly SuppressionDescriptor FieldOrPropertyAttributeListForRelayCommandMethod = new SuppressionDescriptor( id: "MVVMTKSPR0002", suppressedDiagnosticId: "CS0657", justification: "Methods using [RelayCommand] can use [field:] and [property:] attribute lists to forward attributes to the generated fields and properties"); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs similarity index 69% rename from src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs rename to src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs index f27f3969a..57bae864b 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs @@ -14,7 +14,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// /// -/// A diagnostic suppressor to suppress CS0657 warnings for fields with [ObservableProperty] using a [property:] attribute list. +/// A diagnostic suppressor to suppress CS0657 warnings for fields with [ObservableProperty] using a [property:] attribute list (or [set:] or [get:]). /// /// /// That is, this diagnostic suppressor will suppress the following diagnostic: @@ -29,10 +29,10 @@ namespace CommunityToolkit.Mvvm.SourceGenerators; /// /// [DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor : DiagnosticSuppressor +public sealed class ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor : DiagnosticSuppressor { /// - public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(PropertyAttributeListForObservablePropertyField); + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(PropertyAttributeListForObservablePropertyField, PropertyAttributeListForObservablePropertyFieldAccessors); /// public override void ReportSuppressions(SuppressionAnalysisContext context) @@ -43,7 +43,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) // Check that the target is effectively [property:] over a field declaration with at least one variable, which is the only case we are interested in if (syntaxNode is AttributeTargetSpecifierSyntax { Parent.Parent: FieldDeclarationSyntax { Declaration.Variables.Count: > 0 } fieldDeclaration } attributeTarget && - attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword)) + (attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword) || attributeTarget.Identifier.IsKind(SyntaxKind.GetKeyword) || attributeTarget.Identifier.IsKind(SyntaxKind.SetKeyword))) { SemanticModel semanticModel = context.GetSemanticModel(syntaxNode.SyntaxTree); @@ -55,7 +55,16 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) semanticModel.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol observablePropertySymbol && fieldSymbol.HasAttributeWithType(observablePropertySymbol)) { - context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyField, diagnostic)); + // Emit the right suppression based on the attribute modifier. For 'property:', Roslyn + // will emit the 'CS0657' warning, whereas for 'get:' or 'set:', it will emit 'CS0658'. + if (attributeTarget.Identifier.IsKind(SyntaxKind.PropertyKeyword)) + { + context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyField, diagnostic)); + } + else + { + context.ReportSuppression(Suppression.Create(PropertyAttributeListForObservablePropertyFieldAccessors, diagnostic)); + } } } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs new file mode 100644 index 000000000..cd7a8226b --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AccessibilityExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AccessibilityExtensions +{ + /// + /// Converts a given value to the equivalent ."/> + /// + /// The input value to convert. + /// The representing the modifiers for . + public static SyntaxTokenList ToSyntaxTokenList(this Accessibility accessibility) + { + return accessibility switch + { + Accessibility.NotApplicable => TokenList(), + Accessibility.Private => TokenList(Token(SyntaxKind.PrivateKeyword)), + Accessibility.ProtectedAndInternal => TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.ProtectedKeyword)), + Accessibility.Protected => TokenList(Token(SyntaxKind.ProtectedKeyword)), + Accessibility.Internal => TokenList(Token(SyntaxKind.InternalKeyword)), + Accessibility.ProtectedOrInternal => TokenList(Token(SyntaxKind.ProtectedKeyword), Token(SyntaxKind.InternalKeyword)), + Accessibility.Public => TokenList(Token(SyntaxKind.PublicKeyword)), + _ => TokenList() + }; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs new file mode 100644 index 000000000..7556bd45a --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AnalyzerConfigOptionsExtensions.cs @@ -0,0 +1,127 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class AnalyzerConfigOptionsExtensions +{ + /// + /// Checks whether the Windows runtime pack is being used (ie. if the target framework is net8.0-windows10.0.17763.0 or above). + /// + /// The input instance. + /// Whether the Windows runtime pack is being used. + public static bool IsUsingWindowsRuntimePack(this AnalyzerConfigOptions options) + { + return options.GetMSBuildBooleanPropertyValue("_MvvmToolkitIsUsingWindowsRuntimePack"); + } + + /// + /// Checks whether CsWinRT is configured in AOT support mode. + /// + /// The input instance. + /// The input instance in use. + /// Whether CsWinRT is configured in AOT support mode. + public static bool IsCsWinRTAotOptimizerEnabled(this AnalyzerConfigOptions options, Compilation compilation) + { + // If the runtime pack isn't being used, CsWinRT won't be used either. Technically speaking it's possible + // to reference CsWinRT without targeting Windows, but that's not a scenario that is supported anyway. + if (!options.IsUsingWindowsRuntimePack()) + { + return false; + } + + if (options.TryGetMSBuildStringPropertyValue("CsWinRTAotOptimizerEnabled", out string? csWinRTAotOptimizerEnabled)) + { + // If the generators are in opt-in mode, we will not show warnings + if (string.Equals(csWinRTAotOptimizerEnabled, "OptIn", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // The automatic mode will generate marshalling code for all possible scenarios + if (string.Equals(csWinRTAotOptimizerEnabled, "Auto", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // The default value of "true" will run in automatic mode for some scenarios, which we have to check + if (bool.TryParse(csWinRTAotOptimizerEnabled, out bool isCsWinRTAotOptimizerEnabled) && isCsWinRTAotOptimizerEnabled) + { + // The CsWinRT generator will be enabled for AOT scenarios in the following cases: + // - The project is producing a WinRT component + // - The 'CsWinRTAotWarningLevel' is set to '2', ie. all marshalling code even for built-in types should be produced + // - The app is either UWP XAML or WinUI 3 (which is detected by the presence of the 'Button' type + // For additional reference, see the source code at https://github.com/microsoft/CsWinRT. + return + options.GetMSBuildBooleanPropertyValue("CsWinRTComponent") || + options.GetMSBuildInt32PropertyValue("CsWinRTAotWarningLevel") == 2 || + compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.Controls.Button") is not null || + compilation.GetTypeByMetadataName("Windows.UI.Xaml.Controls.Button") is not null; + + } + } + + return false; + } + + /// + /// Gets the boolean value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The default value to return if the property is not found or cannot be parsed. + /// The value of the target MSBuild property. + public static bool GetMSBuildBooleanPropertyValue(this AnalyzerConfigOptions options, string propertyName, bool defaultValue = false) + { + if (options.TryGetMSBuildStringPropertyValue(propertyName, out string? propertyValue)) + { + if (bool.TryParse(propertyValue, out bool booleanPropertyValue)) + { + return booleanPropertyValue; + } + } + + return defaultValue; + } + + /// + /// Gets the integer value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The default value to return if the property is not found or cannot be parsed. + /// The value of the target MSBuild property. + public static int GetMSBuildInt32PropertyValue(this AnalyzerConfigOptions options, string propertyName, int defaultValue = 0) + { + if (options.TryGetMSBuildStringPropertyValue(propertyName, out string? propertyValue)) + { + if (int.TryParse(propertyValue, out int int32PropertyValue)) + { + return int32PropertyValue; + } + } + + return defaultValue; + } + + /// + /// Tries to get a value of a given MSBuild property from an input instance. + /// + /// The input instance. + /// The name of the target MSBuild property. + /// The resulting property value. + /// Whether the property value was retrieved.. + public static bool TryGetMSBuildStringPropertyValue(this AnalyzerConfigOptions options, string propertyName, [NotNullWhen(true)] out string? propertyValue) + { + return options.TryGetValue($"build_property.{propertyName}", out propertyValue); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs index 14f7498af..096e6456e 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -6,6 +6,7 @@ // more info in ThirdPartyNotices.txt in the root of the project. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -53,6 +54,29 @@ properties.Value.Value is T argumentValue && return null; } + /// + /// Tries to get a constructor argument at a given index from the input instance. + /// + /// The type of constructor argument to retrieve. + /// The target instance to get the argument from. + /// The index of the argument to try to retrieve. + /// The resulting argument, if it was found. + /// Whether or not an argument of type at position was found. + public static bool TryGetConstructorArgument(this AttributeData attributeData, int index, [NotNullWhen(true)] out T? result) + { + if (attributeData.ConstructorArguments.Length > index && + attributeData.ConstructorArguments[index].Value is T argument) + { + result = argument; + + return true; + } + + result = default; + + return false; + } + /// /// Gets a given named argument value from an instance, or a fallback value. /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs index ac05bdff6..6a584bbfb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/CompilationExtensions.cs @@ -27,6 +27,16 @@ public static bool HasLanguageVersionAtLeastEqualTo(this Compilation compilation return ((CSharpCompilation)compilation).LanguageVersion >= languageVersion; } + /// + /// Checks whether a given compilation (assumed to be for C#) is using the preview language version. + /// + /// The to consider for analysis. + /// Whether is using the preview language version. + public static bool IsLanguageVersionPreview(this Compilation compilation) + { + return ((CSharpCompilation)compilation).LanguageVersion == LanguageVersion.Preview; + } + /// /// /// Checks whether or not a type with a specified metadata name is accessible from a given instance. @@ -108,6 +118,9 @@ public static bool TryBuildNamedTypeSymbolMap( { ImmutableDictionary.Builder builder = ImmutableDictionary.CreateBuilder(); + // Ensure we always use the right comparer for values, when needed + builder.ValueComparer = SymbolEqualityComparer.Default; + foreach (KeyValuePair pair in typeNames) { if (compilation.GetTypeByMetadataName(pair.Value) is not INamedTypeSymbol attributeSymbol) diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/GeneratorAttributeSyntaxContextWithOptions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/GeneratorAttributeSyntaxContextWithOptions.cs new file mode 100644 index 000000000..2af137911 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/GeneratorAttributeSyntaxContextWithOptions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// +/// +/// The original value. +/// The original value. +internal readonly struct GeneratorAttributeSyntaxContextWithOptions( + GeneratorAttributeSyntaxContext syntaxContext, + AnalyzerConfigOptions globalOptions) +{ + /// + public SyntaxNode TargetNode { get; } = syntaxContext.TargetNode; + + /// + public ISymbol TargetSymbol { get; } = syntaxContext.TargetSymbol; + + /// + public SemanticModel SemanticModel { get; } = syntaxContext.SemanticModel; + + /// + public ImmutableArray Attributes { get; } = syntaxContext.Attributes; + + /// + public AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs index 8b847bcf0..367b16f78 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs @@ -28,6 +28,28 @@ public static IEnumerable GetAllMembers(this INamedTypeSymbol symbol) } } + /// + /// Gets all member symbols from a given instance, including inherited ones, only if they are declared in source. + /// + /// The input instance. + /// A sequence of all member symbols for . + public static IEnumerable GetAllMembersFromSameAssembly(this INamedTypeSymbol symbol) + { + for (INamedTypeSymbol? currentSymbol = symbol; currentSymbol is { SpecialType: not SpecialType.System_Object }; currentSymbol = currentSymbol.BaseType) + { + // Stop early when we reach a base type from another assembly + if (!SymbolEqualityComparer.Default.Equals(currentSymbol.ContainingAssembly, symbol.ContainingAssembly)) + { + yield break; + } + + foreach (ISymbol memberSymbol in currentSymbol.GetMembers()) + { + yield return memberSymbol; + } + } + } + /// /// Gets all member symbols from a given instance, including inherited ones. /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs index a825454fb..964e4dafb 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ISymbolExtensions.cs @@ -175,4 +175,31 @@ public static bool CanBeAccessedFrom(this ISymbol symbol, IAssemblySymbol assemb accessibility == Accessibility.Public || accessibility == Accessibility.Internal && symbol.ContainingAssembly.GivesAccessTo(assembly); } + + /// + /// Gets the location of a given symbol that is in the same syntax tree of a specified attribute, or the first one. + /// + /// The input instance to check. + /// The target instance. + /// The best match. + public static Location? GetLocationFromAttributeDataOrDefault(this ISymbol symbol, AttributeData attributeData) + { + Location? firstLocation = null; + + // Get the syntax tree where the attribute application is located. We use + // it to try to find the symbol location that belongs to the same file. + SyntaxTree? attributeTree = attributeData.ApplicationSyntaxReference?.SyntaxTree; + + foreach (Location location in symbol.Locations) + { + if (location.SourceTree == attributeTree) + { + return location; + } + + firstLocation ??= location; + } + + return firstLocation; + } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs index 6e976501b..b79f743b3 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -33,6 +33,25 @@ public static bool HasOrInheritsFromFullyQualifiedMetadataName(this ITypeSymbol return false; } + /// + /// Checks whether or not a given has or inherits from a specified type. + /// + /// The target instance to check. + /// The type to check for inheritance. + /// Whether or not is or inherits from . + public static bool HasOrInheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) + { + for (ITypeSymbol? currentType = typeSymbol; currentType is not null; currentType = currentType.BaseType) + { + if (SymbolEqualityComparer.Default.Equals(currentType, baseTypeSymbol)) + { + return true; + } + } + + return false; + } + /// /// Checks whether or not a given inherits from a specified type. /// @@ -60,7 +79,7 @@ public static bool InheritsFromFullyQualifiedMetadataName(this ITypeSymbol typeS /// Checks whether or not a given inherits from a specified type. /// /// The target instance to check. - /// The instane to check for inheritance from. + /// The instance to check for inheritance from. /// Whether or not inherits from . public static bool InheritsFromType(this ITypeSymbol typeSymbol, ITypeSymbol baseTypeSymbol) { diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs index d009810eb..1c627db6d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/IncrementalGeneratorInitializationContextExtensions.cs @@ -4,7 +4,11 @@ using System; using System.Collections.Immutable; +using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +#pragma warning disable CS1574 namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -13,6 +17,31 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; /// internal static class IncrementalGeneratorInitializationContextExtensions { + /// + public static IncrementalValuesProvider ForAttributeWithMetadataNameAndOptions( + this IncrementalGeneratorInitializationContext context, + string fullyQualifiedMetadataName, + Func predicate, + Func transform) + { + // Invoke 'ForAttributeWithMetadataName' normally, but just return the context directly + IncrementalValuesProvider syntaxContext = context.SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName, + predicate, + static (context, token) => context); + + // Do the same for the analyzer config options + IncrementalValueProvider configOptions = context.AnalyzerConfigOptionsProvider.Select(static (provider, token) => provider.GlobalOptions); + + // Merge the two and invoke the provided transform on these two values. Neither value + // is equatable, meaning the pipeline will always re-run until this point. This is + // intentional: we don't want any symbols or other expensive objects to be kept alive + // across incremental steps, especially if they could cause entire compilations to be + // rooted, which would significantly increase memory use and introduce more GC pauses. + // In this specific case, flowing non equatable values in a pipeline is therefore fine. + return syntaxContext.Combine(configOptions).Select((input, token) => transform(new GeneratorAttributeSyntaxContextWithOptions(input.Left, input.Right), token)); + } + /// /// Conditionally invokes /// if the value produced by the input is . diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs index c87f9a8fb..4a81b6bee 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs @@ -19,7 +19,7 @@ internal static class SymbolInfoExtensions /// The resulting attribute type symbol, if correctly resolved. /// Whether is resolved to a symbol. /// - /// This can be used to ensure users haven't eg. spelled names incorrecty or missed a using directive. Normally, code would just + /// This can be used to ensure users haven't eg. spelled names incorrectly or missed a using directive. Normally, code would just /// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore /// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own. /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs new file mode 100644 index 000000000..398b17bd5 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SymbolKindExtensions +{ + /// + /// Converts a value to either "field" or "property" based on the kind. + /// + /// The input value. + /// Either "field" or "property" based on . + /// Thrown if is neither nor . + public static string ToFieldOrPropertyKeyword(this SymbolKind kind) + { + return kind switch + { + SymbolKind.Field => "field", + SymbolKind.Property => "property", + _ => throw new ArgumentException($"Unsupported symbol kind '{kind}' for field or property keyword conversion."), + }; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs new file mode 100644 index 000000000..da890724e --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs @@ -0,0 +1,56 @@ +// 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.Immutable; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxKindExtensions +{ + /// + /// Converts an of values to one of their underlying type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray AsUnderlyingType(this ImmutableArray array) + { + ushort[]? underlyingArray = (ushort[]?)(object?)Unsafe.As, SyntaxKind[]?>(ref array); + + return Unsafe.As>(ref underlyingArray); + } + + /// + /// Converts an of values to one of their real type. + /// + /// The input value. + /// The resulting of values. + public static ImmutableArray FromUnderlyingType(this ImmutableArray array) + { + SyntaxKind[]? typedArray = (SyntaxKind[]?)(object?)Unsafe.As, ushort[]?>(ref array); + + return Unsafe.As>(ref typedArray); + } + + /// + /// Converts a value to either "field" or "property" based on the kind. + /// + /// The input value. + /// Either "field" or "property" based on . + /// Thrown if is neither nor . + public static string ToFieldOrPropertyKeyword(this SyntaxKind kind) + { + return kind switch + { + SyntaxKind.FieldDeclaration => "field", + SyntaxKind.PropertyDeclaration => "property", + _ => throw new ArgumentException($"Unsupported syntax kind '{kind}' for field or property keyword conversion."), + }; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs index 52b7ccbfc..c7a08da9d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -30,4 +32,30 @@ public static bool IsFirstSyntaxDeclarationForSymbol(this SyntaxNode syntaxNode, syntaxReference.SyntaxTree == syntaxNode.SyntaxTree && syntaxReference.Span == syntaxNode.Span; } + + /// + /// Checks whether a given is a given type declaration with or potentially with any base types, using only syntax. + /// + /// The type of declaration to check for. + /// The input to check. + /// Whether is a given type declaration with or potentially with any base types. + public static bool IsTypeDeclarationWithOrPotentiallyWithBaseTypes(this SyntaxNode node) + where T : TypeDeclarationSyntax + { + // Immediately bail if the node is not a type declaration of the specified type + if (node is not T typeDeclaration) + { + return false; + } + + // If the base types list is not empty, the type can definitely has implemented interfaces + if (typeDeclaration.BaseList is { Types.Count: > 0 }) + { + return true; + } + + // If the base types list is empty, check if the type is partial. If it is, it means + // that there could be another partial declaration with a non-empty base types list. + return typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword); + } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs new file mode 100644 index 000000000..99c1ecb85 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxTokenListExtensions +{ + /// + /// Checks whether a given value contains any accessibility modifiers. + /// + /// The input value to check. + /// Whether contains any accessibility modifiers. + public static bool ContainsAnyAccessibilityModifiers(this SyntaxTokenList syntaxList) + { + foreach (SyntaxToken token in syntaxList) + { + if (SyntaxFacts.IsAccessibilityModifier(token.Kind())) + { + return true; + } + } + + return false; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs new file mode 100644 index 000000000..18145afa0 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Helpers/ObjectPool{T}.cs @@ -0,0 +1,164 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Ported and adapted from https://github.com/dotnet/roslyn + +using System; +using System.Diagnostics; +using System.Threading; + +#pragma warning disable RS1035, IDE0290 + +namespace Microsoft.CodeAnalysis.PooledObjects; + +/// +/// Generic implementation of object pooling pattern with predefined pool size limit. The main +/// purpose is that limited number of frequently used objects can be kept in the pool for +/// further recycling. +/// +/// Notes: +/// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there +/// is no space in the pool, extra returned objects will be dropped. +/// +/// 2) it is implied that if object was obtained from a pool, the caller will return it back in +/// a relatively short time. Keeping checked out objects for long durations is ok, but +/// reduces usefulness of pooling. Just new up your own. +/// +/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. +/// Rationale: +/// If there is no intent for reusing the object, do not use pool - just use "new". +/// +internal sealed class ObjectPool + where T : class +{ + // Storage for the pool objects. The first item is stored in a dedicated field because we + // expect to be able to satisfy most requests from it. + private T? firstItem; + private readonly Element[] items; + + // The factory is stored for the lifetime of the pool. We will call this only when pool needs to + // expand. compared to "new T()", Func gives more flexibility to implementers and faster + // than "new T()". + private readonly Func factory; + + /// + /// Creates a new instance with a given factory. + /// + /// The factory to use to produce new objects. + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) + { + } + + /// + /// Creates a new instance with a given factory. + /// + /// The factory to use to produce new objects. + /// The size of the pool. + public ObjectPool(Func factory, int size) + { + this.factory = factory; + this.items = new Element[size - 1]; + } + + /// + /// Produces an instance. + /// + /// The instance to return to the pool later. + /// + /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. + /// Note that Free will try to store recycled objects close to the start thus statistically + /// reducing how far we will typically search. + /// + public T Allocate() + { + // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements. + // Note that the initial read is optimistically not synchronized. That is intentional. + // We will interlock only when we have a candidate. in a worst case we may miss some + // recently returned objects. Not a big deal. + T? instance = this.firstItem; + if (instance == null || instance != Interlocked.CompareExchange(ref this.firstItem, null, instance)) + { + instance = AllocateSlow(); + } + + return instance; + } + + /// + /// Slow path to produce a new instance. + /// + /// The instance to return to the pool later. + private T AllocateSlow() + { + Element[] items = this.items; + + for (int i = 0; i < items.Length; i++) + { + T? instance = items[i].Value; + + if (instance is not null) + { + if (instance == Interlocked.CompareExchange(ref items[i].Value, null, instance)) + { + return instance; + } + } + } + + return this.factory(); + } + + /// + /// Returns objects to the pool. + /// + /// The object to return to the pool. + /// + /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. + /// Note that Free will try to store recycled objects close to the start thus statistically + /// reducing how far we will typically search in Allocate. + /// + public void Free(T obj) + { + if (this.firstItem is null) + { + this.firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + /// + /// Slow path to return an object to the pool. + /// + /// The object to return to the pool. + private void FreeSlow(T obj) + { + Element[] items = this.items; + + for (int i = 0; i < items.Length; i++) + { + if (items[i].Value == null) + { + items[i].Value = obj; + + break; + } + } + } + + /// + /// Wrapper to avoid array covariance. + /// + [DebuggerDisplay("{Value,nq}")] + private struct Element + { + /// + /// The value for the current element. + /// + public T? Value; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs index 16ca48c0a..70372a03f 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/Models/CommandInfo.cs @@ -23,8 +23,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.Input.Models; /// Whether or not concurrent executions have been enabled. /// Whether or not exceptions should flow to the task scheduler. /// Whether or not to also generate a cancel command. -/// The sequence of forwarded attributes for the generated field. -/// The sequence of forwarded attributes for the generated property. +/// The sequence of forwarded attributes for the generated members. internal sealed record CommandInfo( string MethodName, string FieldName, @@ -39,5 +38,4 @@ internal sealed record CommandInfo( bool AllowConcurrentExecutions, bool FlowExceptionsToTaskScheduler, bool IncludeCancelCommand, - EquatableArray ForwardedFieldAttributes, - EquatableArray ForwardedPropertyAttributes); + EquatableArray ForwardedAttributes); diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs index e5b753160..d40086135 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Input/RelayCommandGenerator.Execute.cs @@ -141,8 +141,7 @@ public static bool TryGetInfo( semanticModel, token, in builder, - out ImmutableArray fieldAttributes, - out ImmutableArray propertyAttributes); + out ImmutableArray forwardedAttributes); token.ThrowIfCancellationRequested(); @@ -160,8 +159,7 @@ public static bool TryGetInfo( allowConcurrentExecutions, flowExceptionsToTaskScheduler, generateCancelCommand, - fieldAttributes, - propertyAttributes); + forwardedAttributes); diagnostics = builder.ToImmutable(); @@ -196,16 +194,18 @@ public static ImmutableArray GetSyntax(CommandInfo comm : $"{commandInfo.DelegateType}<{string.Join(", ", commandInfo.DelegateTypeArguments)}>"; // Prepare the forwarded field attributes, if any - ImmutableArray forwardedFieldAttributes = - commandInfo.ForwardedFieldAttributes + AttributeListSyntax[] forwardedFieldAttributes = + commandInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.FieldKeyword) .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); + .ToArray(); // Also prepare any forwarded property attributes - ImmutableArray forwardedPropertyAttributes = - commandInfo.ForwardedPropertyAttributes + AttributeListSyntax[] forwardedPropertyAttributes = + commandInfo.ForwardedAttributes + .Where(static a => a.AttributeTarget is SyntaxKind.PropertyKeyword) .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) - .ToImmutableArray(); + .ToArray(); // Construct the generated field as follows: // @@ -225,7 +225,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(RelayCommandGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// The backing field for .")), SyntaxKind.OpenBracketToken, TriviaList()))) - .AddAttributeLists(forwardedFieldAttributes.ToArray()); + .AddAttributeLists(forwardedFieldAttributes); // Prepares the argument to pass the underlying method to invoke using ImmutableArrayBuilder commandCreationArguments = ImmutableArrayBuilder.Rent(); @@ -332,7 +332,7 @@ public static ImmutableArray GetSyntax(CommandInfo comm SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) - .AddAttributeLists(forwardedPropertyAttributes.ToArray()) + .AddAttributeLists(forwardedPropertyAttributes) .WithExpressionBody( ArrowExpressionClause( AssignmentExpression( @@ -972,26 +972,22 @@ private static bool TryGetCanExecuteMemberFromGeneratedProperty( /// The instance for the current run. /// The cancellation token for the current operation. /// The current collection of gathered diagnostics. - /// The resulting field attributes to forward. - /// The resulting property attributes to forward. + /// The resulting attributes to forward. private static void GatherForwardedAttributes( IMethodSymbol methodSymbol, SemanticModel semanticModel, CancellationToken token, in ImmutableArrayBuilder diagnostics, - out ImmutableArray fieldAttributes, - out ImmutableArray propertyAttributes) + out ImmutableArray forwardedAttributes) { - using ImmutableArrayBuilder fieldAttributesInfo = ImmutableArrayBuilder.Rent(); - using ImmutableArrayBuilder propertyAttributesInfo = ImmutableArrayBuilder.Rent(); + using ImmutableArrayBuilder forwardedAttributesInfo = ImmutableArrayBuilder.Rent(); static void GatherForwardedAttributes( IMethodSymbol methodSymbol, SemanticModel semanticModel, CancellationToken token, in ImmutableArrayBuilder diagnostics, - in ImmutableArrayBuilder fieldAttributesInfo, - in ImmutableArrayBuilder propertyAttributesInfo) + in ImmutableArrayBuilder forwardedAttributesInfo) { // Get the single syntax reference for the input method symbol (there should be only one) if (methodSymbol.DeclaringSyntaxReferences is not [SyntaxReference syntaxReference]) @@ -1009,7 +1005,7 @@ static void GatherForwardedAttributes( foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists) { // Same as in the [ObservableProperty] generator, except we're also looking for fields here - if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword)) + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.FieldKeyword) targetIdentifier) { continue; } @@ -1033,7 +1029,7 @@ static void GatherForwardedAttributes( IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); // Try to extract the forwarded attribute - if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out AttributeInfo? attributeInfo)) + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) { diagnostics.Add( InvalidFieldOrPropertyTargetedAttributeExpressionOnRelayCommandMethod, @@ -1044,15 +1040,8 @@ static void GatherForwardedAttributes( continue; } - // Add the new attribute info to the right builder - if (attributeList.Target?.Identifier is SyntaxToken(SyntaxKind.FieldKeyword)) - { - fieldAttributesInfo.Add(attributeInfo); - } - else - { - propertyAttributesInfo.Add(attributeInfo); - } + // Add the new attribute info to the builder + forwardedAttributesInfo.Add(attributeInfo); } } } @@ -1064,17 +1053,16 @@ static void GatherForwardedAttributes( IMethodSymbol partialImplementation = methodSymbol.PartialImplementationPart ?? methodSymbol; // We always give priority to the partial definition, to ensure a predictable and testable ordering - GatherForwardedAttributes(partialDefinition, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo); - GatherForwardedAttributes(partialImplementation, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo); + GatherForwardedAttributes(partialDefinition, semanticModel, token, in diagnostics, in forwardedAttributesInfo); + GatherForwardedAttributes(partialImplementation, semanticModel, token, in diagnostics, in forwardedAttributesInfo); } else { // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications - GatherForwardedAttributes(methodSymbol, semanticModel, token, in diagnostics, in fieldAttributesInfo, in propertyAttributesInfo); + GatherForwardedAttributes(methodSymbol, semanticModel, token, in diagnostics, in forwardedAttributesInfo); } - fieldAttributes = fieldAttributesInfo.ToImmutable(); - propertyAttributes = propertyAttributesInfo.ToImmutable(); + forwardedAttributes = forwardedAttributesInfo.ToImmutable(); } } } diff --git a/src/CommunityToolkit.Mvvm/Collections/ObservableGroupedCollection{TKey,TElement}.cs b/src/CommunityToolkit.Mvvm/Collections/ObservableGroupedCollection{TKey,TElement}.cs index 3879a736d..8b843917e 100644 --- a/src/CommunityToolkit.Mvvm/Collections/ObservableGroupedCollection{TKey,TElement}.cs +++ b/src/CommunityToolkit.Mvvm/Collections/ObservableGroupedCollection{TKey,TElement}.cs @@ -15,7 +15,7 @@ namespace CommunityToolkit.Mvvm.Collections; /// /// The type of the group keys. /// The type of elements in the collection. -public sealed class ObservableGroupedCollection : ObservableCollection>, ILookup +public sealed partial class ObservableGroupedCollection : ObservableCollection>, ILookup where TKey : notnull { /// diff --git a/src/CommunityToolkit.Mvvm/Collections/ReadOnlyObservableGroupedCollection{TKey,TElement}.cs b/src/CommunityToolkit.Mvvm/Collections/ReadOnlyObservableGroupedCollection{TKey,TElement}.cs index e8f9551ca..d730ea7e1 100644 --- a/src/CommunityToolkit.Mvvm/Collections/ReadOnlyObservableGroupedCollection{TKey,TElement}.cs +++ b/src/CommunityToolkit.Mvvm/Collections/ReadOnlyObservableGroupedCollection{TKey,TElement}.cs @@ -17,7 +17,7 @@ namespace CommunityToolkit.Mvvm.Collections; /// /// The type of the group keys. /// The type of elements in the collection. -public sealed class ReadOnlyObservableGroupedCollection : ReadOnlyObservableCollection>, ILookup +public sealed partial class ReadOnlyObservableGroupedCollection : ReadOnlyObservableCollection>, ILookup where TKey : notnull { /// diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets new file mode 100644 index 000000000..fefee0e8e --- /dev/null +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets @@ -0,0 +1,29 @@ + + + + + true + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets new file mode 100644 index 000000000..a8fce2119 --- /dev/null +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.SourceGenerators.targets @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @(MVVMToolkitCurrentCompilerAssemblyIdentity->'%(Version)') + + + true + + + + + + + + + + + + + + true + + + + + + + + + roslyn4.12 + roslyn4.3 + roslyn4.0 + + + + + + + + + + + + + + true + + + + + + + + + + true + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.Windows.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.Windows.targets new file mode 100644 index 000000000..3602b1438 --- /dev/null +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.Windows.targets @@ -0,0 +1,17 @@ + + + + + <_MvvmToolkitIsUsingWindowsRuntimePack>false + <_MvvmToolkitIsUsingWindowsRuntimePack Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0-windows10.0.17763.0'))">true + + + + + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets new file mode 100644 index 000000000..cf89f2def --- /dev/null +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets @@ -0,0 +1,88 @@ + + + + + + true + + + + $(ResolveReferencesDependsOn);MvvmToolkitVerifyWindowsSdkPackageVersion + + + + + + + + + <_MvvmToolkitWindowsSdkPackageMinBuild>38 + <_MvvmToolkitWindowsSdkPackageRecommendedUwpBuild>54 + <_MvvmToolkitWindowsSdkPackageRecommendedWindowsAppSDKBuild>53 + + + <_MvvmToolkitWindowsSdkPackageRecommendedBuild>$(_MvvmToolkitWindowsSdkPackageMinBuild) + <_MvvmToolkitWindowsSdkPackageRecommendedBuild Condition="'$(UseUwp)' == 'true'">$(_MvvmToolkitWindowsSdkPackageRecommendedUwpBuild) + <_MvvmToolkitWindowsSdkPackageRecommendedBuild Condition="'$(UseUwp)' != 'true'">$(_MvvmToolkitWindowsSdkPackageRecommendedWindowsAppSDKBuild) + + + + + <_MvvmToolkitWindowsSdkPackage Include="$(WindowsSdkPackageVersion)" Condition="'$(WindowsSdkPackageVersion)' != ''"> + $(WindowsSdkPackageVersion) + 10.0.$([System.Version]::Parse("$(WindowsSdkPackageVersion.Split('-')[0])").Build).$(_MvvmToolkitWindowsSdkPackageMinBuild) + 10.0.$([System.Version]::Parse("$(WindowsSdkPackageVersion.Split('-')[0])").Build).$(_MvvmToolkitWindowsSdkPackageRecommendedBuild) + + + + <_MvvmToolkitWindowsSdkPackage + Include="@(ResolvedFrameworkReference)" + Condition="'$(WindowsSdkPackageVersion)' == '' AND + '@(ResolvedFrameworkReference)' != '' AND + ('%(Identity)' == 'Microsoft.Windows.SDK.NET.Ref' OR '%(Identity)' == 'Microsoft.Windows.SDK.NET.Ref.Windows')"> + %(ResolvedFrameworkReference.TargetingPackVersion) + 10.0.$([System.Version]::Parse("%(ResolvedFrameworkReference.TargetingPackVersion)").Build).$(_MvvmToolkitWindowsSdkPackageMinBuild) + 10.0.$([System.Version]::Parse("%(ResolvedFrameworkReference.TargetingPackVersion)").Build).$(_MvvmToolkitWindowsSdkPackageRecommendedBuild) + + + + <_MvvmToolkitCompatibleWindowsSdkPackages + Include="@(_MvvmToolkitWindowsSdkPackage)" + Condition="'@(_MvvmToolkitWindowsSdkPackage)' != '' AND $([MSBuild]::VersionGreaterThanOrEquals(%(Referenced), %(Required)))" /> + + + + + <_MvvmToolkitWindowsSdkPackageRequired>@(_MvvmToolkitWindowsSdkPackage->'%(Required)') + <_MvvmToolkitWindowsSdkPackageRecommended>@(_MvvmToolkitWindowsSdkPackage->'%(Recommended)') + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj index 272f56c7f..dd7c98d01 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -1,7 +1,27 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;netstandard2.1;net8.0;net8.0-windows10.0.17763.0 + + + + + 10.0.17763.53 + + + false + + + true + + + 2 @@ -22,23 +42,35 @@ - + - - + + - + + + + + + + + + + + - - - + + + + + @@ -55,6 +87,7 @@ System.Diagnostics.CodeAnalysis.NotNullAttribute; System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute; System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; + System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute; System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute; System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute; System.Runtime.CompilerServices.CallerArgumentExpressionAttribute; @@ -65,11 +98,17 @@ - - - - - + + + + + + + + + + + - - - - + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets index aa4b8966f..394a0a111 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets @@ -1,149 +1,17 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - @(MVVMToolkitCurrentCompilerAssemblyIdentity->'%(Version)') - - - true - - - - - - - - - - - - - - true - - - - - - - - - roslyn4.3 - roslyn4.0 - - - - - - - - - - - - - - true - - - - - - - - - - true - - - - - + + + <_CommunityToolkitMvvmFeatureSwitchesTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.FeatureSwitches.targets + <_CommunityToolkitMvvmSourceGeneratorsTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.SourceGenerators.targets + <_CommunityToolkitMvvmWindowsTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.Windows.targets + <_CommunityToolkitMvvmWindowsSdkTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.WindowsSdk.targets + + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs index 360643eee..08b63b4c6 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs @@ -28,7 +28,7 @@ public sealed class INotifyPropertyChangedAttribute : Attribute { /// /// Gets or sets a value indicating whether or not to also generate all the additional helper methods that are found - /// in as well (eg. . + /// in as well (eg. ). /// If set to , only the event and /// the two overloads will be generated. /// The default value is . diff --git a/src/CommunityToolkit.Mvvm/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 1900b33fe..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,25 +41,37 @@ 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 (!EqualityComparer<string>.Default.Equals(field, value)) /// { +/// OnPropertyChanging(nameof(Name)); +/// OnPropertyChanged(nameof(FullName)); +/// +/// field = value; +/// +/// OnPropertyChanged(nameof(Name)); /// OnPropertyChanged(nameof(FullName)); /// } /// } /// } /// -/// public string Surname +/// public partial string Surname /// { -/// get => surname; +/// get => field; /// set /// { -/// if (SetProperty(ref surname, value)) +/// if (!EqualityComparer<string>.Default.Equals(field, value)) /// { +/// OnPropertyChanging(nameof(Surname)); +/// OnPropertyChanged(nameof(FullName)); +/// +/// field = value; +/// +/// OnPropertyChanged(nameof(Surname)); /// OnPropertyChanged(nameof(FullName)); /// } /// } @@ -67,7 +79,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// } /// /// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)] +/// +/// Just like , this attribute can also be used on fields as well. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] public sealed class NotifyPropertyChangedForAttribute : Attribute { /// diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedRecipientsAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedRecipientsAttribute.cs index f387acd8e..d758bbe17 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedRecipientsAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedRecipientsAttribute.cs @@ -7,7 +7,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// -/// An attribute that can be used to support in generated properties, when applied to fields +/// An attribute that can be used to support in generated properties, when applied to fields and properties /// contained in a type that is either inheriting from , or annotated with . /// When this attribute is used, the generated property setter will also call . /// This allows generated properties to opt-in into broadcasting behavior without having to fallback into a full explicit observable property. @@ -18,7 +18,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// { /// [ObservableProperty] /// [NotifyPropertyChangedRecipients] -/// private string username; +/// public partial string Username; /// } /// /// @@ -27,10 +27,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Username +/// public partial string Username /// { -/// get => username; -/// set => SetProperty(ref username, value, broadcast: true); +/// get => field; +/// set => SetProperty(ref field, value, broadcast: true); /// } /// } /// @@ -39,7 +39,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// This attribute can also be added to a class, and if so it will affect all generated properties in that type and inherited types. /// /// -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +/// +/// Just like , this attribute can also be used on fields as well. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class NotifyPropertyChangedRecipientsAttribute : Attribute { } diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs index 0e765267a..83311270c 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs @@ -8,7 +8,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// -/// An attribute that indicates that a given field should be wrapped by a generated observable property. +/// An attribute that indicates that a given partial property should be implemented by the source generator. /// In order to use this attribute, the containing type has to inherit from , or it /// must be using or . /// If the containing type also implements the (that is, if it either inherits from @@ -20,10 +20,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// partial class MyViewModel : ObservableObject /// { /// [ObservableProperty] -/// private string name; +/// public partial string Name { get; set; } /// /// [ObservableProperty] -/// private bool isEnabled; +/// public partial bool IsEnabled { get; set; } /// } /// /// @@ -31,27 +31,43 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Name +/// public partial string Name /// { -/// get => name; -/// set => SetProperty(ref name, value); +/// get => field; +/// set => SetProperty(ref field, value); /// } /// -/// public bool IsEnabled +/// public partial bool IsEnabled /// { -/// get => isEnabled; -/// set => SetProperty(ref isEnabled, value); +/// get => field; +/// set => SetProperty(ref field, value); /// } /// } /// /// /// +/// +/// In order to use this attribute on partial properties, the .NET 9 SDK is required, and C# preview must +/// be used. If that is not available, this attribute can be used to annotate fields instead, like so: +/// +/// partial class MyViewModel : ObservableObject +/// { +/// [ObservableProperty] +/// private string name; +/// +/// [ObservableProperty] +/// private bool isEnabled; +/// } +/// +/// +/// /// The generated properties will automatically use the UpperCamelCase format for their names, /// which will be derived from the field names. The generator can also recognize fields using either /// the _lowerCamel or m_lowerCamel naming scheme. Otherwise, the first character in the /// source field name will be converted to uppercase (eg. isEnabled to IsEnabled). +/// /// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class ObservablePropertyAttribute : Attribute { } diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableObject.cs b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableObject.cs index f3252a291..895f60311 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableObject.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableObject.cs @@ -57,6 +57,12 @@ protected virtual void OnPropertyChanging(PropertyChangingEventArgs e) { ArgumentNullException.ThrowIfNull(e); + // When support is disabled, just do nothing + if (!FeatureSwitches.EnableINotifyPropertyChangingSupport) + { + return; + } + PropertyChanging?.Invoke(this, e); } @@ -75,7 +81,8 @@ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) /// (optional) The name of the property that changed. protected void OnPropertyChanging([CallerMemberName] string? propertyName = null) { - if (Configuration.IsINotifyPropertyChangingDisabled) + // When support is disabled, avoid instantiating the event args entirely + if (!FeatureSwitches.EnableINotifyPropertyChangingSupport) { return; } diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableRecipient.cs b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableRecipient.cs index 63b28441b..5cd9433e6 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableRecipient.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableRecipient.cs @@ -64,6 +64,12 @@ public bool IsActive "If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " + "path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type. " + "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")] + [RequiresDynamicCode( + "When this property is set to true, the OnActivated() method will be invoked, which will register all necessary message handlers for this recipient. " + + "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " + + "If that is present, the method is AOT safe, as the only methods being invoked to register the messages will be the ones produced by the source generator. " + + "If it isn't, this method will need to dynamically create the generic methods to register messages, which might not be available at runtime. " + + "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")] set { if (SetProperty(ref this.isActive, value, true)) @@ -96,6 +102,11 @@ public bool IsActive "If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " + "path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type. " + "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")] + [RequiresDynamicCode( + "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " + + "If that is present, the method is AOT safe, as the only methods being invoked to register the messages will be the ones produced by the source generator. " + + "If it isn't, this method will need to dynamically create the generic methods to register messages, which might not be available at runtime. " + + "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")] protected virtual void OnActivated() { Messenger.RegisterAll(this); diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableValidator.cs b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableValidator.cs index 02921bf07..6c83c9125 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableValidator.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableValidator.cs @@ -123,6 +123,7 @@ protected ObservableValidator(ValidationContext validationContext) } /// + [Display(AutoGenerateField = false)] public bool HasErrors => this.totalErrors > 0; /// diff --git a/src/CommunityToolkit.Mvvm/DependencyInjection/Ioc.cs b/src/CommunityToolkit.Mvvm/DependencyInjection/Ioc.cs index 3050d27e4..09c1006e0 100644 --- a/src/CommunityToolkit.Mvvm/DependencyInjection/Ioc.cs +++ b/src/CommunityToolkit.Mvvm/DependencyInjection/Ioc.cs @@ -41,7 +41,7 @@ namespace CommunityToolkit.Mvvm.DependencyInjection; /// Ioc.Default.GetService<ILogger>().Log("Hello world!"); /// /// -public sealed class Ioc : IServiceProvider +public sealed partial class Ioc : IServiceProvider { /// /// Gets the default instance. diff --git a/src/CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs b/src/CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs index ed877ed52..e4e13cb89 100644 --- a/src/CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs +++ b/src/CommunityToolkit.Mvvm/Input/AsyncRelayCommand.cs @@ -20,7 +20,7 @@ namespace CommunityToolkit.Mvvm.Input; /// action, and providing an property that notifies changes when /// is invoked and when the returned completes. /// -public sealed class AsyncRelayCommand : IAsyncRelayCommand, ICancellationAwareCommand +public sealed partial class AsyncRelayCommand : IAsyncRelayCommand, ICancellationAwareCommand { /// /// The cached for . diff --git a/src/CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs b/src/CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs index a2b3d7f96..c0e0b3bf7 100644 --- a/src/CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs +++ b/src/CommunityToolkit.Mvvm/Input/AsyncRelayCommand{T}.cs @@ -18,7 +18,7 @@ namespace CommunityToolkit.Mvvm.Input; /// A generic command that provides a more specific version of . /// /// The type of parameter being passed as input to the callbacks. -public sealed class AsyncRelayCommand : IAsyncRelayCommand, ICancellationAwareCommand +public sealed partial class AsyncRelayCommand : IAsyncRelayCommand, ICancellationAwareCommand { /// /// The to invoke when is used. diff --git a/src/CommunityToolkit.Mvvm/Input/Internals/CancelCommand.cs b/src/CommunityToolkit.Mvvm/Input/Internals/CancelCommand.cs index 70f6931ab..547ed1e12 100644 --- a/src/CommunityToolkit.Mvvm/Input/Internals/CancelCommand.cs +++ b/src/CommunityToolkit.Mvvm/Input/Internals/CancelCommand.cs @@ -11,7 +11,7 @@ namespace CommunityToolkit.Mvvm.Input.Internals; /// /// A implementation wrapping to support cancellation. /// -internal sealed class CancelCommand : ICommand +internal sealed partial class CancelCommand : ICommand { /// /// The wrapped instance. diff --git a/src/CommunityToolkit.Mvvm/Input/Internals/DisabledCommand.cs b/src/CommunityToolkit.Mvvm/Input/Internals/DisabledCommand.cs index f7f68f638..4e2f22eb1 100644 --- a/src/CommunityToolkit.Mvvm/Input/Internals/DisabledCommand.cs +++ b/src/CommunityToolkit.Mvvm/Input/Internals/DisabledCommand.cs @@ -10,7 +10,7 @@ namespace CommunityToolkit.Mvvm.Input.Internals; /// /// A reusable instance that is always disabled. /// -internal sealed class DisabledCommand : ICommand +internal sealed partial class DisabledCommand : ICommand { /// public event EventHandler? CanExecuteChanged diff --git a/src/CommunityToolkit.Mvvm/Input/RelayCommand.cs b/src/CommunityToolkit.Mvvm/Input/RelayCommand.cs index 42e0efdfb..cce8a7e00 100644 --- a/src/CommunityToolkit.Mvvm/Input/RelayCommand.cs +++ b/src/CommunityToolkit.Mvvm/Input/RelayCommand.cs @@ -16,7 +16,7 @@ namespace CommunityToolkit.Mvvm.Input; /// method is . This type does not allow you to accept command parameters /// in the and callback methods. /// -public sealed class RelayCommand : IRelayCommand +public sealed partial class RelayCommand : IRelayCommand { /// /// The to invoke when is used. diff --git a/src/CommunityToolkit.Mvvm/Input/RelayCommand{T}.cs b/src/CommunityToolkit.Mvvm/Input/RelayCommand{T}.cs index 66ca947a5..62f7cb9b0 100644 --- a/src/CommunityToolkit.Mvvm/Input/RelayCommand{T}.cs +++ b/src/CommunityToolkit.Mvvm/Input/RelayCommand{T}.cs @@ -18,7 +18,7 @@ namespace CommunityToolkit.Mvvm.Input; /// in the and callback methods. /// /// The type of parameter being passed as input to the callbacks. -public sealed class RelayCommand : IRelayCommand +public sealed partial class RelayCommand : IRelayCommand { /// /// The to invoke when is used. diff --git a/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs b/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs index a007d40eb..72720a899 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs @@ -15,7 +15,7 @@ namespace CommunityToolkit.Mvvm.Messaging; /// /// public sealed class LoginCompletedMessage { } /// -/// Then, register your a recipient for this message: +/// Then, register a recipient for this message: /// /// Messenger.Default.Register<MyRecipientType, LoginCompletedMessage>(this, (r, m) => /// { diff --git a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.Observables.cs b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.Observables.cs index 996e79be3..4675a3193 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.Observables.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.Observables.cs @@ -4,6 +4,14 @@ using System; +// This file has two types which implement interfaces that can be projected to WInRT, ie. +// 'Observable.Recipient' and 'Observable.Recipient', which +// implement 'IDisposable' (which is projected to 'IClosable'). These types are not meant +// to be used in interop scenarios (including in eg. bindings), as they're only meant to +// be used by code behind interacting with System.Reactive APIs. As such, we skip marking +// them partial, as we don't need CCW vtables to be generated for them. +#pragma warning disable CsWinRT1028 + namespace CommunityToolkit.Mvvm.Messaging; /// diff --git a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs index f6f038166..532639c8f 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -90,6 +90,10 @@ public static bool IsRegistered(this IMessenger messenger, object reci "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " + "If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " + "path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type.")] + [RequiresDynamicCode( + "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " + + "If that is present, the method is AOT safe, as the only methods being invoked to register the messages will be the ones produced by the source generator. " + + "If it isn't, this method will need to dynamically create the generic methods to register messages, which might not be available at runtime.")] public static void RegisterAll(this IMessenger messenger, object recipient) { ArgumentNullException.ThrowIfNull(messenger); @@ -113,7 +117,7 @@ public static void RegisterAll(this IMessenger messenger, object recipient) // Try to get the cached delegate, if the generator has run correctly Action? registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue( recipient.GetType(), - [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] static (t) => LoadRegistrationMethodsForType(t)); + LoadRegistrationMethodsForType); if (registrationAction is not null) { @@ -144,6 +148,7 @@ public static void RegisterAll(this IMessenger messenger, object recipient) "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " + "If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " + "path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type.")] + [RequiresDynamicCode("The generic methods to register messages might not be available at runtime.")] public static void RegisterAll(this IMessenger messenger, object recipient, TToken token) where TToken : IEquatable { @@ -156,6 +161,7 @@ public static void RegisterAll(this IMessenger messenger, object recipie // target recipient type, and just invoke it to get the delegate to cache and use later. // In this case we also need to create a generic instantiation of the target method first. [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + [RequiresDynamicCode("The generic methods to register messages might not be available at runtime.")] static Action LoadRegistrationMethodsForType(Type recipientType) { if (recipientType.Assembly.GetType("CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && @@ -173,6 +179,7 @@ static Action LoadRegistrationMethodsForType(Type re // This method is only invoked once per recipient type and token type, so we're not // worried about making it super efficient, and we can use the LINQ code for clarity. // The LINQ codegen bloat is not really important for the same reason. + [RequiresDynamicCode("The generic methods to register messages might not be available at runtime.")] static Action LoadRegistrationMethodsForTypeFallback(Type recipientType) { // Get the collection of validation methods @@ -231,7 +238,7 @@ from registrationMethod in registrationMethods // For more info on this, see the related issue at https://github.com/dotnet/roslyn/issues/5835. Action registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue( recipient.GetType(), - [RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] static (t) => LoadRegistrationMethodsForType(t)); + LoadRegistrationMethodsForType); // Invoke the cached delegate to actually execute the message registration registrationAction(messenger, recipient, token); @@ -428,7 +435,7 @@ public static TMessage Send(this IMessenger messenger, TMessage messag /// The type of token to identify what channel to use to send the message. /// The instance to use to send the message. /// The token indicating what channel to use. - /// The message that has been sen. + /// The message that has been sent. /// /// This method will automatically create a new instance /// just like , and then send it to the right recipients. diff --git a/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs b/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs index c3ac541a7..4e8bbf075 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs @@ -298,7 +298,7 @@ public bool MoveNext() while ((uint)this.index < (uint)this.count) { // We need to preemptively increment the current index so that we still correctly keep track - // of the current position in the dictionary even if the users doesn't access any of the + // of the current position in the dictionary even if the users don't access any of the // available properties in the enumerator. As this is a possibility, we can't rely on one of // them to increment the index before MoveNext is invoked again. We ditch the standard enumerator // API surface here to expose the Key/Value properties directly and minimize the memory copies. diff --git a/src/CommunityToolkit.Mvvm/Properties/Configuration.cs b/src/CommunityToolkit.Mvvm/Properties/Configuration.cs deleted file mode 100644 index d0ca0ade0..000000000 --- a/src/CommunityToolkit.Mvvm/Properties/Configuration.cs +++ /dev/null @@ -1,37 +0,0 @@ -// 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.ComponentModel; - -/// -/// A container for all shared configuration switches for the MVVM Toolkit. -/// -internal static class Configuration -{ - /// - /// The configuration property name for . - /// - private const string DisableINotifyPropertyChangingSupport = "MVVMTOOLKIT_DISABLE_INOTIFYPROPERTYCHANGING"; - - /// - /// Indicates whether or not support for is disabled. - /// - public static readonly bool IsINotifyPropertyChangingDisabled = GetConfigurationValue(DisableINotifyPropertyChangingSupport); - - /// - /// Gets a configuration value for a specified property. - /// - /// The property name to retrieve the value for. - /// The value of the specified configuration setting. - private static bool GetConfigurationValue(string propertyName) - { - if (AppContext.TryGetSwitch(propertyName, out bool isEnabled)) - { - return isEnabled; - } - - return false; - } -} diff --git a/src/CommunityToolkit.Mvvm/Properties/FeatureSwitches.cs b/src/CommunityToolkit.Mvvm/Properties/FeatureSwitches.cs new file mode 100644 index 000000000..816bbcb66 --- /dev/null +++ b/src/CommunityToolkit.Mvvm/Properties/FeatureSwitches.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.Mvvm; + +/// +/// A container for all shared configuration switches for the MVVM Toolkit. +/// +/// +/// +/// This type uses a very specific setup for configuration switches to ensure ILLink can work the best. +/// This mirrors the architecture of feature switches in the runtime as well, and it's needed so that +/// no static constructor is generated for the type. +/// +/// +/// For more info, see . +/// +/// +internal static class FeatureSwitches +{ + /// + /// The configuration property name for . + /// + private const string EnableINotifyPropertyChangingSupportPropertyName = "MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT"; + + /// + /// The backing field for . + /// + private static int enableINotifyPropertyChangingSupport; + + /// + /// Gets a value indicating whether or not support for should be enabled (defaults to ). + /// + public static bool EnableINotifyPropertyChangingSupport + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => GetConfigurationValue(EnableINotifyPropertyChangingSupportPropertyName, ref enableINotifyPropertyChangingSupport, true); + } + + /// + /// Gets a configuration value for a specified property. + /// + /// The property name to retrieve the value for. + /// The cached result for the target configuration value. + /// The default value for the feature switch, if not set. + /// The value of the specified configuration setting. + private static bool GetConfigurationValue(string propertyName, ref int cachedResult, bool defaultValue) + { + // The cached switch value has 3 states: + // 0: unknown. + // 1: true + // -1: false + // + // This method doesn't need to worry about concurrent accesses to the cached result, + // as even if the configuration value is retrieved twice, that'll always be the same. + if (cachedResult < 0) + { + return false; + } + + if (cachedResult > 0) + { + return true; + } + + // Get the configuration switch value, or its default. + // All feature switches have a default set in the .targets file. + if (!AppContext.TryGetSwitch(propertyName, out bool isEnabled)) + { + isEnabled = defaultValue; + } + + // Update the cached result + cachedResult = isEnabled ? 1 : -1; + + return isEnabled; + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/Properties/ILLink.Substitutions.xml b/src/CommunityToolkit.Mvvm/Properties/ILLink.Substitutions.xml new file mode 100644 index 000000000..a934a249c --- /dev/null +++ b/src/CommunityToolkit.Mvvm/Properties/ILLink.Substitutions.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 882db66f3..84e173b48 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ - + all build; analyzers diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index b070ed1b8..7239f4a2e 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -3,16 +3,13 @@ - + NETSTANDARD2_1_OR_GREATER - - - true - true - true - true + + + true + $(NoWarn);MVVMTK0042 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs new file mode 100644 index 000000000..ddeb5ebbf --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.Roslyn4120.UnitTests/Test_ObservablePropertyAttribute_PartialProperties.cs @@ -0,0 +1,1737 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +#if NET6_0_OR_GREATER +using System.Runtime.CompilerServices; +#endif +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ExternalAssembly; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using CommunityToolkit.Mvvm.Messaging.Messages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +#pragma warning disable MVVMTK0032, MVVMTK0033, MVVMTK0034 + +namespace CommunityToolkit.Mvvm.UnitTests; + +// Note: this class is a copy of 'Test_ObservablePropertyAttribute', but using partial properties. +// The two implementations should be kept in sync for all tests, for parity, whenever possible. + +[TestClass] +public partial class Test_ObservablePropertyAttribute_PartialProperties +{ + [TestMethod] + public void Test_ObservablePropertyAttribute_Events() + { + SampleModel model = new(); + + (PropertyChangingEventArgs, int) changing = default; + (PropertyChangedEventArgs, int) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Data); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Data); + }; + + model.Data = 42; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changing.Item2, 0); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changed.Item2, 42); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4225 + [TestMethod] + public void Test_ObservablePropertyAttributeWithinRegion_Events() + { + SampleModel model = new(); + + (PropertyChangingEventArgs, int) changing = default; + (PropertyChangedEventArgs, int) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Counter); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Counter); + }; + + model.Counter = 42; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Counter)); + Assert.AreEqual(changing.Item2, 0); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Counter)); + Assert.AreEqual(changed.Item2, 42); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4225 + [TestMethod] + public void Test_ObservablePropertyAttributeRightBelowRegion_Events() + { + SampleModel model = new(); + + (PropertyChangingEventArgs, string?) changing = default; + (PropertyChangedEventArgs, string?) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Name); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Name); + }; + + model.Name = "Bob"; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Name)); + Assert.AreEqual(changing.Item2, null); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Name)); + Assert.AreEqual(changed.Item2, "Bob"); + } + + [TestMethod] + public void Test_NotifyPropertyChangedForAttribute_Events() + { + DependentPropertyModel model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.Name = "Bob"; + model.Surname = "Ross"; + + CollectionAssert.AreEqual(new[] { nameof(model.Name), nameof(model.FullName), nameof(model.Alias), nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_ValidationAttributes() + { + PropertyInfo nameProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Name))!; + + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 1); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 100); + + PropertyInfo ageProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Age))!; + + Assert.IsNotNull(ageProperty.GetCustomAttribute()); + Assert.AreEqual(ageProperty.GetCustomAttribute()!.Minimum, 0); + Assert.AreEqual(ageProperty.GetCustomAttribute()!.Maximum, 120); + + PropertyInfo emailProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Email))!; + + Assert.IsNotNull(emailProperty.GetCustomAttribute()); + + PropertyInfo comboProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.IfThisWorksThenThatsGreat))!; + + TestValidationAttribute testAttribute = comboProperty.GetCustomAttribute()!; + + Assert.IsNotNull(testAttribute); + Assert.IsNull(testAttribute.O); + Assert.AreEqual(testAttribute.T, typeof(SampleModel)); + Assert.AreEqual(testAttribute.Flag, true); + Assert.AreEqual(testAttribute.D, 6.28); + CollectionAssert.AreEqual(testAttribute.Names, new[] { "Bob", "Ross" }); + + object[]? nestedArray = (object[]?)testAttribute.NestedArray; + + Assert.IsNotNull(nestedArray); + Assert.AreEqual(nestedArray!.Length, 3); + Assert.AreEqual(nestedArray[0], 1); + Assert.AreEqual(nestedArray[1], "Hello"); + Assert.IsTrue(nestedArray[2] is int[]); + CollectionAssert.AreEqual((int[])nestedArray[2], new[] { 2, 3, 4 }); + + Assert.AreEqual(testAttribute.Animal, Animal.Llama); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4216 + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField() + { + ModelWithValueProperty model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.Value = "Hello world"; + + Assert.AreEqual(model.Value, "Hello world"); + + CollectionAssert.AreEqual(new[] { nameof(model.Value) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4216 + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributes() + { + ModelWithValuePropertyWithValidation model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + bool errorsChanged = false; + + model.ErrorsChanged += (s, e) => errorsChanged = true; + + model.Value = "Hello world"; + + Assert.AreEqual(model.Value, "Hello world"); + + // The [NotifyDataErrorInfo] attribute wasn't used, so the property shouldn't be validated + Assert.IsFalse(errorsChanged); + + CollectionAssert.AreEqual(new[] { nameof(model.Value) }, propertyNames); + } + + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributesAndValidation() + { + ModelWithValuePropertyWithAutomaticValidation model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + List errors = new(); + + model.ErrorsChanged += (s, e) => errors.Add(e); + + model.Value = "Bo"; + + Assert.IsTrue(model.HasErrors); + Assert.AreEqual(errors.Count, 1); + Assert.AreEqual(errors[0].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidation.Value)); + + model.Value = "Hello world"; + + Assert.IsFalse(model.HasErrors); + Assert.AreEqual(errors.Count, 2); + Assert.AreEqual(errors[1].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidation.Value)); + } + + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributesAndValidation_WithClassLevelAttribute() + { + ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + List errors = new(); + + model.ErrorsChanged += (s, e) => errors.Add(e); + + model.Value = "Bo"; + + Assert.IsTrue(model.HasErrors); + Assert.AreEqual(errors.Count, 1); + Assert.AreEqual(errors[0].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute.Value)); + + model.Value = "Hello world"; + + Assert.IsFalse(model.HasErrors); + Assert.AreEqual(errors.Count, 2); + Assert.AreEqual(errors[1].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute.Value)); + } + + [TestMethod] + public void Test_ObservablePropertyWithValueNamedField_WithValidationAttributesAndValidation_InheritingClassLevelAttribute() + { + ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + List errors = new(); + + model.ErrorsChanged += (s, e) => errors.Add(e); + + model.Value2 = "Bo"; + + Assert.IsTrue(model.HasErrors); + Assert.AreEqual(errors.Count, 1); + Assert.AreEqual(errors[0].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute.Value2)); + + model.Value2 = "Hello world"; + + Assert.IsFalse(model.HasErrors); + Assert.AreEqual(errors.Count, 2); + Assert.AreEqual(errors[1].PropertyName, nameof(ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute.Value2)); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/4184 + [TestMethod] + public void Test_GeneratedPropertiesWithValidationAttributesOverFields() + { + ViewModelWithValidatableGeneratedProperties model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + // Assign these fields directly to bypass the validation that is executed in the generated setters. + // We only need those generated properties to be there to check whether they are correctly detected. + model.First = "A"; + model.Last = "This is a very long name that exceeds the maximum length of 60 for this property"; + + Assert.IsFalse(model.HasErrors); + + model.RunValidation(); + + Assert.IsTrue(model.HasErrors); + + ValidationResult[] validationErrors = model.GetErrors().ToArray(); + + Assert.AreEqual(validationErrors.Length, 2); + + CollectionAssert.AreEqual(new[] { nameof(ViewModelWithValidatableGeneratedProperties.First) }, validationErrors[0].MemberNames.ToArray()); + CollectionAssert.AreEqual(new[] { nameof(ViewModelWithValidatableGeneratedProperties.Last) }, validationErrors[1].MemberNames.ToArray()); + } + + [TestMethod] + public void Test_NotifyPropertyChangedFor() + { + DependentPropertyModel model = new(); + + List propertyNames = new(); + int canExecuteRequests = 0; + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; + + model.Surname = "Ross"; + + Assert.AreEqual(1, canExecuteRequests); + + CollectionAssert.AreEqual(new[] { nameof(model.Surname), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_NotifyPropertyChangedFor_GeneratedCommand() + { + DependentPropertyModel2 model = new(); + + List propertyNames = new(); + int canExecuteRequests = 0; + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.TestFromMethodCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; + + model.Text = "Ross"; + + Assert.AreEqual(1, canExecuteRequests); + + CollectionAssert.AreEqual(new[] { nameof(model.Text), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_NotifyPropertyChangedFor_IRelayCommandProperty() + { + DependentPropertyModel3 model = new(); + + List propertyNames = new(); + int canExecuteRequests = 0; + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; + + model.Text = "Ross"; + + Assert.AreEqual(1, canExecuteRequests); + + CollectionAssert.AreEqual(new[] { nameof(model.Text), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_NotifyPropertyChangedFor_IAsyncRelayCommandOfTProperty() + { + DependentPropertyModel4 model = new(); + + List propertyNames = new(); + int canExecuteRequests = 0; + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.MyCommand.CanExecuteChanged += (s, e) => canExecuteRequests++; + + model.Text = "Ross"; + + Assert.AreEqual(1, canExecuteRequests); + + CollectionAssert.AreEqual(new[] { nameof(model.Text), nameof(model.FullName), nameof(model.Alias) }, propertyNames); + } + + [TestMethod] + public void Test_OnPropertyChangingAndChangedPartialMethods() + { + ViewModelWithImplementedUpdateMethods model = new(); + + model.Name = nameof(Test_OnPropertyChangingAndChangedPartialMethods); + + Assert.AreEqual(nameof(Test_OnPropertyChangingAndChangedPartialMethods), model.NameChangingValue); + Assert.AreEqual(nameof(Test_OnPropertyChangingAndChangedPartialMethods), model.NameChangedValue); + + model.Number = 99; + + Assert.AreEqual(99, model.NumberChangedValue); + } + + [TestMethod] + public void Test_OnPropertyChangingAndChangedPartialMethods_WithPreviousValues() + { + ViewModelWithImplementedUpdateMethods2 model = new(); + + Assert.AreEqual(null, model.Name); + Assert.AreEqual(0, model.Number); + + CollectionAssert.AreEqual(Array.Empty<(string, string)>(), model.OnNameChangingValues); + CollectionAssert.AreEqual(Array.Empty<(string, string)>(), model.OnNameChangedValues); + CollectionAssert.AreEqual(Array.Empty<(int, int)>(), model.OnNumberChangingValues); + CollectionAssert.AreEqual(Array.Empty<(int, int)>(), model.OnNumberChangedValues); + + model.Name = "Bob"; + + CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangingValues); + CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangedValues); + + Assert.AreEqual("Bob", model.Name); + + CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangingValues); + CollectionAssert.AreEqual(new[] { ((string?)null, "Bob") }, model.OnNameChangedValues); + + model.Name = "Alice"; + + CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangingValues); + CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangedValues); + + Assert.AreEqual("Alice", model.Name); + + CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangingValues); + CollectionAssert.AreEqual(new[] { (null, "Bob"), ("Bob", "Alice") }, model.OnNameChangedValues); + + model.Number = 42; + + CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangingValues); + CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangedValues); + + Assert.AreEqual(42, model.Number); + + CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangingValues); + CollectionAssert.AreEqual(new[] { (0, 42) }, model.OnNumberChangedValues); + + model.Number = 77; + + CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangingValues); + CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangedValues); + + Assert.AreEqual(77, model.Number); + + CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangingValues); + CollectionAssert.AreEqual(new[] { (0, 42), (42, 77) }, model.OnNumberChangedValues); + } + + [TestMethod] + public void Test_OnPropertyChangingAndChangedPartialMethodWithAdditionalValidation() + { + ViewModelWithImplementedUpdateMethodAndAdditionalValidation model = new(); + + // The actual validation is performed inside the model itself. + // This test validates that the order with which methods/events are generated is: + // - OnChanging(value); + // - OnPropertyChanging(); + // - field = value; + // - OnChanged(value); + // - OnPropertyChanged(); + model.Name = "B"; + + Assert.AreEqual("B", model.Name); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithObservableObject() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModel(messenger), + setter: static (model, value) => model.Name = value, + propertyName: nameof(BroadcastingViewModel.Name)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithObservableRecipientAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithAttribute(messenger), + setter: static (model, value) => model.Name = value, + propertyName: nameof(BroadcastingViewModelWithAttribute.Name)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithInheritedObservableRecipientAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithInheritedAttribute(messenger), + setter: static (model, value) => model.Name2 = value, + propertyName: nameof(BroadcastingViewModelWithInheritedAttribute.Name2)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithObservableObject_WithClassLevelAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithClassLevelAttribute(messenger), + setter: static (model, value) => model.Name = value, + propertyName: nameof(BroadcastingViewModelWithClassLevelAttribute.Name)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithObservableRecipientAttribute_WithClassLevelAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithAttributeAndClassLevelAttribute(messenger), + setter: static (model, value) => model.Name = value, + propertyName: nameof(BroadcastingViewModelWithAttributeAndClassLevelAttribute.Name)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithInheritedObservableRecipientAttribute_WithClassLevelAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithInheritedClassLevelAttribute(messenger), + setter: static (model, value) => model.Name2 = value, + propertyName: nameof(BroadcastingViewModelWithInheritedClassLevelAttribute.Name2)); + } + + [TestMethod] + public void Test_NotifyPropertyChangedRecipients_WithInheritedObservableRecipientAttributeAndClassLevelAttribute() + { + Test_NotifyPropertyChangedRecipients_Test( + factory: static messenger => new BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute(messenger), + setter: static (model, value) => model.Name2 = value, + propertyName: nameof(BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute.Name2)); + } + + private void Test_NotifyPropertyChangedRecipients_Test(Func factory, Action setter, string propertyName) + where T : notnull + { + IMessenger messenger = new StrongReferenceMessenger(); + + T model = factory(messenger); + + List<(object Sender, PropertyChangedMessage Message)> messages = new(); + + messenger.Register>(model, (r, m) => messages.Add((r, m))); + + setter(model, "Bob"); + + Assert.AreEqual(1, messages.Count); + Assert.AreSame(model, messages[0].Sender); + Assert.AreEqual(null, messages[0].Message.OldValue); + Assert.AreEqual("Bob", messages[0].Message.NewValue); + Assert.AreEqual(propertyName, messages[0].Message.PropertyName); + + setter(model, "Ross"); + + Assert.AreEqual(2, messages.Count); + Assert.AreSame(model, messages[1].Sender); + Assert.AreEqual("Bob", messages[1].Message.OldValue); + Assert.AreEqual("Ross", messages[1].Message.NewValue); + Assert.AreEqual(propertyName, messages[0].Message.PropertyName); + } + + [TestMethod] + public void Test_ObservableProperty_ObservableRecipientDoesNotBroadcastByDefault() + { + IMessenger messenger = new StrongReferenceMessenger(); + RecipientWithNonBroadcastingProperty model = new(messenger); + + List<(object Sender, PropertyChangedMessage Message)> messages = new(); + + messenger.Register>(model, (r, m) => messages.Add((r, m))); + + model.Name = "Bob"; + model.Name = "Alice"; + model.Name = null; + + // The [NotifyPropertyChangedRecipients] attribute wasn't used, so no messages should have been sent + Assert.AreEqual(messages.Count, 0); + } + +#if NET6_0_OR_GREATER + // See https://github.com/CommunityToolkit/dotnet/issues/155 + [TestMethod] + public void Test_ObservableProperty_NullabilityAnnotations_Simple() + { + // List? + NullabilityInfoContext context = new(); + NullabilityInfo info = context.Create(typeof(NullableRepro).GetProperty(nameof(NullableRepro.NullableList))!); + + Assert.AreEqual(typeof(List), info.Type); + Assert.AreEqual(NullabilityState.Nullable, info.ReadState); + Assert.AreEqual(NullabilityState.Nullable, info.WriteState); + Assert.AreEqual(1, info.GenericTypeArguments.Length); + + NullabilityInfo elementInfo = info.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(string), elementInfo.Type); + Assert.AreEqual(NullabilityState.Nullable, elementInfo.ReadState); + Assert.AreEqual(NullabilityState.Nullable, elementInfo.WriteState); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/155 + [TestMethod] + public void Test_ObservableProperty_NullabilityAnnotations_Complex() + { + // Foo.Bar?, StrongBox.Bar?>?>? + NullabilityInfoContext context = new(); + NullabilityInfo info = context.Create(typeof(NullableRepro).GetProperty(nameof(NullableRepro.NullableMess))!); + + Assert.AreEqual(typeof(Foo.Bar?, StrongBox.Bar?>?>), info.Type); + Assert.AreEqual(NullabilityState.Nullable, info.ReadState); + Assert.AreEqual(NullabilityState.Nullable, info.WriteState); + Assert.AreEqual(2, info.GenericTypeArguments.Length); + + NullabilityInfo leftInfo = info.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(Foo.Bar), leftInfo.Type); + Assert.AreEqual(NullabilityState.Nullable, leftInfo.ReadState); + Assert.AreEqual(NullabilityState.Nullable, leftInfo.WriteState); + Assert.AreEqual(3, leftInfo.GenericTypeArguments.Length); + + NullabilityInfo leftInfo0 = leftInfo.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(string), leftInfo0.Type); + Assert.AreEqual(NullabilityState.Nullable, leftInfo0.ReadState); + Assert.AreEqual(NullabilityState.Nullable, leftInfo0.WriteState); + + NullabilityInfo leftInfo1 = leftInfo.GenericTypeArguments[1]; + + Assert.AreEqual(typeof(int), leftInfo1.Type); + Assert.AreEqual(NullabilityState.NotNull, leftInfo1.ReadState); + Assert.AreEqual(NullabilityState.NotNull, leftInfo1.WriteState); + + NullabilityInfo leftInfo2 = leftInfo.GenericTypeArguments[2]; + + Assert.AreEqual(typeof(object), leftInfo2.Type); + Assert.AreEqual(NullabilityState.Nullable, leftInfo2.ReadState); + Assert.AreEqual(NullabilityState.Nullable, leftInfo2.WriteState); + + NullabilityInfo rightInfo = info.GenericTypeArguments[1]; + + Assert.AreEqual(typeof(StrongBox.Bar?>), rightInfo.Type); + Assert.AreEqual(NullabilityState.Nullable, rightInfo.ReadState); + Assert.AreEqual(NullabilityState.Nullable, rightInfo.WriteState); + Assert.AreEqual(1, rightInfo.GenericTypeArguments.Length); + + NullabilityInfo rightInnerInfo = rightInfo.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(Foo.Bar), rightInnerInfo.Type); + Assert.AreEqual(NullabilityState.Nullable, rightInnerInfo.ReadState); + Assert.AreEqual(NullabilityState.Nullable, rightInnerInfo.WriteState); + Assert.AreEqual(3, rightInnerInfo.GenericTypeArguments.Length); + + NullabilityInfo rightInfo0 = rightInnerInfo.GenericTypeArguments[0]; + + Assert.AreEqual(typeof(int), rightInfo0.Type); + Assert.AreEqual(NullabilityState.NotNull, rightInfo0.ReadState); + Assert.AreEqual(NullabilityState.NotNull, rightInfo0.WriteState); + + NullabilityInfo rightInfo1 = rightInnerInfo.GenericTypeArguments[1]; + + Assert.AreEqual(typeof(string), rightInfo1.Type); + Assert.AreEqual(NullabilityState.Nullable, rightInfo1.ReadState); + Assert.AreEqual(NullabilityState.Nullable, rightInfo1.WriteState); + + NullabilityInfo rightInfo2 = rightInnerInfo.GenericTypeArguments[2]; + + Assert.AreEqual(typeof(object), rightInfo2.Type); + Assert.AreEqual(NullabilityState.NotNull, rightInfo2.ReadState); + Assert.AreEqual(NullabilityState.NotNull, rightInfo2.WriteState); + } +#endif + + // See https://github.com/CommunityToolkit/dotnet/issues/201 + [TestMethod] + public void Test_ObservableProperty_InheritedMembersAsAttributeTargets() + { + ConcreteViewModel model = new(); + + List propertyNames = new(); + List canExecuteChangedArgs = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + model.DoSomethingCommand.CanExecuteChanged += (s, _) => canExecuteChangedArgs.Add(s); + model.ManualCommand.CanExecuteChanged += (s, _) => canExecuteChangedArgs.Add(s); + + model.A = nameof(model.A); + model.B = nameof(model.B); + model.C = nameof(model.C); + model.D = nameof(model.D); + + CollectionAssert.AreEqual(new[] + { + nameof(model.A), + nameof(model.Content), + nameof(model.B), + nameof(model.SomeGeneratedProperty), + nameof(model.C), + nameof(model.D) + }, propertyNames); + + CollectionAssert.AreEqual(new[] { model.DoSomethingCommand, model.ManualCommand }, canExecuteChangedArgs); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/224 + [TestMethod] + public void Test_ObservableProperty_WithinGenericTypeWithMultipleTypeParameters() + { + ModelWithMultipleGenericParameters model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.Value = true; + model.TValue = 42; + model.UValue = "Hello"; + model.List = new List() { 420 }; + + Assert.AreEqual(model.Value, true); + Assert.AreEqual(model.TValue, 42); + Assert.AreEqual(model.UValue, "Hello"); + CollectionAssert.AreEqual(new[] { 420 }, model.List); + + CollectionAssert.AreEqual(new[] { nameof(model.Value), nameof(model.TValue), nameof(model.UValue), nameof(model.List) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/222 + [TestMethod] + public void Test_ObservableProperty_WithBaseViewModelWithObservableObjectAttributeInAnotherAssembly() + { + ModelWithObservablePropertyAndBaseClassInAnotherAssembly model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + Assert.AreEqual(model.OtherProperty, "Ok"); + + model.MyProperty = "A"; + model.OtherProperty = "B"; + + Assert.AreEqual(model.MyProperty, "A"); + Assert.AreEqual(model.OtherProperty, "B"); + + CollectionAssert.AreEqual(new[] { nameof(model.MyProperty), nameof(model.OtherProperty) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/230 + [TestMethod] + public void Test_ObservableProperty_ModelWithCultureAwarePropertyName() + { + ModelWithCultureAwarePropertyName model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.InputFolder = 42; + + Assert.AreEqual(model.InputFolder, 42); + + CollectionAssert.AreEqual(new[] { nameof(model.InputFolder) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/242 + [TestMethod] + public void Test_ObservableProperty_ModelWithNotifyPropertyChangedRecipientsAndDisplayAttributeLast() + { + IMessenger messenger = new StrongReferenceMessenger(); + ModelWithNotifyPropertyChangedRecipientsAndDisplayAttributeLast model = new(messenger); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + object newValue = new(); + bool isMessageReceived = false; + + messenger.Register>(this, (r, m) => + { + if (m.Sender != model) + { + Assert.Fail(); + } + + if (m.NewValue != newValue) + { + Assert.Fail(); + } + + isMessageReceived = true; + }); + + model.SomeProperty = newValue; + + Assert.AreEqual(model.SomeProperty, newValue); + Assert.IsTrue(isMessageReceived); + + CollectionAssert.AreEqual(new[] { nameof(model.SomeProperty) }, propertyNames); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/257 + [TestMethod] + public void Test_ObservableProperty_InheritedModelWithCommandUsingInheritedObservablePropertyForCanExecute() + { + InheritedModelWithCommandUsingInheritedObservablePropertyForCanExecute model = new(); + + Assert.IsFalse(model.SaveCommand.CanExecute(null)); + + model.CanSave = true; + + Assert.IsTrue(model.SaveCommand.CanExecute(null)); + } + + [TestMethod] + public void Test_ObservableProperty_ForwardsSpecialCasesDataAnnotationAttributes() + { + PropertyInfo propertyInfo = typeof(ModelWithAdditionalDataAnnotationAttributes).GetProperty(nameof(ModelWithAdditionalDataAnnotationAttributes.Name))!; + + DisplayAttribute? displayAttribute = (DisplayAttribute?)propertyInfo.GetCustomAttribute(typeof(DisplayAttribute)); + + Assert.IsNotNull(displayAttribute); + Assert.AreEqual(displayAttribute!.Name, "MyProperty"); + Assert.AreEqual(displayAttribute.ResourceType, typeof(List)); + Assert.AreEqual(displayAttribute.Prompt, "Foo bar baz"); + + KeyAttribute? keyAttribute = (KeyAttribute?)propertyInfo.GetCustomAttribute(typeof(KeyAttribute)); + + Assert.IsNotNull(keyAttribute); + + EditableAttribute? editableAttribute = (EditableAttribute?)propertyInfo.GetCustomAttribute(typeof(EditableAttribute)); + + Assert.IsNotNull(keyAttribute); + Assert.IsTrue(editableAttribute!.AllowEdit); + + UIHintAttribute? uiHintAttribute = (UIHintAttribute?)propertyInfo.GetCustomAttribute(typeof(UIHintAttribute)); + + Assert.IsNotNull(uiHintAttribute); + Assert.AreEqual(uiHintAttribute!.UIHint, "MyControl"); + Assert.AreEqual(uiHintAttribute.PresentationLayer, "WPF"); + Assert.AreEqual(uiHintAttribute.ControlParameters.Count, 3); + Assert.IsTrue(uiHintAttribute.ControlParameters.ContainsKey("Foo")); + Assert.IsTrue(uiHintAttribute.ControlParameters.ContainsKey("Bar")); + Assert.IsTrue(uiHintAttribute.ControlParameters.ContainsKey("Baz")); + Assert.AreEqual(uiHintAttribute.ControlParameters["Foo"], 42); + Assert.AreEqual(uiHintAttribute.ControlParameters["Bar"], 3.14); + Assert.AreEqual(uiHintAttribute.ControlParameters["Baz"], "Hello"); + + ScaffoldColumnAttribute? scaffoldColumnAttribute = (ScaffoldColumnAttribute?)propertyInfo.GetCustomAttribute(typeof(ScaffoldColumnAttribute)); + + Assert.IsNotNull(scaffoldColumnAttribute); + Assert.IsTrue(scaffoldColumnAttribute!.Scaffold); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/271 + [TestMethod] + public void Test_ObservableProperty_ModelWithObservablePropertyInRootNamespace() + { + ModelWithObservablePropertyInRootNamespace model = new(); + + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.Number = 3.14f; + + // We mostly just need to verify this class compiles fine with the right generated code + CollectionAssert.AreEqual(propertyNames, new[] { nameof(model.Number) }); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/272 + [TestMethod] + public void Test_ObservableProperty_WithCommandReferencingGeneratedPropertyFromOtherAssembly() + { + ModelWithOverriddenCommandMethodFromExternalBaseModel model = new(); + + Assert.IsFalse(model.HasSaved); + Assert.IsFalse(model.SaveCommand.CanExecute(null)); + + model.CanSave = true; + + Assert.IsTrue(model.SaveCommand.CanExecute(null)); + + model.SaveCommand.Execute(null); + + Assert.IsTrue(model.HasSaved); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/413 + [TestMethod] + public void Test_ObservableProperty_WithExplicitAttributeForProperty() + { + PropertyInfo nameProperty = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.Name))!; + + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 1); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 100); + + PropertyInfo lastNameProperty = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.LastName))!; + + Assert.IsNotNull(lastNameProperty.GetCustomAttribute()); + Assert.AreEqual(lastNameProperty.GetCustomAttribute()!.Name, "lastName"); + Assert.IsNotNull(lastNameProperty.GetCustomAttribute()); + + PropertyInfo justOneSimpleAttributeProperty = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.JustOneSimpleAttribute))!; + + Assert.IsNotNull(justOneSimpleAttributeProperty.GetCustomAttribute()); + + PropertyInfo someComplexValidationAttributeProperty = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.SomeComplexValidationAttribute))!; + + TestValidationAttribute testAttribute = someComplexValidationAttributeProperty.GetCustomAttribute()!; + + Assert.IsNotNull(testAttribute); + Assert.IsNull(testAttribute.O); + Assert.AreEqual(testAttribute.T, typeof(MyViewModelWithExplicitPropertyAttributes)); + Assert.AreEqual(testAttribute.Flag, true); + Assert.AreEqual(testAttribute.D, 6.28); + CollectionAssert.AreEqual(testAttribute.Names, new[] { "Bob", "Ross" }); + + object[]? nestedArray = (object[]?)testAttribute.NestedArray; + + Assert.IsNotNull(nestedArray); + Assert.AreEqual(nestedArray!.Length, 3); + Assert.AreEqual(nestedArray[0], 1); + Assert.AreEqual(nestedArray[1], "Hello"); + Assert.IsTrue(nestedArray[2] is int[]); + CollectionAssert.AreEqual((int[])nestedArray[2], new[] { 2, 3, 4 }); + + Assert.AreEqual(testAttribute.Animal, Animal.Llama); + + PropertyInfo someComplexRandomAttribute = typeof(MyViewModelWithExplicitPropertyAttributes).GetProperty(nameof(MyViewModelWithExplicitPropertyAttributes.SomeComplexRandomAttribute))!; + + Assert.IsNotNull(someComplexRandomAttribute.GetCustomAttribute()); + + PropertyInfoAttribute testAttribute2 = someComplexRandomAttribute.GetCustomAttribute()!; + + Assert.IsNotNull(testAttribute2); + Assert.IsNull(testAttribute2.O); + Assert.AreEqual(testAttribute2.T, typeof(MyViewModelWithExplicitPropertyAttributes)); + Assert.AreEqual(testAttribute2.Flag, true); + Assert.AreEqual(testAttribute2.D, 6.28); + Assert.IsNotNull(testAttribute2.Objects); + Assert.IsTrue(testAttribute2.Objects is object[]); + Assert.AreEqual(((object[])testAttribute2.Objects).Length, 1); + Assert.AreEqual(((object[])testAttribute2.Objects)[0], "Test"); + CollectionAssert.AreEqual(testAttribute2.Names, new[] { "Bob", "Ross" }); + + object[]? nestedArray2 = (object[]?)testAttribute2.NestedArray; + + Assert.IsNotNull(nestedArray2); + Assert.AreEqual(nestedArray2!.Length, 4); + Assert.AreEqual(nestedArray2[0], 1); + Assert.AreEqual(nestedArray2[1], "Hello"); + Assert.AreEqual(nestedArray2[2], 42); + Assert.IsNull(nestedArray2[3]); + + Assert.AreEqual(testAttribute2.Animal, (Animal)67); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/375 + [TestMethod] + public void Test_ObservableProperty_ModelWithObservablePropertyWithUnderscoreAndUppercase() + { + ModelWithObservablePropertyWithUnderscoreAndUppercase model = new(); + + Assert.IsFalse(model.IsReadOnly); + + // Just ensures this builds and the property is generated with the expected name + model.IsReadOnly = true; + + Assert.IsTrue(model.IsReadOnly); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void Test_ObservableProperty_ModelWithDependentPropertyAndPropertyChanging() + { + ModelWithDependentPropertyAndPropertyChanging model = new(); + + List changingArgs = new(); + List changedArgs = new(); + + model.PropertyChanging += (s, e) => changingArgs.Add(e.PropertyName); + model.PropertyChanged += (s, e) => changedArgs.Add(e.PropertyName); + + model.Name = "Bob"; + + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndPropertyChanging.Name), nameof(ModelWithDependentPropertyAndPropertyChanging.FullName) }, changingArgs); + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndPropertyChanging.Name), nameof(ModelWithDependentPropertyAndPropertyChanging.FullName) }, changedArgs); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void Test_ObservableProperty_ModelWithDependentPropertyAndNoPropertyChanging() + { + ModelWithDependentPropertyAndNoPropertyChanging model = new(); + + List changedArgs = new(); + + model.PropertyChanged += (s, e) => changedArgs.Add(e.PropertyName); + + model.Name = "Alice"; + + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndNoPropertyChanging.Name), nameof(ModelWithDependentPropertyAndNoPropertyChanging.FullName) }, changedArgs); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/731 + [TestMethod] + public void Test_ObservableProperty_ForwardedAttributesWithNegativeValues() + { + Assert.AreEqual(PositiveEnum.Something, + typeof(ModelWithForwardedAttributesWithNegativeValues) + .GetProperty(nameof(ModelWithForwardedAttributesWithNegativeValues.Test2))! + .GetCustomAttribute()! + .Value); + + Assert.AreEqual(NegativeEnum.Problem, + typeof(ModelWithForwardedAttributesWithNegativeValues) + .GetProperty(nameof(ModelWithForwardedAttributesWithNegativeValues.Test3))! + .GetCustomAttribute()! + .Value); + } + + public abstract partial class BaseViewModel : ObservableObject + { + public string? Content { get; set; } + + [ObservableProperty] + public partial string? SomeGeneratedProperty { get; set; } + + [RelayCommand] + private void DoSomething() + { + } + + public IRelayCommand ManualCommand { get; } = new RelayCommand(() => { }); + } + + public partial class ConcreteViewModel : BaseViewModel + { + // Inherited property + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(Content))] + public partial string? A { get; set; } + + // Inherited generated property + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(SomeGeneratedProperty))] + public partial string? B { get; set; } + + // Inherited generated command + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(DoSomethingCommand))] + public partial string? C { get; set; } + + // Inherited manual command + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(ManualCommand))] + public partial string? D { get; set; } + } + + public partial class SampleModel : ObservableObject + { + /// + /// This is a sample data field within of type . + /// + [ObservableProperty] + public partial int Data { get; set; } + + #region More properties + + [ObservableProperty] + public partial int Counter { get; set; } + + #endregion + + [ObservableProperty] + public partial string? Name { get; set; } + } + + [INotifyPropertyChanged] + public sealed partial class DependentPropertyModel + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + [NotifyPropertyChangedFor(nameof(Alias))] + public partial string? Name { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName), nameof(Alias))] + [NotifyCanExecuteChangedFor(nameof(MyCommand))] + public partial string? Surname { get; set; } + + public string FullName => $"{Name} {Surname}"; + + public string Alias => $"{Name?.ToLower()}{Surname?.ToLower()}"; + + public RelayCommand MyCommand { get; } = new(() => { }); + } + + [INotifyPropertyChanged] + public sealed partial class DependentPropertyModel2 + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName), nameof(Alias))] + [NotifyCanExecuteChangedFor(nameof(TestFromMethodCommand))] + public partial string? Text { get; set; } + + public string FullName => ""; + + public string Alias => ""; + + public RelayCommand MyCommand { get; } = new(() => { }); + + [RelayCommand] + private void TestFromMethod() + { + } + } + + [INotifyPropertyChanged] + public sealed partial class DependentPropertyModel3 + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName), nameof(Alias))] + [NotifyCanExecuteChangedFor(nameof(MyCommand))] + public partial string? Text { get; set; } + + public string FullName => ""; + + public string Alias => ""; + + public IRelayCommand MyCommand { get; } = new RelayCommand(() => { }); + } + + [INotifyPropertyChanged] + public sealed partial class DependentPropertyModel4 + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName), nameof(Alias))] + [NotifyCanExecuteChangedFor(nameof(MyCommand))] + public partial string? Text { get; set; } + + public string FullName => ""; + + public string Alias => ""; + + public IAsyncRelayCommand MyCommand { get; } = new AsyncRelayCommand(_ => Task.CompletedTask); + } + + public partial class MyFormViewModel : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(1)] + [MaxLength(100)] + public partial string? Name { get; set; } + + [ObservableProperty] + [Range(0, 120)] + public partial int Age { get; set; } + + [ObservableProperty] + [EmailAddress] + public partial string? Email { get; set; } + + [ObservableProperty] + [TestValidation(null, typeof(SampleModel), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Animal.Llama)] + public partial int IfThisWorksThenThatsGreat { get; set; } + } + + private sealed class TestValidationAttribute : ValidationAttribute + { + public TestValidationAttribute(object? o, Type t, bool flag, double d, string[] names) + { + O = o; + T = t; + Flag = flag; + D = d; + Names = names; + } + + public object? O { get; } + + public Type T { get; } + + public bool Flag { get; } + + public double D { get; } + + public string[] Names { get; } + + public object? NestedArray { get; set; } + + public Animal Animal { get; set; } + } + + public enum Animal + { + Cat, + Dog, + Llama + } + + public partial class ModelWithValueProperty : ObservableObject + { + [ObservableProperty] + public partial string? Value { get; set; } + } + + public partial class ModelWithValuePropertyWithValidation : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(5)] + public partial string? Value { get; set; } + } + + public partial class ModelWithValuePropertyWithAutomaticValidation : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(5)] + [NotifyDataErrorInfo] + public partial string? Value { get; set; } + } + + [NotifyDataErrorInfo] + public partial class ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(5)] + public partial string? Value { get; set; } + } + + public partial class ModelWithValuePropertyWithAutomaticValidationInheritingClassLevelAttribute : ModelWithValuePropertyWithAutomaticValidationWithClassLevelAttribute + { + [ObservableProperty] + [Required] + [MinLength(5)] + public partial string? Value2 { get; set; } + } + + public partial class ViewModelWithValidatableGeneratedProperties : ObservableValidator + { + [Required] + [MinLength(2)] + [MaxLength(60)] + [Display(Name = "FirstName")] + [ObservableProperty] + public partial string First { get; set; } = "Bob"; + + [Display(Name = "LastName")] + [Required] + [MinLength(2)] + [MaxLength(60)] + [ObservableProperty] + public partial string Last { get; set; } = "Jones"; + + public void RunValidation() => ValidateAllProperties(); + } + + public partial class ViewModelWithImplementedUpdateMethods : ObservableObject + { + [ObservableProperty] + public partial string? Name { get; set; } = "Bob"; + + [ObservableProperty] + public partial int Number { get; set; } = 42; + + public string? NameChangingValue { get; private set; } + + public string? NameChangedValue { get; private set; } + + public int NumberChangedValue { get; private set; } + + partial void OnNameChanging(string? value) + { + NameChangingValue = value; + } + + partial void OnNameChanged(string? value) + { + NameChangedValue = value; + } + + partial void OnNumberChanged(int value) + { + NumberChangedValue = value; + } + } + + public partial class ViewModelWithImplementedUpdateMethods2 : ObservableObject + { + [ObservableProperty] + public partial string? Name { get; set; } + + [ObservableProperty] + public partial int Number { get; set; } + + public List<(string? Old, string? New)> OnNameChangingValues { get; } = new(); + + public List<(string? Old, string? New)> OnNameChangedValues { get; } = new(); + + public List<(int Old, int New)> OnNumberChangingValues { get; } = new(); + + public List<(int Old, int New)> OnNumberChangedValues { get; } = new(); + + partial void OnNameChanging(string? oldValue, string? newValue) + { + OnNameChangingValues.Add((oldValue, newValue)); + } + + partial void OnNameChanged(string? oldValue, string? newValue) + { + OnNameChangedValues.Add((oldValue, newValue)); + } + + partial void OnNumberChanging(int oldValue, int newValue) + { + OnNumberChangingValues.Add((oldValue, newValue)); + } + + partial void OnNumberChanged(int oldValue, int newValue) + { + OnNumberChangedValues.Add((oldValue, newValue)); + } + } + + public partial class ViewModelWithImplementedUpdateMethodAndAdditionalValidation : ObservableObject + { + private int step; + + [ObservableProperty] + public partial string? Name { get; set; } = "A"; + + partial void OnNameChanging(string? value) + { + Assert.AreEqual(0, this.step); + + this.step = 1; + + Assert.AreEqual("A", Name); + Assert.AreEqual("B", value); + } + + partial void OnNameChanged(string? value) + { + Assert.AreEqual(2, this.step); + + this.step = 3; + + Assert.AreEqual("B", Name); + Assert.AreEqual("B", value); + } + + protected override void OnPropertyChanging(PropertyChangingEventArgs e) + { + base.OnPropertyChanging(e); + + Assert.AreEqual(1, this.step); + + this.step = 2; + + Assert.AreEqual("A", Name); + Assert.AreEqual(nameof(Name), e.PropertyName); + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + Assert.AreEqual(3, this.step); + + Assert.AreEqual("B", Name); + Assert.AreEqual(nameof(Name), e.PropertyName); + } + } + + partial class BroadcastingViewModel : ObservableRecipient + { + public BroadcastingViewModel(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial string? Name { get; set; } + } + + partial class RecipientWithNonBroadcastingProperty : ObservableRecipient + { + public RecipientWithNonBroadcastingProperty(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + public partial string? Name { get; set; } + } + + [ObservableRecipient] + partial class BroadcastingViewModelWithAttribute : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial string? Name { get; set; } + } + + partial class BroadcastingViewModelWithInheritedAttribute : BroadcastingViewModelWithAttribute + { + public BroadcastingViewModelWithInheritedAttribute(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial string? Name2 { get; set; } + } + + [NotifyPropertyChangedRecipients] + partial class BroadcastingViewModelWithClassLevelAttribute : ObservableRecipient + { + public BroadcastingViewModelWithClassLevelAttribute(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + public partial string? Name { get; set; } + } + + partial class BroadcastingViewModelWithInheritedClassLevelAttribute : BroadcastingViewModelWithClassLevelAttribute + { + public BroadcastingViewModelWithInheritedClassLevelAttribute(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + public partial string? Name2 { get; set; } + } + + [ObservableRecipient] + [NotifyPropertyChangedRecipients] + partial class BroadcastingViewModelWithAttributeAndClassLevelAttribute : ObservableObject + { + [ObservableProperty] + public partial string? Name { get; set; } + } + + partial class BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute : BroadcastingViewModelWithAttributeAndClassLevelAttribute + { + public BroadcastingViewModelWithInheritedAttributeAndClassLevelAttribute(IMessenger messenger) + : base(messenger) + { + } + + [ObservableProperty] + public partial string? Name2 { get; set; } + } + +#if NET6_0_OR_GREATER + private partial class NullableRepro : ObservableObject + { + [ObservableProperty] + public partial List? NullableList { get; set; } + + [ObservableProperty] + public partial Foo.Bar?, StrongBox.Bar?>?>? NullableMess { get; set; } + } + + private class Foo + { + public class Bar + { + } + } +#endif + + partial class ModelWithObservablePropertyAndBaseClassInAnotherAssembly : ModelWithObservableObjectAttribute + { + [ObservableProperty] + public partial string? OtherProperty { get; set; } + + public ModelWithObservablePropertyAndBaseClassInAnotherAssembly() + { + OtherProperty = "Ok"; + } + } + + interface IValueHolder + { + public bool Value { get; } + } + + partial class ModelWithMultipleGenericParameters : ObservableObject, IValueHolder + { + [ObservableProperty] + public partial bool Value { get; set; } + + [ObservableProperty] + public partial T? TValue { get; set; } + + [ObservableProperty] + public partial U? UValue { get; set; } + + [ObservableProperty] + public partial List? List { get; set; } + } + + [ObservableObject] + partial class ModelWithCultureAwarePropertyName + { + // This starts with "i" as it's one of the characters that can change when converted to uppercase. + // For instance, when using the Turkish language pack, this would become "İnputFolder" if done wrong. + [ObservableProperty] + public partial int InputFolder { get; set; } + } + + [ObservableRecipient] + public sealed partial class ModelWithNotifyPropertyChangedRecipientsAndDisplayAttributeLast : ObservableValidator + { + [ObservableProperty] + [NotifyPropertyChangedRecipients] + [Display(Name = "Foo bar baz")] + public partial object? SomeProperty { get; set; } + } + + public abstract partial class BaseModelWithObservablePropertyAttribute : ObservableObject + { + [ObservableProperty] + public partial bool CanSave { get; set; } + + public abstract void Save(); + } + + public partial class InheritedModelWithCommandUsingInheritedObservablePropertyForCanExecute : BaseModelWithObservablePropertyAttribute + { + [RelayCommand(CanExecute = nameof(CanSave))] + public override void Save() + { + } + } + + public partial class ModelWithAdditionalDataAnnotationAttributes : ObservableValidator + { + [ObservableProperty] + [Display(Name = "MyProperty", ResourceType = typeof(List), Prompt = "Foo bar baz")] + [Key] + [Editable(true)] + [UIHint("MyControl", "WPF", new object[] { "Foo", 42, "Bar", 3.14, "Baz", "Hello" })] + [ScaffoldColumn(true)] + public partial string? Name { get; set; } + } + + public partial class ModelWithOverriddenCommandMethodFromExternalBaseModel : ModelWithObservablePropertyAndMethod + { + public bool HasSaved { get; private set; } + + [RelayCommand(CanExecute = nameof(CanSave))] + public override void Save() + { + HasSaved = true; + } + } + + public partial class MyViewModelWithExplicitPropertyAttributes : ObservableValidator + { + [ObservableProperty] + [Required] + [MinLength(1)] + [MaxLength(100)] + public partial string? Name { get; set; } + [ObservableProperty] + [JsonPropertyName("lastName")] + [XmlIgnore] + public partial string? LastName { get; set; } + + [ObservableProperty] + [Test] + public partial string? JustOneSimpleAttribute { get; set; } + + [ObservableProperty] + [TestValidation(null, typeof(MyViewModelWithExplicitPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Animal.Llama)] + public partial int SomeComplexValidationAttribute { get; set; } + + [ObservableProperty] + [Test] + [PropertyInfo(null, typeof(MyViewModelWithExplicitPropertyAttributes), true, 6.28, new[] { "Bob", "Ross" }, new object[] { "Test" }, NestedArray = new object[] { 1, "Hello", 42, null! }, Animal = (Animal)67)] + public partial int SomeComplexRandomAttribute { get; set; } + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class TestAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class PropertyInfoAttribute : Attribute + { + public PropertyInfoAttribute(object? o, Type t, bool flag, double d, string[] names, object[] objects) + { + O = o; + T = t; + Flag = flag; + D = d; + Names = names; + Objects = objects; + } + + public object? O { get; } + + public Type T { get; } + + public bool Flag { get; } + + public double D { get; } + + public string[] Names { get; } + + public object? Objects { get; set; } + + public object? NestedArray { get; set; } + + public Animal Animal { get; set; } + } + + private partial class ModelWithObservablePropertyWithUnderscoreAndUppercase : ObservableObject + { + [ObservableProperty] + public partial bool IsReadOnly { get; set; } + } + + private partial class ModelWithForwardedAttributesWithNegativeValues : ObservableObject + { + [ObservableProperty] + public partial bool Test1 { get; set; } + + [ObservableProperty] + [DefaultValue(PositiveEnum.Something)] + public partial PositiveEnum Test2 { get; set; } + + [ObservableProperty] + [DefaultValue(NegativeEnum.Problem)] + public partial NegativeEnum Test3 { get; set; } + + [ObservableProperty] + public partial int Test4 { get; set; } + + public ModelWithForwardedAttributesWithNegativeValues() + { + Test1 = true; + Test2 = PositiveEnum.Else; + } + } + + public enum PositiveEnum + { + Something = 0, + Else = 1 + } + + public enum NegativeEnum + { + Problem = -1, + OK = 0 + } + + private sealed partial class ModelWithDependentPropertyAndPropertyChanging : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + public partial string? Name { get; set; } + + public string? FullName => ""; + } + + [INotifyPropertyChanged(IncludeAdditionalHelperMethods = false)] + private sealed partial class ModelWithDependentPropertyAndNoPropertyChanging + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + public partial string? Name { get; set; } + + public string? FullName => ""; + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj similarity index 62% 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 7f3373c5e..eaa0f012c 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 @@ -1,23 +1,22 @@ - net472;net6.0;net7.0 + net472;net7.0;net8.0 - - - - - - - + + + + + + - - + + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs similarity index 91% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs index e3c69458e..c77c38b7b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs @@ -9,13 +9,13 @@ using CSharpCodeFixTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixTest< CommunityToolkit.Mvvm.SourceGenerators.AsyncVoidReturningRelayCommandMethodAnalyzer, CommunityToolkit.Mvvm.CodeFixers.AsyncVoidReturningRelayCommandMethodCodeFixer, - Microsoft.CodeAnalysis.Testing.Verifiers.MSTestVerifier>; + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< CommunityToolkit.Mvvm.SourceGenerators.AsyncVoidReturningRelayCommandMethodAnalyzer, CommunityToolkit.Mvvm.CodeFixers.AsyncVoidReturningRelayCommandMethodCodeFixer, - Microsoft.CodeAnalysis.Testing.Verifiers.MSTestVerifier>; + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class Test_AsyncVoidReturningRelayCommandMethodCodeFixer @@ -53,7 +53,7 @@ private async Task Foo() { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(RelayCommand).Assembly); @@ -98,7 +98,7 @@ private async Task Foo() { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(RelayCommand).Assembly); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs similarity index 94% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs index 17a4a2c09..2e63a49f9 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs @@ -9,13 +9,13 @@ using CSharpCodeFixTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixTest< CommunityToolkit.Mvvm.SourceGenerators.ClassUsingAttributeInsteadOfInheritanceAnalyzer, CommunityToolkit.Mvvm.CodeFixers.ClassUsingAttributeInsteadOfInheritanceCodeFixer, - Microsoft.CodeAnalysis.Testing.Verifiers.MSTestVerifier>; + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< CommunityToolkit.Mvvm.SourceGenerators.ClassUsingAttributeInsteadOfInheritanceAnalyzer, CommunityToolkit.Mvvm.CodeFixers.ClassUsingAttributeInsteadOfInheritanceCodeFixer, - Microsoft.CodeAnalysis.Testing.Verifiers.MSTestVerifier>; + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class ClassUsingAttributeInsteadOfInheritanceCodeFixer @@ -48,7 +48,7 @@ class C : ObservableObject { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); @@ -97,7 +97,7 @@ public void Dispose() { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); @@ -150,7 +150,7 @@ class TestAttribute : Attribute { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); @@ -203,7 +203,7 @@ class TestAttribute : Attribute { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); @@ -260,7 +260,7 @@ class TestAttribute : Attribute { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); @@ -312,7 +312,7 @@ class TestAttribute : Attribute { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs similarity index 95% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs index dec18e08a..665761405 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs @@ -9,13 +9,13 @@ using CSharpCodeFixTest = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixTest< CommunityToolkit.Mvvm.SourceGenerators.FieldReferenceForObservablePropertyFieldAnalyzer, CommunityToolkit.Mvvm.CodeFixers.FieldReferenceForObservablePropertyFieldCodeFixer, - Microsoft.CodeAnalysis.Testing.Verifiers.MSTestVerifier>; + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< CommunityToolkit.Mvvm.SourceGenerators.FieldReferenceForObservablePropertyFieldAnalyzer, CommunityToolkit.Mvvm.CodeFixers.FieldReferenceForObservablePropertyFieldCodeFixer, - Microsoft.CodeAnalysis.Testing.Verifiers.MSTestVerifier>; + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class Test_FieldReferenceForObservablePropertyFieldCodeFixer @@ -59,7 +59,7 @@ void M() { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); @@ -123,7 +123,7 @@ void M() { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); @@ -195,7 +195,7 @@ void M() { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); @@ -265,7 +265,7 @@ void M() { TestCode = original, FixedCode = @fixed, - ReferenceAssemblies = ReferenceAssemblies.Net.Net60 + ReferenceAssemblies = ReferenceAssemblies.Net.Net80 }; test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); 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..9b7ef7e40 --- /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 + { + [ObservableProperty] + public string {|MVVMTK0044:Bar|} { get; set; } + } + } + """; + + await Test_SourceGeneratorsDiagnostics.VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj similarity index 55% 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 391078d31..6aa049262 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 @@ -1,21 +1,26 @@ - net472;net6.0;net7.0 + net472;net7.0;net8.0 $(DefineConstants);ROSLYN_4_3_1_OR_GREATER - - - - - + + + + + + + + + + - + diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj new file mode 100644 index 000000000..7d0387bee --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests.csproj @@ -0,0 +1,25 @@ + + + + net472;net7.0;net8.0 + $(DefineConstants);ROSLYN_4_3_1_OR_GREATER;ROSLYN_4_12_0_OR_GREATER + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs new file mode 100644 index 000000000..978017735 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -0,0 +1,1153 @@ +// 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 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)); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/969 + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_RequiredProperty_WorksCorrectly() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public required partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public required partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/1013 + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_NewProperty_WorksCorrectly() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class BaseViewModel : ObservableObject + { + public int Number { get; private set; } + } + + partial class MyViewModel : BaseViewModel + { + [ObservableProperty] + public new partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public new partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_VirtualProperty_WorksCorrectly() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public virtual partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public virtual partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + [DataRow("override")] + [DataRow("sealed override")] + public void ObservablePropertyWithValueType_OnPartialProperty_OverrideProperty_WorksCorrectly(string modifiers) + { + string source = $$""" + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class BaseViewModel : ObservableObject + { + public virtual partial int Number { get; private set; } + } + + partial class MyViewModel : BaseViewModel + { + [ObservableProperty] + public {{modifiers}} partial int Number { get; private set; } + } + """; + + string result = $$""" + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public {{modifiers}} partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly1() + { + string source = """ + using 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 CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + internal partial int Number { private get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal partial int Number + { + private get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly3() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + protected internal partial string Name { get; private protected set; } + } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + protected internal partial string Name + { + get => field; + private protected set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + +[TestMethod] + public void ObservablePropertyWithReferenceType_NotNullable_OnPartialProperty_WorksCorrectly() + { + string source = """ + using 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 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 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 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.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.ComponentModel.DataAnnotations; + using System.Windows.Input; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableValidator + { + [ObservableProperty] + [NotifyDataErrorInfo] + [Required] + public partial string Name { get; set; } + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs new file mode 100644 index 000000000..e6845bbe5 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -0,0 +1,1495 @@ +// 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.SourceGenerators.UnitTests.Helpers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +partial class Test_SourceGeneratorsDiagnostics +{ + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_DoesnNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp12); + } + + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_CSharp12_Partial_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0041:ObservableProperty|}] + public partial string Name { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + + // /0/Test0.cs(8,31): error CS8703: The modifier 'partial' is not valid for this item in C# 12.0. Please use language version '13.0' or greater. + DiagnosticResult.CompilerError("CS8703").WithSpan(8, 31, 8, 35).WithArguments("partial", "12.0", "13.0"), + // /0/Test0.cs(8,31): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 31, 8, 35).WithArguments("MyApp.SampleViewModel.Name")); + } + + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsNotPreview_CSharp13_Partial_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0041:ObservableProperty|}] + public partial string Name { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp13, + + // /0/Test0.cs(8,31): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 31, 8, 35).WithArguments("MyApp.SampleViewModel.Name")); + } + + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsPreview_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, languageVersion: LanguageVersion.Preview); + } + + [TestMethod] + public async Task RequireCSharpLanguageVersionPreviewAnalyzer_LanguageVersionIsPreview_Partial_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + + // /0/Test0.cs(8,31): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 31, 8, 35).WithArguments("MyApp.SampleViewModel.Name")); + } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsNotPreview_DoesNotWarn() + { + 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 + { + [ObservableProperty] + private string {|MVVMTK0042:name|}; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsPreview_OnPartialProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnValidPropertyDeclaration_DoesNotWarn1() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string {|CS9248:Name|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0103", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnValidPropertyDeclaration_DoesNotWarn2() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + internal partial string {|CS9248:Name|} { get; private set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0103", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnValidPropertyDeclaration_DoesNotWarn3() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + protected internal partial string {|CS9248:Name|} { get; private protected set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0103", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnNonPartialProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnStaticProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public static partial string {|CS9248:Name|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnReadOnlyProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { get; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnWriteOnlyProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPropertyLevelObservablePropertyAttributeAnalyzer_OnInitOnlyProperty_Warns() + { +#if NET6_0_OR_GREATER + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { get; init; } + } + } + """; +#else + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0043:ObservableProperty|}] + public partial string {|CS9248:Name|} { get; {|CS0518:init|}; } + } + } + """; +#endif + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS0518", "CS9248"]); + } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_LanguageVersionIsPreview_OnStaticField_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private static string name; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnPartialPropertyAnalyzer_CsWinRTAotOptimizerEnabled_Auto_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private static string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_NotTargetingWindows_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: []); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_OptIn_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_False_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "false")]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_Auto_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string {|MVVMTK0045:name|}; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_Auto_NotCSharpPreview_Warns_WithCompilationWarning() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0051:ObservableProperty|}] + private string {|MVVMTK0045:name|}; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_Auto_NotCSharpPreview_MultipleFields_Warns_WithCompilationWarning_ConsistentLocation() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0051:ObservableProperty|}] + private string {|MVVMTK0045:f1|}; + + [ObservableProperty] + private string {|MVVMTK0045:f2|}; + + [ObservableProperty] + private string {|MVVMTK0045:f3|}; + } + + public partial class OtherViewModel : ObservableObject + { + [ObservableProperty] + private string {|MVVMTK0045:f1|}; + + [ObservableProperty] + private string {|MVVMTK0045:f2|}; + + [ObservableProperty] + private string {|MVVMTK0045:f3|}; + } + } + + namespace OtherNamespace + { + public partial class YetAnotherViewModel : ObservableObject + { + [ObservableProperty] + private string {|MVVMTK0045:f1|}; + + [ObservableProperty] + private string {|MVVMTK0045:f2|}; + } + } + """; + + // This test is non deterministic, so run it 10 times to ensure the likelihood of it passing just by luck is almost 0 + for (int i = 0; i < 10; i++) + { + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_NoXaml_Level1_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 1)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_NoXaml_Level2_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string {|MVVMTK0045:name|}; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 2)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_NoXaml_1_Component_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string {|MVVMTK0045:name|}; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 1), ("CsWinRTComponent", true)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_UwpXaml_Level1_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string {|MVVMTK0045:name|}; + } + } + + namespace Windows.UI.Xaml.Controls + { + public class Button; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 1)]); + } + + [TestMethod] + public async Task WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer_TargetingWindows_CsWinRTAotOptimizerEnabled_True_WinUIXaml_Level1_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string {|MVVMTK0045:name|}; + } + } + + namespace Microsoft.UI.Xaml.Controls + { + public class Button; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.Preview, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "true"), ("CsWinRTAotWarningLevel", 1)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_NotTargetingWindows_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty] + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: []); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty] + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_NotIncluded_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["Name"], [])] + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Field_NotIncluded_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["OtherName"], [])] + public partial class SampleViewModel : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Method_NotIncluded_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["OtherMethod"], [])] + public partial class SampleViewModel : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Field_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty] + public partial class {|MVVMTK0047:SampleViewModel|} : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Field_Included_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["Name"], [])] + public partial class {|MVVMTK0047:SampleViewModel|} : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Method_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty] + public partial class {|MVVMTK0048:SampleViewModel|} : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute; + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer_TargetingWindows_BaseType_Method_Included_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["DoStuffCommand"], [])] + public partial class {|MVVMTK0048:SampleViewModel|} : BaseViewModel + { + } + + public partial class BaseViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnValidProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string {|CS9248:Name|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnUnannotatedPartialPropertyWithImplementation_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + public partial string Name { get; set; } + + public partial string Name + { + get => field; + set { } + } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnImplementedProperty_GeneratedByMvvmToolkitGenerator_DoesNotWarn() + { + const string source = """ + using System.CodeDom.Compiler; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + + [GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "1.0.0")] + public partial string Name + { + get => field; + set { } + } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnImplementedProperty_GeneratedByAnotherGenerator_Warns() + { + const string source = """ + using System.CodeDom.Compiler; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0052:ObservableProperty|}] + public partial string Name { get; set; } + + [GeneratedCode("Some.Other.Generator", "1.0.0")] + public partial string Name + { + get => field; + set { } + } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_OnImplementedProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0052:ObservableProperty|}] + public partial string Name { get; set; } + + public partial string Name + { + get => field; + set { } + } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_ReturnsByRef_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0053:ObservableProperty|}] + public partial ref int {|CS9248:Name|} { get; {|CS8147:set|}; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS8147", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_ReturnsByRefReadOnly_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0053:ObservableProperty|}] + public partial ref readonly int {|CS9248:Name|} { get; {|CS8147:set|}; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS8147", "CS9248"]); + } + + [TestMethod] + public async Task InvalidPartialPropertyLevelObservablePropertyAttributeAnalyzer_ReturnsByRefLike_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0054:ObservableProperty|}] + public partial RefStruct {|CS9248:Name|} { get; set; } + } + + public ref struct RefStruct; + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPointerTypeObservablePropertyAttributeAnalyzer_ReturnsValidType_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string {|CS9248:Name|} { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview, [], ["CS9248"]); + } + + [TestMethod] + public async Task InvalidPointerTypeObservablePropertyAttributeAnalyzer_ReturnsPointerType_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public unsafe partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0055:ObservableProperty|}] + public partial int* {|CS9248:Name|} { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task InvalidPointerTypeObservablePropertyAttributeAnalyzer_ReturnsFunctionPointerType_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public unsafe partial class SampleViewModel : ObservableObject + { + [{|MVVMTK0055:ObservableProperty|}] + public partial delegate* unmanaged[Stdcall] {|CS9248:Name|} { get; set; } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_NormalProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_SimilarProperty_NotObservableObject_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : MyBaseViewModel + { + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + + public abstract class MyBaseViewModel + { + protected void SetProperty(ref T location, T value, string propertyName = null) + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_NoGetter_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_NoSetter_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_OtherLocation_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + set + { + string test = field; + + SetProperty(ref test, value); + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_OtherValue_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + set + { + string test = "Bob"; + + SetProperty(ref field, test); + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_WithObservableProperty_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_GetAccessorWithExpressionBody_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => "Hello world"; + set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string {|MVVMTK0056:Name|} + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_WithModifiers_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public new string {|MVVMTK0056:Name|} + { + get => field; + private set => SetProperty(ref field, value); + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } + + [TestMethod] + public async Task UseObservablePropertyOnSemiAutoPropertyAnalyzer_ValidProperty_WithBlocks_Warns() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public new string {|MVVMTK0056:Name|} + { + get + { + return field; + } + private set + { + SetProperty(ref field, value); + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(source, LanguageVersion.Preview); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs new file mode 100644 index 000000000..e9cccaa91 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer.cs @@ -0,0 +1,889 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CSharpCodeFixTest = CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers.CSharpCodeFixWithLanguageVersionTest< + CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnSemiAutoPropertyAnalyzer, + CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForSemiAutoPropertyCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; +using CSharpCodeFixVerifier = Microsoft.CodeAnalysis.CSharp.Testing.CSharpCodeFixVerifier< + CommunityToolkit.Mvvm.SourceGenerators.UseObservablePropertyOnSemiAutoPropertyAnalyzer, + CommunityToolkit.Mvvm.CodeFixers.UsePartialPropertyForSemiAutoPropertyCodeFixer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +[TestClass] +public class Test_UseObservablePropertyOnSemiAutoPropertyCodeFixer +{ + [TestMethod] + public async Task SimpleProperty() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithSemicolonTokenGetAccessor() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + public string Name + { + get; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithMissingUsingDirective() + { + string original = """ + namespace MyApp; + + public class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject + { + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 19, 5, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithLeadingTrivia() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + /// + /// This is a property. + /// + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + /// + /// This is a property. + /// + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(10, 19, 10, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithLeadingTrivia_AndAttributes() + { + string original = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + /// + /// This is a property. + /// + [Test("Targeting property")] + [field: Test("Targeting field")] + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + + public class TestAttribute(string text) : Attribute; + """; + + string @fixed = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + /// + /// This is a property. + /// + [ObservableProperty] + [Test("Targeting property")] + [field: Test("Targeting field")] + public partial string Name { get; set; } + } + + 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(13,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(13, 19, 13, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_Multiple() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + public string FirstName + { + get => field; + set => SetProperty(ref field, value); + } + + public string LastName + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string FirstName { get; set; } + + [ObservableProperty] + public partial string LastName { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 28).WithArguments("MyApp.SampleViewModel", "FirstName"), + + // /0/Test0.cs(13,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(13, 19, 13, 27).WithArguments("MyApp.SampleViewModel", "LastName"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"), + + // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_Multiple_OnlyTriggersOnFirstOne() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + public string FirstName + { + get => field; + set => SetProperty(ref field, value); + } + + private string _lastName; + + public string LastName + { + get => _lastName; + set => SetProperty(ref _lastName, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string FirstName { get; set; } + + private string _lastName; + + public string LastName + { + get => _lastName; + set => SetProperty(ref _lastName, value); + } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 28).WithArguments("MyApp.SampleViewModel", "FirstName"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_Multiple_OnlyTriggersOnSecondOne() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public class SampleViewModel : ObservableObject + { + private string _firstName; + + public string FirstName + { + get => _firstName; + set => SetProperty(ref _firstName, value); + } + + public string LastName + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + private string _firstName; + + public string FirstName + { + get => _firstName; + set => SetProperty(ref _firstName, value); + } + + [ObservableProperty] + public partial string LastName { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(15,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(15, 19, 15, 27).WithArguments("MyApp.SampleViewModel", "LastName"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(16,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(16, 27, 16, 35).WithArguments("MyApp.SampleViewModel.LastName"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithinPartialType() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string Name + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Name can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 23).WithArguments("MyApp.SampleViewModel", "Name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 31).WithArguments("MyApp.SampleViewModel.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithinPartialType_Multiple() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + public string FirstName + { + get => field; + set => SetProperty(ref field, value); + } + + public string LastName + { + get => field; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + public partial string FirstName { get; set; } + + [ObservableProperty] + public partial string LastName { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 19, 7, 28).WithArguments("MyApp.SampleViewModel", "FirstName"), + + // /0/Test0.cs(13,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(13, 19, 13, 27).WithArguments("MyApp.SampleViewModel", "LastName"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"), + + // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_Multiple_WithMissingUsingDirective() + { + string original = """ + namespace MyApp; + + public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject + { + public string FirstName + { + get => field; + set => SetProperty(ref field, value); + } + + public string LastName + { + get => field; + set => SetProperty(ref field, value); + } + + public string PhoneNumber + { + get; + set => SetProperty(ref field, value); + } + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : CommunityToolkit.Mvvm.ComponentModel.ObservableObject + { + [ObservableProperty] + public partial string FirstName { get; set; } + + [ObservableProperty] + public partial string LastName { get; set; } + + [ObservableProperty] + public partial string PhoneNumber { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(5,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.FirstName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(5, 19, 5, 28).WithArguments("MyApp.SampleViewModel", "FirstName"), + + // /0/Test0.cs(11,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.LastName can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(11, 19, 11, 27).WithArguments("MyApp.SampleViewModel", "LastName"), + + // /0/Test0.cs(17,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.PhoneNumber can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(17, 19, 17, 30).WithArguments("MyApp.SampleViewModel", "PhoneNumber"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(8,27): error CS9248: Partial property 'SampleViewModel.FirstName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(8, 27, 8, 36).WithArguments("MyApp.SampleViewModel.FirstName"), + + // /0/Test0.cs(11,27): error CS9248: Partial property 'SampleViewModel.LastName' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(11, 27, 11, 35).WithArguments("MyApp.SampleViewModel.LastName"), + + // /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.PhoneNumber' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 38).WithArguments("MyApp.SampleViewModel.PhoneNumber"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleProperty_WithinPartialType_Multiple_MixedScenario() + { + string original = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [Test("This is an attribute")] + public string Prop1 + { + get => field; + set => SetProperty(ref field, value); + } + + // Single comment + public string Prop2 + { + get => field; + set => SetProperty(ref field, value); + } + + /// + /// This is a property. + /// + public string Prop3 + { + get => field; + set => SetProperty(ref field, value); + } + + /// + /// This is another property. + /// + [Test("Another attribute")] + public string Prop4 + { + get => field; + set => SetProperty(ref field, value); + } + + // Some other single comment + [Test("Yet another attribute")] + public string Prop5 + { + get => field; + set => SetProperty(ref field, value); + } + + [Test("Attribute without trivia")] + public string Prop6 + { + get => field; + set => SetProperty(ref field, value); + } + + public string Prop7 + { + get => field; + set => SetProperty(ref field, value); + } + } + + public class TestAttribute(string text) : Attribute; + """; + + string @fixed = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + [Test("This is an attribute")] + public partial string Prop1 { get; set; } + + // Single comment + [ObservableProperty] + public partial string Prop2 { get; set; } + + /// + /// This is a property. + /// + [ObservableProperty] + public partial string Prop3 { get; set; } + + /// + /// This is another property. + /// + [ObservableProperty] + [Test("Another attribute")] + public partial string Prop4 { get; set; } + + // Some other single comment + [ObservableProperty] + [Test("Yet another attribute")] + public partial string Prop5 { get; set; } + + [ObservableProperty] + [Test("Attribute without trivia")] + public partial string Prop6 { get; set; } + + [ObservableProperty] + public partial string Prop7 { get; set; } + } + + public class TestAttribute(string text) : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(9,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop1 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(9, 19, 9, 24).WithArguments("MyApp.SampleViewModel", "Prop1"), + + // /0/Test0.cs(16,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop2 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(16, 19, 16, 24).WithArguments("MyApp.SampleViewModel", "Prop2"), + + // /0/Test0.cs(25,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop3 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(25, 19, 25, 24).WithArguments("MyApp.SampleViewModel", "Prop3"), + + // /0/Test0.cs(35,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop4 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(35, 19, 35, 24).WithArguments("MyApp.SampleViewModel", "Prop4"), + + // /0/Test0.cs(43,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop5 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(43, 19, 43, 24).WithArguments("MyApp.SampleViewModel", "Prop5"), + + // /0/Test0.cs(50,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop6 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(50, 19, 50, 24).WithArguments("MyApp.SampleViewModel", "Prop6"), + + // /0/Test0.cs(56,19): info MVVMTK0056: The semi-auto property MyApp.SampleViewModel.Prop7 can be converted to a partial property using [ObservableProperty], which is recommended (doing so makes the code less verbose and results in more optimized code) + CSharpCodeFixVerifier.Diagnostic().WithSpan(56, 19, 56, 24).WithArguments("MyApp.SampleViewModel", "Prop7"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,27): error CS9248: Partial property 'SampleViewModel.Prop1' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(10, 27, 10, 32).WithArguments("MyApp.SampleViewModel.Prop1"), + + // /0/Test0.cs(14,27): error CS9248: Partial property 'SampleViewModel.Prop2' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(14, 27, 14, 32).WithArguments("MyApp.SampleViewModel.Prop2"), + + // /0/Test0.cs(20,27): error CS9248: Partial property 'SampleViewModel.Prop3' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(20, 27, 20, 32).WithArguments("MyApp.SampleViewModel.Prop3"), + + // /0/Test0.cs(27,27): error CS9248: Partial property 'SampleViewModel.Prop4' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(27, 27, 27, 32).WithArguments("MyApp.SampleViewModel.Prop4"), + + // /0/Test0.cs(32,27): error CS9248: Partial property 'SampleViewModel.Prop5' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(32, 27, 32, 32).WithArguments("MyApp.SampleViewModel.Prop5"), + + // /0/Test0.cs(36,27): error CS9248: Partial property 'SampleViewModel.Prop6' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(36, 27, 36, 32).WithArguments("MyApp.SampleViewModel.Prop6"), + + // /0/Test0.cs(39,27): error CS9248: Partial property 'SampleViewModel.Prop7' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(39, 27, 39, 32).WithArguments("MyApp.SampleViewModel.Prop7"), + }); + + await test.RunAsync(); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs new file mode 100644 index 000000000..f72ca8261 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_UsePartialPropertyForObservablePropertyCodeFixer.cs @@ -0,0 +1,998 @@ +// 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(6,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 17, 6, 18).WithArguments("C", "i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithAdditionalAttributes1() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello")] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor("hello")] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 17, 7, 18).WithArguments("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(8,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(8, 17, 8, 18).WithArguments("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(9,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(9, 17, 9, 18).WithArguments("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(15,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(15, 17, 15, 18).WithArguments("C", "i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(12,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(12, 24, 12, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithNoReferences_WithSimpleComment() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment + [ObservableProperty] + private int i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + // This is a comment + [ObservableProperty] + public partial int I { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 17, 7, 18).WithArguments("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(8,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(8, 17, 8, 18).WithArguments("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(10,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(10, 17, 10, 18).WithArguments("C", "i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(10, 24, 10, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithSomeReferences() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i; + + public void M() + { + i = 42; + } + + public int N() => i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } + + public void M() + { + I = 42; + } + + public int N() => I; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 17, 6, 18).WithArguments("C", "i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleField_WithInitializer1() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i = 42; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } = 42; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 17, 6, 18).WithArguments("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(7,33): info MVVMTK0042: The field C.items using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 33, 7, 38).WithArguments("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(7,33): info MVVMTK0042: The field C.items using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 33, 7, 38).WithArguments("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(); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/971 + [TestMethod] + public async Task SimpleField_WithNoReferences_WithRequiredModifier() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + internal required string foo; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public required partial string Foo { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,30): info MVVMTK0042: The field C.foo using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 30, 6, 33).WithArguments("C", "foo"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,36): error CS9248: Partial property 'C.Foo' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 36, 6, 39).WithArguments("C.Foo"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFieldWithSomeReferences_WithSomeThisExpressions() + { + string original = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + private int i; + + public void M() + { + i = 42; + this.i = 42; + } + + public int N() => i; + + public int P() => this.i + Q(i) + Q(this.i); + + private int Q(int i) => this.i + i; + } + """; + + string @fixed = """ + using CommunityToolkit.Mvvm.ComponentModel; + + partial class C : ObservableObject + { + [ObservableProperty] + public partial int I { get; set; } + + public void M() + { + I = 42; + I = 42; + } + + public int N() => I; + + public int P() => I + Q(I) + Q(I); + + private int Q(int i) => I + i; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,17): info MVVMTK0042: The field C.i using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 17, 6, 18).WithArguments("C", "i"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,24): error CS9248: Partial property 'C.I' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(6, 24, 6, 25).WithArguments("C.I"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFields_WithMultipleAttributes_SingleProperty() + { + string original = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + [ObservableProperty, NotifyPropertyChangedFor("Age")] private string name = String.Empty; + } + """; + + string @fixed = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + [ObservableProperty, NotifyPropertyChangedFor("Age")] + public partial string Name { get; set; } = String.Empty; + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,74): info MVVMTK0042: The field Class1.name using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 74, 6, 78).WithArguments("Class1", "name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,27): error CS8050: Only auto-implemented properties, or properties that use the 'field' keyword, can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(7, 27, 7, 31), + + // /0/Test0.cs(7,27): error CS9248: Partial property 'Class1.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 27, 7, 31).WithArguments("Class1.Name"), + }); + + await test.RunAsync(); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/1007 + [TestMethod] + public async Task SimpleFields_WithMultipleAttributes_WithNoBlankLines() + { + string original = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + [ObservableProperty, NotifyPropertyChangedFor("Age")] private string name = String.Empty; + [ObservableProperty] private int age; + } + """; + + string @fixed = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + [ObservableProperty, NotifyPropertyChangedFor("Age")] + public partial string Name { get; set; } = String.Empty; + + [ObservableProperty] + public partial int Age { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(6,74): info MVVMTK0042: The field Class1.name using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(6, 74, 6, 78).WithArguments("Class1", "name"), + + // /0/Test0.cs(7,38): info MVVMTK0042: The field Class1.age using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(7, 38, 7, 41).WithArguments("Class1", "age"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(7,27): error CS8050: Only auto-implemented properties, or properties that use the 'field' keyword, can have initializers. + DiagnosticResult.CompilerError("CS8050").WithSpan(7, 27, 7, 31), + + // /0/Test0.cs(7,27): error CS9248: Partial property 'Class1.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(7, 27, 7, 31).WithArguments("Class1.Name"), + + // /0/Test0.cs(10,24): error CS9248: Partial property 'Class1.Age' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(10, 24, 10, 27).WithArguments("Class1.Age"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFields_WithMultipleAttributes_WithMixedBuckets_1() + { + string original = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + // Leading trivia + [ObservableProperty, NotifyPropertyChangedFor("A"), Display] + [NotifyPropertyChangedFor("B")] + private string _name; + } + """; + + string @fixed = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + // Leading trivia + [ObservableProperty, NotifyPropertyChangedFor("A"), Display] + [NotifyPropertyChangedFor("B")] + public partial string Name { get; set; } + } + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,20): info MVVMTK0042: The field Class1._name using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(10, 20, 10, 25).WithArguments("Class1", "_name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(10,27): error CS9248: Partial property 'Class1.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(10, 27, 10, 31).WithArguments("Class1.Name"), + }); + + await test.RunAsync(); + } + + [TestMethod] + public async Task SimpleFields_WithMultipleAttributes_WithMixedBuckets_2() + { + string original = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + // Leading trivia + [NotifyPropertyChangedFor("B")] + [ObservableProperty, NotifyPropertyChangedFor("A"), Display, Test] + [NotifyPropertyChangedFor("C")] + [property: UIHint("name"), Test] + private string name; + } + + public class TestAttribute : Attribute; + """; + + string @fixed = """ + using System; + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + public partial class Class1 : ObservableObject + { + // Leading trivia + [NotifyPropertyChangedFor("B")] + [ObservableProperty, NotifyPropertyChangedFor("A"), Display] + [field: Test] + [NotifyPropertyChangedFor("C")] + [UIHint("name"), Test] + public partial string Name { get; set; } + } + + public class TestAttribute : Attribute; + """; + + CSharpCodeFixTest test = new(LanguageVersion.Preview) + { + TestCode = original, + FixedCode = @fixed, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }; + + test.TestState.AdditionalReferences.Add(typeof(ObservableObject).Assembly); + test.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(12,20): info MVVMTK0042: The field Class1.name using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well) + CSharpCodeFixVerifier.Diagnostic().WithSpan(12, 20, 12, 24).WithArguments("Class1", "name"), + }); + + test.FixedState.ExpectedDiagnostics.AddRange(new[] + { + // /0/Test0.cs(13,27): error CS9248: Partial property 'Class1.Name' must have an implementation part. + DiagnosticResult.CompilerError("CS9248").WithSpan(13, 27, 13, 31).WithArguments("Class1.Name"), + }); + + await test.RunAsync(); + } +} 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 a2610aad7..14a15ee60 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs @@ -2,15 +2,18 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Testing.Verifiers; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; #if NET472 using System.ComponentModel.DataAnnotations; #endif @@ -21,7 +24,7 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; /// A custom that uses a specific C# language version to parse code. /// /// The type of the analyzer to test. -internal sealed class CSharpAnalyzerWithLanguageVersionTest : CSharpAnalyzerTest +internal sealed class CSharpAnalyzerWithLanguageVersionTest : CSharpAnalyzerTest where TAnalyzer : DiagnosticAnalyzer, new() { /// @@ -30,7 +33,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) @@ -50,7 +53,9 @@ public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVe { CSharpAnalyzerWithLanguageVersionTest test = new(languageVersion) { TestCode = source }; -#if NET6_0_OR_GREATER +#if NET8_0_OR_GREATER + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET6_0_OR_GREATER test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net60; #else test.TestState.ReferenceAssemblies = ReferenceAssemblies.NetFramework.Net472.Default; @@ -62,4 +67,38 @@ public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVe return test.RunAsync(CancellationToken.None); } + + /// + /// The language version to use to run the test. + public static Task VerifyAnalyzerAsync(string source, LanguageVersion languageVersion, (string PropertyName, object PropertyValue)[] editorconfig) + { + CSharpAnalyzerWithLanguageVersionTest test = new(languageVersion) { TestCode = source }; + +#if NET8_0_OR_GREATER + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET6_0_OR_GREATER + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net60; +#else + test.TestState.ReferenceAssemblies = ReferenceAssemblies.NetFramework.Net472.Default; + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(RequiredAttribute).Assembly.Location)); +#endif + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(ObservableObject).Assembly.Location)); + + // Add any editorconfig properties, if present + if (editorconfig.Length > 0) + { + test.SolutionTransforms.Add((solution, projectId) => + solution.AddAnalyzerConfigDocument( + DocumentId.CreateNewId(projectId), + "MvvmToolkitAnalyzers.editorconfig", + SourceText.From($""" + is_global = true + {string.Join(Environment.NewLine, editorconfig.Select(static p => $"build_property.{p.PropertyName} = {p.PropertyValue}"))} + """, + Encoding.UTF8), + filePath: "/MvvmToolkitAnalyzers.editorconfig")); + } + + return test.RunAsync(CancellationToken.None); + } } diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs new file mode 100644 index 000000000..55127c5c8 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpCodeFixWithLanguageVersionTest{TAnalyzer,TCodeFix,TVerifier}.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests.Helpers; + +/// +/// A custom that uses a specific C# language version to parse code. +/// +/// The type of the analyzer to produce diagnostics. +/// The type of code fix to test. +/// The type of verifier to use to validate the code fixer. +internal sealed class CSharpCodeFixWithLanguageVersionTest : CSharpCodeFixTest + where TAnalyzer : DiagnosticAnalyzer, new() + where TCodeFix : CodeFixProvider, new() + where TVerifier : IVerifier, new() +{ + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion languageVersion; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The C# language version to use to parse code. + public CSharpCodeFixWithLanguageVersionTest(LanguageVersion languageVersion) + { + this.languageVersion = languageVersion; + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(this.languageVersion, DocumentationMode.Diagnose); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.cs index d19c52247..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() @@ -2301,7 +2301,7 @@ partial class MyViewModel : ObservableObject [ObservableProperty] double @object; - partial void OnObjectChanged(object oldValue, object NewValue) + partial void OnObjectChanged(double oldValue, double NewValue) { } } @@ -2367,13 +2367,1040 @@ public double Object VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void ObservableProperty_NotifyPropertyChangedFor_WithNotifyPropertyChanging() + { + // Using integers for properties to avoid needing conditional code in the expected results for nullability attributes. + // This test and the one below are only validating the generation related to INotifyPropertyChanging, and nothing else. + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private int firstName; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private int lastName; + + 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 int FirstName + { + get => firstName; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(firstName, value)) + { + OnFirstNameChanging(value); + OnFirstNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FirstName); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FullName); + firstName = value; + OnFirstNameChanged(value); + OnFirstNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FirstName); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName); + } + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public int LastName + { + get => lastName; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(lastName, value)) + { + OnLastNameChanging(value); + OnLastNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.LastName); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FullName); + lastName = value; + OnLastNameChanged(value); + OnLastNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.LastName); + 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 OnFirstNameChanging(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 OnFirstNameChanging(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 OnFirstNameChanged(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 OnFirstNameChanged(int oldValue, int newValue); + /// 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 OnLastNameChanging(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 OnLastNameChanging(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 OnLastNameChanged(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 OnLastNameChanged(int oldValue, int newValue); + } + } + """; + + string changingArgs = """ + // + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Mvvm.ComponentModel.__Internals + { + /// + /// A helper type providing cached, reusable instances + /// for all properties generated with . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This type is not intended to be used directly by user code")] + internal static class __KnownINotifyPropertyChangingArgs + { + /// The cached instance for all "FirstName" generated properties. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + public static readonly global::System.ComponentModel.PropertyChangingEventArgs FirstName = new global::System.ComponentModel.PropertyChangingEventArgs("FirstName"); + /// The cached instance for all "FullName" generated properties. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + public static readonly global::System.ComponentModel.PropertyChangingEventArgs FullName = new global::System.ComponentModel.PropertyChangingEventArgs("FullName"); + /// The cached instance for all "LastName" generated properties. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + public static readonly global::System.ComponentModel.PropertyChangingEventArgs LastName = new global::System.ComponentModel.PropertyChangingEventArgs("LastName"); + } + } + """; + + string changedArgs = """ + // + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Mvvm.ComponentModel.__Internals + { + /// + /// A helper type providing cached, reusable instances + /// for all properties generated with . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This type is not intended to be used directly by user code")] + internal static class __KnownINotifyPropertyChangedArgs + { + /// The cached instance for all "FirstName" generated properties. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + public static readonly global::System.ComponentModel.PropertyChangedEventArgs FirstName = new global::System.ComponentModel.PropertyChangedEventArgs("FirstName"); + /// The cached instance for all "FullName" generated properties. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + public static readonly global::System.ComponentModel.PropertyChangedEventArgs FullName = new global::System.ComponentModel.PropertyChangedEventArgs("FullName"); + /// The cached instance for all "LastName" generated properties. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + public static readonly global::System.ComponentModel.PropertyChangedEventArgs LastName = new global::System.ComponentModel.PropertyChangedEventArgs("LastName"); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result), ("__KnownINotifyPropertyChangingArgs.g.cs", changingArgs), ("__KnownINotifyPropertyChangedArgs.g.cs", changedArgs)); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void ObservableProperty_NotifyPropertyChangedFor_WithoutNotifyPropertyChanging() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + abstract class BaseViewModel + { + } + + [INotifyPropertyChanged(IncludeAdditionalHelperMethods = false)] + partial class MyViewModel : BaseViewModel + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private int firstName; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private int lastName; + + 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 int FirstName + { + get => firstName; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(firstName, value)) + { + OnFirstNameChanging(value); + OnFirstNameChanging(default, value); + firstName = value; + OnFirstNameChanged(value); + OnFirstNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FirstName); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName); + } + } + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public int LastName + { + get => lastName; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(lastName, value)) + { + OnLastNameChanging(value); + OnLastNameChanging(default, value); + lastName = value; + OnLastNameChanged(value); + OnLastNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.LastName); + 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 OnFirstNameChanging(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 OnFirstNameChanging(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 OnFirstNameChanged(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 OnFirstNameChanged(int oldValue, int newValue); + /// 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 OnLastNameChanging(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 OnLastNameChanging(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 OnLastNameChanged(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 OnLastNameChanged(int oldValue, int newValue); + } + } + """; + + string changedArgs = """ + // + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Mvvm.ComponentModel.__Internals + { + /// + /// A helper type providing cached, reusable instances + /// for all properties generated with . + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.DebuggerNonUserCode] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This type is not intended to be used directly by user code")] + internal static class __KnownINotifyPropertyChangedArgs + { + /// The cached instance for all "FirstName" generated properties. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + public static readonly global::System.ComponentModel.PropertyChangedEventArgs FirstName = new global::System.ComponentModel.PropertyChangedEventArgs("FirstName"); + /// The cached instance for all "FullName" generated properties. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + public static readonly global::System.ComponentModel.PropertyChangedEventArgs FullName = new global::System.ComponentModel.PropertyChangedEventArgs("FullName"); + /// The cached instance for all "LastName" generated properties. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + public static readonly global::System.ComponentModel.PropertyChangedEventArgs LastName = new global::System.ComponentModel.PropertyChangedEventArgs("LastName"); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result), ("__KnownINotifyPropertyChangingArgs.g.cs", null), ("__KnownINotifyPropertyChangedArgs.g.cs", changedArgs)); + } + + [TestMethod] + public void ObservableProperty_NotifyDataErrorInfo_EmitsTrimAnnotationsWhenNeeded() + { + string source = """ + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableValidator + { + [ObservableProperty] + [NotifyDataErrorInfo] + [Required] + private string? name; + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.DataAnnotations.RequiredAttribute()] + public string? Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string? newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.DataAnnotations.RequiredAttribute()] + public string? Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = 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() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_NotifyDataErrorInfo_EmitsTrimAnnotationsWhenNeeded_AppendToNullableAttribute() + { + string source = """ + using System.ComponentModel.DataAnnotations; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableValidator + { + [ObservableProperty] + [NotifyDataErrorInfo] + [Required] + private string name; + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.DataAnnotations.RequiredAttribute()] + public string Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] + [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.ComponentModel.DataAnnotations.RequiredAttribute()] + public string Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = 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() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_NotNullableProperty_OfReferenceType_WithChangedMethods() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + string name; + + partial void OnNameChanged(string? oldValue, string NewValue) + { + } + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public string Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + string? __oldValue = name; + OnNameChanging(value); + OnNameChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public string Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + string? __oldValue = name; + OnNameChanging(value); + OnNameChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_NotNullableProperty_OfUnconstrainedGenericType_WithChangedMethods() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + T value; + + partial void OnValueChanged(T? oldValue, T NewValue) + { + } + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Value + { + get => value; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("value")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(this.value, value)) + { + T? __oldValue = this.value; + OnValueChanging(value); + OnValueChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Value); + this.value = value; + OnValueChanged(value); + OnValueChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Value); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T? oldValue, T newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Value + { + get => value; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(this.value, value)) + { + T? __oldValue = this.value; + OnValueChanging(value); + OnValueChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Value); + this.value = value; + OnValueChanged(value); + OnValueChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Value); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T? oldValue, T newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithForwardedAttributes_OnPropertyAccessors() + { + string source = """ + using System.ComponentModel; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [property: Test("Property1")] + [property: Test("Property2")] + [property: Test("Property3")] + [get: Test("Get1")] + [get: Test("Get2")] + [set: Test("Set1")] + [set: Test("Set2")] + private object? a; + } + + public class TestAttribute : Attribute + { + public TestAttribute(string value) + { + } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::MyApp.TestAttribute("Property1")] + [global::MyApp.TestAttribute("Property2")] + [global::MyApp.TestAttribute("Property3")] + public object? A + { + [global::MyApp.TestAttribute("Get1")] + [global::MyApp.TestAttribute("Get2")] + get => a; + [global::MyApp.TestAttribute("Set1")] + [global::MyApp.TestAttribute("Set2")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(a, value)) + { + OnAChanging(value); + OnAChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.A); + a = value; + OnAChanged(value); + OnAChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.A); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanging(object? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanging(object? oldValue, object? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanged(object? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnAChanged(object? oldValue, object? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + /// /// Generates the requested sources /// /// The input source to process. /// The generators to apply to the input syntax tree. /// The source files to compare. - private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, params (string Filename, string Text)[] results) + private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, params (string Filename, string? Text)[] results) { VerifyGenerateSources(source, generators, LanguageVersion.CSharp10, results); } @@ -2385,7 +3412,7 @@ private static void VerifyGenerateSources(string source, IIncrementalGenerator[] /// The generators to apply to the input syntax tree. /// The language version to use. /// The source files to compare. - private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, LanguageVersion languageVersion, params (string Filename, string Text)[] results) + private static void VerifyGenerateSources(string source, IIncrementalGenerator[] generators, LanguageVersion languageVersion, params (string Filename, string? Text)[] results) { // Ensure CommunityToolkit.Mvvm and System.ComponentModel.DataAnnotations are loaded Type observableObjectType = typeof(ObservableObject); @@ -2415,22 +3442,30 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() // Ensure that no diagnostics were generated CollectionAssert.AreEquivalent(Array.Empty(), diagnostics); - foreach ((string filename, string text) in results) + foreach ((string filename, string? text) in results) { - string filePath = filename; + if (text is not null) + { + string filePath = filename; - // Update the assembly version using the version from the assembly of the input generators. - // This allows the tests to not need updates whenever the version of the MVVM Toolkit changes. - string expectedText = text.Replace("", $"\"{generators[0].GetType().Assembly.GetName().Version}\""); + // Update the assembly version using the version from the assembly of the input generators. + // This allows the tests to not need updates whenever the version of the MVVM Toolkit changes. + string expectedText = text.Replace("", $"\"{generators[0].GetType().Assembly.GetName().Version}\""); #if !ROSLYN_4_3_1_OR_GREATER - // Adjust the filenames for the legacy Roslyn 4.0 - filePath = filePath.Replace('`', '_'); + // Adjust the filenames for the legacy Roslyn 4.0 + filePath = filePath.Replace('`', '_'); #endif - SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filePath); + SyntaxTree generatedTree = outputCompilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == filePath); - Assert.AreEqual(expectedText, generatedTree.ToString()); + Assert.AreEqual(expectedText, generatedTree.ToString()); + } + else + { + // If the text is null, verify that the file was not generated at all + Assert.IsFalse(outputCompilation.SyntaxTrees.Any(tree => Path.GetFileName(tree.FilePath) == filename)); + } } GC.KeepAlive(observableObjectType); diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index fc767e712..cb866377d 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() @@ -664,7 +664,7 @@ private async Task GreetUserAsync(User user) } [TestMethod] - public void NameCollisionForGeneratedObservableProperty() + public async Task NameCollisionForGeneratedObservableProperty_PascalCaseField_Warns() { string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -674,12 +674,51 @@ namespace MyApp public partial class SampleViewModel : ObservableObject { [ObservableProperty] - private string Name; + private string {|MVVMTK0014:Name|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0014"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task NameCollisionForGeneratedObservableProperty_CamelCaseField_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string name; + } + } + """; + + // Using C# 9 here because the generated code will emit [MemberNotNull] on the property setter, which requires C# 9 + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); + } + + [TestMethod] + public async Task NameCollisionForGeneratedObservableProperty_PascalCaseProperty_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [ObservableProperty] + private string Name { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); } [TestMethod] @@ -1006,24 +1045,85 @@ public partial class A } [TestMethod] - public void InvalidContainingTypeForObservablePropertyFieldError() + public async Task InvalidContainingTypeForObservableProperty_OnField_Warns() { string source = """ + using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; namespace MyApp { public partial class MyViewModel : INotifyPropertyChanged + { + [ObservableProperty] + public int {|MVVMTK0019:number|}; + + public event PropertyChangedEventHandler PropertyChanged; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task InvalidContainingTypeForObservableProperty_OnField_InValidType_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject { [ObservableProperty] public int number; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task InvalidContainingTypeForObservableProperty_OnPartialProperty_Warns() + { + string source = """ + using System.ComponentModel; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : INotifyPropertyChanged + { + [ObservableProperty] + public int {|MVVMTK0019:Number|} { get; set; } public event PropertyChangedEventHandler PropertyChanged; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0019"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); + } + + [TestMethod] + public async Task InvalidContainingTypeForObservableProperty_OnPartialProperty_InValidType_DoesNotWarn() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public int Number { get; set; } + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp8); } [TestMethod] @@ -1206,7 +1306,26 @@ private void GreetUser(object value) } [TestMethod] - public void InvalidObservablePropertyError_Object() + public async Task InvalidObservablePropertyError_Object() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public object {|MVVMTK0024:property|}; + } + } + """; + + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); + } + + [TestMethod] + public async Task InvalidObservablePropertyError_Object_WithProperty() { string source = """ using CommunityToolkit.Mvvm.ComponentModel; @@ -1216,16 +1335,16 @@ namespace MyApp public partial class MyViewModel : ObservableObject { [ObservableProperty] - public object property; + public object {|MVVMTK0024:Property|} { get; set; } } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.Preview); } [TestMethod] - public void InvalidObservablePropertyError_PropertyChangingEventArgs() + public async Task InvalidObservablePropertyError_PropertyChangingEventArgs() { string source = """ using System.ComponentModel; @@ -1236,16 +1355,16 @@ namespace MyApp public partial class MyViewModel : ObservableObject { [ObservableProperty] - public PropertyChangingEventArgs property; + public PropertyChangingEventArgs {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] - public void InvalidObservablePropertyError_PropertyChangedEventArgs() + public async Task InvalidObservablePropertyError_PropertyChangedEventArgs() { string source = """ using System.ComponentModel; @@ -1256,16 +1375,16 @@ namespace MyApp public partial class MyViewModel : ObservableObject { [ObservableProperty] - public PropertyChangedEventArgs property; + public PropertyChangedEventArgs {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] - public void InvalidObservablePropertyError_CustomTypeDerivedFromPropertyChangedEventArgs() + public async Task InvalidObservablePropertyError_CustomTypeDerivedFromPropertyChangedEventArgs() { string source = """ using System.ComponentModel; @@ -1284,12 +1403,12 @@ public MyPropertyChangedEventArgs(string propertyName) public partial class MyViewModel : ObservableObject { [ObservableProperty] - public MyPropertyChangedEventArgs property; + public MyPropertyChangedEventArgs {|MVVMTK0024:property|}; } } """; - VerifyGeneratedDiagnostics(source, "MVVMTK0024"); + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } [TestMethod] @@ -1880,13 +1999,429 @@ internal static class IsExternalInit await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(source, LanguageVersion.CSharp9); } + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_NotTargetingWindows_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp10, + editorconfig: []); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_DoesNotWarn() + { + const string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + + namespace MyApp + { + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp10, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_NotIncluded1_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty([], [])] + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_NotIncluded2_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["OtherCommand"], [])] + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_NotIncluded3_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["OtherCommand"], ["DoStuffCommand"])] + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void DoStuff() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_Bindable_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty] + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void {|MVVMTK0046:DoStuff|}() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp10, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_Bindable_Included1_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["DoStuffCommand"], [])] + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void {|MVVMTK0046:DoStuff|}() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer_TargetingWindows_Bindable_Included2_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + using WinRT; + + namespace MyApp + { + [GeneratedBindableCustomProperty(["Blah", "", "DoStuffCommand"], [])] + public partial class SampleViewModel : ObservableObject + { + [RelayCommand] + private void {|MVVMTK0046:DoStuff|}() + { + } + } + } + + namespace WinRT + { + public class GeneratedBindableCustomPropertyAttribute(string[] a, string[] b) : Attribute + { + } + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp12, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer_NotTargetingWindows_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + [ObservableObject] + public partial class SampleViewModel : BaseType + { + } + + public class BaseType + { + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp10, + editorconfig: []); + } + + [TestMethod] + public async Task WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer_TargetingWindows_NoCsWinRTAotOptimizer_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + [ObservableObject] + public partial class SampleViewModel : BaseType + { + } + + public class BaseType + { + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp10, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true)]); + } + + [TestMethod] + public async Task WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer_TargetingWindows_NoBaseType_ObservableObject_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + [ObservableObject] + public partial class SampleViewModel + { + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp10, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + + [TestMethod] + public async Task WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer_TargetingWindows_NoBaseType_INotifyPropertyChanged_DoesNotWarn() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + [INotifyPropertyChanged] + public partial class SampleViewModel + { + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp10, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + + [TestMethod] + public async Task WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer_TargetingWindows_BaseType_ObservableObject_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + [ObservableObject] + public partial class {|MVVMTK0050:SampleViewModel|} : BaseType + { + } + + public class BaseType + { + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp10, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + + [TestMethod] + public async Task WinRTClassUsingNotifyPropertyChangedAttributesAnalyzer_TargetingWindows_BaseType_INotifyPropertyChanged_Warns() + { + const string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + [INotifyPropertyChanged] + public partial class {|MVVMTK0049:SampleViewModel|} : BaseType + { + } + + public class BaseType + { + } + """; + + await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync( + source, + LanguageVersion.CSharp10, + editorconfig: [("_MvvmToolkitIsUsingWindowsRuntimePack", true), ("CsWinRTAotOptimizerEnabled", "auto")]); + } + + /// + /// 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. + 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. - private static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion) + /// The diagnostic ids to expect for the input source code. + /// The list of diagnostic ids to ignore in the final compilation. + internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion, string[] generatorDiagnosticsIds, string[] ignoredDiagnosticIds) where TAnalyzer : DiagnosticAnalyzer, new() { await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(markdownSource, languageVersion); @@ -1903,9 +2438,9 @@ private static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration "Foo()") - string source = Regex.Replace(markdownSource, @"{\|((?:,?\w+)+):(.+)\|}", m => m.Groups[2].Value); + string source = Regex.Replace(markdownSource, @"{\|((?:,?\w+)+):(.+?)\|}", m => m.Groups[2].Value); - VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion)), generators, Array.Empty()); + VerifyGeneratedDiagnostics(CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithLanguageVersion(languageVersion)), generators, generatorDiagnosticsIds, ignoredDiagnosticIds); } /// @@ -1914,12 +2449,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 +2463,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 +2480,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 +2499,10 @@ from assembly in AppDomain.CurrentDomain.GetAssemblies() // Compute diagnostics for the final compiled output (just include errors) List outputCompilationDiagnostics = outputCompilation.GetDiagnostics().Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error).ToList(); - Assert.IsTrue(outputCompilationDiagnostics.Count == 0, $"resultingIds: {string.Join(", ", outputCompilationDiagnostics)}"); + // Filtered diagnostics + List filteredDiagnostics = outputCompilationDiagnostics.Where(diagnostic => !ignoredDiagnosticIds.Contains(diagnostic.Id)).ToList(); + + Assert.IsTrue(filteredDiagnostics.Count == 0, $"resultingIds: {string.Join(", ", filteredDiagnostics)}"); } GC.KeepAlive(observableObjectType); diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_INotifyPropertyChangedAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_INotifyPropertyChangedAttribute.cs index 095a9fcb0..d4992254e 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_INotifyPropertyChangedAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_INotifyPropertyChangedAttribute.cs @@ -105,6 +105,37 @@ public partial class SampleModelWithINPCAndObservableProperties private int y; } +#if ROSLYN_4_12_0_OR_GREATER + [TestMethod] + public void Test_INotifyPropertyChanged_WithGeneratedPartialProperties() + { + Assert.IsTrue(typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(SampleModelWithINPCAndObservablePartialProperties))); + Assert.IsFalse(typeof(INotifyPropertyChanging).IsAssignableFrom(typeof(SampleModelWithINPCAndObservablePartialProperties))); + + SampleModelWithINPCAndObservablePartialProperties model = new(); + List eventArgs = new(); + + model.PropertyChanged += (s, e) => eventArgs.Add(e); + + model.X = 42; + model.Y = 66; + + Assert.AreEqual(eventArgs.Count, 2); + Assert.AreEqual(eventArgs[0].PropertyName, nameof(SampleModelWithINPCAndObservablePartialProperties.X)); + Assert.AreEqual(eventArgs[1].PropertyName, nameof(SampleModelWithINPCAndObservablePartialProperties.Y)); + } + + [INotifyPropertyChanged] + public partial class SampleModelWithINPCAndObservablePartialProperties + { + [ObservableProperty] + public partial int X { get; set; } + + [ObservableProperty] + public partial int Y { get; set; } + } +#endif + [TestMethod] public void Test_INotifyPropertyChanged_WithGeneratedProperties_ExternalNetStandard20Assembly() { diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index 5a557206f..65ab55fe9 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs @@ -709,12 +709,8 @@ public void Test_ObservableProperty_NullabilityAnnotations_Complex() NullabilityInfo rightInfo2 = rightInnerInfo.GenericTypeArguments[2]; Assert.AreEqual(typeof(object), rightInfo2.Type); - //Assert.AreEqual(NullabilityState.NotNull, rightInfo2.ReadState); - //Assert.AreEqual(NullabilityState.NotNull, rightInfo2.WriteState); - - // The commented out lines are to work around a bug in the NullabilityInfo API in .NET 6. - // This has been fixed for .NET 7: https://github.com/dotnet/runtime/pull/63556. The test - // cases above can be uncommented when the .NET 7 target (or a more recent version) is added. + Assert.AreEqual(NullabilityState.NotNull, rightInfo2.ReadState); + Assert.AreEqual(NullabilityState.NotNull, rightInfo2.WriteState); } #endif @@ -1040,6 +1036,39 @@ public void Test_ObservableProperty_ModelWithObservablePropertyWithUnderscoreAnd Assert.IsTrue(model.IsReadOnly); } + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void Test_ObservableProperty_ModelWithDependentPropertyAndPropertyChanging() + { + ModelWithDependentPropertyAndPropertyChanging model = new(); + + List changingArgs = new(); + List changedArgs = new(); + + model.PropertyChanging += (s, e) => changingArgs.Add(e.PropertyName); + model.PropertyChanged += (s, e) => changedArgs.Add(e.PropertyName); + + model.Name = "Bob"; + + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndPropertyChanging.Name), nameof(ModelWithDependentPropertyAndPropertyChanging.FullName) }, changingArgs); + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndPropertyChanging.Name), nameof(ModelWithDependentPropertyAndPropertyChanging.FullName) }, changedArgs); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/711 + [TestMethod] + public void Test_ObservableProperty_ModelWithDependentPropertyAndNoPropertyChanging() + { + ModelWithDependentPropertyAndNoPropertyChanging model = new(); + + List changedArgs = new(); + + model.PropertyChanged += (s, e) => changedArgs.Add(e.PropertyName); + + model.Name = "Alice"; + + CollectionAssert.AreEqual(new[] { nameof(ModelWithDependentPropertyAndNoPropertyChanging.Name), nameof(ModelWithDependentPropertyAndNoPropertyChanging.FullName) }, changedArgs); + } + #if NET6_0_OR_GREATER [TestMethod] public void Test_ObservableProperty_MemberNotNullAttributeIsPresent() @@ -1745,4 +1774,49 @@ public enum NegativeEnum Problem = -1, OK = 0 } + + private sealed partial class ModelWithDependentPropertyAndPropertyChanging : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private string? name; + + public string? FullName => ""; + } + + [INotifyPropertyChanged(IncludeAdditionalHelperMethods = false)] + private sealed partial class ModelWithDependentPropertyAndNoPropertyChanging + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + private string? name; + + public string? FullName => ""; + } + +#if NET6_0_OR_GREATER + // See https://github.com/CommunityToolkit/dotnet/issues/939 + public partial class ModelWithSecondaryPropertySetFromGeneratedSetter_DoesNotWarn : ObservableObject + { + [ObservableProperty] + [set: MemberNotNull(nameof(B))] + private string a; + + // This type validates forwarding attributes on generated accessors. In particular, there should + // be no nullability warning on this constructor (CS8618), thanks to 'MemberNotNullAttribute("B")' + // being forwarded to the generated setter in the generated property (see linked issue). + public ModelWithSecondaryPropertySetFromGeneratedSetter_DoesNotWarn() + { + A = ""; + } + + public string B { get; private set; } + + [MemberNotNull(nameof(B))] + partial void OnAChanged(string? oldValue, string newValue) + { + B = ""; + } + } +#endif } diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableValidator.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableValidator.cs index af4b9f4be..7b09d1073 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableValidator.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableValidator.cs @@ -607,7 +607,20 @@ public void Test_ObservableValidator_ValidateAllProperties_IncludeInheritedPrope Assert.IsFalse(model.HasErrors); Assert.IsTrue(events.Count == 1); - Assert.IsTrue(events.Any(e => e.PropertyName == nameof(DerivedModelWithValidatableProperties.Name))); } + Assert.IsTrue(events.Any(e => e.PropertyName == nameof(DerivedModelWithValidatableProperties.Name))); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/881 + [TestMethod] + public void Test_ObservableValidator_HasErrors_IncludeNonAutogenerateAttribute() + { + DerivedModelWithValidatableProperties model = new(); + + DisplayAttribute? displayAttribute = model.GetType().GetProperty(nameof(ObservableValidator.HasErrors))?.GetCustomAttribute(); + + Assert.IsNotNull(displayAttribute); + Assert.IsFalse(displayAttribute.AutoGenerateField); + } public class Person : ObservableValidator { diff --git a/version.json b/version.json index 6bb3919ac..b2cb92bde 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "8.2.2-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