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

Commit ea1b7fd

Browse files
authored
Merge pull request #21 from episerver/user/jb/rewrite-partial-controllers
User/jb/rewrite partial controllers
2 parents a6e520f + 1ec4be6 commit ea1b7fd

File tree

5 files changed

+312
-21
lines changed

5 files changed

+312
-21
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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 EpiPartialControllerAnalyzer : DiagnosticAnalyzer
19+
{
20+
/// <summary>
21+
/// The diagnostic ID for diagnostics produced by this analyzer.
22+
/// </summary>
23+
public const string DiagnosticId = "EP0006";
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 = "Index";
32+
internal static readonly string PartialView = "PartialView";
33+
private static readonly string[] BaseTypes = new[] { "PartialContentController", "BlockController", "PartialContentComponent", "BlockComponent" };
34+
35+
private static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.EpiPartialControllerTitle), Resources.ResourceManager, typeof(Resources));
36+
private static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.EpiPartialControllerFormat), Resources.ResourceManager, typeof(Resources));
37+
private static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.EpiPartialControllerDescription), 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 override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);
42+
43+
public override void Initialize(AnalysisContext context)
44+
{
45+
if (context is null)
46+
{
47+
throw new ArgumentNullException(nameof(context));
48+
}
49+
50+
context.EnableConcurrentExecution();
51+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
52+
53+
context.RegisterSyntaxNodeAction(AnalyzeIndexMethod, SyntaxKind.MethodDeclaration);
54+
context.RegisterCompilationStartAction(compilationContext =>
55+
{
56+
compilationContext.RegisterSyntaxNodeAction(AnalyzePartialViewUsage, SyntaxKind.InvocationExpression);
57+
});
58+
}
59+
60+
private void AnalyzePartialViewUsage(SyntaxNodeAnalysisContext context)
61+
{
62+
var identifierExpression = (context.Node as InvocationExpressionSyntax)?.Expression as IdentifierNameSyntax;
63+
if (identifierExpression is null)
64+
{
65+
return;
66+
}
67+
68+
// If the accessed member isn't named "PartialView" bail out
69+
if (!PartialView.Equals(identifierExpression.Identifier.Text))
70+
{
71+
return;
72+
}
73+
74+
//Only change if inside a partial controller
75+
if (!IsPartialController(FindClassDeclaration(context.Node)))
76+
{
77+
return;
78+
}
79+
80+
var diagnostic = Diagnostic.Create(Rule, identifierExpression.GetLocation(), ImmutableDictionary.Create<string, string?>().Add(PartialView, true.ToString()));
81+
context.ReportDiagnostic(diagnostic);
82+
}
83+
84+
private void AnalyzeIndexMethod(SyntaxNodeAnalysisContext context)
85+
{
86+
var methodDirective = (MethodDeclarationSyntax)context.Node;
87+
88+
var namespaceName = methodDirective.Identifier.Text?.ToString();
89+
90+
if (namespaceName is null)
91+
{
92+
return;
93+
}
94+
95+
if (namespaceName.Equals(MethodName, StringComparison.Ordinal))
96+
{
97+
if (methodDirective.ReturnType.ToString().ToUpperInvariant() == "ACTIONRESULT" && IsPartialController(methodDirective.Parent as ClassDeclarationSyntax))
98+
{
99+
var diagnostic = Diagnostic.Create(Rule, methodDirective.GetLocation(), methodDirective.ToFullString());
100+
context.ReportDiagnostic(diagnostic);
101+
}
102+
}
103+
}
104+
105+
private static bool IsPartialController(ClassDeclarationSyntax classDirective)
106+
{
107+
if (classDirective is null || classDirective.BaseList is null)
108+
{
109+
return false;
110+
}
111+
112+
foreach (var baseType in classDirective.BaseList.Types)
113+
{
114+
if (baseType.Type is GenericNameSyntax genericNameSyntax)
115+
{
116+
if (BaseTypes.Contains(genericNameSyntax.Identifier.Text, StringComparer.OrdinalIgnoreCase))
117+
{
118+
return true;
119+
}
120+
}
121+
}
122+
123+
return false;
124+
}
125+
126+
private static ClassDeclarationSyntax FindClassDeclaration(SyntaxNode syntaxNode)
127+
{
128+
var currentNode = syntaxNode;
129+
while (currentNode != null)
130+
{
131+
if (currentNode is ClassDeclarationSyntax)
132+
{
133+
break;
134+
}
135+
currentNode = currentNode.Parent;
136+
}
137+
138+
return currentNode as ClassDeclarationSyntax;
139+
}
140+
}
141+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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 = "EP0006 CodeFix Provider")]
25+
public class EpiPartialControllerCodeFixProvider : 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(EpiPartialControllerAnalyzer.DiagnosticId);
30+
private const string MvcNamespace = "Microsoft.AspNetCore.Mvc";
31+
32+
public sealed override FixAllProvider GetFixAllProvider() =>
33+
// See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers
34+
WellKnownFixAllProviders.BatchFixer;
35+
36+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
37+
{
38+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
39+
40+
if (root is null)
41+
{
42+
return;
43+
}
44+
45+
var diagnostic = context.Diagnostics.First();
46+
var diagnosticSpan = diagnostic.Location.SourceSpan;
47+
var node = root.FindNode(diagnosticSpan);
48+
if (node is null)
49+
{
50+
return;
51+
}
52+
53+
if (diagnostic.Properties.ContainsKey(EpiPartialControllerAnalyzer.PartialView) && node is IdentifierNameSyntax identifierNameSyntax)
54+
{
55+
context.RegisterCodeFix(
56+
CodeAction.Create(
57+
Resources.EpiPartialControllerTitle,
58+
c => ReplacePartialViewMethodAsync(context.Document, identifierNameSyntax, c),
59+
nameof(Resources.EpiPartialControllerTitle)),
60+
diagnostic);
61+
}
62+
else if (node is MethodDeclarationSyntax methodDeclaration)
63+
{
64+
65+
context.RegisterCodeFix(
66+
CodeAction.Create(
67+
Resources.EpiPartialControllerTitle,
68+
c => ReplaceNameAndReturnParameterAsync(context.Document, methodDeclaration, c),
69+
nameof(Resources.EpiPartialControllerTitle)),
70+
diagnostic);
71+
}
72+
}
73+
74+
private static async Task<Document> ReplaceNameAndReturnParameterAsync(Document document, MethodDeclarationSyntax localDeclaration, CancellationToken cancellationToken)
75+
{
76+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
77+
78+
var updatedName = localDeclaration.WithIdentifier(SyntaxFactory.Identifier("InvokeComponent"));
79+
var updatedReturnType = updatedName.WithReturnType(SyntaxFactory.ParseTypeName("IViewComponentResult"));
80+
var updatedAccessibility = updatedReturnType.WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.ProtectedKeyword), SyntaxFactory.Token(SyntaxKind.OverrideKeyword)));
81+
var newRoot = root!.ReplaceNode(localDeclaration, updatedAccessibility);
82+
83+
// Return document with transformed tree.
84+
var updatedDocument = document.WithSyntaxRoot(newRoot);
85+
86+
var compilationRoot = (await updatedDocument.GetSyntaxTreeAsync()).GetCompilationUnitRoot();
87+
if (compilationRoot.Usings.Any(u => u.Name.ToString() == MvcNamespace))
88+
{
89+
var editor = await DocumentEditor.CreateAsync(updatedDocument, cancellationToken).ConfigureAwait(false);
90+
var documentRoot = (CompilationUnitSyntax)editor.OriginalRoot;
91+
var usingDirective = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(MvcNamespace).WithLeadingTrivia(SyntaxFactory.Whitespace(" ")))
92+
.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
93+
documentRoot = compilationRoot.AddUsings(usingDirective);
94+
editor.ReplaceNode(editor.OriginalRoot, documentRoot);
95+
updatedDocument = editor.GetChangedDocument();
96+
}
97+
98+
return updatedDocument;
99+
}
100+
101+
private static async Task<Document> ReplacePartialViewMethodAsync(Document document, IdentifierNameSyntax identifierNameSyntax, CancellationToken cancellationToken)
102+
{
103+
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
104+
105+
var updatedName = identifierNameSyntax.WithIdentifier(SyntaxFactory.Identifier("View"));
106+
var newRoot = root!.ReplaceNode(identifierNameSyntax, updatedName);
107+
108+
// Return document with transformed tree.
109+
return document.WithSyntaxRoot(newRoot);
110+
}
111+
}
112+
}

src/EpiSourceUpdater/EpiSourceUpdaterServiceProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public void AddServices(IExtensionServiceCollection services)
5555
services.Services.AddTransient<DiagnosticAnalyzer, FindUIConfigurationReplacementAnalyzer>(); // EP0003
5656
services.Services.AddTransient<DiagnosticAnalyzer, EpiObsoleteTypesAnalyzer>(); // EP0004
5757
services.Services.AddTransient<DiagnosticAnalyzer, EpiObsoleteUsingAnalyzer>(); // EP0005
58+
services.Services.AddTransient<DiagnosticAnalyzer, EpiPartialControllerAnalyzer>(); // EP0006
5859

5960
// Upgrade Step.
6061
services.Services.AddUpgradeStep<FindReplaceUpgradeStep>();
@@ -66,6 +67,7 @@ public void AddServices(IExtensionServiceCollection services)
6667
services.Services.AddTransient<CodeFixProvider, FindUIConfigurationReplacementCodeFixProvider>(); // EP0003
6768
services.Services.AddTransient<CodeFixProvider, EpiObsoleteTypesCodeFixProvider>(); // EP0004
6869
services.Services.AddTransient<CodeFixProvider, EpiObsoleteUsingCodeFixProvider>(); // EP0005
70+
services.Services.AddTransient<CodeFixProvider, EpiPartialControllerCodeFixProvider>(); // EP0006
6971
}
7072
}
7173
}

0 commit comments

Comments
 (0)