aspnetcore/src/Microsoft.AspNetCore.Mvc.An.../ApiActionsAreAttributeRoute...

188 lines
7.4 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[ExportCodeFixProvider(LanguageNames.CSharp)]
[Shared]
public class ApiActionsAreAttributeRoutedFixProvider : CodeFixProvider
{
private static readonly RouteAttributeInfo[] RouteAttributes = new[]
{
new RouteAttributeInfo("HttpGet", TypeNames.HttpGetAttribute, new[] { "Get", "Find" }),
new RouteAttributeInfo("HttpPost", TypeNames.HttpPostAttribute, new[] { "Post", "Create", "Update" }),
new RouteAttributeInfo("HttpDelete", TypeNames.HttpDeleteAttribute, new[] { "Delete", "Remove" }),
new RouteAttributeInfo("HttpPut", TypeNames.HttpPutAttribute, new[] { "Put", "Create", "Update" }),
};
public sealed override ImmutableArray<string> FixableDiagnosticIds =>
ImmutableArray.Create(DiagnosticDescriptors.MVC1000_ApiActionsMustBeAttributeRouted.Id);
public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
if (context.Diagnostics.Length == 0)
{
return;
}
var rootNode = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
Debug.Assert(context.Diagnostics.Length == 1);
var diagnostic = context.Diagnostics[0];
var methodName = diagnostic.Properties[ApiActionsAreAttributeRoutedAnalyzer.MethodNameKey];
var matchedByKeyword = false;
foreach (var routeInfo in RouteAttributes)
{
foreach (var keyword in routeInfo.KeyWords)
{
// Determine if the method starts with a conventional key and only show relevant routes.
// For e.g. FindPetByCategory would result in HttpGet attribute.
if (methodName.StartsWith(keyword, StringComparison.Ordinal))
{
matchedByKeyword = true;
var title = $"Add {routeInfo.Name} attribute";
context.RegisterCodeFix(
CodeAction.Create(
title,
createChangedDocument: cancellationToken => CreateChangedDocumentAsync(routeInfo.Type, cancellationToken),
equivalenceKey: title),
context.Diagnostics);
}
}
}
if (!matchedByKeyword)
{
foreach (var routeInfo in RouteAttributes)
{
var title = $"Add {routeInfo.Name} attribute";
context.RegisterCodeFix(
CodeAction.Create(
title,
createChangedDocument: cancellationToken => CreateChangedDocumentAsync(routeInfo.Type, cancellationToken),
equivalenceKey: title),
context.Diagnostics);
}
}
async Task<Document> CreateChangedDocumentAsync(string attributeName, CancellationToken cancellationToken)
{
var methodNode = (MethodDeclarationSyntax)rootNode.FindNode(context.Span);
var editor = await DocumentEditor.CreateAsync(context.Document, cancellationToken).ConfigureAwait(false);
var compilation = editor.SemanticModel.Compilation;
var attributeMetadata = compilation.GetTypeByMetadataName(attributeName);
var fromRouteAttribute = compilation.GetTypeByMetadataName(TypeNames.FromRouteAttribute);
attributeName = attributeMetadata.ToMinimalDisplayString(editor.SemanticModel, methodNode.SpanStart);
// Remove the Attribute suffix from type names e.g. "HttpGetAttribute" -> "HttpGet"
if (attributeName.EndsWith("Attribute", StringComparison.Ordinal))
{
attributeName = attributeName.Substring(0, attributeName.Length - "Attribute".Length);
}
var method = editor.SemanticModel.GetDeclaredSymbol(methodNode);
var attribute = SyntaxFactory.Attribute(
SyntaxFactory.ParseName(attributeName));
var route = GetRoute(fromRouteAttribute, method);
if (!string.IsNullOrEmpty(route))
{
attribute = attribute.AddArgumentListArguments(
SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(route))));
}
editor.AddAttribute(methodNode, attribute);
return editor.GetChangedDocument();
}
}
private static string GetRoute(ITypeSymbol fromRouteAttribute, IMethodSymbol method)
{
StringBuilder routeNameBuilder = null;
foreach (var parameter in method.Parameters)
{
if (IsIdParameter(parameter.Name) || parameter.HasAttribute(fromRouteAttribute))
{
if (routeNameBuilder == null)
{
routeNameBuilder = new StringBuilder(parameter.Name.Length + 2);
}
else
{
routeNameBuilder.Append("/");
}
routeNameBuilder
.Append("{")
.Append(parameter.Name)
.Append("}");
}
}
return routeNameBuilder?.ToString();
}
private static bool IsIdParameter(string name)
{
// Check if the parameter is named "id" (e.g. int id) or ends in Id (e.g. personId)
if (name == null || name.Length < 2)
{
return false;
}
if (string.Equals("id", name, StringComparison.Ordinal))
{
return true;
}
if (name.Length > 3 && name.EndsWith("Id", StringComparison.Ordinal) && char.IsLower(name[name.Length - 3]))
{
return true;
}
return false;
}
private struct RouteAttributeInfo
{
public RouteAttributeInfo(string name, string type, string[] keywords)
{
Name = name;
Type = type;
KeyWords = keywords;
}
public string Name { get; }
public string Type { get; }
public string[] KeyWords { get; }
}
}
}