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 documentEditor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);
var addUsingDirective = false;
foreach (var statusCode in statusCodes.OrderBy(s => s)) 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)) if (!declaredResponseMetadata.Any(m => m.IsDefault && m.AttributeSource == context.Method))
@ -64,7 +67,23 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
documentEditor.RemoveNode(attributeSyntax); 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) private async Task<CodeActionContext> CreateCodeActionContext(CancellationToken cancellationToken)
@ -75,12 +94,37 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
var methodSyntax = methodReturnStatement.FirstAncestorOrSelf<MethodDeclarationSyntax>(); var methodSyntax = methodReturnStatement.FirstAncestorOrSelf<MethodDeclarationSyntax>();
var method = semanticModel.GetDeclaredSymbol(methodSyntax, cancellationToken); var method = semanticModel.GetDeclaredSymbol(methodSyntax, cancellationToken);
var statusCodesType = semanticModel.Compilation.GetTypeByMetadataName(ApiSymbolNames.HttpStatusCodes);
var statusCodeConstants = GetStatusCodeConstants(statusCodesType);
var symbolCache = new ApiControllerSymbolCache(semanticModel.Compilation); 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; 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) private ICollection<int> CalculateStatusCodesToApply(CodeActionContext context, IList<DeclaredApiResponseMetadata> declaredResponseMetadata)
{ {
if (!SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(context.SymbolCache, context.SemanticModel, context.MethodSyntax, context.CancellationToken, out var actualResponseMetadata)) 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; 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( return SyntaxFactory.Attribute(
SyntaxFactory.ParseName(ApiSymbolNames.ProducesResponseTypeAttribute) SyntaxFactory.ParseName(ApiSymbolNames.ProducesResponseTypeAttribute)
.WithAdditionalAnnotations(Simplifier.Annotation), .WithAdditionalAnnotations(Simplifier.Annotation),
SyntaxFactory.AttributeArgumentList().AddArguments( SyntaxFactory.AttributeArgumentList().AddArguments(
SyntaxFactory.AttributeArgument( SyntaxFactory.AttributeArgument(statusCodeSyntax)));
SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(statusCode))))); }
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() private static AttributeSyntax CreateProducesDefaultResponseTypeAttribute()
@ -124,22 +185,25 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
private readonly struct CodeActionContext private readonly struct CodeActionContext
{ {
public CodeActionContext( public CodeActionContext(SemanticModel semanticModel,
SemanticModel semanticModel,
ApiControllerSymbolCache symbolCache, ApiControllerSymbolCache symbolCache,
IMethodSymbol method, IMethodSymbol method,
MethodDeclarationSyntax methodSyntax, MethodDeclarationSyntax methodSyntax,
Dictionary<int, string> statusCodeConstants,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
SemanticModel = semanticModel; SemanticModel = semanticModel;
SymbolCache = symbolCache; SymbolCache = symbolCache;
Method = method; Method = method;
MethodSyntax = methodSyntax; MethodSyntax = methodSyntax;
StatusCodeConstants = statusCodeConstants;
CancellationToken = cancellationToken; CancellationToken = cancellationToken;
} }
public MethodDeclarationSyntax MethodSyntax { get; } public MethodDeclarationSyntax MethodSyntax { get; }
public Dictionary<int, string> StatusCodeConstants { get; }
public IMethodSymbol Method { get; } public IMethodSymbol Method { get; }
public SemanticModel SemanticModel { 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 ProducesDefaultResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute";
public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute"; 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))] [assembly: Microsoft.AspNetCore.Mvc.ApiConventionType(typeof(Microsoft.AspNetCore.Mvc.DefaultApiConventions))]
namespace TestApp._OUTPUT_ namespace TestApp._OUTPUT_
@ -17,9 +18,9 @@ namespace TestApp._OUTPUT_
{ {
public class CodeFixAddsFullyQualifiedProducesResponseType : BaseController public class CodeFixAddsFullyQualifiedProducesResponseType : BaseController
{ {
[Microsoft.AspNetCore.Mvc.ProducesResponseType(202)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status202Accepted)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(400)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status400BadRequest)]
[Microsoft.AspNetCore.Mvc.ProducesResponseType(404)] [Microsoft.AspNetCore.Mvc.ProducesResponseType(StatusCodes.Status404NotFound)]
[Microsoft.AspNetCore.Mvc.ProducesDefaultResponseType] [Microsoft.AspNetCore.Mvc.ProducesDefaultResponseType]
public object GetItem(int id) 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] [ApiController]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class CodeFixAddsMissingStatusCodes : ControllerBase public class CodeFixAddsMissingStatusCodes : ControllerBase
{ {
[ProducesResponseType(404)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetItem(int id) public IActionResult GetItem(int id)
{ {
if (id == 0) 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] [ApiController]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class CodeFixAddsMissingStatusCodes : ControllerBase public class CodeFixAddsMissingStatusCodes : ControllerBase
{ {
[ProducesResponseType(404)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(200)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(400)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType] [ProducesDefaultResponseType]
public IActionResult GetItem(int id) 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] [ApiController]
[Route("[controller]/[action]")] [Route("[controller]/[action]")]
public class CodeFixAddsStatusCodesController : ControllerBase public class CodeFixAddsStatusCodesController : ControllerBase
{ {
[ProducesResponseType(200)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(404)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType] [ProducesDefaultResponseType]
public IActionResult GetItem(int id) public IActionResult GetItem(int id)
{ {

View File

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

View File

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