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 2c852ca11..6510902b7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -25,7 +25,7 @@ - + diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 81b40fd16..50ebd69aa 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,7 +34,7 @@ jobs: # Test solution # - # Run .NET 8 unit tests + # 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 @@ -42,10 +42,6 @@ jobs: - 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/dotnet Community Toolkit.sln b/dotnet Community Toolkit.sln index 688e32367..dc557355a 100644 --- a/dotnet Community Toolkit.sln +++ b/dotnet Community Toolkit.sln @@ -36,7 +36,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{88C6FFBE-3 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Diagnostics", "src\CommunityToolkit.Diagnostics\CommunityToolkit.Diagnostics.csproj", "{76F89522-CA28-458D-801D-947AB033A758}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn401", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.csproj", "{E24D1146-5AD8-498F-A518-4890D8BF4937}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.csproj", "{E24D1146-5AD8-498F-A518-4890D8BF4937}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Diagnostics.UnitTests", "tests\CommunityToolkit.Diagnostics.UnitTests\CommunityToolkit.Diagnostics.UnitTests.csproj", "{35E48D4D-6433-4B70-98A9-BA544921EE04}" EndProject @@ -61,27 +61,35 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Inter EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.SourceGenerators", "src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.shproj", "{5E7F1212-A54B-40CA-98C5-1FF5CD1A1638}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn431", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.csproj", "{DF455C40-B18E-4890-8758-7CCCB5CA7052}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031", "src\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.csproj", "{DF455C40-B18E-4890-8758-7CCCB5CA7052}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.UnitTests", "tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.shproj", "{B8DCD82E-B53B-4249-AD4E-F9B99ACB9334}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn401.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn401.UnitTests\CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj", "{AD9C3223-8E37-4FD4-A0D4-A45119551D3A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn4001.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4001.UnitTests\CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj", "{AD9C3223-8E37-4FD4-A0D4-A45119551D3A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn431.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn431.UnitTests\CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj", "{5B44F7F1-DCA2-4776-924E-A266F7BBF753}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.Roslyn4031.UnitTests", "tests\CommunityToolkit.Mvvm.Roslyn4031.UnitTests\CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj", "{5B44F7F1-DCA2-4776-924E-A266F7BBF753}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.SourceGenerators.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.UnitTests.shproj", "{FB59CE88-7732-4A63-B5BD-AC5681B7DA1A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj", "{F3799252-7A66-4533-89D8-B3C312052D95}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj", "{F3799252-7A66-4533-89D8-B3C312052D95}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj", "{FE3EA695-EA0F-4E5F-9257-E059AAA23B10}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests", "tests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests\CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj", "{FE3EA695-EA0F-4E5F-9257-E059AAA23B10}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "CommunityToolkit.Mvvm.ExternalAssembly", "tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.shproj", "{E827A9CD-405F-43E4-84C7-68CC7E845CDC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj", "{ECFE93AA-4B98-4292-B3FA-9430D513B4F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj", "{ECFE93AA-4B98-4292-B3FA-9430D513B4F9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj", "{4FCD501C-1BB5-465C-AD19-356DAB6600C6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031", "tests\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031\CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj", "{4FCD501C-1BB5-465C-AD19-356DAB6600C6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommunityToolkit.Mvvm.CodeFixers", "src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.csproj", "{E79DCA2A-4C59-499F-85BD-F45215ED6B72}" +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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -457,6 +465,66 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -479,6 +547,7 @@ Global {E827A9CD-405F-43E4-84C7-68CC7E845CDC} = {B30036C4-D514-4E5B-A323-587A061772CE} {ECFE93AA-4B98-4292-B3FA-9430D513B4F9} = {B30036C4-D514-4E5B-A323-587A061772CE} {4FCD501C-1BB5-465C-AD19-356DAB6600C6} = {B30036C4-D514-4E5B-A323-587A061772CE} + {C342302D-A263-42D6-B8EE-01DEF8192690} = {B30036C4-D514-4E5B-A323-587A061772CE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5403B0C4-F244-4F73-A35C-FE664D0F4345} @@ -487,14 +556,19 @@ Global tests\CommunityToolkit.Mvvm.ExternalAssembly\CommunityToolkit.Mvvm.ExternalAssembly.projitems*{4fcd501c-1bb5-465c-ad19-356dab6600c6}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{5b44f7f1-dca2-4776-924e-a266f7bbf753}*SharedItemsImports = 5 src\CommunityToolkit.Mvvm.SourceGenerators\CommunityToolkit.Mvvm.SourceGenerators.projitems*{5e7f1212-a54b-40ca-98c5-1ff5cd1a1638}*SharedItemsImports = 13 + src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{98572004-d29a-486e-8053-6d409557ce44}*SharedItemsImports = 5 + src\CommunityToolkit.Mvvm.CodeFixers\CommunityToolkit.Mvvm.CodeFixers.projitems*{a2ebda90-b720-430d-83f5-c6bcc355232c}*SharedItemsImports = 13 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{ad9c3223-8e37-4fd4-a0d4-a45119551d3a}*SharedItemsImports = 5 tests\CommunityToolkit.Mvvm.UnitTests\CommunityToolkit.Mvvm.UnitTests.projitems*{b8dcd82e-b53b-4249-ad4e-f9b99acb9334}*SharedItemsImports = 13 + 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..1880a952c --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.403", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Common/CommunityToolkit.Common.csproj b/src/CommunityToolkit.Common/CommunityToolkit.Common.csproj index ddf81b30c..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;net8.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 fec859625..5f14958a6 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;net8.0 + netstandard2.0;netstandard2.1;net8.0 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 2cf7770af..1f17262a5 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;net8.0 + netstandard2.0;netstandard2.1;net7.0;net8.0 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/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 dfae52a13..e7e64c788 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -210,7 +210,7 @@ public static ReadOnlySpan2D AsSpan2D(this ReadOnlySpan span, int offse /// The reference to the target item to get the index for. /// The index of within , or -1. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int IndexOf(this ReadOnlySpan span, in T value) + public static unsafe int IndexOf(this ReadOnlySpan span, ref readonly T value) { ref T r0 = ref MemoryMarshal.GetReference(span); ref T r1 = ref Unsafe.AsRef(in value); diff --git a/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs index 7fb71727a..21f0725e7 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -148,10 +148,15 @@ public static Span Cast(this Span span) /// The reference to the target item to get the index for. /// The index of within , or -1. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe int IndexOf(this Span span, ref T value) + public static unsafe int IndexOf(this Span span, ref readonly T value) { ref T r0 = ref MemoryMarshal.GetReference(span); - IntPtr byteOffset = Unsafe.ByteOffset(ref r0, ref value); + IntPtr byteOffset = +#if NET8_0_OR_GREATER + Unsafe.ByteOffset(ref r0, in value); +#else + Unsafe.ByteOffset(ref r0, ref Unsafe.AsRef(in value)); +#endif nint elementOffset = byteOffset / (nint)(uint)sizeof(T); 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..40339da45 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.projitems @@ -0,0 +1,20 @@ + + + + $(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..114924f1c --- /dev/null +++ b/src/CommunityToolkit.Mvvm.CodeFixers/CommunityToolkit.Mvvm.CodeFixers.props @@ -0,0 +1,43 @@ + + + + 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 + + + 4.12.0-3.final + + + + + + + + + + + \ 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..a8067ace3 --- /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 update the class declaration to inherit from ObservableObject instead + context.RegisterCodeFix( + CodeAction.Create( + title: "Use a partial property", + createChangedDocument: token => ConvertToPartialProperty(context.Document, root, fieldDeclaration, 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.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..730713ab0 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -75,3 +75,21 @@ 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/mvvmtk0050 diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems index c4cbb324e..cc4351664 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.projitems @@ -40,17 +40,30 @@ + + + + + + + + + + + - + + + @@ -64,6 +77,9 @@ + + + diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props b/src/CommunityToolkit.Mvvm.SourceGenerators/CommunityToolkit.Mvvm.SourceGenerators.props index 6c5a90f5e..0e176df51 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 + + + 4.12.0-3.final 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 8c3bca4da..c2c8feed6 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/PropertyInfo.cs @@ -3,36 +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 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 or not the generated property should be marked as required. /// Whether the old property value is being directly referenced. -/// Indicates whether the property is of a reference type or an unconstrained type parameter. +/// Indicates whether the property is of a reference type or an unconstrained type parameter. /// Indicates whether to include nullability annotations on the setter. /// Indicates whether to annotate the setter as requiring unreferenced code. /// The sequence of forwarded attributes for the generated property. internal sealed record PropertyInfo( + SyntaxKind AnnotatedMemberKind, string TypeNameWithNullabilityAnnotations, string FieldName, string PropertyName, + Accessibility PropertyAccessibility, + Accessibility GetterAccessibility, + Accessibility SetterAccessibility, EquatableArray PropertyChangingNames, EquatableArray PropertyChangedNames, EquatableArray NotifiedCommandNames, bool NotifyPropertyChangedRecipients, bool NotifyDataErrorInfo, + bool IsRequired, 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 d029295bd..3505d05f7 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.Execute.cs @@ -31,36 +31,120 @@ partial class ObservablePropertyGenerator internal static class Execute { /// - /// Processes a given field. + /// Checks whether an input syntax node is a candidate property declaration for the generator. /// - /// The instance to process. - /// The input instance to process. + /// The input syntax node to check. + /// The used to cancel the operation, if needed. + /// Whether is a candidate property declaration. + public static bool IsCandidatePropertyDeclaration(SyntaxNode node, CancellationToken token) + { + // Matches a valid field declaration, for legacy support + static bool IsCandidateField(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) + { + // The node must represent a field declaration + if (node is not VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { AttributeLists.Count: > 0 } fieldNode } }) + { + containingTypeNode = null; + + return false; + } + + containingTypeNode = (TypeDeclarationSyntax?)fieldNode.Parent; + + return true; + } + + // Check that the target is a valid field or partial property + if (!IsCandidateField(node, out TypeDeclarationSyntax? parentNode) && + !InvalidPropertyLevelObservablePropertyAttributeAnalyzer.IsValidCandidateProperty(node, out parentNode)) + { + return false; + } + + // The candidate member must be in a type with a base type (as it must derive from ObservableObject) + return parentNode?.IsTypeDeclarationWithOrPotentiallyWithBaseTypes() == true; + } + + /// + /// Checks whether a given candidate node is valid given a compilation. + /// + /// The instance to process. + /// The instance for the current run. + /// Whether is valid. + public static bool IsCandidateValidForCompilation(SyntaxNode node, SemanticModel semanticModel) + { + // At least C# 8 is always required + if (!semanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) + { + return false; + } + + // If the target is a property, we only support using C# preview. + // This is because the generator is relying on the 'field' keyword. + if (node is PropertyDeclarationSyntax && !semanticModel.Compilation.IsLanguageVersionPreview()) + { + return false; + } + + // All other cases are supported, the syntax filter is already validating that + return true; + } + + /// + /// Gets the candidate after the initial filtering. + /// + /// The input syntax node to convert. + /// The resulting instance. + public static MemberDeclarationSyntax GetCandidateMemberDeclaration(SyntaxNode node) + { + // If the node is a property declaration, just return it directly. Note that we don't have + // to check whether we're using Roslyn 4.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) { + // Special case for downlevel: if a field has the 'partial' modifier, ignore it. + // This is because older compilers might parse a partial property as a field. + // In that case, we ignore it here and rely on Roslyn producing a build error. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration) && memberSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + propertyInfo = null; + diagnostics = ImmutableArray.Empty; + + return false; + } + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); // Validate the target type - if (!IsTargetTypeValid(fieldSymbol, out bool shouldInvokeOnPropertyChanging)) + if (!IsTargetTypeValid(memberSymbol, out bool shouldInvokeOnPropertyChanging)) { - builder.Add( - InvalidContainingTypeForObservablePropertyFieldError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); - propertyInfo = null; diagnostics = builder.ToImmutable(); @@ -69,25 +153,20 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); - // Override the property changing support if explicitly disabled - shouldInvokeOnPropertyChanging &= GetEnableINotifyPropertyChangingSupport(options); + // Override the property changing support if explicitly disabled. + // This setting is enabled by default, for backwards compatibility. + shouldInvokeOnPropertyChanging &= options.GetMSBuildBooleanPropertyValue("MvvmToolkitEnableINotifyPropertyChangingSupport", defaultValue: true); token.ThrowIfCancellationRequested(); // 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(); @@ -100,14 +179,8 @@ 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(); @@ -125,15 +198,15 @@ public static bool TryGetInfo( bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients = false; bool hasOrInheritsClassLevelNotifyDataErrorInfo = false; bool hasAnyValidationAttributes = false; - bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(fieldSymbol, propertyName); + bool isOldPropertyValueDirectlyReferenced = IsOldPropertyValueDirectlyReferenced(memberSymbol, propertyName); token.ThrowIfCancellationRequested(); // Get the nullability info for the property GetNullabilityInfo( - fieldSymbol, + memberSymbol, semanticModel, - out bool isReferenceTypeOrUnconstraindTypeParameter, + out bool isReferenceTypeOrUnconstrainedTypeParameter, out bool includeMemberNotNullOnSetAccessor); token.ThrowIfCancellationRequested(); @@ -142,7 +215,7 @@ public static bool TryGetInfo( propertyChangedNames.Add(propertyName); // Get the class-level [NotifyPropertyChangedRecipients] setting, if any - if (TryGetIsNotifyingRecipients(fieldSymbol, out bool isBroadcastTargetValid)) + if (TryGetIsNotifyingRecipients(memberSymbol, out bool isBroadcastTargetValid)) { notifyRecipients = isBroadcastTargetValid; hasOrInheritsClassLevelNotifyPropertyChangedRecipients = true; @@ -151,7 +224,7 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Get the class-level [NotifyDataErrorInfo] setting, if any - if (TryGetNotifyDataErrorInfo(fieldSymbol, out bool isValidationTargetValid)) + if (TryGetNotifyDataErrorInfo(memberSymbol, out bool isValidationTargetValid)) { notifyDataErrorInfo = isValidationTargetValid; hasOrInheritsClassLevelNotifyDataErrorInfo = true; @@ -160,19 +233,19 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); // Gather attributes info - foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) + foreach (AttributeData attributeData in memberSymbol.GetAttributes()) { token.ThrowIfCancellationRequested(); // Gather dependent property and command names - if (TryGatherDependentPropertyChangedNames(fieldSymbol, attributeData, in propertyChangedNames, in builder) || - TryGatherDependentCommandNames(fieldSymbol, attributeData, in notifiedCommandNames, in builder)) + if (TryGatherDependentPropertyChangedNames(memberSymbol, attributeData, in propertyChangedNames, in builder) || + TryGatherDependentCommandNames(memberSymbol, attributeData, in notifiedCommandNames, in builder)) { continue; } // Check whether the property should also notify recipients - if (TryGetIsNotifyingRecipients(fieldSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyPropertyChangedRecipients, out isBroadcastTargetValid)) + if (TryGetIsNotifyingRecipients(memberSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyPropertyChangedRecipients, out isBroadcastTargetValid)) { notifyRecipients = isBroadcastTargetValid; @@ -180,7 +253,7 @@ public static bool TryGetInfo( } // Check whether the property should also be validated - if (TryGetNotifyDataErrorInfo(fieldSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyDataErrorInfo, out isValidationTargetValid)) + if (TryGetNotifyDataErrorInfo(memberSymbol, attributeData, in builder, hasOrInheritsClassLevelNotifyDataErrorInfo, out isValidationTargetValid)) { notifyDataErrorInfo = isValidationTargetValid; @@ -192,96 +265,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); } @@ -290,9 +306,10 @@ public static bool TryGetInfo( { builder.Add( MissingValidationAttributesForNotifyDataErrorInfoError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), + memberSymbol.ContainingType, + memberSymbol.Name); } token.ThrowIfCancellationRequested(); @@ -316,17 +333,43 @@ public static bool TryGetInfo( token.ThrowIfCancellationRequested(); + // Retrieve the accessibility values for all components + if (!TryGetAccessibilityModifiers( + memberSyntax, + memberSymbol, + out Accessibility propertyAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility)) + { + propertyInfo = null; + diagnostics = builder.ToImmutable(); + + return false; + } + + token.ThrowIfCancellationRequested(); + + // Check whether the property should be required + bool isRequired = GetIsRequiredProperty(memberSymbol); + + token.ThrowIfCancellationRequested(); + propertyInfo = new PropertyInfo( + memberSyntax.Kind(), typeNameWithNullabilityAnnotations, fieldName, propertyName, + propertyAccessibility, + getterAccessibility, + setterAccessibility, effectivePropertyChangingNames, effectivePropertyChangedNames, notifiedCommandNames.ToImmutable(), notifyRecipients, notifyDataErrorInfo, + isRequired, isOldPropertyValueDirectlyReferenced, - isReferenceTypeOrUnconstraindTypeParameter, + isReferenceTypeOrUnconstrainedTypeParameter, includeMemberNotNullOnSetAccessor, includeRequiresUnreferencedCodeOnSetAccessor, forwardedAttributes.ToImmutable()); @@ -336,43 +379,22 @@ public static bool TryGetInfo( return true; } - /// - /// Gets the value for the "MvvmToolkitEnableINotifyPropertyChangingSupport" property. - /// - /// The options in use for the generator. - /// The value for the "MvvmToolkitEnableINotifyPropertyChangingSupport" property. - public static bool GetEnableINotifyPropertyChangingSupport(AnalyzerConfigOptions options) - { - if (options.TryGetValue("build_property.MvvmToolkitEnableINotifyPropertyChangingSupport", out string? propertyValue)) - { - if (bool.TryParse(propertyValue, out bool enableINotifyPropertyChangingSupport)) - { - return enableINotifyPropertyChangingSupport; - } - } - - // This setting is enabled by default, for backwards compatibility. - // Note that this path should never be reached, as the default - // value is also set in a .targets file bundled in the package. - return true; - } - /// /// Validates the containing type for a given field being annotated. /// - /// 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; @@ -385,7 +407,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 @@ -405,13 +427,13 @@ private static bool IsGeneratedPropertyInvalid(string propertyName, ITypeSymbol /// /// Tries to gather dependent properties from the given attribute. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The target collection of dependent property names to populate. /// The current collection of gathered diagnostics. /// Whether or not was an attribute containing any dependent properties. private static bool TryGatherDependentPropertyChangedNames( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder propertyChangedNames, in ImmutableArrayBuilder diagnostics) @@ -419,16 +441,16 @@ private static bool TryGatherDependentPropertyChangedNames( // Validates a property name using existing properties bool IsPropertyNameValid(string propertyName) { - return fieldSymbol.ContainingType.GetAllMembers(propertyName).OfType().Any(); + return memberSymbol.ContainingType.GetAllMembers(propertyName).OfType().Any(); } // Validate a property name including generated properties too bool IsPropertyNameValidWithGeneratedMembers(string propertyName) { - foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers()) + foreach (ISymbol member in memberSymbol.ContainingType.GetAllMembers()) { if (member is IFieldSymbol otherFieldSymbol && - !SymbolEqualityComparer.Default.Equals(fieldSymbol, otherFieldSymbol) && + !SymbolEqualityComparer.Default.Equals(memberSymbol, otherFieldSymbol) && otherFieldSymbol.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") && propertyName == GetGeneratedPropertyName(otherFieldSymbol)) { @@ -455,9 +477,9 @@ bool IsPropertyNameValidWithGeneratedMembers(string propertyName) { diagnostics.Add( NotifyPropertyChangedForInvalidTargetError, - fieldSymbol, + memberSymbol, dependentPropertyName ?? "", - fieldSymbol.ContainingType); + memberSymbol.ContainingType); } } @@ -470,13 +492,13 @@ bool IsPropertyNameValidWithGeneratedMembers(string propertyName) /// /// Tries to gather dependent commands from the given attribute. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The target collection of dependent command names to populate. /// The current collection of gathered diagnostics. /// Whether or not was an attribute containing any dependent commands. private static bool TryGatherDependentCommandNames( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder notifiedCommandNames, in ImmutableArrayBuilder diagnostics) @@ -486,7 +508,7 @@ bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembe { // Each target must be a string matching the name of a property from the containing type of the annotated field, and the // property must be of type IRelayCommand, or any type that implements that interface (to avoid generating invalid code). - if (fieldSymbol.ContainingType.GetAllMembers(commandName).OfType().FirstOrDefault() is IPropertySymbol propertySymbol) + if (memberSymbol.ContainingType.GetAllMembers(commandName).OfType().FirstOrDefault() is IPropertySymbol propertySymbol) { // If there is a property member with the specified name, check that it's valid. If it isn't, the // target is definitely not valid, and the additional checks below can just be skipped. The property @@ -515,7 +537,7 @@ bool IsCommandNameValid(string commandName, out bool shouldLookForGeneratedMembe // Validate a command name including generated command too bool IsCommandNameValidWithGeneratedMembers(string commandName) { - foreach (ISymbol member in fieldSymbol.ContainingType.GetAllMembers()) + foreach (ISymbol member in memberSymbol.ContainingType.GetAllMembers()) { if (member is IMethodSymbol methodSymbol && methodSymbol.HasAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.Input.RelayCommandAttribute") && @@ -545,9 +567,9 @@ bool IsCommandNameValidWithGeneratedMembers(string commandName) { diagnostics.Add( NotifyCanExecuteChangedForInvalidTargetError, - fieldSymbol, + memberSymbol, commandName ?? "", - fieldSymbol.ContainingType); + memberSymbol.ContainingType); } } @@ -560,16 +582,16 @@ bool IsCommandNameValidWithGeneratedMembers(string commandName) /// /// Checks whether a given generated property should also notify recipients. /// - /// The input instance to process. + /// The input instance to process. /// Whether or not the the property is in a valid target that can notify recipients. - /// Whether or not the generated property for is in a type annotated with [NotifyPropertyChangedRecipients]. - private static bool TryGetIsNotifyingRecipients(IFieldSymbol fieldSymbol, out bool isBroadcastTargetValid) + /// Whether or not the generated property for is in a type annotated with [NotifyPropertyChangedRecipients]. + private static bool TryGetIsNotifyingRecipients(ISymbol memberSymbol, out bool isBroadcastTargetValid) { - if (fieldSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") == true) + if (memberSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyPropertyChangedRecipientsAttribute") == true) { // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || - fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || + memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) { isBroadcastTargetValid = true; @@ -591,14 +613,14 @@ private static bool TryGetIsNotifyingRecipients(IFieldSymbol fieldSymbol, out bo /// /// Checks whether a given generated property should also notify recipients. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The current collection of gathered diagnostics. - /// Indicates wether the containing type of has or inherits [NotifyPropertyChangedRecipients]. + /// Indicates wether the containing type of has or inherits [NotifyPropertyChangedRecipients]. /// Whether or not the the property is in a valid target that can notify recipients. - /// Whether or not the generated property for used [NotifyPropertyChangedRecipients]. + /// Whether or not the generated property for used [NotifyPropertyChangedRecipients]. private static bool TryGetIsNotifyingRecipients( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder diagnostics, bool hasOrInheritsClassLevelNotifyPropertyChangedRecipients, @@ -610,15 +632,16 @@ private static bool TryGetIsNotifyingRecipients( if (hasOrInheritsClassLevelNotifyPropertyChangedRecipients) { diagnostics.Add( - UnnecessaryNotifyPropertyChangedRecipientsAttributeOnFieldWarning, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + UnnecessaryNotifyPropertyChangedRecipientsAttributeOnMemberWarning, + memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), + memberSymbol.ContainingType, + memberSymbol.Name); } // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || - fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipient") || + memberSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableRecipientAttribute")) { isBroadcastTargetValid = true; @@ -627,10 +650,10 @@ private static bool TryGetIsNotifyingRecipients( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( - InvalidContainingTypeForNotifyPropertyChangedRecipientsFieldError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + InvalidContainingTypeForNotifyPropertyChangedRecipientsMemberError, + memberSymbol, + memberSymbol.ContainingType, + memberSymbol.Name); isBroadcastTargetValid = false; @@ -645,15 +668,15 @@ private static bool TryGetIsNotifyingRecipients( /// /// Checks whether a given generated property should also validate its value. /// - /// The input instance to process. + /// The input instance to process. /// Whether or not the the property is in a valid target that can validate values. - /// Whether or not the generated property for used [NotifyDataErrorInfo]. - private static bool TryGetNotifyDataErrorInfo(IFieldSymbol fieldSymbol, out bool isValidationTargetValid) + /// Whether or not the generated property for used [NotifyDataErrorInfo]. + private static bool TryGetNotifyDataErrorInfo(ISymbol memberSymbol, out bool isValidationTargetValid) { - if (fieldSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") == true) + if (memberSymbol.ContainingType?.HasOrInheritsAttributeWithFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.NotifyDataErrorInfoAttribute") == true) { // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { isValidationTargetValid = true; @@ -674,14 +697,14 @@ private static bool TryGetNotifyDataErrorInfo(IFieldSymbol fieldSymbol, out bool /// /// Checks whether a given generated property should also validate its value. /// - /// The input instance to process. - /// The instance for . + /// The input instance to process. + /// The instance for . /// The current collection of gathered diagnostics. - /// Indicates wether the containing type of has or inherits [NotifyDataErrorInfo]. + /// Indicates whether the containing type of has or inherits [NotifyDataErrorInfo]. /// Whether or not the the property is in a valid target that can validate values. - /// Whether or not the generated property for used [NotifyDataErrorInfo]. + /// Whether or not the generated property for used [NotifyDataErrorInfo]. private static bool TryGetNotifyDataErrorInfo( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, AttributeData attributeData, in ImmutableArrayBuilder diagnostics, bool hasOrInheritsClassLevelNotifyDataErrorInfo, @@ -693,14 +716,15 @@ private static bool TryGetNotifyDataErrorInfo( if (hasOrInheritsClassLevelNotifyDataErrorInfo) { diagnostics.Add( - UnnecessaryNotifyDataErrorInfoAttributeOnFieldWarning, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + UnnecessaryNotifyDataErrorInfoAttributeOnMemberWarning, + memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), + memberSymbol.ContainingType, + memberSymbol.Name); } // If the containing type is valid, track it - if (fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) + if (memberSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservableValidator")) { isValidationTargetValid = true; @@ -710,9 +734,10 @@ private static bool TryGetNotifyDataErrorInfo( // Otherwise just emit the diagnostic and then ignore the attribute diagnostics.Add( MissingObservableValidatorInheritanceForNotifyDataErrorInfoError, - fieldSymbol, - fieldSymbol.ContainingType, - fieldSymbol.Name); + memberSymbol, + memberSymbol.Kind.ToFieldOrPropertyKeyword(), + memberSymbol.ContainingType, + memberSymbol.Name); isValidationTargetValid = false; @@ -727,13 +752,13 @@ private static bool TryGetNotifyDataErrorInfo( /// /// Checks whether the generated code has to directly reference the old property value. /// - /// The input instance to process. + /// The input instance to process. /// The name of the property being generated. /// Whether the generated code needs direct access to the old property value. - private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbol, string propertyName) + private static bool IsOldPropertyValueDirectlyReferenced(ISymbol memberSymbol, string propertyName) { // Check OnChanging( oldValue, newValue) first - foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changing")) + foreach (ISymbol symbol in memberSymbol.ContainingType.GetMembers($"On{propertyName}Changing")) { // No need to be too specific as we're not expecting false positives (which also wouldn't really // cause any problems anyway, just produce slightly worse codegen). Just checking the number of @@ -745,7 +770,7 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo } // Do the same for OnChanged( oldValue, newValue) - foreach (ISymbol symbol in fieldSymbol.ContainingType.GetMembers($"On{propertyName}Changed")) + foreach (ISymbol symbol in memberSymbol.ContainingType.GetMembers($"On{propertyName}Changed")) { if (symbol is IMethodSymbol { Parameters.Length: 2 }) { @@ -759,13 +784,13 @@ private static bool IsOldPropertyValueDirectlyReferenced(IFieldSymbol fieldSymbo /// /// Gets the nullability info on the generated property /// - /// The input instance to process. + /// The input instance to process. /// The instance for the current run. /// Whether the property type supports nullability. /// Whether should be used on the setter. /// private static void GetNullabilityInfo( - IFieldSymbol fieldSymbol, + ISymbol memberSymbol, SemanticModel semanticModel, out bool isReferenceTypeOrUnconstraindTypeParameter, out bool includeMemberNotNullOnSetAccessor) @@ -773,7 +798,19 @@ private static void GetNullabilityInfo( // We're using IsValueType here and not IsReferenceType to also cover unconstrained type parameter cases. // This will cover both reference types as well T when the constraints are not struct or unmanaged. // If this is true, it means the field storage can potentially be in a null state (even if not annotated). - isReferenceTypeOrUnconstraindTypeParameter = !fieldSymbol.Type.IsValueType; + isReferenceTypeOrUnconstraindTypeParameter = !GetPropertyType(memberSymbol).IsValueType; + + // Special case if the target member is a partial property. In this case, the type should always match the + // declared type of the property declaration, and there is no need for the attribute on the setter. This + // is because assigning the property in the constructor will directly assign to the backing field, and not + // doing so from the constructor will cause Roslyn to emit a warning. Additionally, Roslyn can always see + // that the backing field is being assigned from the setter, so the attribute is just never needed here. + if (memberSymbol.Kind is SymbolKind.Property) + { + includeMemberNotNullOnSetAccessor = false; + + return; + } // This is used to avoid nullability warnings when setting the property from a constructor, in case the field // was marked as not nullable. Nullability annotations are assumed to always be enabled to make the logic simpler. @@ -795,10 +832,202 @@ private static void GetNullabilityInfo( // Of course, this can only be the case if the field type is also of a type that could be in a null state. includeMemberNotNullOnSetAccessor = isReferenceTypeOrUnconstraindTypeParameter && - fieldSymbol.Type.NullableAnnotation != NullableAnnotation.Annotated && + GetPropertyType(memberSymbol).NullableAnnotation != NullableAnnotation.Annotated && semanticModel.Compilation.HasAccessibleTypeWithMetadataName("System.Diagnostics.CodeAnalysis.MemberNotNullAttribute"); } + /// + /// Gathers all forwarded attributes from the given member syntax. + /// + /// The instance to process. + /// The input instance to process. + /// The instance for the current run. + /// The collection of forwarded attributes to add new ones to. + /// The current collection of gathered diagnostics. + /// The cancellation token for the current operation. + private static void GatherLegacyForwardedAttributes( + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, + SemanticModel semanticModel, + in ImmutableArrayBuilder forwardedAttributes, + in ImmutableArrayBuilder diagnostics, + CancellationToken token) + { + // For properties, we never need to forward any attributes with explicit targets either, because + // they can already "just work" when used with 'field'. As for 'get' and 'set', they can just be + // added directly to the partial declarations of the property accessors. + if (memberSyntax.IsKind(SyntaxKind.PropertyDeclaration)) + { + return; + } + + // Also track the current attribute for forwarding if it is of any of the following types: + // - Display attributes (System.ComponentModel.DataAnnotations.DisplayAttribute) + // - UI hint attributes(System.ComponentModel.DataAnnotations.UIHintAttribute) + // - Scaffold column attributes (System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute) + // - Editable attributes (System.ComponentModel.DataAnnotations.EditableAttribute) + // - Key attributes (System.ComponentModel.DataAnnotations.KeyAttribute) + // + // All of these have special handling and are always forwarded when a field is being targeted. + // That is because these attributes really only mean anything when used on generated properties. + foreach (AttributeData attributeData in memberSymbol.GetAttributes()) + { + if (attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.UIHintAttribute") == true || + attributeData.AttributeClass?.HasOrInheritsFromFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.ScaffoldColumnAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.DisplayAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.EditableAttribute") == true || + attributeData.AttributeClass?.HasFullyQualifiedMetadataName("System.ComponentModel.DataAnnotations.KeyAttribute") == true) + { + forwardedAttributes.Add(AttributeInfo.Create(attributeData)); + } + } + + // Gather explicit forwarded attributes info + foreach (AttributeListSyntax attributeList in memberSyntax.AttributeLists) + { + // Only look for attribute lists explicitly targeting the (generated) property or one of its accessors. Roslyn will + // normally emit a CS0657 warning (invalid target), but that is automatically suppressed by a dedicated diagnostic + // suppressor that recognizes uses of this target specifically to support [ObservableProperty]. + if (attributeList.Target?.Identifier is not SyntaxToken(SyntaxKind.PropertyKeyword or SyntaxKind.GetKeyword or SyntaxKind.SetKeyword) targetIdentifier) + { + continue; + } + + token.ThrowIfCancellationRequested(); + + foreach (AttributeSyntax attribute in attributeList.Attributes) + { + // Roslyn ignores attributes in an attribute list with an invalid target, so we can't get the AttributeData as usual. + // To reconstruct all necessary attribute info to generate the serialized model, we use the following steps: + // - We try to get the attribute symbol from the semantic model, for the current attribute syntax. In case this is not + // available (in theory it shouldn't, but it can be), we try to get it from the candidate symbols list for the node. + // If there are no candidates or more than one, we just issue a diagnostic and stop processing the current attribute. + // The returned symbols might be method symbols (constructor attribute) so in that case we can get the declaring type. + // - We then go over each attribute argument expression and get the operation for it. This will still be available even + // though the rest of the attribute is not validated nor bound at all. From the operation we can still retrieve all + // constant values to build the AttributeInfo model. After all, attributes only support constant values, typeof(T) + // expressions, or arrays of either these two types, or of other arrays with the same rules, recursively. + // - From the syntax, we can also determine the identifier names for named attribute arguments, if any. + // There is no need to validate anything here: the attribute will be forwarded as is, and then Roslyn will validate on the + // generated property. Users will get the same validation they'd have had directly over the field. The only drawback is the + // lack of IntelliSense when constructing attributes over the field, but this is the best we can do from this end anyway. + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeTypeSymbol)) + { + diagnostics.Add( + InvalidPropertyTargetedAttributeOnObservablePropertyField, + attribute, + memberSymbol, + attribute.Name); + + continue; + } + + IEnumerable attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); + + // Try to extract the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, targetIdentifier.Kind(), token, out AttributeInfo? attributeInfo)) + { + diagnostics.Add( + InvalidPropertyTargetedAttributeExpressionOnObservablePropertyField, + attribute, + memberSymbol, + attribute.Name); + + continue; + } + + forwardedAttributes.Add(attributeInfo); + } + } + } + + /// + /// Tries to get the accessibility of the property and accessors, if possible. + /// If the target member is not a property, it will use the defaults. + /// + /// The instance to process. + /// The input instance to process. + /// The accessibility of the property, if available. + /// The accessibility of the accessor, if available. + /// The accessibility of the accessor, if available. + /// Whether the property was valid and the accessibilities could be retrieved. + private static bool TryGetAccessibilityModifiers( + MemberDeclarationSyntax memberSyntax, + ISymbol memberSymbol, + out Accessibility propertyAccessibility, + out Accessibility getterAccessibility, + out Accessibility setterAccessibility) + { + // For legacy support for fields, the property that is generated is public, and neither + // accessors will have any accessibility modifiers. To customize the accessibility, + // partial properties should be used instead. + if (memberSyntax.IsKind(SyntaxKind.FieldDeclaration)) + { + propertyAccessibility = Accessibility.Public; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; + + return true; + } + + propertyAccessibility = Accessibility.NotApplicable; + getterAccessibility = Accessibility.NotApplicable; + setterAccessibility = Accessibility.NotApplicable; + + // Ensure that we have a getter and a setter, and that the setter is not init-only + if (memberSymbol is not IPropertySymbol { GetMethod: { } getMethod, SetMethod: { IsInitOnly: false } setMethod }) + { + return false; + } + + // At this point the node is definitely a property, just do a sanity check + if (memberSyntax is not PropertyDeclarationSyntax propertySyntax) + { + return false; + } + + // Track the property accessibility if explicitly set + if (propertySyntax.Modifiers.ContainsAnyAccessibilityModifiers()) + { + propertyAccessibility = memberSymbol.DeclaredAccessibility; + } + + // Track the accessors accessibility, if explicitly set + foreach (AccessorDeclarationSyntax accessor in propertySyntax.AccessorList?.Accessors ?? []) + { + if (!accessor.Modifiers.ContainsAnyAccessibilityModifiers()) + { + continue; + } + + switch (accessor.Kind()) + { + case SyntaxKind.GetAccessorDeclaration: + getterAccessibility = getMethod.DeclaredAccessibility; + break; + case SyntaxKind.SetAccessorDeclaration: + setterAccessibility = setMethod.DeclaredAccessibility; + break; + } + } + + return true; + } + + /// + /// Checks whether an input member is a required property. + /// + /// The input instance to process. + /// Whether is a required property. + private static bool GetIsRequiredProperty(ISymbol memberSymbol) + { +#if ROSLYN_4_3_1_OR_GREATER + return memberSymbol is IPropertySymbol { IsRequired: true }; +#else + return false; +#endif + } + /// /// Gets a instance with the cached args for property changing notifications. /// @@ -834,18 +1063,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); @@ -865,6 +1097,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: @@ -872,7 +1111,7 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf // __oldValue = ; setterStatements.Add( LocalDeclarationStatement( - VariableDeclaration(propertyType) + VariableDeclaration(GetPropertyTypeForOldValue(propertyInfo)) .AddVariables( VariableDeclarator(Identifier("__oldValue")) .WithInitializer(EqualsValueClause(setterFieldExpression))))); @@ -1001,6 +1240,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)) @@ -1025,19 +1267,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: // @@ -1065,15 +1321,19 @@ public static MemberDeclarationSyntax GetPropertySyntax(PropertyInfo propertyInf AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("The type of the current instance cannot be statically discovered."))))))); } + // Also add any forwarded attributes + setAccessor = setAccessor.AddAttributeLists(forwardedSetAccessorAttributes); + // Construct the generated property as follows: // - // /// + // // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // - // public + // // { - // get => ; + // + // get => ; // // } return @@ -1084,17 +1344,45 @@ 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(); + +#if ROSLYN_4_3_1_OR_GREATER + // Add the 'required' modifier if the original member also had it + if (propertyInfo.IsRequired) + { + propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.RequiredKeyword)); + } +#endif + + // Add the 'partial' modifier if the original member is a partial property + if (propertyInfo.AnnotatedMemberKind is SyntaxKind.PropertyDeclaration) + { + propertyModifiers = propertyModifiers.Add(Token(SyntaxKind.PartialKeyword)); + } + + return propertyModifiers; + } + /// /// Gets the instances for the OnPropertyChanging and OnPropertyChanged methods for the input field. /// @@ -1128,17 +1416,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: // @@ -1224,6 +1503,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. /// @@ -1338,13 +1661,19 @@ private static FieldDeclarationSyntax CreateFieldDeclaration(string fullyQualifi } /// - /// Get the generated property name for an input field. + /// Get the generated property name for an input field or property. /// - /// The input instance to process. - /// The generated property name for . - public static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol) + /// The input instance to process. + /// The generated property name for . + public static string GetGeneratedPropertyName(ISymbol memberSymbol) { - string propertyName = fieldSymbol.Name; + // If the input is a property, just always match the name exactly + if (memberSymbol is IPropertySymbol propertySymbol) + { + return propertySymbol.Name; + } + + string propertyName = memberSymbol.Name; if (propertyName.StartsWith("m_")) { diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 68801d449..9957f3bbe 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -27,25 +27,25 @@ public void Initialize(IncrementalGeneratorInitializationContext context) IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> propertyInfoWithErrors = context.ForAttributeWithMetadataNameAndOptions( "CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute", - static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, + Execute.IsCandidatePropertyDeclaration, static (context, token) => { - if (!context.SemanticModel.Compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp8)) + MemberDeclarationSyntax memberSyntax = Execute.GetCandidateMemberDeclaration(context.TargetNode); + + // Validate that the candidate is valid for the current compilation + if (!Execute.IsCandidateValidForCompilation(memberSyntax, context.SemanticModel)) { return default; } - FieldDeclarationSyntax fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; - IFieldSymbol fieldSymbol = (IFieldSymbol)context.TargetSymbol; - // Get the hierarchy info for the target symbol, and try to gather the property info - HierarchyInfo hierarchy = HierarchyInfo.From(fieldSymbol.ContainingType); + HierarchyInfo hierarchy = HierarchyInfo.From(context.TargetSymbol.ContainingType); token.ThrowIfCancellationRequested(); _ = Execute.TryGetInfo( - fieldDeclaration, - fieldSymbol, + memberSyntax, + context.TargetSymbol, context.SemanticModel, context.GlobalOptions, token, diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/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/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs new file mode 100644 index 000000000..0b65550d0 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/InvalidPropertyLevelObservablePropertyAttributeAnalyzer.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error whenever [ObservableProperty] is used on an invalid property declaration. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidPropertyLevelObservablePropertyAttributeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidPropertyDeclarationForObservableProperty); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the symbol for [ObservableProperty] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is not INamedTypeSymbol observablePropertySymbol) + { + return; + } + + context.RegisterSymbolAction(context => + { + // We're intentionally only looking for properties here + if (context.Symbol is not IPropertySymbol propertySymbol) + { + return; + } + + // If the property isn't using [ObservableProperty], there's nothing to do + if (!propertySymbol.TryGetAttributeWithType(observablePropertySymbol, out AttributeData? observablePropertyAttribute)) + { + return; + } + + // Check that the property has valid syntax + foreach (SyntaxReference propertyReference in propertySymbol.DeclaringSyntaxReferences) + { + SyntaxNode propertyNode = propertyReference.GetSyntax(context.CancellationToken); + + if (!IsValidCandidateProperty(propertyNode, out _)) + { + context.ReportDiagnostic(Diagnostic.Create( + InvalidPropertyDeclarationForObservableProperty, + observablePropertyAttribute.GetLocation(), + propertySymbol.ContainingType, + propertySymbol)); + } + } + }, SymbolKind.Property); + }); + } + + /// + /// Checks whether a given property declaration has valid syntax. + /// + /// The input node to validate. + /// The resulting node for the containing type of the property, if valid. + /// Whether is a valid property. + internal static bool IsValidCandidateProperty(SyntaxNode node, out TypeDeclarationSyntax? containingTypeNode) + { + // The node must be a property declaration with two accessors + if (node is not PropertyDeclarationSyntax { AccessorList.Accessors: { Count: 2 } accessors, AttributeLists.Count: > 0 } property) + { + containingTypeNode = null; + + return false; + } + + // The property must be partial (we'll check that it's a declaration from its symbol) + if (!property.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + containingTypeNode = null; + + return false; + } + + // The accessors must be a get and a set (with any accessibility) + if (accessors[0].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration) || + accessors[1].Kind() is not (SyntaxKind.GetAccessorDeclaration or SyntaxKind.SetAccessorDeclaration)) + { + containingTypeNode = null; + + return false; + } + + containingTypeNode = (TypeDeclarationSyntax?)property.Parent; + + return true; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/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..29255adb5 --- /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)); + } + }, 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/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..7e2aa7e57 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTGeneratedBindableCustomPropertyWithBasesMemberAnalyzer.cs @@ -0,0 +1,137 @@ +// 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 + foreach (IFieldSymbol fieldSymbol in FindObservablePropertyFields(typeSymbol, observablePropertySymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField, + typeSymbol.GetLocationFromAttributeDataOrDefault(generatedBindableCustomPropertyAttribute), + typeSymbol, + fieldSymbol.ContainingType, + fieldSymbol.Name)); + } + + // Warn on all [RelayCommand] methods + foreach (IMethodSymbol methodSymbol in FindRelayCommandMethods(typeSymbol, relayCommandSymbol)) + { + 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..4fa8040ba --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTObservablePropertyOnFieldsIsNotAotCompatibleAnalyzer.cs @@ -0,0 +1,100 @@ +// 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 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)); + + // Notify that we did produce at least one diagnostic. Note: callbacks can run in parallel, so the order + // is not guaranteed. As such, there's no point in using an interlocked compare exchange operation here, + // since we couldn't rely on the value being written actually being the "first" occurrence anyway. + // So we can just do a normal volatile read for better performance. + Volatile.Write(ref firstObservablePropertyAttribute, observablePropertyAttribute); + } + }, SymbolKind.Field); + + // 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())); + } + }); + }); + } +} + +#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..7cbcf136a --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Analyzers/WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using static CommunityToolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.Mvvm.SourceGenerators; + +/// +/// A diagnostic analyzer that generates an error when [RelayCommand] is used on a method inside a type with [GeneratedBindableCustomProperty]. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatibleAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // This analyzer is only enabled when CsWinRT is also used + if (!context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.IsUsingWindowsRuntimePack()) + { + return; + } + + // Get the 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 using [GeneratedBindableCustomProperty], emit a warning + if (typeSymbol.HasAttributeWithType(generatedBindableCustomPropertySymbol)) + { + context.ReportDiagnostic(Diagnostic.Create( + WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible, + methodSymbol.GetLocationFromAttributeDataOrDefault(relayCommandAttribute), + methodSymbol)); + } + }, SymbolKind.Method); + }); + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 431e5da40..adbb28771 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -34,6 +34,16 @@ 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"; + /// /// Gets a indicating when a duplicate declaration of would happen. /// @@ -117,17 +127,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 +330,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 +378,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 +412,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 +490,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 +634,7 @@ internal static class DiagnosticDescriptors category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must be using valid expressions.", - helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0037"); + description: "All attributes targeting the generated property for a field annotated with [ObservableProperty] must be using valid expressions."); /// /// Gets a indicating when a method with [RelayCommand] is using an invalid attribute targeting the field or property. @@ -640,8 +649,7 @@ internal static class DiagnosticDescriptors category: typeof(RelayCommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must be using valid expressions.", - helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0038"); + description: "All attributes targeting the generated field or property for a method annotated with [RelayCommand] must be using valid expressions."); /// /// Gets a indicating when a method with [RelayCommand] is async void. @@ -674,4 +682,181 @@ 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( + 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( + id: UseObservablePropertyOnPartialPropertyId, + title: "Prefer using [ObservableProperty] on partial properties", + messageFormat: """The field {0}.{1} using [ObservableProperty] can be converted to a partial property instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Info, + isEnabledByDefault: true, + description: "Fields using [ObservableProperty] can be converted to partial properties instead, which is recommended (doing so improves the developer experience and allows other generators and analyzers to correctly see the generated property as well).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0042"); + + /// + /// Gets a indicating when [ObservableProperty] is applied to a property with an invalid declaration. + /// + /// Format: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be a partial property with a getter and a setter that is not init-only)". + /// + /// + public static readonly DiagnosticDescriptor InvalidPropertyDeclarationForObservableProperty = new DiagnosticDescriptor( + id: "MVVMTK0043", + title: "Invalid property declaration for [ObservableProperty]", + messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as its declaration is not valid (it must be a partial property with a getter and a setter that is not init-only)", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Properties annotated with [ObservableProperty] must be partial properties with a getter and a setter that is not init-only.", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0043"); + + /// + /// Gets a indicating when [ObservableProperty] is applied to a property when an unsupported version of Roslyn is used. + /// + /// Format: "The property {0}.{1} cannot be used to generate an observable property, as the current Roslyn version being used is not high enough (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK)". + /// + /// + public static readonly DiagnosticDescriptor UnsupportedRoslynVersionForObservablePartialPropertySupport = new DiagnosticDescriptor( + id: "MVVMTK0044", + title: "Unsupported Roslyn version for using [ObservableProperty] on partial properties", + messageFormat: "The property {0}.{1} cannot be used to generate an observable property, as the current Roslyn version being used is not high enough (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK)", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Using [ObservableProperty] with (partial) properties requires a higher version of Roslyn (remove [ObservableProperty] or target a field instead, or upgrade to at least Visual Studio 2022 version 17.12 and the .NET 9 SDK).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0044"); + + /// + /// 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( + 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], which is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator)". + /// + /// + public static readonly DiagnosticDescriptor WinRTRelayCommandIsNotGeneratedBindableCustomPropertyCompatible = new( + id: "MVVMTK0046", + title: "Using [RelayCommand] is not compatible with [GeneratedBindableCustomProperty]", + messageFormat: """The method {0} using [RelayCommand] within a type also using [GeneratedBindableCustomProperty], which is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator)""", + category: typeof(RelayCommandGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using [RelayCommand] on methods within a type also using [GeneratedBindableCustomProperty] is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated command property that is produced by the MVVM Toolkit generator).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0046"); + + /// + /// Gets a for when [GeneratedBindableCustomProperty] is used on a type that also uses [ObservableProperty] on any declared or inherited fields. + /// + /// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}: combining the two generators is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)". + /// + /// + public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseObservablePropertyOnField = new( + id: "MVVMTK0047", + title: "Using [GeneratedBindableCustomProperty] is not compatible with [ObservableProperty] on fields", + messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [ObservableProperty] on its declared (or inherited) field {1}.{2}: combining the two generators is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)""", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using [GeneratedBindableCustomProperty] on types that also use [ObservableProperty] on any declared (or inherited) fields is not supported, and partial properties should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0047"); + + /// + /// Gets a for when [GeneratedBindableCustomProperty] is used on a type that also uses [RelayCommand] on any declared or inherited methods. + /// + /// Format: "The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1}: combining the two generators is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)". + /// + /// + public static readonly DiagnosticDescriptor WinRTGeneratedBindableCustomPropertyWithBaseRelayCommand = new( + id: "MVVMTK0048", + title: "Using [GeneratedBindableCustomProperty] is not compatible with [RelayCommand]", + messageFormat: """The type {0} using [GeneratedBindableCustomProperty] is also using [RelayCommand] on its inherited method {1}: combining the two generators is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator)""", + category: typeof(RelayCommandGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Using [GeneratedBindableCustomProperty] on types that also use [RelayCommand] on any inherited methods is not supported, and a manually declared command property should be used instead (the [GeneratedBindableCustomProperty] generator cannot see the generated property that is produced by the MVVM Toolkit generator).", + helpLinkUri: "https://aka.ms/mvvmtoolkit/errors/mvvmtk0048"); + + /// + /// 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( + 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); } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs index 4a6c803a9..2f82c8b3a 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -17,7 +17,15 @@ internal static class SuppressionDescriptors public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyField = new( id: "MVVMTKSPR0001", suppressedDiagnosticId: "CS0657", - justification: "Fields using [ObservableProperty] can use [property:] attribute lists to forward attributes to the generated properties"); + justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); + + /// + /// Gets a for a field using [ObservableProperty] with an attribute list targeting a get or set accessor. + /// + public static readonly SuppressionDescriptor PropertyAttributeListForObservablePropertyFieldAccessors = new( + id: "MVVMTKSPR0001", + suppressedDiagnosticId: "CS0658", + justification: "Fields using [ObservableProperty] can use [property:], [set:] and [set:] attribute lists to forward attributes to the generated properties"); /// /// Gets a for a method using [RelayCommand] with an attribute list targeting a field or property. diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithPropertyTargetDiagnosticSuppressor.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Diagnostics/Suppressors/ObservablePropertyAttributeWithSupportedTargetDiagnosticSuppressor.cs similarity index 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/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/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/SymbolInfoExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs index c87f9a8fb..4a81b6bee 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolInfoExtensions.cs @@ -19,7 +19,7 @@ internal static class SymbolInfoExtensions /// The resulting attribute type symbol, if correctly resolved. /// Whether is resolved to a symbol. /// - /// This can be used to ensure users haven't eg. spelled names incorrecty or missed a using directive. Normally, code would just + /// This can be used to ensure users haven't eg. spelled names incorrectly or missed a using directive. Normally, code would just /// not compile if that was the case, but that doesn't apply for attributes using invalid targets. In that case, Roslyn will ignore /// any errors, meaning the generator has to validate the type symbols are correctly resolved on its own. /// diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs new file mode 100644 index 000000000..398b17bd5 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SymbolKindExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SymbolKindExtensions +{ + /// + /// Converts a value to either "field" or "property" based on the kind. + /// + /// The input value. + /// Either "field" or "property" based on . + /// Thrown if is neither nor . + public static string ToFieldOrPropertyKeyword(this SymbolKind kind) + { + return kind switch + { + SymbolKind.Field => "field", + SymbolKind.Property => "property", + _ => throw new ArgumentException($"Unsupported symbol kind '{kind}' for field or property keyword conversion."), + }; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs new file mode 100644 index 000000000..a178dcc82 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxKindExtensions.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxKindExtensions +{ + /// + /// Converts a value to either "field" or "property" based on the kind. + /// + /// The input value. + /// Either "field" or "property" based on . + /// Thrown if is neither nor . + public static string ToFieldOrPropertyKeyword(this SyntaxKind kind) + { + return kind switch + { + SyntaxKind.FieldDeclaration => "field", + SyntaxKind.PropertyDeclaration => "property", + _ => throw new ArgumentException($"Unsupported syntax kind '{kind}' for field or property keyword conversion."), + }; + } +} diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs index 52b7ccbfc..c7a08da9d 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxNodeExtensions.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; @@ -30,4 +32,30 @@ public static bool IsFirstSyntaxDeclarationForSymbol(this SyntaxNode syntaxNode, syntaxReference.SyntaxTree == syntaxNode.SyntaxTree && syntaxReference.Span == syntaxNode.Span; } + + /// + /// Checks whether a given is a given type declaration with or potentially with any base types, using only syntax. + /// + /// The type of declaration to check for. + /// The input to check. + /// Whether is a given type declaration with or potentially with any base types. + public static bool IsTypeDeclarationWithOrPotentiallyWithBaseTypes(this SyntaxNode node) + where T : TypeDeclarationSyntax + { + // Immediately bail if the node is not a type declaration of the specified type + if (node is not T typeDeclaration) + { + return false; + } + + // If the base types list is not empty, the type can definitely has implemented interfaces + if (typeDeclaration.BaseList is { Types.Count: > 0 }) + { + return true; + } + + // If the base types list is empty, check if the type is partial. If it is, it means + // that there could be another partial declaration with a non-empty base types list. + return typeDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword); + } } diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs new file mode 100644 index 000000000..99c1ecb85 --- /dev/null +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Extensions/SyntaxTokenListExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxTokenListExtensions +{ + /// + /// Checks whether a given value contains any accessibility modifiers. + /// + /// The input value to check. + /// Whether contains any accessibility modifiers. + public static bool ContainsAnyAccessibilityModifiers(this SyntaxTokenList syntaxList) + { + foreach (SyntaxToken token in syntaxList) + { + if (SyntaxFacts.IsAccessibilityModifier(token.Kind())) + { + return true; + } + } + + return false; + } +} diff --git a/src/CommunityToolkit.Mvvm.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/CommunityToolkit.Mvvm.FeatureSwitches.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets index bf6d6ed6c..fefee0e8e 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.FeatureSwitches.targets @@ -16,9 +16,9 @@ - + + roslyn4.12 roslyn4.3 roslyn4.0 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..1655776c5 --- /dev/null +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.WindowsSdk.targets @@ -0,0 +1,88 @@ + + + + + + true + + + + $(ResolveReferencesDependsOn);MvvmToolkitVerifyWindowsSdkPackageVersion + + + + + + + + + <_MvvmToolkitWindowsSdkPackageMinBuild>38 + <_MvvmToolkitWindowsSdkPackageRecommendedUwpBuild>39 + <_MvvmToolkitWindowsSdkPackageRecommendedWindowsAppSDKBuild>41 + + + <_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 ef94a0800..69a5befaf 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.csproj @@ -1,7 +1,7 @@ - netstandard2.0;netstandard2.1;net6.0;net8.0;net8.0-windows10.0.17763.0 + netstandard2.0;netstandard2.1;net8.0;net8.0-windows10.0.17763.0 - 10.0.17763.41 + 10.0.17763.53 - + - + - - - + + + + + @@ -99,9 +101,13 @@ + + + + - - - - + + + + + + \ No newline at end of file diff --git a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets index 0bd5d5d6a..394a0a111 100644 --- a/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets +++ b/src/CommunityToolkit.Mvvm/CommunityToolkit.Mvvm.targets @@ -4,10 +4,14 @@ <_CommunityToolkitMvvmFeatureSwitchesTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.FeatureSwitches.targets <_CommunityToolkitMvvmSourceGeneratorsTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.SourceGenerators.targets + <_CommunityToolkitMvvmWindowsTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.Windows.targets + <_CommunityToolkitMvvmWindowsSdkTargets>$(MSBuildThisFileDirectory)CommunityToolkit.Mvvm.WindowsSdk.targets + + \ No newline at end of file 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 09178f9f5..d963ed72c 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/NotifyPropertyChangedForAttribute.cs @@ -13,7 +13,7 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// used, the generated property setter will also call (or the equivalent /// method in the target class) for the properties specified in the attribute data. This can be useful to keep the code compact when /// there are one or more dependent properties that should also be reported as updated when the value of the annotated observable -/// property is changed. If this attribute is used in a field without , it is ignored. +/// property is changed. If this attribute is used on a property without , it is ignored. /// /// In order to use this attribute, the containing type has to implement the interface /// and expose a method with the same signature as . If the containing @@ -27,11 +27,11 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// { /// [ObservableProperty] /// [NotifyPropertyChangedFor(nameof(FullName))] -/// private string name; +/// public partial string Name { get; set; } /// /// [ObservableProperty] /// [NotifyPropertyChangedFor(nameof(FullName))] -/// private string surname; +/// public partial string Surname { get; set; } /// /// public string FullName => $"{Name} {Surname}"; /// } @@ -41,17 +41,17 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// /// partial class MyViewModel /// { -/// public string Name +/// public partial string Name /// { -/// get => name; +/// get => field; /// set /// { -/// if (!EqualityComparer<string>.Default.Equals(name, value)) +/// if (!EqualityComparer<string>.Default.Equals(field, value)) /// { /// OnPropertyChanging(nameof(Name)); /// OnPropertyChanged(nameof(FullName)); /// -/// name = value; +/// field = value; /// /// OnPropertyChanged(nameof(Name)); /// OnPropertyChanged(nameof(FullName)); @@ -59,17 +59,17 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// } /// } /// -/// public string Surname +/// public partial string Surname /// { -/// get => surname; +/// get => field; /// set /// { -/// if (!EqualityComparer<string>.Default.Equals(name, value)) +/// if (!EqualityComparer<string>.Default.Equals(field, value)) /// { /// OnPropertyChanging(nameof(Surname)); /// OnPropertyChanged(nameof(FullName)); /// -/// surname = value; +/// field = value; /// /// OnPropertyChanged(nameof(Surname)); /// OnPropertyChanged(nameof(FullName)); @@ -79,7 +79,10 @@ namespace CommunityToolkit.Mvvm.ComponentModel; /// } /// /// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)] +/// +/// Just like , this attribute can also be used on fields as well. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] public sealed class NotifyPropertyChangedForAttribute : Attribute { /// diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/Attributes/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/Messaging/IMessenger.cs b/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs index a007d40eb..72720a899 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/IMessenger.cs @@ -15,7 +15,7 @@ namespace CommunityToolkit.Mvvm.Messaging; /// /// public sealed class LoginCompletedMessage { } /// -/// Then, register your a recipient for this message: +/// Then, register a recipient for this message: /// /// Messenger.Default.Register<MyRecipientType, LoginCompletedMessage>(this, (r, m) => /// { diff --git a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs index 6febfb2e1..532639c8f 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -435,7 +435,7 @@ public static TMessage Send(this IMessenger messenger, TMessage messag /// The type of token to identify what channel to use to send the message. /// The instance to use to send the message. /// The token indicating what channel to use. - /// The message that has been sen. + /// The message that has been sent. /// /// This method will automatically create a new instance /// just like , and then send it to the right recipients. diff --git a/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs b/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs index c3ac541a7..4e8bbf075 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/Internals/System/Collections.Generic/Dictionary2.cs @@ -298,7 +298,7 @@ public bool MoveNext() while ((uint)this.index < (uint)this.count) { // We need to preemptively increment the current index so that we still correctly keep track - // of the current position in the dictionary even if the users doesn't access any of the + // of the current position in the dictionary even if the users don't access any of the // available properties in the enumerator. As this is a possibility, we can't rely on one of // them to increment the index before MoveNext is invoked again. We ditch the standard enumerator // API surface here to expose the Key/Value properties directly and minimize the memory copies. diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 23f65c476..7239f4a2e 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -7,14 +7,6 @@ NETSTANDARD2_1_OR_GREATER - - - true - true - true - true - - true diff --git a/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj b/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj index db70cacd7..e47e06627 100644 --- a/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj +++ b/tests/CommunityToolkit.Common.UnitTests/CommunityToolkit.Common.UnitTests.csproj @@ -1,13 +1,13 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 - - - + + + diff --git a/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj b/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj index 2c02dcade..633cc0fa4 100644 --- a/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj +++ b/tests/CommunityToolkit.Diagnostics.UnitTests/CommunityToolkit.Diagnostics.UnitTests.csproj @@ -1,13 +1,13 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 - - - + + + diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj b/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj index ce059f06f..772b868c5 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/CommunityToolkit.HighPerformance.UnitTests.csproj @@ -1,7 +1,7 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 true $(NoWarn);CA2252 @@ -11,9 +11,9 @@ - - - + + + \ No newline at end of file diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Streams/Test_ReadOnlySequenceStream.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Streams/Test_ReadOnlySequenceStream.cs new file mode 100644 index 000000000..0cb053f2a --- /dev/null +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Streams/Test_ReadOnlySequenceStream.cs @@ -0,0 +1,407 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.HighPerformance.UnitTests.Streams; + +[TestClass] +public partial class Test_ReadOnlySequenceStream +{ + [TestMethod] + public void Test_ReadOnlySequenceStream_Lifecycle() + { + ReadOnlySequence sequence = CreateReadOnlySequence(new byte[100]); + + Stream stream = sequence.AsStream(); + + Assert.IsTrue(stream.CanRead); + Assert.IsTrue(stream.CanSeek); + Assert.IsFalse(stream.CanWrite); + Assert.AreEqual(stream.Length, sequence.Length); + Assert.AreEqual(stream.Position, 0); + + stream.Dispose(); + + Assert.IsFalse(stream.CanRead); + Assert.IsFalse(stream.CanSeek); + Assert.IsFalse(stream.CanWrite); + + _ = Assert.ThrowsException(() => stream.Length); + _ = Assert.ThrowsException(() => stream.Position); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_Seek() + { + Stream stream = CreateReadOnlySequence(new byte[50], new byte[50]).AsStream(); + + Assert.AreEqual(stream.Position, 0); + + stream.Position = 42; + + Assert.AreEqual(stream.Position, 42); + + _ = Assert.ThrowsException(() => stream.Position = -1); + _ = Assert.ThrowsException(() => stream.Position = 120); + + _ = stream.Seek(0, SeekOrigin.Begin); + + _ = Assert.ThrowsException(() => stream.Seek(-1, SeekOrigin.Begin)); + _ = Assert.ThrowsException(() => stream.Seek(120, SeekOrigin.Begin)); + + Assert.AreEqual(stream.Position, 0); + + _ = stream.Seek(-1, SeekOrigin.End); + + _ = Assert.ThrowsException(() => stream.Seek(20, SeekOrigin.End)); + _ = Assert.ThrowsException(() => stream.Seek(-120, SeekOrigin.End)); + + Assert.AreEqual(stream.Position, stream.Length - 1); + + _ = stream.Seek(42, SeekOrigin.Begin); + _ = stream.Seek(20, SeekOrigin.Current); + _ = stream.Seek(-30, SeekOrigin.Current); + + _ = Assert.ThrowsException(() => stream.Seek(-64, SeekOrigin.Current)); + _ = Assert.ThrowsException(() => stream.Seek(80, SeekOrigin.Current)); + + Assert.AreEqual(stream.Position, 32); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_Read_Array() + { + Memory data = CreateRandomData(64); + + Stream stream = CreateReadOnlySequence(data).AsStream(); + + stream.Position = 0; + + byte[] result = new byte[data.Length]; + + int bytesRead = stream.Read(result, 0, result.Length); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result)); + + stream.Dispose(); + + _ = Assert.ThrowsException(() => stream.Read(result, 0, result.Length)); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_NotFromStart_Read_Array() + { + const int offset = 8; + + Memory data = CreateRandomData(64); + + Stream stream = CreateReadOnlySequence(data).AsStream(); + + stream.Position = offset; + + byte[] result = new byte[data.Length - offset]; + + int bytesRead = stream.Read(result, 0, result.Length); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, result.Length + offset); + Assert.IsTrue(data.Span.Slice(offset).SequenceEqual(result)); + + stream.Dispose(); + + _ = Assert.ThrowsException(() => stream.Read(result, 0, result.Length)); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_ReadByte() + { + Memory data = new byte[] { 1, 128, 255, 32 }; + + Stream stream = CreateReadOnlySequence(data.Slice(0,2), data.Slice(2, 2)).AsStream(); + + Span result = stackalloc byte[4]; + + foreach (ref byte value in result) + { + value = checked((byte)stream.ReadByte()); + } + + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result)); + + int exitCode = stream.ReadByte(); + + Assert.AreEqual(exitCode, -1); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_Read_Span() + { + Memory data = CreateRandomData(64); + + Stream stream = CreateReadOnlySequence(data).AsStream(); + + stream.Position = 0; + + Span result = new byte[data.Length]; + + int bytesRead = stream.Read(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_ReadAsync_Memory() + { + Memory data = CreateRandomData(64); + + Stream stream = CreateReadOnlySequence(data).AsStream(); + + Memory result = new byte[data.Length]; + + int bytesRead = await stream.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(stream.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_SigleSegment_CopyTo() + { + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data).AsStream(); + + Stream destination = new byte[100].AsMemory().AsStream(); + + source.CopyTo(destination); + + Assert.AreEqual(source.Position, destination.Position); + + destination.Position = 0; + + Memory result = new byte[data.Length]; + + int bytesRead = destination.Read(result.Span); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public void Test_ReadOnlySequenceStream_CopyTo() + { + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data.Slice(0, 32), data.Slice(32)).AsStream(); + + Stream destination = new byte[100].AsMemory().AsStream(); + + source.CopyTo(destination); + + Assert.AreEqual(source.Position, destination.Position); + + destination.Position = 0; + + Memory result = new byte[data.Length]; + + int bytesRead = destination.Read(result.Span); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_SigleSegment_CopyToAsync() + { + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data).AsStream(); + + Stream destination = new byte[100].AsMemory().AsStream(); + + await source.CopyToAsync(destination); + + Assert.AreEqual(source.Position, destination.Position); + + destination.Position = 0; + + Memory result = new byte[data.Length]; + + int bytesRead = await destination.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_SigleSegment_NotFromStart_CopyToAsync() + { + const int offset = 8; + + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data).AsStream(); + + source.Position = offset; + + Stream destination = new byte[100].AsMemory().AsStream(); + + await source.CopyToAsync(destination); + + Assert.AreEqual(source.Position, destination.Position + offset); + + destination.Position = 0; + + Memory result = new byte[data.Length - offset]; + + int bytesRead = await destination.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length - offset); + Assert.IsTrue(data.Span.Slice(offset).SequenceEqual(result.Span)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_MultipleSegments_CopyToAsync() + { + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data.Slice(0, 16), data.Slice(16, 16), data.Slice(32, 16), data.Slice(48, 16)).AsStream(); + + Stream destination = new byte[100].AsMemory().AsStream(); + + await source.CopyToAsync(destination); + + Assert.AreEqual(source.Position, destination.Position); + + destination.Position = 0; + + Memory result = new byte[data.Length]; + + int bytesRead = await destination.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length); + Assert.IsTrue(data.Span.SequenceEqual(result.Span)); + } + + [TestMethod] + public async Task Test_ReadOnlySequenceStream_MultipleSegments_NotFromStart_CopyToAsync() + { + const int offset = 8; + + Memory data = CreateRandomData(64); + + Stream source = CreateReadOnlySequence(data.Slice(0, 16), data.Slice(16, 16), data.Slice(32, 16), data.Slice(48, 16)).AsStream(); + + source.Position = offset; + + Stream destination = new byte[100].AsMemory().AsStream(); + + await source.CopyToAsync(destination); + + Assert.AreEqual(source.Position, destination.Position + offset); + + destination.Position = 0; + + Memory result = new byte[data.Length - offset]; + + int bytesRead = await destination.ReadAsync(result); + + Assert.AreEqual(bytesRead, result.Length); + Assert.AreEqual(destination.Position, data.Length - offset); + Assert.IsTrue(data.Span.Slice(offset).SequenceEqual(result.Span)); + } + + /// + /// Creates a random array filled with random data. + /// + /// The number of array items to create. + /// The returned random array. + private static byte[] CreateRandomData(int count) + { + Random? random = new(DateTime.Now.Ticks.GetHashCode()); + + byte[] data = new byte[count]; + + foreach (ref byte n in MemoryMarshal.AsBytes(data.AsSpan())) + { + n = (byte)random.Next(0, byte.MaxValue); + } + + return data; + } + + /// + /// Creates a value from the input segments. + /// + /// The input segments. + /// The resulting value. + private static ReadOnlySequence CreateReadOnlySequence(params ReadOnlyMemory[] segments) + { + if (segments is not { Length: > 0 }) + { + return ReadOnlySequence.Empty; + } + + if (segments.Length == 1) + { + return new(segments[0]); + } + + ReadOnlySequenceSegmentOfByte first = new(segments[0]); + ReadOnlySequenceSegmentOfByte last = first; + long length = first.Memory.Length; + + for (int i = 1; i < segments.Length; i++) + { + ReadOnlyMemory segment = segments[i]; + + length += segment.Length; + + last = last.Append(segment); + } + + return new(first, 0, last, (int)(length - last.RunningIndex)); + } + + /// + /// A custom that supports appending new segments. + /// + private sealed class ReadOnlySequenceSegmentOfByte : ReadOnlySequenceSegment + { + public ReadOnlySequenceSegmentOfByte(ReadOnlyMemory memory) + { + Memory = memory; + } + + public ReadOnlySequenceSegmentOfByte Append(ReadOnlyMemory memory) + { + ReadOnlySequenceSegmentOfByte nextSegment = new(memory) + { + RunningIndex = RunningIndex + Memory.Length + }; + + Next = nextSegment; + + return nextSegment; + } + } +} diff --git a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj index 76f768ef0..5853d5a5c 100644 --- a/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests/CommunityToolkit.Mvvm.DisableINotifyPropertyChanging.UnitTests.csproj @@ -1,18 +1,18 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 - - - + + + - + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj similarity index 81% rename from tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj rename to tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj index ee36d9f08..ae18e53f9 100644 --- a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn401.csproj +++ b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4001.csproj @@ -14,7 +14,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj similarity index 77% rename from tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj rename to tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj index 164e42e50..4a58a1151 100644 --- a/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn431.csproj +++ b/tests/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031/CommunityToolkit.Mvvm.ExternalAssembly.Roslyn4031.csproj @@ -12,7 +12,7 @@ - + diff --git a/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj index 5c35d66d3..acb777815 100644 --- a/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Internals.UnitTests/CommunityToolkit.Mvvm.Internals.UnitTests.csproj @@ -1,13 +1,13 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 - - - + + + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj similarity index 64% rename from tests/CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj index 115f23003..eb46db98f 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn401.UnitTests/CommunityToolkit.Mvvm.Roslyn401.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.Roslyn4001.UnitTests.csproj @@ -1,23 +1,23 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 true - - - + + + - + - + - + diff --git a/tests/CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj similarity index 64% rename from tests/CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj index c1973e0eb..b70464cb4 100644 --- a/tests/CommunityToolkit.Mvvm.Roslyn431.UnitTests/CommunityToolkit.Mvvm.Roslyn431.UnitTests.csproj +++ b/tests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.Roslyn4031.UnitTests.csproj @@ -1,23 +1,23 @@ - net472;net6.0;net7.0;net8.0 + net472;net7.0;net8.0 true - - - + + + - + - + - + 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 75% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests.csproj index c1717ef47..3656cfdf1 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,22 +1,22 @@ - net472;net6.0;net7.0;net8.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 98% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs index 5062572d1..c77c38b7b 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_AsyncVoidReturningRelayCommandMethodCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.AsyncVoidReturningRelayCommandMethodCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class Test_AsyncVoidReturningRelayCommandMethodCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs similarity index 99% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs index 018c5fc63..2e63a49f9 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_ClassUsingAttributeInsteadOfInheritanceCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.ClassUsingAttributeInsteadOfInheritanceCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class ClassUsingAttributeInsteadOfInheritanceCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs similarity index 99% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs index 596a11b66..665761405 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_FieldReferenceForObservablePropertyFieldCodeFixer.cs @@ -15,7 +15,7 @@ CommunityToolkit.Mvvm.CodeFixers.FieldReferenceForObservablePropertyFieldCodeFixer, Microsoft.CodeAnalysis.Testing.DefaultVerifier>; -namespace CommunityToolkit.Mvvm.SourceGenerators.Roslyn401.UnitTests; +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; [TestClass] public class Test_FieldReferenceForObservablePropertyFieldCodeFixer diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4001.UnitTests/Test_UnsupportedRoslynVersionForPartialPropertyAnalyzer.cs new file mode 100644 index 000000000..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 59% rename from tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn431.UnitTests.csproj rename to tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4031.UnitTests.csproj index 7323b4a69..495ef1dca 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;net8.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..eba477975 --- /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..9d7092888 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsCodegen.cs @@ -0,0 +1,928 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +partial class Test_SourceGeneratorsCodegen +{ + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithNoModifiers_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + partial int Number { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + partial int Number + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + // See https://github.com/CommunityToolkit/dotnet/issues/969 + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_RequiredProperty_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public required partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public required partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly1() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public partial int Number { get; private set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial int Number + { + get => field; + private set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly2() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + internal partial int Number { private get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal partial int Number + { + private get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNumberChanging(value); + OnNumberChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Number); + field = value; + OnNumberChanged(value); + OnNumberChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Number); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanging(int oldValue, int newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNumberChanged(int oldValue, int newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithValueType_OnPartialProperty_WithExplicitModifiers_WorksCorrectly3() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public partial class MyViewModel : ObservableObject + { + [ObservableProperty] + protected internal partial string Name { get; private protected set; } + } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + protected internal partial string Name + { + get => field; + private protected set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + +[TestMethod] + public void ObservablePropertyWithReferenceType_NotNullable_OnPartialProperty_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public partial string Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservablePropertyWithReferenceType_Nullable_OnPartialProperty_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + #nullable enable + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + public partial string? Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string? Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string? newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string? newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyPropertyChange_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(FullName))] + public partial string Name { get; set; } + + public string FullName => ""; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.FullName); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.FullName); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyCanExecuteChange_WorksCorrectly() + { + string source = """ + using System; + using CommunityToolkit.Mvvm.ComponentModel; + using CommunityToolkit.Mvvm.Input; + + namespace MyApp; + + partial class MyViewModel : ObservableRecipient + { + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(TestCommand))] + public partial string Name { get; set; } + + public IRelayCommand TestCommand => null; + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + TestCommand.NotifyCanExecuteChanged(); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyRecipients_WorksCorrectly() + { + string source = """ + using System; + using System.Windows.Input; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableRecipient + { + [ObservableProperty] + [NotifyPropertyChangedRecipients] + public partial string Name { get; set; } + } + """; + + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + string __oldValue = field; + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + Broadcast(__oldValue, value, "Name"); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_OnPartialProperty_AlsoNotifyDataErrorInfo_WorksCorrectly() + { + string source = """ + using System; + using System.ComponentModel.DataAnnotations; + using System.Windows.Input; + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableValidator + { + [ObservableProperty] + [NotifyDataErrorInfo] + [Required] + public partial string Name { get; set; } + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + [global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public partial string Name + { + get => field; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(field, value)) + { + OnNameChanging(value); + OnNameChanging(default, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + field = value; + ValidateProperty(value, "Name"); + OnNameChanged(value); + OnNameChanged(default, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, LanguageVersion.Preview, ("MyApp.MyViewModel.g.cs", result)); + } +} diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs new file mode 100644 index 000000000..0cadc6439 --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.Roslyn4120.UnitTests/Test_SourceGeneratorsDiagnostics.cs @@ -0,0 +1,745 @@ +// 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, [], ["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_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_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_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)]); + } +} 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 d76d8f356..14a15ee60 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Helpers/CSharpAnalyzerWithLanguageVersionTest{TAnalyzer}.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -10,6 +13,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; #if NET472 using System.ComponentModel.DataAnnotations; #endif @@ -29,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) @@ -63,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 c3e48fba6..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) { } } @@ -3018,6 +3018,382 @@ public string Name VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); } + [TestMethod] + public void ObservableProperty_NotNullableProperty_OfReferenceType_WithChangedMethods() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + string name; + + partial void OnNameChanged(string? oldValue, string NewValue) + { + } + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public string Name + { + get => name; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("name")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + string? __oldValue = name; + OnNameChanging(value); + OnNameChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public string Name + { + get => name; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(name, value)) + { + string? __oldValue = name; + OnNameChanging(value); + OnNameChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Name); + name = value; + OnNameChanged(value); + OnNameChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Name); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanging(string? oldValue, string newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnNameChanged(string? oldValue, string newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } + + [TestMethod] + public void ObservableProperty_NotNullableProperty_OfUnconstrainedGenericType_WithChangedMethods() + { + string source = """ + using CommunityToolkit.Mvvm.ComponentModel; + + namespace MyApp; + + partial class MyViewModel : ObservableObject + { + [ObservableProperty] + T value; + + partial void OnValueChanged(T? oldValue, T NewValue) + { + } + } + """; + +#if NET6_0_OR_GREATER + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Value + { + get => value; + [global::System.Diagnostics.CodeAnalysis.MemberNotNull("value")] + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(this.value, value)) + { + T? __oldValue = this.value; + OnValueChanging(value); + OnValueChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Value); + this.value = value; + OnValueChanged(value); + OnValueChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Value); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T? oldValue, T newValue); + } + } + """; +#else + string result = """ + // + #pragma warning disable + #nullable enable + namespace MyApp + { + /// + partial class MyViewModel + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public T Value + { + get => value; + set + { + if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(this.value, value)) + { + T? __oldValue = this.value; + OnValueChanging(value); + OnValueChanging(__oldValue, value); + OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Value); + this.value = value; + OnValueChanged(value); + OnValueChanged(__oldValue, value); + OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Value); + } + } + } + + /// Executes the logic for when is changing. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T value); + /// Executes the logic for when is changing. + /// The previous property value that is being replaced. + /// The new property value being set. + /// This method is invoked right before the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanging(T? oldValue, T newValue); + /// Executes the logic for when just changed. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T value); + /// Executes the logic for when just changed. + /// The previous property value that was replaced. + /// The new property value that was set. + /// This method is invoked right after the value of is changed. + [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", )] + partial void OnValueChanged(T? oldValue, T newValue); + } + } + """; +#endif + + VerifyGenerateSources(source, new[] { new ObservablePropertyGenerator() }, ("MyApp.MyViewModel`1.g.cs", result)); + } + + [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 /// diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsDiagnostics.cs index fc767e712..8030231aa 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,254 @@ 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_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 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. - private static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion) + internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion) + where TAnalyzer : DiagnosticAnalyzer, new() + { + await VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(markdownSource, languageVersion, [], []); + } + + /// + /// Verifies the diagnostic errors for a given analyzer, and that all available source generators can run successfully with the input source (including subsequent compilation). + /// + /// The type of the analyzer to test. + /// The input source to process with diagnostic annotations. + /// The language version to use to parse code and run tests. + /// The diagnostic ids to expect for the input source code. + /// The list of diagnostic ids to ignore in the final compilation. + internal static async Task VerifyAnalyzerDiagnosticsAndSuccessfulGeneration(string markdownSource, LanguageVersion languageVersion, string[] generatorDiagnosticsIds, string[] ignoredDiagnosticIds) where TAnalyzer : DiagnosticAnalyzer, new() { await CSharpAnalyzerWithLanguageVersionTest.VerifyAnalyzerAsync(markdownSource, languageVersion); @@ -1903,9 +2263,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 +2274,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 +2288,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 +2305,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 +2324,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_ObservablePropertyAttribute.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservablePropertyAttribute.cs index 495a9cd59..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 @@ -1797,4 +1793,30 @@ private sealed partial class ModelWithDependentPropertyAndNoPropertyChanging public string? FullName => ""; } + +#if NET6_0_OR_GREATER + // See https://github.com/CommunityToolkit/dotnet/issues/939 + public partial class ModelWithSecondaryPropertySetFromGeneratedSetter_DoesNotWarn : ObservableObject + { + [ObservableProperty] + [set: MemberNotNull(nameof(B))] + private string a; + + // This type validates forwarding attributes on generated accessors. In particular, there should + // be no nullability warning on this constructor (CS8618), thanks to 'MemberNotNullAttribute("B")' + // being forwarded to the generated setter in the generated property (see linked issue). + public ModelWithSecondaryPropertySetFromGeneratedSetter_DoesNotWarn() + { + A = ""; + } + + public string B { get; private set; } + + [MemberNotNull(nameof(B))] + partial void OnAChanged(string? oldValue, string newValue) + { + B = ""; + } + } +#endif } diff --git a/version.json b/version.json index 72763352d..305f00251 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "8.3.0-build.{height}", + "version": "8.4.0-preview3", "publicReleaseRefSpec": [ "^refs/heads/main$", // we release out of main "^refs/heads/dev$", // we release out of dev