From ffdbea9dc1aa5888ddca3db24b15864e6eae4e01 Mon Sep 17 00:00:00 2001 From: Kristian Hellang Date: Wed, 8 Aug 2018 12:50:46 +0200 Subject: [PATCH] Add analyzer support for status code methods and constructors --- .../CodeAnalysisExtensions.cs | 2 +- .../MvcFacts.cs | 5 + .../ApiControllerSymbolCache.cs | 10 + .../ApiSymbolNames.cs | 4 + .../SymbolApiResponseMetadataProvider.cs | 173 +++++++++++++++++- .../ControllerBase.cs | 11 +- .../StatusCodeValueAttribute.cs | 12 ++ .../StatusCodeResult.cs | 2 +- ...AttributeCodeFixProviderIntegrationTest.cs | 12 ++ ...ForNonExistingStatusCodeConstants.Input.cs | 17 ++ ...orNonExistingStatusCodeConstants.Output.cs | 22 +++ ...tusCodesFromConstructorParameters.Input.cs | 38 ++++ ...usCodesFromConstructorParameters.Output.cs | 44 +++++ ...dsStatusCodesFromMethodParameters.Input.cs | 38 ++++ ...sStatusCodesFromMethodParameters.Output.cs | 44 +++++ ...sStatusCodesFromObjectInitializer.Input.cs | 51 ++++++ ...StatusCodesFromObjectInitializer.Output.cs | 57 ++++++ 17 files changed, 526 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/StatusCodeValueAttribute.cs create mode 100644 test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants.Input.cs create mode 100644 test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants.Output.cs create mode 100644 test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromConstructorParameters.Input.cs create mode 100644 test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromConstructorParameters.Output.cs create mode 100644 test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromMethodParameters.Input.cs create mode 100644 test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromMethodParameters.Output.cs create mode 100644 test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromObjectInitializer.Input.cs create mode 100644 test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromObjectInitializer.Output.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs index 7856b27b02..72da1b1f4c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/CodeAnalysisExtensions.cs @@ -123,7 +123,7 @@ namespace Microsoft.CodeAnalysis return false; } - private static bool HasAttribute(this ISymbol symbol, ITypeSymbol attribute) + public static bool HasAttribute(this ISymbol symbol, ITypeSymbol attribute) { foreach (var declaredAttribute in symbol.GetAttributes()) { diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/MvcFacts.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/MvcFacts.cs index 90651247c4..fc06eda6bb 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/MvcFacts.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/MvcFacts.cs @@ -116,6 +116,11 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers return false; } + if (!method.ReturnsVoid) + { + return false; + } + if (method.Parameters.Length != disposableDispose.Parameters.Length) { return false; diff --git a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiControllerSymbolCache.cs b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiControllerSymbolCache.cs index c2a6f167d2..e92c0927ca 100644 --- a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiControllerSymbolCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiControllerSymbolCache.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Diagnostics; using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Mvc.Api.Analyzers @@ -24,6 +25,11 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers ProducesDefaultResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesDefaultResponseTypeAttribute); ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.ProducesResponseTypeAttribute); + StatusCodeValueAttribute = compilation.GetTypeByMetadataName(ApiSymbolNames.StatusCodeValueAttribute); + + var statusCodeActionResult = compilation.GetTypeByMetadataName(ApiSymbolNames.IStatusCodeActionResult); + StatusCodeActionResultStatusProperty = (IPropertySymbol)statusCodeActionResult.GetMembers("StatusCode")[0]; + var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable); var members = disposable.GetMembers(nameof(IDisposable.Dispose)); IDisposableDispose = members.Length == 1 ? (IMethodSymbol)members[0] : null; @@ -47,6 +53,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers public IMethodSymbol IDisposableDispose { get; } + public IPropertySymbol StatusCodeActionResultStatusProperty { get; } + public ITypeSymbol ModelStateDictionary { get; } public INamedTypeSymbol NonActionAttribute { get; } @@ -56,5 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers public INamedTypeSymbol ProducesDefaultResponseTypeAttribute { get; } public INamedTypeSymbol ProducesResponseTypeAttribute { get; } + + public INamedTypeSymbol StatusCodeValueAttribute { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs index 0771fafc22..563c3ad570 100644 --- a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs +++ b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/ApiSymbolNames.cs @@ -23,6 +23,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers public const string IActionResult = "Microsoft.AspNetCore.Mvc.IActionResult"; + public const string IStatusCodeActionResult = "Microsoft.AspNetCore.Mvc.Infrastructure.IStatusCodeActionResult"; + public const string ModelStateDictionary = "Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary"; public const string NonActionAttribute = "Microsoft.AspNetCore.Mvc.NonActionAttribute"; @@ -34,5 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute"; public const string HttpStatusCodes = "Microsoft.AspNetCore.Http.StatusCodes"; + + public const string StatusCodeValueAttribute = "Microsoft.AspNetCore.Mvc.Infrastructure.StatusCodeValueAttribute"; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/SymbolApiResponseMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/SymbolApiResponseMetadataProvider.cs index 5de70bae0a..7e3744757e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/SymbolApiResponseMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Api.Analyzers/SymbolApiResponseMetadataProvider.cs @@ -61,7 +61,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers } return Array.Empty(); - } + } + private static IMethodSymbol GetMethodFromConventionMethodAttribute(ApiControllerSymbolCache symbolCache, IMethodSymbol method) { var attribute = method.GetAttributes(symbolCache.ApiConventionMethodAttribute, inherit: true) @@ -222,6 +223,12 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType()) { + if (returnStatementSyntax.IsMissing || returnStatementSyntax.Expression.IsMissing) + { + // Ignore malformed return statements. + continue; + } + var responseMetadata = InspectReturnStatementSyntax( symbolCache, semanticModel, @@ -248,11 +255,6 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers CancellationToken cancellationToken) { var returnExpression = returnStatementSyntax.Expression; - if (returnExpression.IsMissing) - { - return null; - } - var typeInfo = semanticModel.GetTypeInfo(returnExpression, cancellationToken); if (typeInfo.Type.TypeKind == TypeKind.Error) { @@ -267,25 +269,176 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers if (defaultStatusCodeAttribute != null) { - var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute); - if (statusCode == null) + var defaultStatusCode = GetDefaultStatusCode(defaultStatusCodeAttribute); + if (defaultStatusCode == null) { // Unable to read the status code even though the attribute exists. return null; } - return new ActualApiResponseMetadata(returnStatementSyntax, statusCode.Value); + return new ActualApiResponseMetadata(returnStatementSyntax, defaultStatusCode.Value); } - else if (!symbolCache.IActionResult.IsAssignableFrom(statementReturnType)) + + if (!symbolCache.IActionResult.IsAssignableFrom(statementReturnType)) { // Return expression does not have a DefaultStatusCodeAttribute and it is not // an instance of IActionResult. Must be returning the "model". return new ActualApiResponseMetadata(returnStatementSyntax); } + int statusCode; + switch (returnExpression) + { + case InvocationExpressionSyntax invocation: + // Covers the 'return StatusCode(200)' case. + if (TryGetParameterStatusCode(symbolCache, semanticModel, invocation.Expression, invocation.ArgumentList, cancellationToken, out statusCode)) + { + return new ActualApiResponseMetadata(returnStatementSyntax, statusCode); + } + break; + + case ObjectCreationExpressionSyntax creation: + // Covers the 'return new ObjectResult(...) { StatusCode = 200 }' case. + if (TryGetInitializerStatusCode(symbolCache, semanticModel, creation.Initializer, cancellationToken, out statusCode)) + { + return new ActualApiResponseMetadata(returnStatementSyntax, statusCode); + } + + // Covers the 'return new StatusCodeResult(200) case. + if (TryGetParameterStatusCode(symbolCache, semanticModel, creation, creation.ArgumentList, cancellationToken, out statusCode)) + { + return new ActualApiResponseMetadata(returnStatementSyntax, statusCode); + } + break; + } + return null; } + private static bool TryGetInitializerStatusCode( + in ApiControllerSymbolCache symbolCache, + SemanticModel semanticModel, + InitializerExpressionSyntax initializer, + CancellationToken cancellationToken, + out int statusCode) + { + if (initializer == null) + { + statusCode = default; + return false; + } + + for (var i = 0; i < initializer.Expressions.Count; i++) + { + if (!(initializer.Expressions[i] is AssignmentExpressionSyntax assignment)) + { + continue; + } + + if (assignment.Left is IdentifierNameSyntax identifier) + { + var symbolInfo = semanticModel.GetSymbolInfo(identifier, cancellationToken); + + if (symbolInfo.Symbol is IPropertySymbol property && IsInterfaceImplementation(property, symbolCache.StatusCodeActionResultStatusProperty)) + { + return TryGetExpressionStatusCode(semanticModel, assignment.Right, cancellationToken, out statusCode); + } + } + } + + statusCode = default; + return false; + } + + private static bool IsInterfaceImplementation(IPropertySymbol property, IPropertySymbol statusCodeActionResultStatusProperty) + { + if (property.Name != statusCodeActionResultStatusProperty.Name) + { + return false; + } + + for (var i = 0; i < property.ExplicitInterfaceImplementations.Length; i++) + { + if (property.ExplicitInterfaceImplementations[i] == statusCodeActionResultStatusProperty) + { + return true; + } + } + + var implementedProperty = property.ContainingType.FindImplementationForInterfaceMember(statusCodeActionResultStatusProperty); + return implementedProperty == property; + } + + private static bool TryGetParameterStatusCode( + in ApiControllerSymbolCache symbolCache, + SemanticModel semanticModel, + ExpressionSyntax expression, + BaseArgumentListSyntax argumentList, + CancellationToken cancellationToken, + out int statusCode) + { + var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); + + if (!(symbolInfo.Symbol is IMethodSymbol method)) + { + statusCode = default; + return false; + } + + for (var i = 0; i < method.Parameters.Length; i++) + { + var parameter = method.Parameters[i]; + if (!parameter.HasAttribute(symbolCache.StatusCodeValueAttribute)) + { + continue; + } + + + var argument = argumentList.Arguments[parameter.Ordinal]; + return TryGetExpressionStatusCode(semanticModel, argument.Expression, cancellationToken, out statusCode); + } + + statusCode = default; + return false; + } + + private static bool TryGetExpressionStatusCode( + SemanticModel semanticModel, + ExpressionSyntax expression, + CancellationToken cancellationToken, + out int statusCode) + { + if (expression is LiteralExpressionSyntax literal && literal.Token.Value is int literalStatusCode) + { + // Covers the 'return StatusCode(200)' case. + statusCode = literalStatusCode; + return true; + } + + if (expression is IdentifierNameSyntax || expression is MemberAccessExpressionSyntax) + { + var symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken); + + if (symbolInfo.Symbol is IFieldSymbol field && field.HasConstantValue && field.ConstantValue is int constantStatusCode) + { + // Covers the 'return StatusCode(StatusCodes.Status200OK)' case. + // It also covers the 'return StatusCode(StatusCode)' case, where 'StatusCode' is a constant field. + statusCode = constantStatusCode; + return true; + } + + if (symbolInfo.Symbol is ILocalSymbol local && local.HasConstantValue && local.ConstantValue is int localStatusCode) + { + // Covers the 'return StatusCode(statusCode)' case, where 'statusCode' is a local constant. + statusCode = localStatusCode; + return true; + } + } + + statusCode = default; + return false; + } + private static bool ShouldDescendIntoChildren(SyntaxNode syntaxNode) { return !syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement) && diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs index c51f809da2..520f8a5fbe 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Internal; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; @@ -200,7 +201,7 @@ namespace Microsoft.AspNetCore.Mvc /// The status code to set on the response. /// The created object for the response. [NonAction] - public virtual StatusCodeResult StatusCode(int statusCode) + public virtual StatusCodeResult StatusCode([StatusCodeValue] int statusCode) => new StatusCodeResult(statusCode); /// @@ -210,10 +211,12 @@ namespace Microsoft.AspNetCore.Mvc /// The value to set on the . /// The created object for the response. [NonAction] - public virtual ObjectResult StatusCode(int statusCode, object value) + public virtual ObjectResult StatusCode([StatusCodeValue] int statusCode, object value) { - var result = new ObjectResult(value); - result.StatusCode = statusCode; + var result = new ObjectResult(value) + { + StatusCode = statusCode + }; return result; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/StatusCodeValueAttribute.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/StatusCodeValueAttribute.cs new file mode 100644 index 0000000000..944f958d62 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/StatusCodeValueAttribute.cs @@ -0,0 +1,12 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.Infrastructure +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + internal sealed class StatusCodeValueAttribute : Attribute + { + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/StatusCodeResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/StatusCodeResult.cs index c3ec5614c0..c95b69237e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/StatusCodeResult.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/StatusCodeResult.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc /// with the given . /// /// The HTTP status code of the response. - public StatusCodeResult(int statusCode) + public StatusCodeResult([StatusCodeValue] int statusCode) { StatusCode = statusCode; } diff --git a/test/Mvc.Api.Analyzers.Test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs b/test/Mvc.Api.Analyzers.Test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs index beea4d68a7..755fe9b136 100644 --- a/test/Mvc.Api.Analyzers.Test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs +++ b/test/Mvc.Api.Analyzers.Test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs @@ -33,6 +33,18 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers [Fact] public Task CodeFixAddsFullyQualifiedProducesResponseType() => RunTest(); + [Fact] + public Task CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants() => RunTest(); + + [Fact] + public Task CodeFixAddsStatusCodesFromMethodParameters() => RunTest(); + + [Fact] + public Task CodeFixAddsStatusCodesFromConstructorParameters() => RunTest(); + + [Fact] + public Task CodeFixAddsStatusCodesFromObjectInitializer() => RunTest(); + private async Task RunTest([CallerMemberName] string testMethod = "") { // Arrange diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants.Input.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants.Input.cs new file mode 100644 index 0000000000..93668878b1 --- /dev/null +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants.Input.cs @@ -0,0 +1,17 @@ +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsNumericLiteralForNonExistingStatusCodeConstantsController : ControllerBase + { + public IActionResult GetItem(int id) + { + if (id == 0) + { + return StatusCode(345); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants.Output.cs new file mode 100644 index 0000000000..82a8f44a3b --- /dev/null +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsNumericLiteralForNonExistingStatusCodeConstants.Output.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsNumericLiteralForNonExistingStatusCodeConstantsController : ControllerBase + { + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(345)] + [ProducesDefaultResponseType] + public IActionResult GetItem(int id) + { + if (id == 0) + { + return StatusCode(345); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromConstructorParameters.Input.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromConstructorParameters.Input.cs new file mode 100644 index 0000000000..0fe4637037 --- /dev/null +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromConstructorParameters.Input.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsStatusCodesFromConstructorParametersController : ControllerBase + { + private const int FieldStatusCode = 201; + + public IActionResult GetItem(int id) + { + if (id == 0) + { + return new StatusCodeResult(422); + } + + if (id == 1) + { + return new StatusCodeResult(StatusCodes.Status202Accepted); + } + + if (id == 2) + { + const int localStatusCode = 204; + + return new StatusCodeResult(localStatusCode); + } + + if (id == 3) + { + return new StatusCodeResult(FieldStatusCode); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromConstructorParameters.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromConstructorParameters.Output.cs new file mode 100644 index 0000000000..dbdf8d7fae --- /dev/null +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromConstructorParameters.Output.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsStatusCodesFromConstructorParametersController : ControllerBase + { + private const int FieldStatusCode = 201; + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [ProducesDefaultResponseType] + public IActionResult GetItem(int id) + { + if (id == 0) + { + return new StatusCodeResult(422); + } + + if (id == 1) + { + return new StatusCodeResult(StatusCodes.Status202Accepted); + } + + if (id == 2) + { + const int localStatusCode = 204; + + return new StatusCodeResult(localStatusCode); + } + + if (id == 3) + { + return new StatusCodeResult(FieldStatusCode); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromMethodParameters.Input.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromMethodParameters.Input.cs new file mode 100644 index 0000000000..8bc4a372e1 --- /dev/null +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromMethodParameters.Input.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsStatusCodesFromMethodParametersController : ControllerBase + { + private const int FieldStatusCode = 201; + + public IActionResult GetItem(int id) + { + if (id == 0) + { + return StatusCode(422); + } + + if (id == 1) + { + return StatusCode(StatusCodes.Status202Accepted); + } + + if (id == 2) + { + const int localStatusCode = 204; + + return StatusCode(localStatusCode); + } + + if (id == 3) + { + return StatusCode(FieldStatusCode); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromMethodParameters.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromMethodParameters.Output.cs new file mode 100644 index 0000000000..d1ad5d182d --- /dev/null +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromMethodParameters.Output.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsStatusCodesFromMethodParametersController : ControllerBase + { + private const int FieldStatusCode = 201; + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [ProducesDefaultResponseType] + public IActionResult GetItem(int id) + { + if (id == 0) + { + return StatusCode(422); + } + + if (id == 1) + { + return StatusCode(StatusCodes.Status202Accepted); + } + + if (id == 2) + { + const int localStatusCode = 204; + + return StatusCode(localStatusCode); + } + + if (id == 3) + { + return StatusCode(FieldStatusCode); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromObjectInitializer.Input.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromObjectInitializer.Input.cs new file mode 100644 index 0000000000..7041164c5e --- /dev/null +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromObjectInitializer.Input.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsStatusCodesFromObjectInitializerController : ControllerBase + { + private const int FieldStatusCode = 201; + + public IActionResult GetItem(int id) + { + if (id == 0) + { + return new ObjectResult(new object()) + { + StatusCode = 422 + }; + } + + if (id == 1) + { + return new ObjectResult(new object()) + { + StatusCode = StatusCodes.Status202Accepted + }; + } + + if (id == 2) + { + const int localStatusCode = 204; + + return new ObjectResult(new object()) + { + StatusCode = localStatusCode + }; + } + + if (id == 3) + { + return new ObjectResult(new object()) + { + ContentTypes = { "application/json" }, + StatusCode = FieldStatusCode + }; + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromObjectInitializer.Output.cs b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromObjectInitializer.Output.cs new file mode 100644 index 0000000000..8f35baeee7 --- /dev/null +++ b/test/Mvc.Api.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodesFromObjectInitializer.Output.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Mvc.Api.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsStatusCodesFromObjectInitializerController : ControllerBase + { + private const int FieldStatusCode = 201; + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status202Accepted)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + [ProducesDefaultResponseType] + public IActionResult GetItem(int id) + { + if (id == 0) + { + return new ObjectResult(new object()) + { + StatusCode = 422 + }; + } + + if (id == 1) + { + return new ObjectResult(new object()) + { + StatusCode = StatusCodes.Status202Accepted + }; + } + + if (id == 2) + { + const int localStatusCode = 204; + + return new ObjectResult(new object()) + { + StatusCode = localStatusCode + }; + } + + if (id == 3) + { + return new ObjectResult(new object()) + { + ContentTypes = { "application/json" }, + StatusCode = FieldStatusCode + }; + } + + return Ok(new object()); + } + } +}