diff --git a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/AddResponseTypeAttributeCodeFixAction.cs b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/AddResponseTypeAttributeCodeFixAction.cs index 27a7ca1c35..9457429a12 100644 --- a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/AddResponseTypeAttributeCodeFixAction.cs +++ b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/AddResponseTypeAttributeCodeFixAction.cs @@ -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(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 CreateCodeActionContext(CancellationToken cancellationToken) @@ -75,12 +94,37 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers var methodSyntax = methodReturnStatement.FirstAncestorOrSelf(); 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 GetStatusCodeConstants(INamespaceOrTypeSymbol statusCodesType) + { + var statusCodeConstants = new Dictionary(); + + 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 CalculateStatusCodesToApply(CodeActionContext context, IList 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 statusCodeConstants, CancellationToken cancellationToken) { SemanticModel = semanticModel; SymbolCache = symbolCache; Method = method; MethodSyntax = methodSyntax; + StatusCodeConstants = statusCodeConstants; CancellationToken = cancellationToken; } public MethodDeclarationSyntax MethodSyntax { get; } + public Dictionary StatusCodeConstants { get; } + public IMethodSymbol Method { get; } public SemanticModel SemanticModel { get; } diff --git a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs index 9ec9bdfa0f..0771fafc22 100644 --- a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs +++ b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs @@ -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"; } } diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Output.cs index 67c467796b..accec3f2d4 100644 --- a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Output.cs +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Output.cs @@ -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) { diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Input.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Input.cs index a2992eb108..65eb3a68ae 100644 --- a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Input.cs +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Input.cs @@ -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) diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Output.cs index 136b633726..8453b10ac5 100644 --- a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Output.cs +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Output.cs @@ -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) { diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Output.cs index 09e944a4b8..0da7b06e5c 100644 --- a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Output.cs +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Output.cs @@ -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) { diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Output.cs index d51b75a841..dc64099289 100644 --- a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Output.cs +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Output.cs @@ -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 GetItem(string id) { diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Output.cs index 94e564948d..657b5e9690 100644 --- a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Output.cs +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Output.cs @@ -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 GetItem(int id) { diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Output.cs index 24be3e391c..d8d7215a59 100644 --- a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Output.cs +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Output.cs @@ -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 GetItem(int id) {