Use StatusCodes constants instead of literals in the ProducesResponseType code fix (#8234)

* Use StatusCodes constants instead of literals
This commit is contained in:
Kristian Hellang 2018-08-08 23:41:30 +02:00 committed by Pranav K
parent 3c19cede7d
commit da1189e6f1
9 changed files with 106 additions and 29 deletions

View File

@ -40,9 +40,12 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
}
var documentEditor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);
var addUsingDirective = false;
foreach (var statusCode in statusCodes.OrderBy(s => s))
{
documentEditor.AddAttribute(context.MethodSyntax, CreateProducesResponseTypeAttribute(statusCode));
documentEditor.AddAttribute(context.MethodSyntax, CreateProducesResponseTypeAttribute(context, statusCode, out var addUsing));
addUsingDirective |= addUsing;
}
if (!declaredResponseMetadata.Any(m => m.IsDefault && m.AttributeSource == context.Method))
@ -64,7 +67,23 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
documentEditor.RemoveNode(attributeSyntax);
}
return documentEditor.GetChangedDocument();
var document = documentEditor.GetChangedDocument();
var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is CompilationUnitSyntax compilationUnit && addUsingDirective)
{
const string @namespace = "Microsoft.AspNetCore.Http";
var declaredUsings = new HashSet<string>(compilationUnit.Usings.Select(x => x.Name.ToString()));
if (!declaredUsings.Contains(@namespace))
{
root = compilationUnit.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(@namespace)));
}
}
return document.WithSyntaxRoot(root);
}
private async Task<CodeActionContext> CreateCodeActionContext(CancellationToken cancellationToken)
@ -75,12 +94,37 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
var methodSyntax = methodReturnStatement.FirstAncestorOrSelf<MethodDeclarationSyntax>();
var method = semanticModel.GetDeclaredSymbol(methodSyntax, cancellationToken);
var statusCodesType = semanticModel.Compilation.GetTypeByMetadataName(ApiSymbolNames.HttpStatusCodes);
var statusCodeConstants = GetStatusCodeConstants(statusCodesType);
var symbolCache = new ApiControllerSymbolCache(semanticModel.Compilation);
var codeActionContext = new CodeActionContext(semanticModel, symbolCache, method, methodSyntax, cancellationToken);
var codeActionContext = new CodeActionContext(semanticModel, symbolCache, method, methodSyntax, statusCodeConstants, cancellationToken);
return codeActionContext;
}
private static Dictionary<int, string> GetStatusCodeConstants(INamespaceOrTypeSymbol statusCodesType)
{
var statusCodeConstants = new Dictionary<int, string>();
if (statusCodesType != null)
{
foreach (var member in statusCodesType.GetMembers())
{
if (member is IFieldSymbol field &&
field.Type.SpecialType == SpecialType.System_Int32 &&
field.Name.StartsWith("Status") &&
field.HasConstantValue &&
field.ConstantValue is int statusCode)
{
statusCodeConstants[statusCode] = field.Name;
}
}
}
return statusCodeConstants;
}
private ICollection<int> CalculateStatusCodesToApply(CodeActionContext context, IList<DeclaredApiResponseMetadata> declaredResponseMetadata)
{
if (!SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(context.SymbolCache, context.SemanticModel, context.MethodSyntax, context.CancellationToken, out var actualResponseMetadata))
@ -105,14 +149,31 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
return statusCodes;
}
private static AttributeSyntax CreateProducesResponseTypeAttribute(int statusCode)
private static AttributeSyntax CreateProducesResponseTypeAttribute(CodeActionContext context, int statusCode, out bool addUsingDirective)
{
var statusCodeSyntax = CreateStatusCodeSyntax(context, statusCode, out addUsingDirective);
return SyntaxFactory.Attribute(
SyntaxFactory.ParseName(ApiSymbolNames.ProducesResponseTypeAttribute)
.WithAdditionalAnnotations(Simplifier.Annotation),
SyntaxFactory.AttributeArgumentList().AddArguments(
SyntaxFactory.AttributeArgument(
SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(statusCode)))));
SyntaxFactory.AttributeArgument(statusCodeSyntax)));
}
private static ExpressionSyntax CreateStatusCodeSyntax(CodeActionContext context, int statusCode, out bool addUsingDirective)
{
if (context.StatusCodeConstants.TryGetValue(statusCode, out var constantName))
{
addUsingDirective = true;
return SyntaxFactory.MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
SyntaxFactory.ParseTypeName(ApiSymbolNames.HttpStatusCodes)
.WithAdditionalAnnotations(Simplifier.Annotation),
SyntaxFactory.IdentifierName(constantName));
}
addUsingDirective = false;
return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(statusCode));
}
private static AttributeSyntax CreateProducesDefaultResponseTypeAttribute()
@ -124,22 +185,25 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
private readonly struct CodeActionContext
{
public CodeActionContext(
SemanticModel semanticModel,
public CodeActionContext(SemanticModel semanticModel,
ApiControllerSymbolCache symbolCache,
IMethodSymbol method,
MethodDeclarationSyntax methodSyntax,
Dictionary<int, string> statusCodeConstants,
CancellationToken cancellationToken)
{
SemanticModel = semanticModel;
SymbolCache = symbolCache;
Method = method;
MethodSyntax = methodSyntax;
StatusCodeConstants = statusCodeConstants;
CancellationToken = cancellationToken;
}
public MethodDeclarationSyntax MethodSyntax { get; }
public Dictionary<int, string> StatusCodeConstants { get; }
public IMethodSymbol Method { get; }
public SemanticModel SemanticModel { get; }

View File

@ -32,5 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
public const string ProducesDefaultResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute";
public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute";
public const string HttpStatusCodes = "Microsoft.AspNetCore.Http.StatusCodes";
}
}

View File

@ -1,4 +1,5 @@

using Microsoft.AspNetCore.Http;
[assembly: Microsoft.AspNetCore.Mvc.ApiConventionType(typeof(Microsoft.AspNetCore.Mvc.DefaultApiConventions))]
namespace TestApp._OUTPUT_
@ -17,9 +18,9 @@ namespace TestApp._OUTPUT_
{
public class CodeFixAddsFullyQualifiedProducesResponseType : BaseController
{
[Microsoft.AspNetCore.Mvc.ProducesResponseType(202)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(400)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(404)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status202Accepted)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status400BadRequest)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status404NotFound)]
[Microsoft.AspNetCore.Mvc.ProducesDefaultResponseType]
public object GetItem(int id)
{

View File

@ -1,10 +1,12 @@
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsMissingStatusCodes : ControllerBase
{
[ProducesResponseType(404)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetItem(int id)
{
if (id == 0)

View File

@ -1,12 +1,14 @@
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsMissingStatusCodes : ControllerBase
{
[ProducesResponseType(404)]
[ProducesResponseType(200)]
[ProducesResponseType(400)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
public IActionResult GetItem(int id)
{

View File

@ -1,11 +1,13 @@
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesController : ControllerBase
{
[ProducesResponseType(200)]
[ProducesResponseType(404)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public IActionResult GetItem(int id)
{

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
@ -8,9 +9,9 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
[Route("[controller]/[action]")]
public class CodeFixAddsSuccessStatusCode : ControllerBase
{
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public ActionResult<object> GetItem(string id)
{

View File

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
@ -8,8 +9,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
[Route("[controller]/[action]")]
public class CodeFixWithConventionAddsMissingStatusCodes : ControllerBase
{
[ProducesResponseType(202)]
[ProducesResponseType(404)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public ActionResult<string> GetItem(int id)
{

View File

@ -1,11 +1,13 @@
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_
{
[ApiController]
[Route("[controller]/[action]")]
public class CodeFixWithConventionMethodAddsMissingStatusCodes : ControllerBase
{
[ProducesResponseType(202)]
[ProducesResponseType(404)]
[ProducesResponseType(StatusCodes.Status202Accepted)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
public ActionResult<string> GetItem(int id)
{