Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.

Commit 215f09e

Browse files
committed
Added analyzer/codefixer for partial route registration
1 parent 68a547d commit 215f09e

File tree

2 files changed

+124
-10
lines changed

2 files changed

+124
-10
lines changed

src/EpiSourceUpdater/EpiPartialRouterAnalyzer.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public sealed class EpiPartialRouterAnalyzer : EpiSubTypeAnalyzer
2929

3030

3131
private static readonly string MethodName = "RoutePartial";
32+
private static readonly string RegistrationMethod = "RegisterPartialRouter";
3233
private static readonly string[] BaseTypes = new[] { "IPartialRouter" };
3334

3435
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.EpiPartialRouterTitle), Resources.ResourceManager, typeof(Resources));
@@ -54,6 +55,10 @@ public override void Initialize(AnalysisContext context)
5455
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
5556

5657
context.RegisterSyntaxNodeAction(AnalyzeIfInterfaceMethod, SyntaxKind.MethodDeclaration);
58+
context.RegisterCompilationStartAction(compilationContext =>
59+
{
60+
compilationContext.RegisterSyntaxNodeAction(AnalyzePartialRouteRegistration, SyntaxKind.InvocationExpression);
61+
});
5762
}
5863

5964
private void AnalyzeIfInterfaceMethod(SyntaxNodeAnalysisContext context)
@@ -77,5 +82,27 @@ private void AnalyzeIfInterfaceMethod(SyntaxNodeAnalysisContext context)
7782
}
7883
}
7984
}
85+
86+
private void AnalyzePartialRouteRegistration(SyntaxNodeAnalysisContext context)
87+
{
88+
var memberExpression = (context.Node as InvocationExpressionSyntax)?.Expression as MemberAccessExpressionSyntax;
89+
if (memberExpression is null)
90+
{
91+
return;
92+
}
93+
94+
// If the accessed member isn't named "RegisterPartialRouter" bail out
95+
if (!RegistrationMethod.Equals(memberExpression.Name.Identifier.Text))
96+
{
97+
return;
98+
}
99+
100+
//If we have already acted by adding a comment then we should not report
101+
if (!memberExpression.GetLeadingTrivia().Any(t => t.Kind() == SyntaxKind.SingleLineCommentTrivia && t.ToString().StartsWith("//Partial router should be registered in ioc container")))
102+
{
103+
var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation());
104+
context.ReportDiagnostic(diagnostic);
105+
}
106+
}
80107
}
81108
}

src/EpiSourceUpdater/EpiPartialRouterCodeFixProvider.cs

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class EpiPartialRouterCodeFixProvider : CodeFixProvider
2929
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(EpiPartialRouterAnalyzer.DiagnosticId);
3030
private const string RoutingNamespace = "EPiServer.Core.Routing";
3131
private const string RoutingPipelineNamespace = "EPiServer.Core.Routing.Pipeline";
32+
private const string DependencyInjectionNamespace = "Microsoft.Extensions.DependencyInjection";
3233

3334
public sealed override FixAllProvider GetFixAllProvider() =>
3435
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
@@ -45,18 +46,26 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
4546

4647
var diagnostic = context.Diagnostics.First();
4748
var diagnosticSpan = diagnostic.Location.SourceSpan;
48-
var methodDeclaration = root.FindNode(diagnosticSpan) as ClassDeclarationSyntax;
49-
if (methodDeclaration is null)
49+
var declaration = root.FindNode(diagnosticSpan);
50+
if (declaration is ClassDeclarationSyntax classDeclaration)
5051
{
51-
return;
52+
context.RegisterCodeFix(
53+
CodeAction.Create(
54+
Resources.EpiPartialRouterTitle,
55+
c => ReplaceParametersAsync(context.Document, classDeclaration, c),
56+
nameof(Resources.EpiPartialRouterTitle)),
57+
diagnostic);
5258
}
53-
54-
context.RegisterCodeFix(
55-
CodeAction.Create(
56-
Resources.EpiPartialRouterTitle,
57-
c => ReplaceParametersAsync(context.Document, methodDeclaration, c),
58-
nameof(Resources.EpiPartialRouterTitle)),
59-
diagnostic);
59+
else if (declaration is InvocationExpressionSyntax invocationExpression)
60+
{
61+
context.RegisterCodeFix(
62+
CodeAction.Create(
63+
Resources.EpiPartialRouterTitle,
64+
c => HandlePartialRouteRegistration(context.Document, invocationExpression, c),
65+
nameof(Resources.EpiPartialRouterTitle)),
66+
diagnostic);
67+
}
68+
6069
}
6170

6271
private static async Task<Document> ReplaceParametersAsync(Document document, ClassDeclarationSyntax localDeclaration, CancellationToken cancellationToken)
@@ -92,5 +101,83 @@ private static MethodDeclarationSyntax FindMethod(ClassDeclarationSyntax classDe
92101
{
93102
return classDeclaration.Members.OfType<MethodDeclarationSyntax>().FirstOrDefault(m => m.Identifier.Text == methodName);
94103
}
104+
105+
private async Task<Document> HandlePartialRouteRegistration(Document document, InvocationExpressionSyntax invocationExpression, CancellationToken cancellationToken)
106+
{
107+
//find the registration statement
108+
var currentCodeBlock = GetSurroundingBlock(invocationExpression);
109+
var getPartialRouterArgument = invocationExpression.ArgumentList.Arguments[0].Expression;
110+
111+
if (currentCodeBlock is not null)
112+
{
113+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
114+
var index = -1;
115+
var statements = currentCodeBlock.Statements;
116+
//find where registration statement is
117+
for (var i = 0; i < statements.Count; i++)
118+
{
119+
var statement = statements[i];
120+
if (statement is ExpressionStatementSyntax expressionStatement && expressionStatement.Expression == invocationExpression)
121+
{
122+
index = i;
123+
break;
124+
}
125+
}
126+
127+
if (index > -1)
128+
{
129+
var statement = statements[index];
130+
statements = statements.RemoveAt(index);
131+
var methodDeclaration = GetSurroundingMethod(invocationExpression);
132+
if (methodDeclaration is not null && methodDeclaration.Identifier.Text == "ConfigureContainer")
133+
{
134+
var parameterName = methodDeclaration.ParameterList.Parameters[0].Identifier.Text;
135+
if (getPartialRouterArgument is ObjectCreationExpressionSyntax objectCreationExpression)
136+
{
137+
statements = statements.Insert(index, SyntaxFactory.ParseStatement($"{parameterName}.Services.AddSingleton<IPartialRouter, {objectCreationExpression.Type}>();")
138+
.WithLeadingTrivia(statement.GetLeadingTrivia()).WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed));
139+
}
140+
else
141+
{
142+
statements = statements.Insert(index, SyntaxFactory.ParseStatement($"{parameterName}.Services.AddSingleton<IPartialRouter>({getPartialRouterArgument.ToString()});")
143+
.WithLeadingTrivia(statement.GetLeadingTrivia()).WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed));
144+
}
145+
}
146+
else
147+
{
148+
//The registration is done somewhere where we do not have access to IServiceCollection, add a comment on how registration should be done
149+
var parameterType = getPartialRouterArgument is ObjectCreationExpressionSyntax objectCreationExpression ? objectCreationExpression.Type.ToString() : "CustomPartialRouter";
150+
var comment = SyntaxFactory.Comment($"//Partial router should be registered in ioc container like 'services.AddSingleton<IPartialRouter, {parameterType}>()'");
151+
statements = statements.Insert(index, statement.WithLeadingTrivia(statement.GetLeadingTrivia().Add(comment).Add(SyntaxFactory.CarriageReturnLineFeed)));
152+
}
153+
154+
var newCodeBlock = currentCodeBlock.WithStatements(statements);
155+
var newroot = root.ReplaceNode(currentCodeBlock, newCodeBlock);
156+
return await document.WithSyntaxRoot(newroot).AddUsingIfMissingAsync(cancellationToken, DependencyInjectionNamespace);
157+
}
158+
}
159+
160+
return document;
161+
}
162+
163+
private BlockSyntax GetSurroundingBlock(InvocationExpressionSyntax invocationExpression)
164+
{
165+
var parent = invocationExpression.Parent;
166+
while (parent is not null && parent is not BlockSyntax)
167+
{
168+
parent = parent.Parent;
169+
}
170+
return parent as BlockSyntax;
171+
}
172+
173+
private MethodDeclarationSyntax GetSurroundingMethod(InvocationExpressionSyntax invocationExpression)
174+
{
175+
var parent = invocationExpression.Parent;
176+
while (parent is not null && parent is not MethodDeclarationSyntax)
177+
{
178+
parent = parent.Parent;
179+
}
180+
return parent as MethodDeclarationSyntax;
181+
}
95182
}
96183
}

0 commit comments

Comments
 (0)