private static IList <DeclaredApiResponseMetadata> GetResponseMetadataFromConventions( ApiControllerSymbolCache symbolCache, IMethodSymbol method, IReadOnlyList <AttributeData> attributes) { foreach (var attribute in attributes) { if (attribute.ConstructorArguments.Length != 1 || attribute.ConstructorArguments[0].Kind != TypedConstantKind.Type || !(attribute.ConstructorArguments[0].Value is ITypeSymbol conventionType)) { continue; } foreach (var conventionMethod in conventionType.GetMembers().OfType <IMethodSymbol>()) { if (!conventionMethod.IsStatic || conventionMethod.DeclaredAccessibility != Accessibility.Public) { continue; } if (!SymbolApiConventionMatcher.IsMatch(symbolCache, method, conventionMethod)) { continue; } return(GetResponseMetadataFromMethodAttributes(symbolCache, conventionMethod)); } } return(Array.Empty <DeclaredApiResponseMetadata>()); }
private static IMethodSymbol GetMethodFromConventionMethodAttribute(ApiControllerSymbolCache symbolCache, IMethodSymbol method) { var attribute = method.GetAttributes(symbolCache.ApiConventionMethodAttribute, inherit: true) .FirstOrDefault(); if (attribute == null) { return(null); } if (attribute.ConstructorArguments.Length != 2) { return(null); } if (attribute.ConstructorArguments[0].Kind != TypedConstantKind.Type || !(attribute.ConstructorArguments[0].Value is ITypeSymbol conventionType)) { return(null); } if (attribute.ConstructorArguments[1].Kind != TypedConstantKind.Primitive || !(attribute.ConstructorArguments[1].Value is string conventionMethodName)) { return(null); } var conventionMethod = conventionType.GetMembers(conventionMethodName) .FirstOrDefault(m => m.Kind == SymbolKind.Method && m.IsStatic && m.DeclaredAccessibility == Accessibility.Public); return((IMethodSymbol)conventionMethod); }
public async Task GetResponseMetadata_ReturnsValuesFromApiConventionMethodAttribute() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.GetResponseMetadata_ReturnsValuesFromApiConventionMethodAttribute)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty <AttributeData>()); // Assert Assert.Collection( result, metadata => { Assert.Equal(200, metadata.StatusCode); Assert.NotNull(metadata.Attribute); }, metadata => { Assert.Equal(404, metadata.StatusCode); Assert.NotNull(metadata.Attribute); }); }
public async Task ShouldEvaluateMethod_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 = ApiConventionAnalyzer.ShouldEvaluateMethod(symbolCache, method); // Assert Assert.False(result); }
internal static bool ShouldEvaluateMethod(ApiControllerSymbolCache symbolCache, IMethodSymbol method) { if (method == null) { return(false); } if (method.ReturnsVoid || method.ReturnType.TypeKind == TypeKind.Error) { return(false); } if (!MvcFacts.IsController(method.ContainingType, symbolCache.ControllerAttribute, symbolCache.NonControllerAttribute)) { return(false); } if (!method.ContainingType.HasAttribute(symbolCache.IApiBehaviorMetadata, inherit: true)) { return(false); } if (!MvcFacts.IsControllerAction(method, symbolCache.NonActionAttribute, symbolCache.IDisposableDispose)) { return(false); } return(true); }
internal IReadOnlyList <AttributeData> GetConventionTypeAttributes(ApiControllerSymbolCache symbolCache, IMethodSymbol method) { var attributes = method.ContainingType.GetAttributes(symbolCache.ApiConventionTypeAttribute).ToArray(); if (attributes.Length == 0) { attributes = method.ContainingAssembly.GetAttributes(symbolCache.ApiConventionTypeAttribute).ToArray(); } return(attributes); }
internal static bool IsMatch(ApiControllerSymbolCache symbolCache, IMethodSymbol method, IMethodSymbol conventionMethod) { return(MethodMatches() && ParametersMatch()); bool MethodMatches() { var methodNameMatchBehavior = GetNameMatchBehavior(symbolCache, conventionMethod); if (!IsNameMatch(method.Name, conventionMethod.Name, methodNameMatchBehavior)) { return(false); } return(true); } bool ParametersMatch() { var methodParameters = method.Parameters; var conventionMethodParameters = conventionMethod.Parameters; for (var i = 0; i < conventionMethodParameters.Length; i++) { var conventionParameter = conventionMethodParameters[i]; if (conventionParameter.IsParams) { return(true); } if (methodParameters.Length <= i) { return(false); } var nameMatchBehavior = GetNameMatchBehavior(symbolCache, conventionParameter); var typeMatchBehavior = GetTypeMatchBehavior(symbolCache, conventionParameter); if (!IsTypeMatch(methodParameters[i].Type, conventionParameter.Type, typeMatchBehavior) || !IsNameMatch(methodParameters[i].Name, conventionParameter.Name, nameMatchBehavior)) { return(false); } } // Ensure convention has at least as many parameters as the method. params convention argument are handled // inside the for loop. return(methodParameters.Length == conventionMethodParameters.Length); } }
internal static SymbolApiConventionTypeMatchBehavior GetTypeMatchBehavior(ApiControllerSymbolCache symbolCache, ISymbol symbol) { var attribute = symbol.GetAttributes(symbolCache.ApiConventionTypeMatchAttribute).FirstOrDefault(); if (attribute == null || attribute.ConstructorArguments.Length != 1 || attribute.ConstructorArguments[0].Kind != TypedConstantKind.Enum) { return(SymbolApiConventionTypeMatchBehavior.AssignableFrom); } var intValue = (int)attribute.ConstructorArguments[0].Value; return((SymbolApiConventionTypeMatchBehavior)intValue); }
public async Task GetResponseMetadata_ReturnsEmptySequence_IfNoAttributesArePresent_ForPostAction() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerWithoutConvention)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerWithoutConvention.PostPerson)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty <AttributeData>()); // Assert Assert.Empty(result); }
public async Task IsApiControllerAction_ReturnsFalse_IfContainingTypeIsNotController() { // Arrange var compilation = await GetCompilation(); var symbolCache = new ApiControllerSymbolCache(compilation); var type = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_IndexModel).FullName); var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_IndexModel.OnGet)).First(); // Act var result = ApiControllerFacts.IsApiControllerAction(symbolCache, method); // Assert Assert.False(result); }
public async Task IsApiControllerAction_ReturnsTrue_ForValidActionMethods() { // Arrange var compilation = await GetCompilation(); var symbolCache = new ApiControllerSymbolCache(compilation); 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); }
public async Task GetResponseMetadata_IgnoresProducesAttribute() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesAttribute)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty <AttributeData>()); // Assert Assert.Empty(result); }
public async Task ShouldEvaluateMethod_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 = ApiConventionAnalyzer.ShouldEvaluateMethod(symbolCache, method); // Assert Assert.False(result); }
internal static IList <DeclaredApiResponseMetadata> GetDeclaredResponseMetadata( ApiControllerSymbolCache symbolCache, IMethodSymbol method, IReadOnlyList <AttributeData> conventionTypeAttributes) { var metadataItems = GetResponseMetadataFromMethodAttributes(symbolCache, method); if (metadataItems.Count != 0) { return(metadataItems); } metadataItems = GetResponseMetadataFromConventions(symbolCache, method, conventionTypeAttributes); return(metadataItems); }
public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.RegisterCompilationStartAction(compilationStartAnalysisContext => { var symbolCache = new ApiControllerSymbolCache(compilationStartAnalysisContext.Compilation); if (symbolCache.ApiConventionTypeAttribute == null || symbolCache.ApiConventionTypeAttribute.TypeKind == TypeKind.Error) { // No-op if we can't find types we care about. return; } InitializeWorker(compilationStartAnalysisContext, symbolCache); }); }
private async Task RunMatchTest(string methodName, string conventionMethodName, bool expected) { var compilation = await GetCompilationAsync(); var symbolCache = new ApiControllerSymbolCache(compilation); var testController = compilation.GetTypeByMetadataName(TestControllerName); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = (IMethodSymbol)testController.GetMembers(methodName).First(); var conventionMethod = (IMethodSymbol)testConvention.GetMembers(conventionMethodName).First(); // Act var result = IsMatch(symbolCache, method, conventionMethod); // Assert Assert.Equal(expected, result); }
public async Task GetNameMatchBehavior_ReturnsExact_WhenNoNameMatchBehaviorAttributeIsSpecified() { // Arrange var expected = SymbolApiConventionNameMatchBehavior.Exact; var compilation = await GetCompilationAsync(); var symbolCache = new ApiControllerSymbolCache(compilation); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = testConvention.GetMembers(nameof(TestConvention.MethodWithRandomAttributes)).First(); // Act var result = GetNameMatchBehavior(symbolCache, method); // Assert Assert.Equal(expected, result); }
public async Task GetNameMatchBehavior_ReturnsValueFromAttributes() { // Arrange var expected = SymbolApiConventionNameMatchBehavior.Prefix; var compilation = await GetCompilationAsync(); var symbolCache = new ApiControllerSymbolCache(compilation); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = testConvention.GetMembers(nameof(TestConvention.Get)).First(); // Act var result = GetNameMatchBehavior(symbolCache, method); // Assert Assert.Equal(expected, result); }
public async Task GetTypeMatchBehavior_ReturnsValueFromAttributes() { // Arrange var expected = SymbolApiConventionTypeMatchBehavior.Any; var compilation = await GetCompilationAsync(); var symbolCache = new ApiControllerSymbolCache(compilation); var testConvention = compilation.GetTypeByMetadataName(TestConventionName); var method = (IMethodSymbol)testConvention.GetMembers(nameof(TestConvention.MethodWithAnyTypeMatchBehaviorParameter)).First(); var parameter = method.Parameters[0]; // Act var result = GetTypeMatchBehavior(symbolCache, parameter); // Assert Assert.Equal(expected, result); }
private static IList <DeclaredApiResponseMetadata> GetResponseMetadataFromConventions( ApiControllerSymbolCache symbolCache, IMethodSymbol method, IReadOnlyList <AttributeData> attributes) { var conventionMethod = GetMethodFromConventionMethodAttribute(symbolCache, method); if (conventionMethod == null) { conventionMethod = MatchConventionMethod(symbolCache, method, attributes); } if (conventionMethod != null) { return(GetResponseMetadataFromMethodAttributes(symbolCache, conventionMethod)); } return(Array.Empty <DeclaredApiResponseMetadata>()); }
private async Task <ActualApiResponseMetadata?> RunInspectReturnStatementSyntax(string source, string test) { var project = DiagnosticProject.Create(GetType().Assembly, new[] { source }); var compilation = await project.GetCompilationAsync(); var symbolCache = new ApiControllerSymbolCache(compilation); var returnType = compilation.GetTypeByMetadataName($"{Namespace}.{test}"); var syntaxTree = returnType.DeclaringSyntaxReferences[0].SyntaxTree; var method = (IMethodSymbol)returnType.GetMembers().First(); var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan); var returnStatement = methodSyntax.DescendantNodes().OfType <ReturnStatementSyntax>().First(); return(SymbolApiResponseMetadataProvider.InspectReturnStatementSyntax( symbolCache, compilation.GetSemanticModel(syntaxTree), returnStatement, CancellationToken.None)); }
public async Task GetResponseMetadata_ReturnsValueFromProducesResponseType_WhenStatusCodeIsSpecifiedInConstructorWithResponseType() { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}"); var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructor)).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty <AttributeData>()); // Assert Assert.Collection( result, metadata => { Assert.Equal(202, metadata.StatusCode); Assert.NotNull(metadata.Attribute); Assert.Null(metadata.Convention); }); }
private async Task GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues(string typeName, string methodName) { // Arrange var compilation = await GetResponseMetadataCompilation(); var controller = compilation.GetTypeByMetadataName($"{Namespace}.{typeName}"); var method = (IMethodSymbol)controller.GetMembers(methodName).First(); var symbolCache = new ApiControllerSymbolCache(compilation); // Act var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty <AttributeData>()); // Assert Assert.Collection( result, metadata => { Assert.Equal(200, metadata.StatusCode); Assert.NotNull(metadata.Attribute); Assert.Null(metadata.Convention); }); }
private static ApiConventionContext GetContext(Compilation compilation, int[] expectedStatusCodes) { var symbolCache = new ApiControllerSymbolCache(compilation); var context = new ApiConventionContext( symbolCache,
private void InitializeWorker(CompilationStartAnalysisContext compilationStartAnalysisContext, ApiControllerSymbolCache symbolCache) { compilationStartAnalysisContext.RegisterSyntaxNodeAction(syntaxNodeContext => { var methodSyntax = (MethodDeclarationSyntax)syntaxNodeContext.Node; var semanticModel = syntaxNodeContext.SemanticModel; var method = semanticModel.GetDeclaredSymbol(methodSyntax, syntaxNodeContext.CancellationToken); if (!ShouldEvaluateMethod(symbolCache, method)) { return; } var conventionAttributes = GetConventionTypeAttributes(symbolCache, method); var expectedResponseMetadata = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, conventionAttributes); var actualResponseMetadata = new HashSet <int>(); var context = new ApiConventionContext( symbolCache, syntaxNodeContext, expectedResponseMetadata, actualResponseMetadata); var hasUndocumentedStatusCodes = false; foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType <ReturnStatementSyntax>()) { hasUndocumentedStatusCodes |= VisitReturnStatementSyntax(context, returnStatementSyntax); } if (hasUndocumentedStatusCodes) { // 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 < expectedResponseMetadata.Count; i++) { var expectedStatusCode = expectedResponseMetadata[i].StatusCode; if (!actualResponseMetadata.Contains(expectedStatusCode)) { context.SyntaxNodeContext.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode, methodSyntax.Identifier.GetLocation(), expectedStatusCode)); } } }, SyntaxKind.MethodDeclaration); }
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 conventionAttributes = GetConventionTypeAttributes(symbolCache, method); var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, conventionAttributes); var hasUnreadableStatusCodes = SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, cancellationToken, out var actualResponseMetadata); var hasUndocumentedStatusCodes = false; foreach (var item in actualResponseMetadata) { var location = item.ReturnStatement.GetLocation(); if (item.IsDefaultResponse) { if (!(HasStatusCode(declaredResponseMetadata, 200) || HasStatusCode(declaredResponseMetadata, 201))) { hasUndocumentedStatusCodes = true; syntaxNodeContext.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult, location)); } } else if (!HasStatusCode(declaredResponseMetadata, item.StatusCode)) { hasUndocumentedStatusCodes = true; syntaxNodeContext.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode, location, item.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 expectedStatusCode = declaredResponseMetadata[i].StatusCode; if (!HasStatusCode(actualResponseMetadata, expectedStatusCode)) { syntaxNodeContext.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode, methodSyntax.Identifier.GetLocation(), expectedStatusCode)); } } }, SyntaxKind.MethodDeclaration); }
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 misisng 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 = SymbolApiResponseMetadataProvider.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( DiagnosticDescriptors.MVC1007_ApiActionsDoNotRequireExplicitModelValidationCheck, ifStatement.GetLocation(), additionalLocations: additionalLocations)); }, OperationKind.Conditional); }
private static IList <DeclaredApiResponseMetadata> GetResponseMetadataFromMethodAttributes(ApiControllerSymbolCache symbolCache, IMethodSymbol methodSymbol) { var metadataItems = new List <DeclaredApiResponseMetadata>(); var responseMetadataAttributes = methodSymbol.GetAttributes(symbolCache.ProducesResponseTypeAttribute, inherit: true); foreach (var attribute in responseMetadataAttributes) { var statusCode = GetStatusCode(attribute); var metadata = new DeclaredApiResponseMetadata(statusCode, attribute, convention: null); metadataItems.Add(metadata); } return(metadataItems); }