diff --git a/all.sln b/all.sln index 337ac2f35..01a31fc0a 100644 --- a/all.sln +++ b/all.sln @@ -159,6 +159,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Analyzers", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Analyzers.Test", "test\Dapr.Workflow.Analyzers.Test\Dapr.Workflow.Analyzers.Test.csproj", "{CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Pubsub.Analyzers", "src\Dapr.Pubsub.Analyzers\Dapr.Pubsub.Analyzers.csproj", "{984BBCC6-C827-430A-8796-C4EB55E7D979}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Pubsub.Analyzers.Test", "test\Dapr.Pubsub.Analyzers.Test\Dapr.Pubsub.Analyzers.Test.csproj", "{A716C2AB-1B38-4284-AC94-2C3D9B758534}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -415,6 +419,14 @@ Global {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Release|Any CPU.Build.0 = Release|Any CPU + {984BBCC6-C827-430A-8796-C4EB55E7D979}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {984BBCC6-C827-430A-8796-C4EB55E7D979}.Debug|Any CPU.Build.0 = Debug|Any CPU + {984BBCC6-C827-430A-8796-C4EB55E7D979}.Release|Any CPU.ActiveCfg = Release|Any CPU + {984BBCC6-C827-430A-8796-C4EB55E7D979}.Release|Any CPU.Build.0 = Release|Any CPU + {A716C2AB-1B38-4284-AC94-2C3D9B758534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A716C2AB-1B38-4284-AC94-2C3D9B758534}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A716C2AB-1B38-4284-AC94-2C3D9B758534}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A716C2AB-1B38-4284-AC94-2C3D9B758534}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -435,62 +447,64 @@ Global {78FC19B2-396C-4ED2-BFD9-6C5667C61666} = {DD020B34-460F-455F-8D17-CF4A949F100B} {B615B353-476C-43B9-A776-B193B0DBD256} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {A11DC259-D1DB-4686-AD28-A427D0BABA83} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} - {2EC50C79-782D-4985-ABB1-AD07F35D1621} = {A11DC259-D1DB-4686-AD28-A427D0BABA83} + {2EC50C79-782D-4985-ABB1-AD07F35D1621} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {15A16323-2CCA-472E-BE79-07259DAD5F6F} = {A11DC259-D1DB-4686-AD28-A427D0BABA83} {5BACBA51-03FE-4CE1-B0F5-9E9C2A132FAB} = {A11DC259-D1DB-4686-AD28-A427D0BABA83} {3160CC92-1D6E-42CB-AE89-9401C8CEC5CB} = {A11DC259-D1DB-4686-AD28-A427D0BABA83} {02374BD0-BF0B-40F8-A04A-C4C4D61D4992} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} - {7957E852-1291-4FAA-9034-FB66CE817FF1} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992} - {626D74DD-4F37-4F74-87A3-5A6888684F5E} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992} + {7957E852-1291-4FAA-9034-FB66CE817FF1} = {A11DC259-D1DB-4686-AD28-A427D0BABA83} + {626D74DD-4F37-4F74-87A3-5A6888684F5E} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {CC0A5C98-ACDE-4139-BA2F-2995A9B8E18C} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992} {A7F41094-8648-446B-AECD-DCC2CC871F73} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} - {F70AC78E-8925-4770-832A-2FC67A620EB2} = {A7F41094-8648-446B-AECD-DCC2CC871F73} - {8B570E70-0E73-4042-A4B6-1CC3CC782A65} = {A7F41094-8648-446B-AECD-DCC2CC871F73} - {4AA9E7B7-36BF-4AAE-BFA3-C9CE8740F4A0} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {345FC3FB-D1E9-4AE8-9052-17D20AB01FA2} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {2AED1542-A8ED-488D-B6D0-E16AB5D6EF6C} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {F70AC78E-8925-4770-832A-2FC67A620EB2} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992} + {8B570E70-0E73-4042-A4B6-1CC3CC782A65} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992} + {4AA9E7B7-36BF-4AAE-BFA3-C9CE8740F4A0} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {345FC3FB-D1E9-4AE8-9052-17D20AB01FA2} = {A7F41094-8648-446B-AECD-DCC2CC871F73} + {2AED1542-A8ED-488D-B6D0-E16AB5D6EF6C} = {A7F41094-8648-446B-AECD-DCC2CC871F73} {E8212911-344B-4638-ADC3-B215BCDCAFD1} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {F80F837E-D2FC-4FFC-B68F-3CF0EC015F66} = {A7F41094-8648-446B-AECD-DCC2CC871F73} + {F80F837E-D2FC-4FFC-B68F-3CF0EC015F66} = {DD020B34-460F-455F-8D17-CF4A949F100B} {5BE7F505-7D77-4C3A-ABFD-54088774DAA7} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {35031EDB-C0DE-453A-8335-D2EBEA2FC640} = {A7F41094-8648-446B-AECD-DCC2CC871F73} + {35031EDB-C0DE-453A-8335-D2EBEA2FC640} = {DD020B34-460F-455F-8D17-CF4A949F100B} {07578B6C-9B96-4B3D-BA2E-7800EFCA7F99} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} - {5C61ABED-7623-4C28-A5C9-C5972A0F669C} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {5C61ABED-7623-4C28-A5C9-C5972A0F669C} = {A7F41094-8648-446B-AECD-DCC2CC871F73} {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} = {A7F41094-8648-446B-AECD-DCC2CC871F73} - {4A175C27-EAFE-47E7-90F6-873B37863656} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} - {DDC41278-FB60-403A-B969-2AEBD7C2D83C} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} - {8CA09061-2BEF-4506-A763-07062D2BD6AC} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {4A175C27-EAFE-47E7-90F6-873B37863656} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {DDC41278-FB60-403A-B969-2AEBD7C2D83C} = {A7F41094-8648-446B-AECD-DCC2CC871F73} + {8CA09061-2BEF-4506-A763-07062D2BD6AC} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {7592AFA4-426B-42F3-AE82-957C86814482} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} - {61C24126-F39D-4BEA-96DC-FC87BA730554} = {7592AFA4-426B-42F3-AE82-957C86814482} - {CB903D21-4869-42EF-BDD6-5B1CFF674337} = {7592AFA4-426B-42F3-AE82-957C86814482} - {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625} = {27C5D71D-0721-4221-9286-B94AB07B58CF} - {7C06FE2D-6C62-48F5-A505-F0D715C554DE} = {7592AFA4-426B-42F3-AE82-957C86814482} - {AF89083D-4715-42E6-93E9-38497D12A8A6} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} - {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {61C24126-F39D-4BEA-96DC-FC87BA730554} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {CB903D21-4869-42EF-BDD6-5B1CFF674337} = {A7F41094-8648-446B-AECD-DCC2CC871F73} + {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} + {7C06FE2D-6C62-48F5-A505-F0D715C554DE} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} + {AF89083D-4715-42E6-93E9-38497D12A8A6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {7592AFA4-426B-42F3-AE82-957C86814482} + {DFBABB04-50E9-42F6-B470-310E1B545638} = {7592AFA4-426B-42F3-AE82-957C86814482} {B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF} - {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {273F2527-1658-4CCF-8DC6-600E921188C5} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {CDB47863-BEBD-4841-A807-46D868962521} = {7592AFA4-426B-42F3-AE82-957C86814482} + {273F2527-1658-4CCF-8DC6-600E921188C5} = {DD020B34-460F-455F-8D17-CF4A949F100B} {2F3700EF-1CDA-4C15-AC88-360230000ECD} = {DD020B34-460F-455F-8D17-CF4A949F100B} {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} - {11011FF8-77EA-4B25-96C0-29D4D486EF1C} = {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} - {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} - {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} - {945DD3B7-94E5-435E-B3CB-796C20A652C7} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} - {FD3E9371-3134-4235-8E80-32226DFB4B1F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} - {D83B27F3-4401-42F5-843E-147566B4999A} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} - {00359961-0C50-4BB1-A794-8B06DE991639} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} - {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9} = {DD020B34-460F-455F-8D17-CF4A949F100B} - {0EAE36A1-B578-4F13-A113-7A477ECA1BDA} = {27C5D71D-0721-4221-9286-B94AB07B58CF} - {290D1278-F613-4DF3-9DF5-F37E38CDC363} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} - {C8BB6A85-A7EA-40C0-893D-F36F317829B3} = {27C5D71D-0721-4221-9286-B94AB07B58CF} - {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {11011FF8-77EA-4B25-96C0-29D4D486EF1C} = {A7F41094-8648-446B-AECD-DCC2CC871F73} + {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {945DD3B7-94E5-435E-B3CB-796C20A652C7} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {FD3E9371-3134-4235-8E80-32226DFB4B1F} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {D83B27F3-4401-42F5-843E-147566B4999A} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {00359961-0C50-4BB1-A794-8B06DE991639} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9} = {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} + {0EAE36A1-B578-4F13-A113-7A477ECA1BDA} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {290D1278-F613-4DF3-9DF5-F37E38CDC363} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {C8BB6A85-A7EA-40C0-893D-F36F317829B3} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} - {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} - {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} {55A7D436-CC8C-47E6-B43A-DFE32E0FE38C} = {27C5D71D-0721-4221-9286-B94AB07B58CF} {CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {984BBCC6-C827-430A-8796-C4EB55E7D979} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {A716C2AB-1B38-4284-AC94-2C3D9B758534} = {DD020B34-460F-455F-8D17-CF4A949F100B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj index 12b512fbb..ec158a43d 100644 --- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj +++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..0db9df24c --- /dev/null +++ b/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Shipped.md @@ -0,0 +1,7 @@ +## Release 1.16 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +DAPR2001| Usage | Warning | Call MapSubscribeHandler \ No newline at end of file diff --git a/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..f2b7fad65 --- /dev/null +++ b/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Unshipped.md @@ -0,0 +1,2 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/src/Dapr.Pubsub.Analyzers/Dapr.Pubsub.Analyzers.csproj b/src/Dapr.Pubsub.Analyzers/Dapr.Pubsub.Analyzers.csproj new file mode 100644 index 000000000..4775012db --- /dev/null +++ b/src/Dapr.Pubsub.Analyzers/Dapr.Pubsub.Analyzers.csproj @@ -0,0 +1,38 @@ + + + + netstandard2.0 + + enable + enable + true + + + + + + + + + true + + + false + + + false + + + This package contains Roslyn analyzers for actors. + $(PackageTags) + + + + + + + + + + + diff --git a/src/Dapr.Pubsub.Analyzers/MapSubscribeHandlerCodeFixProvider.cs b/src/Dapr.Pubsub.Analyzers/MapSubscribeHandlerCodeFixProvider.cs new file mode 100644 index 000000000..38dae50ba --- /dev/null +++ b/src/Dapr.Pubsub.Analyzers/MapSubscribeHandlerCodeFixProvider.cs @@ -0,0 +1,114 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.Pubsub.Analyzers; + +/// +/// Provides a code fix for the DAPR2001 diagnostic. +/// +public class MapSubscribeHandlerCodeFixProvider : CodeFixProvider +{ + /// + /// Gets the diagnostic IDs that this provider can fix. + /// + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR2001"); + + /// + /// Gets the FixAllProvider for this code fix provider. + /// + /// The FixAllProvider. + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + /// + /// Registers code fixes for the specified diagnostics. + /// + /// A containing the context in which the code fix is being applied. + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var title = "Call MapSubscribeHandler"; + context.RegisterCodeFix( + CodeAction.Create( + title, + createChangedDocument: c => AddMapSubscribeHandlerAsync(context.Document, context.Diagnostics.First(), c), + equivalenceKey: title), + context.Diagnostics); + return Task.CompletedTask; + } + + private async Task AddMapSubscribeHandlerAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken); + var invocationExpressions = root!.DescendantNodes().OfType(); + + var createBuilderInvocation = invocationExpressions + .FirstOrDefault(invocation => + { + return invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "CreateBuilder" && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == "WebApplication"; + }); + + var variableDeclarator = createBuilderInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var variableName = variableDeclarator.Identifier.Text; + + var buildInvocation = invocationExpressions + .FirstOrDefault(invocation => + { + return invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == "Build" && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.Text == variableName; + }); + + var buildVariableDeclarator = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var buildVariableName = buildVariableDeclarator.Identifier.Text; + + var mapSubscribeHandlerInvocation = SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(buildVariableName), + SyntaxFactory.IdentifierName("MapSubscribeHandler")))); + + if (buildInvocation?.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock) + { + var localDeclaration = buildInvocation + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var newParentBlock = parentBlock.InsertNodesAfter(localDeclaration, new[] { mapSubscribeHandlerInvocation }); + root = root.ReplaceNode(parentBlock, newParentBlock); + } + else + { + var buildInvocationGlobalStatement = buildInvocation? + .AncestorsAndSelf() + .OfType() + .FirstOrDefault(); + + var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault(); + var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement!, + new[] { SyntaxFactory.GlobalStatement(mapSubscribeHandlerInvocation) }); + root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax); + } + + return document.WithSyntaxRoot(root); + } +} diff --git a/src/Dapr.Pubsub.Analyzers/SubscriptionAnalyzer.cs b/src/Dapr.Pubsub.Analyzers/SubscriptionAnalyzer.cs new file mode 100644 index 000000000..1ddc17a44 --- /dev/null +++ b/src/Dapr.Pubsub.Analyzers/SubscriptionAnalyzer.cs @@ -0,0 +1,139 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Dapr.Pubsub.Analyzers; + +/// +/// Analyzes the subscription methods to ensure proper usage of MapSubscribeHandler. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class SubscriptionAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor DiagnosticDescriptorMapSubscribeHandler = new( + "DAPR2001", + "Call MapSubscribeHandler", + "Call app.MapSubscribeHandler to map endpoints for Dapr subscriptions", + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// Gets the supported diagnostics for this analyzer. + /// + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptorMapSubscribeHandler); + + /// + /// Initializes the analyzer. + /// + /// The analysis context. + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeMapSubscribeHandler, SyntaxKind.CompilationUnit); + } + + private void AnalyzeMapSubscribeHandler(SyntaxNodeAnalysisContext context) + { + var withTopicInvocations = FindInvocations(context, "WithTopic"); + var methodsWithTopicAttribute = FindMethodsWithTopicAttribute(context); + var invocationsWithTopicAttribute = FindInvocationsWithTopicAttribute(context); + + bool invokedByWebApplication = false; + var mapSubscribeHandlerInvocation = FindInvocations(context, "MapSubscribeHandler")?.FirstOrDefault(); + + if (mapSubscribeHandlerInvocation?.Expression is MemberAccessExpressionSyntax memberAccess) + { + var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess.Expression); + if (symbolInfo.Symbol is ILocalSymbol localSymbol) + { + var type = localSymbol.Type; + if (type.ToDisplayString() == "Microsoft.AspNetCore.Builder.WebApplication") + { + invokedByWebApplication = true; + } + } + } + + foreach (var withTopicInvocation in withTopicInvocations) + { + if (mapSubscribeHandlerInvocation == null || !invokedByWebApplication) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapSubscribeHandler, withTopicInvocation.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + + foreach (var methodWithTopicAttribute in methodsWithTopicAttribute) + { + if (mapSubscribeHandlerInvocation == null || !invokedByWebApplication) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapSubscribeHandler, methodWithTopicAttribute.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + + foreach (var invocationWithTopicAttribute in invocationsWithTopicAttribute) + { + if (mapSubscribeHandlerInvocation == null || !invokedByWebApplication) + { + var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapSubscribeHandler, invocationWithTopicAttribute.GetLocation()); + context.ReportDiagnostic(diagnostic); + } + } + } + + private List FindInvocations(SyntaxNodeAnalysisContext context, string methodName) + { + var invocations = new List(); + + foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + invocations.AddRange(root.DescendantNodes().OfType() + .Where(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name.Identifier.Text == methodName)); + } + + return invocations; + } + + private List FindMethodsWithTopicAttribute(SyntaxNodeAnalysisContext context) + { + var methodsWithTopicAttribute = new List(); + + foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + methodsWithTopicAttribute.AddRange(root.DescendantNodes() + .OfType() + .Where(method => method.AttributeLists + .SelectMany(attributeList => attributeList.Attributes) + .Any(attribute => attribute.Name.ToString() == "Topic" || attribute.Name.ToString().EndsWith(".Topic")))); + } + + return methodsWithTopicAttribute; + } + + private List FindInvocationsWithTopicAttribute(SyntaxNodeAnalysisContext context) + { + var invocationsWithTopicAttributeParameter = new List(); + + foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees) + { + var root = syntaxTree.GetRoot(); + invocationsWithTopicAttributeParameter.AddRange(root.DescendantNodes() + .OfType() + .Where(invocation => invocation.ArgumentList.Arguments + .Any(argument => argument.Expression is ParenthesizedLambdaExpressionSyntax parenthesizedLambdaExpression && + parenthesizedLambdaExpression.AttributeLists + .SelectMany(attributeList => attributeList.Attributes) + .Any(attribute => attribute.Name.ToString() == "Topic" || attribute.Name.ToString().EndsWith(".Topic"))))); + } + + return invocationsWithTopicAttributeParameter; + } +} diff --git a/test/Dapr.Pubsub.Analyzers.Test/Dapr.Pubsub.Analyzers.Test.csproj b/test/Dapr.Pubsub.Analyzers.Test/Dapr.Pubsub.Analyzers.Test.csproj new file mode 100644 index 000000000..ee8315f20 --- /dev/null +++ b/test/Dapr.Pubsub.Analyzers.Test/Dapr.Pubsub.Analyzers.Test.csproj @@ -0,0 +1,35 @@ + + + + enable + enable + false + true + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/test/Dapr.Pubsub.Analyzers.Test/MapSubscribeHandlerCodeFixProviderTests.cs b/test/Dapr.Pubsub.Analyzers.Test/MapSubscribeHandlerCodeFixProviderTests.cs new file mode 100644 index 000000000..5a50012d5 --- /dev/null +++ b/test/Dapr.Pubsub.Analyzers.Test/MapSubscribeHandlerCodeFixProviderTests.cs @@ -0,0 +1,72 @@ +namespace Dapr.Pubsub.Analyzers.Test; + +public class MapSubscribeHandlerCodeFixProviderTests +{ + [Fact] + public async Task MapSubscribeHandler() + { + var code = @" + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + + app.MapPost(""/subscribe"", () => {}) + .WithTopic(""pubSubName"", ""topicName""); + } + } + "; + + var expectedChangedCode = @" + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + + app.MapSubscribeHandler(); + + app.MapPost(""/subscribe"", () => {}) + .WithTopic(""pubSubName"", ""topicName""); + } + } + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } + + [Fact] + public async Task MapSubscribeHandler_TopLevelStatements() + { + var code = @" + using Microsoft.AspNetCore.Builder; + + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + + app.MapPost(""/subscribe"", () => {}) + .WithTopic(""pubSubName"", ""topicName""); + "; + + var expectedChangedCode = @" + using Microsoft.AspNetCore.Builder; + + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + + app.MapSubscribeHandler(); + + app.MapPost(""/subscribe"", () => {}) + .WithTopic(""pubSubName"", ""topicName""); + "; + + await VerifyCodeFix.RunTest(code, expectedChangedCode); + } +} diff --git a/test/Dapr.Pubsub.Analyzers.Test/SubscriptionAnalyzerTests.cs b/test/Dapr.Pubsub.Analyzers.Test/SubscriptionAnalyzerTests.cs new file mode 100644 index 000000000..3712b0b2a --- /dev/null +++ b/test/Dapr.Pubsub.Analyzers.Test/SubscriptionAnalyzerTests.cs @@ -0,0 +1,60 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Pubsub.Analyzers.Test; + +public class SubscriptionAnalyzerTests +{ + public class MapSubscribeHandler + { + [Fact] + public async Task ReportDiagnostic_WithTopic() + { + var testCode = @" + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + + app.MapPost(""/subscribe"", () => {}) + .WithTopic(""pubSubName"", ""topicName""); + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR2001", DiagnosticSeverity.Warning) + .WithSpan(11, 25, 12, 66).WithMessage("Call app.MapSubscribeHandler to map endpoints for Dapr subscriptions"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task ReportDiagnostic_TopicAttribute() + { + var testCode = @" + using Dapr; + using Microsoft.AspNetCore.Builder; + + public static class Program + { + public static void Main() + { + var builder = WebApplication.CreateBuilder(); + var app = builder.Build(); + + app.MapPost(""/subscribe"", [Topic(""pubsubName"", ""topicName"")] () => {}); + } + } + "; + + var expected = VerifyAnalyzer.Diagnostic("DAPR2001", DiagnosticSeverity.Warning) + .WithSpan(12, 25, 12, 95).WithMessage("Call app.MapSubscribeHandler to map endpoints for Dapr subscriptions"); + + await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected); + } + } + +} diff --git a/test/Dapr.Pubsub.Analyzers.Test/Utilities.cs b/test/Dapr.Pubsub.Analyzers.Test/Utilities.cs new file mode 100644 index 000000000..25cbefe57 --- /dev/null +++ b/test/Dapr.Pubsub.Analyzers.Test/Utilities.cs @@ -0,0 +1,75 @@ +using System.Collections.Immutable; +using System.Reflection; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; + +namespace Dapr.Pubsub.Analyzers.Test; + +internal static class Utilities +{ + public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code) + { + var workspace = new AdhocWorkspace(); + +#if NET6_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net60; +#elif NET7_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net70; +#elif NET8_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET9_0 + var referenceAssemblies = ReferenceAssemblies.Net.Net90; +#endif + + // Create a new project with necessary references + var project = workspace.AddProject("TestProject", LanguageNames.CSharp) + .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication) + .WithSpecificDiagnosticOptions(new Dictionary + { + { "CS1701", ReportDiagnostic.Suppress } + })) + //.AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default)) + .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(IHost))) + .AddMetadataReferences(GetAllReferencesNeededForType(typeof(DaprEndpointConventionBuilderExtensions))); + + // Add the document to the project + var document = project.AddDocument("TestDocument.cs", code); + + // Get the syntax tree and create a compilation + var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null"); + var compilation = CSharpCompilation.Create("TestCompilation") + .AddSyntaxTrees(syntaxTree) + .AddReferences(project.MetadataReferences) + .WithOptions(project.CompilationOptions!); + + var compilationWithAnalyzer = compilation.WithAnalyzers( + ImmutableArray.Create( + new SubscriptionAnalyzer())); + + // Get diagnostics from the compilation + var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync(); + return (diagnostics, document, workspace); + } + + public static MetadataReference[] GetAllReferencesNeededForType(Type type) + { + var files = GetAllAssemblyFilesNeededForType(type); + + return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray(); + } + + private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type) + { + return type.Assembly.GetReferencedAssemblies() + .Select(x => Assembly.Load(x.FullName)) + .Append(type.Assembly) + .Select(x => x.Location) + .ToImmutableArray(); + } +} diff --git a/test/Dapr.Pubsub.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Pubsub.Analyzers.Test/VerifyAnalyzer.cs new file mode 100644 index 000000000..8ca244348 --- /dev/null +++ b/test/Dapr.Pubsub.Analyzers.Test/VerifyAnalyzer.cs @@ -0,0 +1,69 @@ +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; + +namespace Dapr.Pubsub.Analyzers.Test; + +internal static class VerifyAnalyzer +{ + public static DiagnosticResult Diagnostic(string diagnosticId, DiagnosticSeverity diagnosticSeverity) + { + return new DiagnosticResult(diagnosticId, diagnosticSeverity); + } + + public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected) + { + await VerifyAnalyzerAsync(source, null, expected); + } + + public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected) + { + var test = new Test { TestCode = source }; + +#if NET6_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net60; +#elif NET7_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net70; +#elif NET8_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; +#elif NET9_0 + test.ReferenceAssemblies = ReferenceAssemblies.Net.Net90; +#endif + + if (program != null) + { + test.TestState.Sources.Add(("Program.cs", program)); + } + + var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(SubscriptionAnalyzer)).ToList(); + //metadataReferences.AddRange(await test.ReferenceAssemblies.ResolveAsync(LanguageNames.CSharp, default)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); + metadataReferences.AddRange(Utilities.GetAllReferencesNeededForType(typeof(WebApplication))); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(EndpointRouteBuilderExtensions).Assembly.Location)); + metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprEndpointConventionBuilderExtensions).Assembly.Location)); + + foreach (var reference in metadataReferences) + { + test.TestState.AdditionalReferences.Add(reference); + } + + test.ExpectedDiagnostics.AddRange(expected); + await test.RunAsync(CancellationToken.None); + } + + private class Test : CSharpAnalyzerTest + { + public Test() + { + SolutionTransforms.Add((solution, projectId) => + { + var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!; + solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); + return solution; + }); + } + } +} diff --git a/test/Dapr.Pubsub.Analyzers.Test/VerifyCodeFix.cs b/test/Dapr.Pubsub.Analyzers.Test/VerifyCodeFix.cs new file mode 100644 index 000000000..593abacda --- /dev/null +++ b/test/Dapr.Pubsub.Analyzers.Test/VerifyCodeFix.cs @@ -0,0 +1,56 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Dapr.Pubsub.Analyzers.Test; + +internal class VerifyCodeFix +{ + public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new() + { + var (diagnostics, document, workspace) = await Utilities.GetDiagnosticsAdvanced(code); + + Assert.Single(diagnostics); + + var diagnostic = diagnostics[0]; + + var codeFixProvider = new T(); + + CodeAction? registeredCodeAction = null; + + var context = new CodeFixContext(document, diagnostic, (codeAction, _) => + { + if (registeredCodeAction != null) + throw new Exception("Code action was registered more than once"); + + registeredCodeAction = codeAction; + + }, CancellationToken.None); + + await codeFixProvider.RegisterCodeFixesAsync(context); + + if (registeredCodeAction == null) + throw new Exception("Code action was not registered"); + + var operations = await registeredCodeAction.GetOperationsAsync(CancellationToken.None); + + foreach (var operation in operations) + { + operation.Apply(workspace, CancellationToken.None); + } + + var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null"); + var newCode = (await updatedDocument.GetTextAsync()).ToString(); + + // Normalize whitespace + string NormalizeWhitespace(string input) + { + var separator = new[] { ' ', '\r', '\n' }; + return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries)); + } + + var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode); + var normalizedNewCode = NormalizeWhitespace(newCode); + + Assert.Equal(normalizedExpectedCode, normalizedNewCode); + } +}