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