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

Commit 68a547d

Browse files
committed
Added analyzer/codefixer to rewrite partial router methods
1 parent bbef4a5 commit 68a547d

9 files changed

+252
-62
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Microsoft.CodeAnalysis.CSharp.Syntax;
4+
using Microsoft.CodeAnalysis.Editing;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
namespace Epi.Source.Updater
13+
{
14+
internal static class DocumentExtensions
15+
{
16+
public static async Task<Document> AddUsingIfMissingAsync(this Document document, CancellationToken cancellationToken, params string[] namespaces)
17+
{
18+
var compilationRoot = (await document.GetSyntaxTreeAsync()).GetCompilationUnitRoot();
19+
var missingUsings = namespaces.Where(n => !compilationRoot.Usings.Any(u => u.Name.ToString() == n))
20+
.Select(n => SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(n).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")))
21+
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed));
22+
if (missingUsings.Any())
23+
{
24+
var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false);
25+
var documentRoot = (CompilationUnitSyntax)editor.OriginalRoot;
26+
documentRoot = documentRoot.AddUsings(missingUsings.ToArray());
27+
editor.ReplaceNode(editor.OriginalRoot, documentRoot);
28+
document = editor.GetChangedDocument();
29+
}
30+
31+
return document;
32+
}
33+
}
34+
}

src/EpiSourceUpdater/EpiDisplayChannelCodeFixProvider.cs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -68,20 +68,7 @@ private static async Task<Document> ReplaceNameAndReturnParameterAsync(Document
6868

6969
// Return document with transformed tree.
7070
var updatedDocument = document.WithSyntaxRoot(newRoot);
71-
72-
var compilationRoot = (await updatedDocument.GetSyntaxTreeAsync()).GetCompilationUnitRoot();
73-
if (!compilationRoot.Usings.Any(u => u.Name.ToString() == HttpNamespace))
74-
{
75-
var editor = await DocumentEditor.CreateAsync(updatedDocument, cancellationToken).ConfigureAwait(false);
76-
var documentRoot = (CompilationUnitSyntax)editor.OriginalRoot;
77-
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(HttpNamespace).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")))
78-
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
79-
documentRoot = compilationRoot.AddUsings(usingDirective);
80-
editor.ReplaceNode(editor.OriginalRoot, documentRoot);
81-
updatedDocument = editor.GetChangedDocument();
82-
}
83-
84-
return updatedDocument;
71+
return await updatedDocument.AddUsingIfMissingAsync(cancellationToken, HttpNamespace);
8572
}
8673
}
8774
}

src/EpiSourceUpdater/EpiMetadataAwareCodeFixProvider.cs

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,7 @@ private async Task<Document> RefactorToDisplayModeProvider(Document document, Cl
7272

7373
// Return document with transformed tree.
7474
var updatedDocument = document.WithSyntaxRoot(newRoot);
75-
76-
var compilationRoot = (await updatedDocument.GetSyntaxTreeAsync()).GetCompilationUnitRoot();
77-
if (!compilationRoot.Usings.Any(u => u.Name.ToString() == MicrosoftMetadataNamespace))
78-
{
79-
var editor = await DocumentEditor.CreateAsync(updatedDocument, cancellationToken).ConfigureAwait(false);
80-
var documentRoot = (CompilationUnitSyntax)editor.OriginalRoot;
81-
documentRoot = AddUsing(compilationRoot, MicrosoftMetadataNamespace);
82-
83-
editor.ReplaceNode(editor.OriginalRoot, documentRoot);
84-
updatedDocument = editor.GetChangedDocument();
85-
}
86-
87-
return updatedDocument;
75+
return await updatedDocument.AddUsingIfMissingAsync(cancellationToken, MicrosoftMetadataNamespace);
8876
}
8977

9078
private ClassDeclarationSyntax RefactorMethod(ClassDeclarationSyntax classDeclaration)
@@ -131,12 +119,5 @@ private ClassDeclarationSyntax RefactorMethod(ClassDeclarationSyntax classDeclar
131119

132120
return classDeclaration;
133121
}
134-
135-
private static CompilationUnitSyntax AddUsing(CompilationUnitSyntax compilationRoot, string namespaceToAdd)
136-
{
137-
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceToAdd).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")))
138-
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
139-
return compilationRoot.AddUsings(usingDirective);
140-
}
141122
}
142123
}

src/EpiSourceUpdater/EpiPartialControllerCodeFixProvider.cs

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -84,34 +84,7 @@ private static async Task<Document> ReplaceNameAndReturnParameterAsync(Document
8484
// Return document with transformed tree.
8585
var updatedDocument = document.WithSyntaxRoot(newRoot);
8686

87-
var compilationRoot = (await updatedDocument.GetSyntaxTreeAsync()).GetCompilationUnitRoot();
88-
var missingMicrosoftMvcNamespace = !compilationRoot.Usings.Any(u => u.Name.ToString() == MicrosoftMvcNamespace);
89-
var missingEpiserverMvcNamespace = !compilationRoot.Usings.Any(u => u.Name.ToString() == EPiServerMvcNamespace);
90-
if (missingMicrosoftMvcNamespace || missingEpiserverMvcNamespace)
91-
{
92-
var editor = await DocumentEditor.CreateAsync(updatedDocument, cancellationToken).ConfigureAwait(false);
93-
var documentRoot = (CompilationUnitSyntax)editor.OriginalRoot;
94-
if (missingMicrosoftMvcNamespace)
95-
{
96-
documentRoot = AddUsing(compilationRoot, MicrosoftMvcNamespace);
97-
}
98-
if (missingEpiserverMvcNamespace)
99-
{
100-
documentRoot = AddUsing(compilationRoot, EPiServerMvcNamespace);
101-
}
102-
103-
editor.ReplaceNode(editor.OriginalRoot, documentRoot);
104-
updatedDocument = editor.GetChangedDocument();
105-
}
106-
107-
return updatedDocument;
108-
}
109-
110-
private static CompilationUnitSyntax AddUsing(CompilationUnitSyntax compilationRoot, string namespaceToAdd)
111-
{
112-
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceToAdd).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")))
113-
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
114-
return compilationRoot.AddUsings(usingDirective);
87+
return await updatedDocument.AddUsingIfMissingAsync(cancellationToken, MicrosoftMvcNamespace, EPiServerMvcNamespace);
11588
}
11689

11790
private static async Task<Document> ReplacePartialViewMethodAsync(Document document, IdentifierNameSyntax identifierNameSyntax, CancellationToken cancellationToken)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CSharp;
6+
using Microsoft.CodeAnalysis.CSharp.Syntax;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using System;
9+
using System.Collections.Immutable;
10+
using System.Linq;
11+
12+
namespace Epi.Source.Updater
13+
{
14+
/// <summary>
15+
/// Analyzer for identifying and removing obsolet types or methods.
16+
/// </summary>
17+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
18+
public sealed class EpiPartialRouterAnalyzer : EpiSubTypeAnalyzer
19+
{
20+
/// <summary>
21+
/// The diagnostic ID for diagnostics produced by this analyzer.
22+
/// </summary>
23+
public const string DiagnosticId = "EP0009";
24+
25+
/// <summary>
26+
/// The diagnsotic category for diagnostics produced by this analyzer.
27+
/// </summary>
28+
private const string Category = "Upgrade";
29+
30+
31+
private static readonly string MethodName = "RoutePartial";
32+
private static readonly string[] BaseTypes = new[] { "IPartialRouter" };
33+
34+
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.EpiPartialRouterTitle), Resources.ResourceManager, typeof(Resources));
35+
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.EpiPartialRouterMessageFormat), Resources.ResourceManager, typeof(Resources));
36+
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.EpiPartialRouterDescription), Resources.ResourceManager, typeof(Resources));
37+
38+
private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
39+
40+
public EpiPartialRouterAnalyzer() : base(BaseTypes)
41+
{
42+
}
43+
44+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
45+
46+
public override void Initialize(AnalysisContext context)
47+
{
48+
if (context is null)
49+
{
50+
throw new ArgumentNullException(nameof(context));
51+
}
52+
53+
context.EnableConcurrentExecution();
54+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
55+
56+
context.RegisterSyntaxNodeAction(AnalyzeIfInterfaceMethod, SyntaxKind.MethodDeclaration);
57+
}
58+
59+
private void AnalyzeIfInterfaceMethod(SyntaxNodeAnalysisContext context)
60+
{
61+
var methodDirective = (MethodDeclarationSyntax)context.Node;
62+
63+
var namespaceName = methodDirective.Identifier.Text?.ToString();
64+
65+
if (namespaceName is null)
66+
{
67+
return;
68+
}
69+
70+
if (namespaceName.Equals(MethodName, StringComparison.Ordinal))
71+
{
72+
var parameters = methodDirective.ParameterList.Parameters;
73+
if (IsSubType(methodDirective.Parent as ClassDeclarationSyntax))
74+
{
75+
var diagnostic = Diagnostic.Create(Rule, methodDirective.Parent.GetLocation(), methodDirective.ToFullString());
76+
context.ReportDiagnostic(diagnostic);
77+
}
78+
}
79+
}
80+
}
81+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.CodeAnalysis;
5+
using Microsoft.CodeAnalysis.CodeActions;
6+
using Microsoft.CodeAnalysis.CodeFixes;
7+
using Microsoft.CodeAnalysis.CSharp;
8+
using Microsoft.CodeAnalysis.CSharp.Syntax;
9+
using Microsoft.CodeAnalysis.Editing;
10+
using Microsoft.CodeAnalysis.Simplification;
11+
using System;
12+
using System.Collections.Immutable;
13+
using System.Linq;
14+
using System.Threading;
15+
using System.Threading.Tasks;
16+
17+
namespace Epi.Source.Updater
18+
{
19+
/// <summary>
20+
/// As with the analzyers, code fix providers that are registered into Upgrade Assistant's
21+
/// dependency injection container (by IExtensionServiceProvider) will be used during
22+
/// the source update step.
23+
/// </summary>
24+
[ExportCodeFixProvider(LanguageNames.CSharp, Name = "EP0009 CodeFix Provider")]
25+
public class EpiPartialRouterCodeFixProvider : CodeFixProvider
26+
{
27+
// The Upgrade Assistant will only use analyzers that have an associated code fix provider registered including
28+
// the analyzer's ID in the code fix provider's FixableDiagnosticIds array.
29+
public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(EpiPartialRouterAnalyzer.DiagnosticId);
30+
private const string RoutingNamespace = "EPiServer.Core.Routing";
31+
private const string RoutingPipelineNamespace = "EPiServer.Core.Routing.Pipeline";
32+
33+
public sealed override FixAllProvider GetFixAllProvider() =>
34+
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
35+
WellKnownFixAllProviders.BatchFixer;
36+
37+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
38+
{
39+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
40+
41+
if (root is null)
42+
{
43+
return;
44+
}
45+
46+
var diagnostic = context.Diagnostics.First();
47+
var diagnosticSpan = diagnostic.Location.SourceSpan;
48+
var methodDeclaration = root.FindNode(diagnosticSpan) as ClassDeclarationSyntax;
49+
if (methodDeclaration is null)
50+
{
51+
return;
52+
}
53+
54+
context.RegisterCodeFix(
55+
CodeAction.Create(
56+
Resources.EpiPartialRouterTitle,
57+
c => ReplaceParametersAsync(context.Document, methodDeclaration, c),
58+
nameof(Resources.EpiPartialRouterTitle)),
59+
diagnostic);
60+
}
61+
62+
private static async Task<Document> ReplaceParametersAsync(Document document, ClassDeclarationSyntax localDeclaration, CancellationToken cancellationToken)
63+
{
64+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
65+
66+
var routePartialMethod = FindMethod(localDeclaration, "RoutePartial");
67+
var parameters = routePartialMethod.ParameterList.Parameters;
68+
var obsoleteParameter = routePartialMethod.ParameterList.Parameters[1];
69+
parameters = parameters.Remove(obsoleteParameter);
70+
parameters = parameters.Add(obsoleteParameter.WithType(SyntaxFactory.ParseTypeName("UrlResolverContext")));
71+
var updatedMethod = routePartialMethod.WithParameterList(SyntaxFactory.ParameterList(parameters));
72+
73+
var newRoot = root.ReplaceNode(routePartialMethod, updatedMethod);
74+
var updatedDocument = document.WithSyntaxRoot(newRoot);
75+
localDeclaration = newRoot.FindNode(localDeclaration.Span) as ClassDeclarationSyntax;
76+
77+
var virtualPathMethod = FindMethod(localDeclaration, "GetPartialVirtualPath");
78+
parameters = virtualPathMethod.ParameterList.Parameters;
79+
parameters = parameters.RemoveAt(3);
80+
parameters = parameters.RemoveAt(2);
81+
parameters = parameters.RemoveAt(1);
82+
parameters = parameters.Add(SyntaxFactory.Parameter(SyntaxFactory.Identifier("urlGeneratorContext")).WithType(SyntaxFactory.ParseTypeName("UrlGeneratorContext")));
83+
updatedMethod = virtualPathMethod.WithParameterList(SyntaxFactory.ParameterList(parameters));
84+
85+
newRoot = newRoot.ReplaceNode(virtualPathMethod, updatedMethod);
86+
updatedDocument = document.WithSyntaxRoot(newRoot);
87+
88+
return await updatedDocument.AddUsingIfMissingAsync(cancellationToken, RoutingNamespace, RoutingPipelineNamespace);
89+
}
90+
91+
private static MethodDeclarationSyntax FindMethod(ClassDeclarationSyntax classDeclaration, string methodName)
92+
{
93+
return classDeclaration.Members.OfType<MethodDeclarationSyntax>().FirstOrDefault(m => m.Identifier.Text == methodName);
94+
}
95+
}
96+
}

src/EpiSourceUpdater/EpiSourceUpdaterServiceProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public void AddServices(IExtensionServiceCollection services)
5858
services.Services.AddTransient<DiagnosticAnalyzer, EpiPartialControllerAnalyzer>(); // EP0006
5959
services.Services.AddTransient<DiagnosticAnalyzer, EpiDisplayChannelAnalyzer>(); // EP0007
6060
services.Services.AddTransient<DiagnosticAnalyzer, EpiMetadataAwareAnalyzer>(); // EP0008
61+
services.Services.AddTransient<DiagnosticAnalyzer, EpiPartialRouterAnalyzer>(); // EP0009
6162

6263
// Upgrade Step.
6364
services.Services.AddUpgradeStep<FindReplaceUpgradeStep>();
@@ -72,6 +73,7 @@ public void AddServices(IExtensionServiceCollection services)
7273
services.Services.AddTransient<CodeFixProvider, EpiPartialControllerCodeFixProvider>(); // EP0006
7374
services.Services.AddTransient<CodeFixProvider, EpiDisplayChannelCodeFixProvider>(); // EP0007
7475
services.Services.AddTransient<CodeFixProvider, EpiMetadataAwareCodeFixProvider>(); // EP0008
76+
services.Services.AddTransient<CodeFixProvider, EpiPartialRouterCodeFixProvider>(); // EP0009
7577
}
7678
}
7779
}

src/EpiSourceUpdater/Resources.Designer.cs

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EpiSourceUpdater/Resources.resx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,15 @@
180180
<data name="EpiPartialControllerTitle" xml:space="preserve">
181181
<value>CMS partial controllers will be updated</value>
182182
</data>
183+
<data name="EpiPartialRouterDescription" xml:space="preserve">
184+
<value>CMS partial routers will be updated.</value>
185+
</data>
186+
<data name="EpiPartialRouterMessageFormat" xml:space="preserve">
187+
<value>Method signatures will be updated</value>
188+
</data>
189+
<data name="EpiPartialRouterTitle" xml:space="preserve">
190+
<value>CMS partial routers will be updated</value>
191+
</data>
183192
<data name="MakeConstDescription" xml:space="preserve">
184193
<value>Variables that are not set after they are created should be marked as constant</value>
185194
</data>

0 commit comments

Comments
 (0)