Add analyzer support for status code methods and constructors

This commit is contained in:
Kristian Hellang 2018-08-08 12:50:46 +02:00 committed by Pranav K
parent 2a426dfea5
commit ffdbea9dc1
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
17 changed files with 526 additions and 16 deletions

View File

@ -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())
{

View File

@ -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;

View File

@ -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; }
}
}

View File

@ -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";
}
}

View File

@ -61,7 +61,8 @@ namespace Microsoft.AspNetCore.Mvc.Api.Analyzers
}
return Array.Empty<DeclaredApiResponseMetadata>();
}
}
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<ReturnStatementSyntax>())
{
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) &&

View File

@ -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
/// <param name="statusCode">The status code to set on the response.</param>
/// <returns>The created <see cref="StatusCodeResult"/> object for the response.</returns>
[NonAction]
public virtual StatusCodeResult StatusCode(int statusCode)
public virtual StatusCodeResult StatusCode([StatusCodeValue] int statusCode)
=> new StatusCodeResult(statusCode);
/// <summary>
@ -210,10 +211,12 @@ namespace Microsoft.AspNetCore.Mvc
/// <param name="value">The value to set on the <see cref="ObjectResult"/>.</param>
/// <returns>The created <see cref="ObjectResult"/> object for the response.</returns>
[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;
}

View File

@ -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
{
}
}

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Mvc
/// with the given <paramref name="statusCode"/>.
/// </summary>
/// <param name="statusCode">The HTTP status code of the response.</param>
public StatusCodeResult(int statusCode)
public StatusCodeResult([StatusCodeValue] int statusCode)
{
StatusCode = statusCode;
}

View File

@ -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

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}