public async Task IsApiControllerAction_ReturnsFalse_IfMethodReturnTypeIsInvalid()
        {
            // Arrange
            var source      = @"
using Microsoft.AspNetCore.Mvc;

namespace TestNamespace
{
    [ApiController]
    public class TestController : ControllerBase
    {
        public DoesNotExist Get(int id)
        {
            if (id == 0)
            {
                return NotFound();
            }

            return new DoesNotExist(id);
        }
    }
}";
            var project     = DiagnosticProject.Create(GetType().Assembly, new[] { source });
            var compilation = await project.GetCompilationAsync();

            var symbolCache = new ApiControllerSymbolCache(compilation);
            var method      = (IMethodSymbol)compilation.GetTypeByMetadataName("TestNamespace.TestController").GetMembers("Get").First();

            // Act
            var result = ApiControllerFacts.IsApiControllerAction(symbolCache, method);

            // Assert
            Assert.False(result);
        }
        private void InitializeWorker(CompilationStartAnalysisContext compilationStartAnalysisContext, ApiControllerSymbolCache symbolCache)
        {
            compilationStartAnalysisContext.RegisterOperationAction(operationStartContext =>
            {
                var method = (IMethodSymbol)operationStartContext.ContainingSymbol;
                if (!ApiControllerFacts.IsApiControllerAction(symbolCache, method))
                {
                    return;
                }

                var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
                var hasUnreadableStatusCodes = !ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(symbolCache, (IMethodBodyOperation)operationStartContext.Operation, operationStartContext.CancellationToken, out var actualResponseMetadata);

                var hasUndocumentedStatusCodes = false;
                foreach (var actualMetadata in actualResponseMetadata)
                {
                    var location = actualMetadata.ReturnOperation.ReturnedValue.Syntax.GetLocation();

                    if (!DeclaredApiResponseMetadata.Contains(declaredResponseMetadata, actualMetadata))
                    {
                        hasUndocumentedStatusCodes = true;
                        if (actualMetadata.IsDefaultResponse)
                        {
                            operationStartContext.ReportDiagnostic(Diagnostic.Create(
                                                                       ApiDiagnosticDescriptors.API1001_ActionReturnsUndocumentedSuccessResult,
                                                                       location));
                        }
                        else
                        {
                            operationStartContext.ReportDiagnostic(Diagnostic.Create(
                                                                       ApiDiagnosticDescriptors.API1000_ActionReturnsUndocumentedStatusCode,
                                                                       location,
                                                                       actualMetadata.StatusCode));
                        }
                    }
                }

                if (hasUndocumentedStatusCodes || hasUnreadableStatusCodes)
                {
                    // If we produced analyzer warnings about undocumented status codes, don't attempt to determine
                    // if there are documented status codes that are missing from the method body.
                    return;
                }

                for (var i = 0; i < declaredResponseMetadata.Count; i++)
                {
                    var declaredMetadata = declaredResponseMetadata[i];
                    if (!Contains(actualResponseMetadata, declaredMetadata))
                    {
                        operationStartContext.ReportDiagnostic(Diagnostic.Create(
                                                                   ApiDiagnosticDescriptors.API1002_ActionDoesNotReturnDocumentedStatusCode,
                                                                   method.Locations[0],
                                                                   declaredMetadata.StatusCode));
                    }
                }
            }, OperationKind.MethodBody);
        }
        public async Task IsApiControllerAction_ReturnsFalse_IfContainingTypeIsNotAction()
        {
            // Arrange
            var compilation = await GetCompilation();

            var symbolCache = new ApiControllerSymbolCache(compilation);
            var type        = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_NotAction).FullName);
            var method      = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_NotAction.Index)).First();

            // Act
            var result = ApiControllerFacts.IsApiControllerAction(symbolCache, method);

            // Assert
            Assert.False(result);
        }
Beispiel #4
0
        public async Task IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssembly()
        {
            // Arrange
            var compilation = await GetCompilation(nameof(IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssembly));

            var symbolCache = new ApiControllerSymbolCache(compilation);
            var type        = compilation.GetTypeByMetadataName(typeof(IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssemblyController).FullName);
            var method      = (IMethodSymbol)type.GetMembers(nameof(IsApiControllerAction_ReturnsTrue_IfAttributeIsDeclaredOnAssemblyController.Action)).First();

            // Act
            var result = ApiControllerFacts.IsApiControllerAction(symbolCache, method);

            // Assert
            Assert.True(result);
        }
        public async Task IsApiControllerAction_ReturnsTrue_ForValidActionMethods()
        {
            // Arrange
            var compilation = await GetCompilation();

            Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache));
            var type   = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_Valid).FullName);
            var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_Valid.Index)).First();

            // Act
            var result = ApiControllerFacts.IsApiControllerAction(symbolCache, method);

            // Assert
            Assert.True(result);
        }
        private void InitializeWorker(CompilationStartAnalysisContext context, ApiControllerSymbolCache symbolCache)
        {
            context.RegisterOperationAction(operationAnalysisContext =>
            {
                var ifOperation = (IConditionalOperation)operationAnalysisContext.Operation;
                if (!(ifOperation.Syntax is IfStatementSyntax ifStatement))
                {
                    return;
                }

                if (ifOperation.WhenTrue == null || ifOperation.WhenFalse != null)
                {
                    // We only support expressions of the format
                    // if (!ModelState.IsValid)
                    // or
                    // if (ModelState.IsValid == false)
                    // If the conditional is missing a true condition or has an else expression, skip this operation.
                    return;
                }

                var parent = ifOperation.Parent;
                if (parent?.Kind == OperationKind.Block)
                {
                    parent = parent?.Parent;
                }

                if (parent?.Kind != OperationKind.MethodBodyOperation)
                {
                    // Only support top-level ModelState IsValid checks.
                    return;
                }

                var trueStatement = UnwrapSingleStatementBlock(ifOperation.WhenTrue);
                if (trueStatement.Kind != OperationKind.Return)
                {
                    // We need to verify that the if statement does a ModelState.IsValid check and that the block inside contains
                    // a single return statement returning a 400. We'l get to it in just a bit
                    return;
                }

                if (!(parent.Syntax is MethodDeclarationSyntax methodSyntax))
                {
                    return;
                }

                var semanticModel = operationAnalysisContext.Compilation.GetSemanticModel(methodSyntax.SyntaxTree);
                var methodSymbol  = semanticModel.GetDeclaredSymbol(methodSyntax, operationAnalysisContext.CancellationToken);

                if (!ApiControllerFacts.IsApiControllerAction(symbolCache, methodSymbol))
                {
                    // Not a ApiController. Nothing to do here.
                    return;
                }

                if (!IsModelStateIsValidCheck(symbolCache, ifOperation.Condition))
                {
                    return;
                }

                var returnOperation = (IReturnOperation)trueStatement;

                var returnValue = returnOperation.ReturnedValue;
                if (returnValue == null ||
                    !symbolCache.IActionResult.IsAssignableFrom(returnValue.Type))
                {
                    return;
                }

                var returnStatementSyntax = (ReturnStatementSyntax)returnOperation.Syntax;
                var actualMetadata        = ActualApiResponseMetadataFactory.InspectReturnStatementSyntax(
                    symbolCache,
                    semanticModel,
                    returnStatementSyntax,
                    operationAnalysisContext.CancellationToken);

                if (actualMetadata == null || actualMetadata.Value.StatusCode != 400)
                {
                    return;
                }

                var additionalLocations = new[]
                {
                    ifStatement.GetLocation(),
                    returnStatementSyntax.GetLocation(),
                };

                operationAnalysisContext.ReportDiagnostic(
                    Diagnostic.Create(
                        ApiDiagnosticDescriptors.API1003_ApiActionsDoNotRequireExplicitModelValidationCheck,
                        ifStatement.GetLocation(),
                        additionalLocations: additionalLocations));
            }, OperationKind.Conditional);
        }
        private void InitializeWorker(CompilationStartAnalysisContext compilationStartAnalysisContext, ApiControllerSymbolCache symbolCache)
        {
            compilationStartAnalysisContext.RegisterSyntaxNodeAction(syntaxNodeContext =>
            {
                var cancellationToken = syntaxNodeContext.CancellationToken;
                var methodSyntax      = (MethodDeclarationSyntax)syntaxNodeContext.Node;
                var semanticModel     = syntaxNodeContext.SemanticModel;
                var method            = semanticModel.GetDeclaredSymbol(methodSyntax, syntaxNodeContext.CancellationToken);

                if (!ApiControllerFacts.IsApiControllerAction(symbolCache, method))
                {
                    return;
                }

                var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
                var hasUnreadableStatusCodes = !ActualApiResponseMetadataFactory.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, cancellationToken, out var actualResponseMetadata);

                var hasUndocumentedStatusCodes = false;
                foreach (var actualMetadata in actualResponseMetadata)
                {
                    var location = actualMetadata.ReturnStatement.GetLocation();

                    if (!DeclaredApiResponseMetadata.Contains(declaredResponseMetadata, actualMetadata))
                    {
                        hasUndocumentedStatusCodes = true;
                        if (actualMetadata.IsDefaultResponse)
                        {
                            syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
                                                                   ApiDiagnosticDescriptors.API1001_ActionReturnsUndocumentedSuccessResult,
                                                                   location));
                        }
                        else
                        {
                            syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
                                                                   ApiDiagnosticDescriptors.API1000_ActionReturnsUndocumentedStatusCode,
                                                                   location,
                                                                   actualMetadata.StatusCode));
                        }
                    }
                }

                if (hasUndocumentedStatusCodes || hasUnreadableStatusCodes)
                {
                    // If we produced analyzer warnings about undocumented status codes, don't attempt to determine
                    // if there are documented status codes that are missing from the method body.
                    return;
                }

                for (var i = 0; i < declaredResponseMetadata.Count; i++)
                {
                    var declaredMetadata = declaredResponseMetadata[i];
                    if (!Contains(actualResponseMetadata, declaredMetadata))
                    {
                        syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
                                                               ApiDiagnosticDescriptors.API1002_ActionDoesNotReturnDocumentedStatusCode,
                                                               methodSyntax.Identifier.GetLocation(),
                                                               declaredMetadata.StatusCode));
                    }
                }
            }, SyntaxKind.MethodDeclaration);
        }