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

            // Assert
            Assert.Collection(
                result,
                metadata =>
            {
                Assert.Equal(200, metadata.StatusCode);
                Assert.NotNull(metadata.Attribute);
            },
                metadata =>
            {
                Assert.Equal(404, metadata.StatusCode);
                Assert.NotNull(metadata.Attribute);
            },
                metadata =>
            {
                Assert.True(metadata.IsDefault);
            });
        }
        protected override async Task <Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
        {
            var context = await CreateCodeActionContext(cancellationToken).ConfigureAwait(false);

            var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(context.SymbolCache, context.Method);

            var statusCodes = CalculateStatusCodesToApply(context, declaredResponseMetadata);

            if (statusCodes.Count == 0)
            {
                return(_document);
            }

            var documentEditor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);

            var addUsingDirective = false;

            foreach (var statusCode in statusCodes.OrderBy(s => s))
            {
                documentEditor.AddAttribute(context.MethodSyntax, CreateProducesResponseTypeAttribute(context, statusCode, out var addUsing));
                addUsingDirective |= addUsing;
            }

            if (!declaredResponseMetadata.Any(m => m.IsDefault && m.AttributeSource == context.Method))
            {
                // Add a ProducesDefaultResponseTypeAttribute if the method does not already have one.
                documentEditor.AddAttribute(context.MethodSyntax, CreateProducesDefaultResponseTypeAttribute());
            }

            var apiConventionMethodAttribute = context.Method.GetAttributes(context.SymbolCache.ApiConventionMethodAttribute).FirstOrDefault();

            if (apiConventionMethodAttribute != null)
            {
                // Remove [ApiConventionMethodAttribute] declared on the method since it's no longer required
                var attributeSyntax = await apiConventionMethodAttribute
                                      .ApplicationSyntaxReference
                                      .GetSyntaxAsync(cancellationToken)
                                      .ConfigureAwait(false);

                documentEditor.RemoveNode(attributeSyntax);
            }

            var document = documentEditor.GetChangedDocument();

            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            if (root is CompilationUnitSyntax compilationUnit && addUsingDirective)
            {
                const string @namespace = "Microsoft.AspNetCore.Http";

                var declaredUsings = new HashSet <string>(compilationUnit.Usings.Select(x => x.Name.ToString()));

                if (!declaredUsings.Contains(@namespace))
                {
                    root = compilationUnit.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(@namespace)));
                }
            }

            return(document.WithSyntaxRoot(root));
        }
Пример #3
0
        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 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);

            // Assert
            Assert.Collection(
                result,
                metadata => Assert.True(metadata.IsImplicit));
        }
        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);

            // Assert
            Assert.Collection(
                result,
                metadata => Assert.True(metadata.IsImplicit));
        }
Пример #6
0
        protected override async Task <Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
        {
            var context = await CreateCodeActionContext(cancellationToken).ConfigureAwait(false);

            var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(context.SymbolCache, context.Method);

            var statusCodes = CalculateStatusCodesToApply(context, declaredResponseMetadata);

            if (statusCodes.Count == 0)
            {
                return(_document);
            }

            var documentEditor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);

            foreach (var statusCode in statusCodes.OrderBy(s => s))
            {
                documentEditor.AddAttribute(context.MethodSyntax, CreateProducesResponseTypeAttribute(statusCode));
            }

            if (!declaredResponseMetadata.Any(m => m.IsDefault && m.AttributeSource == context.Method))
            {
                // Add a ProducesDefaultResponseTypeAttribute if the method does not already have one.
                documentEditor.AddAttribute(context.MethodSyntax, CreateProducesDefaultResponseTypeAttribute());
            }

            var apiConventionMethodAttribute = context.Method.GetAttributes(context.SymbolCache.ApiConventionMethodAttribute).FirstOrDefault();

            if (apiConventionMethodAttribute != null)
            {
                // Remove [ApiConventionMethodAttribute] declared on the method since it's no longer required
                var attributeSyntax = await apiConventionMethodAttribute
                                      .ApplicationSyntaxReference
                                      .GetSyntaxAsync(cancellationToken)
                                      .ConfigureAwait(false);

                documentEditor.RemoveNode(attributeSyntax);
            }

            return(documentEditor.GetChangedDocument());
        }
        private async Task GetResponseMetadata_WorksForInvalidOrUnsupportedAttributes(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);

            // Assert
            Assert.Collection(
                result,
                metadata =>
            {
                Assert.Equal(200, metadata.StatusCode);
                Assert.Same(method, metadata.AttributeSource);
            });
        }
        public async Task GetDeclaredResponseMetadata_ApiConventionTypeAttributeOnBaseType_Works()
        {
            // Arrange
            var type        = typeof(GetDeclaredResponseMetadata_ApiConventionTypeAttributeOnBaseType);
            var compilation = await GetResponseMetadataCompilation();

            var controller  = compilation.GetTypeByMetadataName(type.FullName);
            var method      = (IMethodSymbol)controller.GetMembers().First();
            var symbolCache = new ApiControllerSymbolCache(compilation);

            // Act
            var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);

            // Assert
            // We should expect 3 entries specified by DefaultApiConventions.Post
            Assert.Collection(
                result.OrderBy(r => r.StatusCode),
                metadata => Assert.True(metadata.IsDefault),
                metadata => Assert.Equal(201, metadata.StatusCode),
                metadata => Assert.Equal(400, metadata.StatusCode));
        }
        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);

            // Assert
            Assert.Collection(
                result,
                metadata =>
            {
                Assert.Equal(202, metadata.StatusCode);
                Assert.NotNull(metadata.Attribute);
                Assert.Equal(method, metadata.AttributeSource);
            });
        }
Пример #10
0
        public async Task GetResponseMetadata_WithProducesResponseTypeAndApiConventionMethod()
        {
            // Arrange
            var compilation = await GetResponseMetadataCompilation();

            var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}");
            var method     = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.GetResponseMetadata_WithProducesResponseTypeAndApiConventionMethod)).First();

            Assert.True(ApiControllerSymbolCache.TryCreate(compilation, out var symbolCache));

            // Act
            var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);

            // Assert
            Assert.Collection(
                result,
                metadata =>
            {
                Assert.Equal(204, metadata.StatusCode);
                Assert.NotNull(metadata.Attribute);
            });
        }
        protected override async Task <Document> GetChangedDocumentAsync(CancellationToken cancellationToken)
        {
            var nullableContext = await CreateCodeActionContext(cancellationToken).ConfigureAwait(false);

            if (nullableContext == null)
            {
                return(_document);
            }

            var context = nullableContext.Value;

            var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(context.SymbolCache, context.Method);
            var errorResponseType        = SymbolApiResponseMetadataProvider.GetErrorResponseType(context.SymbolCache, context.Method);

            var results = CalculateStatusCodesToApply(context, declaredResponseMetadata);

            if (results.Count == 0)
            {
                return(_document);
            }

            var documentEditor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false);

            var addUsingDirective = false;

            foreach (var item in results.OrderBy(s => s.statusCode))
            {
                var statusCode = item.statusCode;
                var returnType = item.typeSymbol;

                AttributeSyntax attributeSyntax;
                bool            addUsing;

                if (statusCode >= 400 && returnType != null && !SymbolEqualityComparer.Default.Equals(returnType, errorResponseType))
                {
                    // If a returnType was discovered and is different from the errorResponseType, use it in the result.
                    attributeSyntax = CreateProducesResponseTypeAttribute(context, statusCode, returnType, out addUsing);
                }
                else
                {
                    attributeSyntax = CreateProducesResponseTypeAttribute(context, statusCode, out addUsing);
                }

                documentEditor.AddAttribute(context.MethodSyntax, attributeSyntax);
                addUsingDirective |= addUsing;
            }

            if (!declaredResponseMetadata.Any(m => m.IsDefault && SymbolEqualityComparer.Default.Equals(m.AttributeSource, context.Method)))
            {
                // Add a ProducesDefaultResponseTypeAttribute if the method does not already have one.
                documentEditor.AddAttribute(context.MethodSyntax, CreateProducesDefaultResponseTypeAttribute());
            }

            var apiConventionMethodAttribute = context.Method.GetAttributes(context.SymbolCache.ApiConventionMethodAttribute).FirstOrDefault();

            if (apiConventionMethodAttribute != null)
            {
                // Remove [ApiConventionMethodAttribute] declared on the method since it's no longer required
                var attributeSyntax = await apiConventionMethodAttribute
                                      .ApplicationSyntaxReference
                                      .GetSyntaxAsync(cancellationToken)
                                      .ConfigureAwait(false);

                documentEditor.RemoveNode(attributeSyntax);
            }

            var document = documentEditor.GetChangedDocument();

            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            if (root is CompilationUnitSyntax compilationUnit && addUsingDirective)
            {
                const string @namespace = "Microsoft.AspNetCore.Http";

                var declaredUsings = new HashSet <string>(compilationUnit.Usings.Select(x => x.Name.ToString()));

                if (!declaredUsings.Contains(@namespace))
                {
                    root = compilationUnit.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(@namespace)));
                }
            }

            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }

            return(document.WithSyntaxRoot(root));
        }
        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);
        }