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

Commit 58f6b0e

Browse files
Merge pull request #27 from episerver/user/jb/partial-routers
User/jb/partial routers
2 parents bbef4a5 + 215f09e commit 58f6b0e

9 files changed

+366
-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: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 RegistrationMethod = "RegisterPartialRouter";
33+
private static readonly string[] BaseTypes = new[] { "IPartialRouter" };
34+
35+
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.EpiPartialRouterTitle), Resources.ResourceManager, typeof(Resources));
36+
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.EpiPartialRouterMessageFormat), Resources.ResourceManager, typeof(Resources));
37+
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.EpiPartialRouterDescription), Resources.ResourceManager, typeof(Resources));
38+
39+
private static readonly DiagnosticDescriptor Rule = new(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
40+
41+
public EpiPartialRouterAnalyzer() : base(BaseTypes)
42+
{
43+
}
44+
45+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
46+
47+
public override void Initialize(AnalysisContext context)
48+
{
49+
if (context is null)
50+
{
51+
throw new ArgumentNullException(nameof(context));
52+
}
53+
54+
context.EnableConcurrentExecution();
55+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
56+
57+
context.RegisterSyntaxNodeAction(AnalyzeIfInterfaceMethod, SyntaxKind.MethodDeclaration);
58+
context.RegisterCompilationStartAction(compilationContext =>
59+
{
60+
compilationContext.RegisterSyntaxNodeAction(AnalyzePartialRouteRegistration, SyntaxKind.InvocationExpression);
61+
});
62+
}
63+
64+
private void AnalyzeIfInterfaceMethod(SyntaxNodeAnalysisContext context)
65+
{
66+
var methodDirective = (MethodDeclarationSyntax)context.Node;
67+
68+
var namespaceName = methodDirective.Identifier.Text?.ToString();
69+
70+
if (namespaceName is null)
71+
{
72+
return;
73+
}
74+
75+
if (namespaceName.Equals(MethodName, StringComparison.Ordinal))
76+
{
77+
var parameters = methodDirective.ParameterList.Parameters;
78+
if (IsSubType(methodDirective.Parent as ClassDeclarationSyntax))
79+
{
80+
var diagnostic = Diagnostic.Create(Rule, methodDirective.Parent.GetLocation(), methodDirective.ToFullString());
81+
context.ReportDiagnostic(diagnostic);
82+
}
83+
}
84+
}
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+
}
107+
}
108+
}

0 commit comments

Comments
 (0)