diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/AddResponseTypeAttributeCodeFixAction.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/AddResponseTypeAttributeCodeFixAction.cs new file mode 100644 index 0000000000..56f2373203 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/AddResponseTypeAttributeCodeFixAction.cs @@ -0,0 +1,152 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Simplification; + +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + internal sealed class AddResponseTypeAttributeCodeFixAction : CodeAction + { + private readonly Document _document; + private readonly Diagnostic _diagnostic; + + public AddResponseTypeAttributeCodeFixAction(Document document, Diagnostic diagnostic) + { + _document = document; + _diagnostic = diagnostic; + } + + public override string Title => "Add ProducesResponseType attributes."; + + protected override async Task 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 CreateCodeActionContext(CancellationToken cancellationToken) + { + var root = await _document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var semanticModel = await _document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var methodReturnStatement = (ReturnStatementSyntax)root.FindNode(_diagnostic.Location.SourceSpan); + var methodSyntax = methodReturnStatement.FirstAncestorOrSelf(); + var method = semanticModel.GetDeclaredSymbol(methodSyntax, cancellationToken); + + var symbolCache = new ApiControllerSymbolCache(semanticModel.Compilation); + + var codeActionContext = new CodeActionContext(semanticModel, symbolCache, method, methodSyntax, cancellationToken); + return codeActionContext; + } + + private ICollection CalculateStatusCodesToApply(CodeActionContext context, IList declaredResponseMetadata) + { + if (!SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(context.SymbolCache, context.SemanticModel, context.MethodSyntax, context.CancellationToken, out var actualResponseMetadata)) + { + // If we cannot parse metadata correctly, don't offer fixes. + return Array.Empty(); + } + + var statusCodes = new HashSet(); + foreach (var metadata in actualResponseMetadata) + { + if (DeclaredApiResponseMetadata.TryGetDeclaredMetadata(declaredResponseMetadata, metadata, result: out var declaredMetadata) && + declaredMetadata.AttributeSource == context.Method) + { + // A ProducesResponseType attribute is declared on the method for the current status code. + continue; + } + + statusCodes.Add(metadata.IsDefaultResponse ? 200 : metadata.StatusCode); + } + + return statusCodes; + } + + private static AttributeSyntax CreateProducesResponseTypeAttribute(int statusCode) + { + return SyntaxFactory.Attribute( + SyntaxFactory.ParseName(SymbolNames.ProducesResponseTypeAttribute) + .WithAdditionalAnnotations(Simplifier.Annotation), + SyntaxFactory.AttributeArgumentList().AddArguments( + SyntaxFactory.AttributeArgument( + SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(statusCode))))); + } + + private static AttributeSyntax CreateProducesDefaultResponseTypeAttribute() + { + return SyntaxFactory.Attribute( + SyntaxFactory.ParseName(SymbolNames.ProducesDefaultResponseTypeAttribute) + .WithAdditionalAnnotations(Simplifier.Annotation)); + } + + private readonly struct CodeActionContext + { + public CodeActionContext( + SemanticModel semanticModel, + ApiControllerSymbolCache symbolCache, + IMethodSymbol method, + MethodDeclarationSyntax methodSyntax, + CancellationToken cancellationToken) + { + SemanticModel = semanticModel; + SymbolCache = symbolCache; + Method = method; + MethodSyntax = methodSyntax; + CancellationToken = cancellationToken; + } + + public MethodDeclarationSyntax MethodSyntax { get; } + + public IMethodSymbol Method { get; } + + public SemanticModel SemanticModel { get; } + + public ApiControllerSymbolCache SymbolCache { get; } + + public CancellationToken CancellationToken { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/AddResponseTypeAttributeCodeFixProvider.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/AddResponseTypeAttributeCodeFixProvider.cs new file mode 100644 index 0000000000..fc69730061 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/AddResponseTypeAttributeCodeFixProvider.cs @@ -0,0 +1,40 @@ +// 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.Collections.Immutable; +using System.Composition; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + [ExportCodeFixProvider(LanguageNames.CSharp)] + [Shared] + public class AddResponseTypeAttributeCodeFixProvider : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( + DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode.Id, + DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult.Id); + + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + if (context.Diagnostics.Length == 0) + { + return Task.CompletedTask; + } + + var diagnostic = context.Diagnostics[0]; + if ((diagnostic.Descriptor.Id != DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode.Id) && + (diagnostic.Descriptor.Id != DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult.Id)) + { + return Task.CompletedTask; + } + + var codeFix = new AddResponseTypeAttributeCodeFixAction(context.Document, diagnostic); + + context.RegisterCodeFix(codeFix, diagnostic); + return Task.CompletedTask; + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiControllerSymbolCache.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiControllerSymbolCache.cs index a28cee2458..56097763d5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiControllerSymbolCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiControllerSymbolCache.cs @@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers ModelStateDictionary = compilation.GetTypeByMetadataName(SymbolNames.ModelStateDictionary); NonActionAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonActionAttribute); NonControllerAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonControllerAttribute); + ProducesDefaultResponseTypeAttribute = compilation.GetTypeByMetadataName(SymbolNames.ProducesDefaultResponseTypeAttribute); ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(SymbolNames.ProducesResponseTypeAttribute); var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable); @@ -58,6 +59,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers public INamedTypeSymbol NonControllerAttribute { get; } + public INamedTypeSymbol ProducesDefaultResponseTypeAttribute { get; } + public INamedTypeSymbol ProducesResponseTypeAttribute { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiConventionAnalyzer.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiConventionAnalyzer.cs index 30b4b507d8..39d810f42e 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiConventionAnalyzer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/ApiConventionAnalyzer.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -51,34 +50,32 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers return; } - var conventionAttributes = GetConventionTypeAttributes(symbolCache, method); - var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, conventionAttributes); + var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); + var hasUnreadableStatusCodes = !SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, cancellationToken, out var actualResponseMetadata); - var hasUnreadableStatusCodes = SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, cancellationToken, out var actualResponseMetadata); var hasUndocumentedStatusCodes = false; - foreach (var item in actualResponseMetadata) + foreach (var actualMetadata in actualResponseMetadata) { - var location = item.ReturnStatement.GetLocation(); + var location = actualMetadata.ReturnStatement.GetLocation(); - if (item.IsDefaultResponse) + if (!DeclaredApiResponseMetadata.Contains(declaredResponseMetadata, actualMetadata)) { - if (!(HasStatusCode(declaredResponseMetadata, 200) || HasStatusCode(declaredResponseMetadata, 201))) + hasUndocumentedStatusCodes = true; + if (actualMetadata.IsDefaultResponse) { - hasUndocumentedStatusCodes = true; syntaxNodeContext.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult, location)); } - } - else if (!HasStatusCode(declaredResponseMetadata, item.StatusCode)) + else { - hasUndocumentedStatusCodes = true; syntaxNodeContext.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode, location, - item.StatusCode)); + actualMetadata.StatusCode)); } } + } if (hasUndocumentedStatusCodes || hasUnreadableStatusCodes) { @@ -89,59 +86,24 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers for (var i = 0; i < declaredResponseMetadata.Count; i++) { - var expectedStatusCode = declaredResponseMetadata[i].StatusCode; - if (!HasStatusCode(actualResponseMetadata, expectedStatusCode)) + var declaredMetadata = declaredResponseMetadata[i]; + if (!Contains(actualResponseMetadata, declaredMetadata)) { syntaxNodeContext.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode, methodSyntax.Identifier.GetLocation(), - expectedStatusCode)); + declaredMetadata.StatusCode)); } } }, SyntaxKind.MethodDeclaration); } - internal IReadOnlyList 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 HasStatusCode(IList declaredApiResponseMetadata, int statusCode) - { - if (declaredApiResponseMetadata.Count == 0) - { - // When no status code is declared, a 200 OK is implied. - return statusCode == 200; - } - - for (var i = 0; i < declaredApiResponseMetadata.Count; i++) - { - if (declaredApiResponseMetadata[i].StatusCode == statusCode) - { - return true; - } - } - - return false; - } - - internal static bool HasStatusCode(IList actualResponseMetadata, int statusCode) + internal static bool Contains(IList actualResponseMetadata, DeclaredApiResponseMetadata declaredMetadata) { for (var i = 0; i < actualResponseMetadata.Count; i++) { - if (actualResponseMetadata[i].IsDefaultResponse) - { - return statusCode == 200 || statusCode == 201; - } - - else if(actualResponseMetadata[i].StatusCode == statusCode) + if (declaredMetadata.Matches(actualResponseMetadata[i])) { return true; } diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/DeclaredApiResponseMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/DeclaredApiResponseMetadata.cs index 75aa335209..e2cbd6c994 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/DeclaredApiResponseMetadata.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/DeclaredApiResponseMetadata.cs @@ -1,23 +1,92 @@ // 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.Collections.Generic; using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Mvc.Analyzers { internal readonly struct DeclaredApiResponseMetadata { - public DeclaredApiResponseMetadata(int statusCode, AttributeData attributeData, IMethodSymbol convention) + public static DeclaredApiResponseMetadata ImplicitResponse { get; } = + new DeclaredApiResponseMetadata(statusCode: 0, attributeData: null, attributeSource: null, @implicit: true, @default: false); + + public static DeclaredApiResponseMetadata ForProducesResponseType(int statusCode, AttributeData attributeData, IMethodSymbol attributeSource) + { + return new DeclaredApiResponseMetadata(statusCode, attributeData, attributeSource, @implicit: false, @default: false); + } + + public static DeclaredApiResponseMetadata ForProducesDefaultResponse(AttributeData attributeData, IMethodSymbol attributeSource) + { + return new DeclaredApiResponseMetadata(statusCode: 0, attributeData, attributeSource, @implicit: false, @default: true); + } + + private DeclaredApiResponseMetadata( + int statusCode, + AttributeData attributeData, + IMethodSymbol attributeSource, + bool @implicit, + bool @default) { StatusCode = statusCode; Attribute = attributeData; - Convention = convention; + AttributeSource = attributeSource; + IsImplicit = @implicit; + IsDefault = @default; } public int StatusCode { get; } public AttributeData Attribute { get; } - public IMethodSymbol Convention { get; } + public IMethodSymbol AttributeSource { get; } + + public bool IsImplicit { get; } + + public bool IsDefault { get; } + + internal static bool Contains(IList declaredApiResponseMetadata, ActualApiResponseMetadata actualMetadata) + { + return TryGetDeclaredMetadata(declaredApiResponseMetadata, actualMetadata, out _); + } + + internal static bool TryGetDeclaredMetadata( + IList declaredApiResponseMetadata, + ActualApiResponseMetadata actualMetadata, + out DeclaredApiResponseMetadata result) + { + for (var i = 0; i < declaredApiResponseMetadata.Count; i++) + { + var declaredMetadata = declaredApiResponseMetadata[i]; + + if (declaredMetadata.Matches(actualMetadata)) + { + result = declaredMetadata; + return true; + } + } + + result = default; + return false; + } + + internal bool Matches(ActualApiResponseMetadata actualMetadata) + { + if (actualMetadata.IsDefaultResponse) + { + return IsImplicit || StatusCode == 200 || StatusCode == 201; + } + else if (actualMetadata.StatusCode == StatusCode) + { + return true; + } + else if (actualMetadata.StatusCode >= 400 && IsDefault) + { + // ProducesDefaultResponse matches any failure code + return true; + } + + return false; + } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolApiResponseMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolApiResponseMetadataProvider.cs index 9f51139f23..545955782f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolApiResponseMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolApiResponseMetadataProvider.cs @@ -16,11 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers private const string StatusCodeProperty = "StatusCode"; private const string StatusCodeConstructorParameter = "statusCode"; private static readonly Func _shouldDescendIntoChildren = ShouldDescendIntoChildren; + private static readonly IList DefaultResponseMetadatas = new[] + { + DeclaredApiResponseMetadata.ImplicitResponse, + }; internal static IList GetDeclaredResponseMetadata( ApiControllerSymbolCache symbolCache, - IMethodSymbol method, - IReadOnlyList conventionTypeAttributes) + IMethodSymbol method) { var metadataItems = GetResponseMetadataFromMethodAttributes(symbolCache, method); if (metadataItems.Count != 0) @@ -28,19 +31,28 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers return metadataItems; } + var conventionTypeAttributes = GetConventionTypes(symbolCache, method); metadataItems = GetResponseMetadataFromConventions(symbolCache, method, conventionTypeAttributes); + + if (metadataItems.Count == 0) + { + // If no metadata can be gleaned either through explicit attributes on the method or via a convention, + // declare an implicit 200 status code. + metadataItems = DefaultResponseMetadatas; + } + return metadataItems; } private static IList GetResponseMetadataFromConventions( ApiControllerSymbolCache symbolCache, IMethodSymbol method, - IReadOnlyList attributes) + IReadOnlyList conventionTypes) { var conventionMethod = GetMethodFromConventionMethodAttribute(symbolCache, method); if (conventionMethod == null) { - conventionMethod = MatchConventionMethod(symbolCache, method, attributes); + conventionMethod = MatchConventionMethod(symbolCache, method, conventionTypes); } if (conventionMethod != null) @@ -49,8 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers } return Array.Empty(); - } - + } private static IMethodSymbol GetMethodFromConventionMethodAttribute(ApiControllerSymbolCache symbolCache, IMethodSymbol method) { var attribute = method.GetAttributes(symbolCache.ApiConventionMethodAttribute, inherit: true) @@ -87,17 +98,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers private static IMethodSymbol MatchConventionMethod( ApiControllerSymbolCache symbolCache, IMethodSymbol method, - IReadOnlyList attributes) + IReadOnlyList conventionTypes) { - foreach (var attribute in attributes) + foreach (var conventionType in conventionTypes) { - 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()) { if (!conventionMethod.IsStatic || conventionMethod.DeclaredAccessibility != Accessibility.Public) @@ -122,14 +126,45 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers foreach (var attribute in responseMetadataAttributes) { var statusCode = GetStatusCode(attribute); - var metadata = new DeclaredApiResponseMetadata(statusCode, attribute, convention: null); + var metadata = DeclaredApiResponseMetadata.ForProducesResponseType(statusCode, attribute, attributeSource: methodSymbol); metadataItems.Add(metadata); } + var producesDefaultResponse = methodSymbol.GetAttributes(symbolCache.ProducesDefaultResponseTypeAttribute, inherit: true).FirstOrDefault(); + if (producesDefaultResponse != null) + { + metadataItems.Add(DeclaredApiResponseMetadata.ForProducesDefaultResponse(producesDefaultResponse, methodSymbol)); + } + return metadataItems; } + internal static IReadOnlyList GetConventionTypes(ApiControllerSymbolCache symbolCache, IMethodSymbol method) + { + var attributes = method.ContainingType.GetAttributes(symbolCache.ApiConventionTypeAttribute).ToArray(); + if (attributes.Length == 0) + { + attributes = method.ContainingAssembly.GetAttributes(symbolCache.ApiConventionTypeAttribute).ToArray(); + } + + var conventionTypes = new List(); + for (var i = 0; i < attributes.Length; i++) + { + var attribute = attributes[i]; + if (attribute.ConstructorArguments.Length != 1 || + attribute.ConstructorArguments[0].Kind != TypedConstantKind.Type || + !(attribute.ConstructorArguments[0].Value is ITypeSymbol conventionType)) + { + continue; + } + + conventionTypes.Add(conventionType); + } + + return conventionTypes; + } + internal static int GetStatusCode(AttributeData attribute) { const int DefaultStatusCode = 200; @@ -183,7 +218,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { actualResponseMetadata = new List(); - var hasUnreadableReturnStatements = false; + var allReturnStatementsReadable = true; foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType()) { @@ -199,11 +234,11 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers } else { - hasUnreadableReturnStatements = true; + allReturnStatementsReadable = false; } } - return hasUnreadableReturnStatements; + return allReturnStatementsReadable; } internal static ActualApiResponseMetadata? InspectReturnStatementSyntax( diff --git a/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs index db0874e74e..ca00478262 100644 --- a/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs +++ b/src/Microsoft.AspNetCore.Mvc.Analyzers/SymbolNames.cs @@ -47,6 +47,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers public const string PartialMethod = "Partial"; + public const string ProducesDefaultResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute"; + public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute"; public const string RenderPartialMethod = "RenderPartial"; diff --git a/test/Mvc.Analyzers.Test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs b/test/Mvc.Analyzers.Test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs new file mode 100644 index 0000000000..cfcf6f63f7 --- /dev/null +++ b/test/Mvc.Analyzers.Test/AddResponseTypeAttributeCodeFixProviderIntegrationTest.cs @@ -0,0 +1,70 @@ +// 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; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Analyzer.Testing; +using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Analyzers +{ + public class AddResponseTypeAttributeCodeFixProviderIntegrationTest + { + private MvcDiagnosticAnalyzerRunner AnalyzerRunner { get; } = new MvcDiagnosticAnalyzerRunner(new ApiConventionAnalyzer()); + + private CodeFixRunner CodeFixRunner => CodeFixRunner.Default; + + [Fact] + public Task CodeFixAddsStatusCodes() => RunTest(); + + [Fact] + public Task CodeFixAddsMissingStatusCodes() => RunTest(); + + [Fact] + public Task CodeFixWithConventionAddsMissingStatusCodes() => RunTest(); + + [Fact] + public Task CodeFixWithConventionMethodAddsMissingStatusCodes() => RunTest(); + + [Fact] + public Task CodeFixAddsSuccessStatusCode() => RunTest(); + + [Fact] + public Task CodeFixAddsFullyQualifiedProducesResponseType() => RunTest(); + + private async Task RunTest([CallerMemberName] string testMethod = "") + { + // Arrange + var project = GetProject(testMethod); + var controllerDocument = project.DocumentIds[0]; + + var expectedOutput = Read(testMethod + ".Output"); + + // Act + var diagnostics = await AnalyzerRunner.GetDiagnosticsAsync(project); + var actualOutput = await CodeFixRunner.ApplyCodeFixAsync( + new AddResponseTypeAttributeCodeFixProvider(), + project.GetDocument(controllerDocument), + diagnostics[0]); + + Assert.Equal(expectedOutput, actualOutput, ignoreLineEndingDifferences: true); + } + + private Project GetProject(string testMethod) + { + var testSource = Read(testMethod + ".Input"); + return DiagnosticProject.Create(GetType().Assembly, new[] { testSource }); + } + + private string Read(string fileName) + { + return MvcTestSource.Read(GetType().Name, fileName) + .Source + .Replace("_INPUT_", "_TEST_") + .Replace("_OUTPUT_", "_TEST_"); + } + } +} diff --git a/test/Mvc.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs b/test/Mvc.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs index ace7293af7..e31f22e8ce 100644 --- a/test/Mvc.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs +++ b/test/Mvc.Analyzers.Test/SymbolApiResponseMetadataProviderTest.cs @@ -1,8 +1,6 @@ // 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; -using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -29,10 +27,12 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert - Assert.Empty(result); + Assert.Collection( + result, + metadata => Assert.True(metadata.IsImplicit)); } [Fact] @@ -45,10 +45,12 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert - Assert.Empty(result); + Assert.Collection( + result, + metadata => Assert.True(metadata.IsImplicit)); } [Fact] @@ -61,10 +63,12 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert - Assert.Empty(result); + Assert.Collection( + result, + metadata => Assert.True(metadata.IsImplicit)); } [Fact] @@ -77,7 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( @@ -86,7 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { Assert.Equal(201, metadata.StatusCode); Assert.NotNull(metadata.Attribute); - Assert.Null(metadata.Convention); + Assert.Equal(method, metadata.AttributeSource); }); } @@ -100,7 +104,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( @@ -109,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { Assert.Equal(202, metadata.StatusCode); Assert.NotNull(metadata.Attribute); - Assert.Null(metadata.Convention); + Assert.Equal(method, metadata.AttributeSource); }); } @@ -123,7 +127,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( @@ -132,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { Assert.Equal(203, metadata.StatusCode); Assert.NotNull(metadata.Attribute); - Assert.Null(metadata.Convention); + Assert.Equal(method, metadata.AttributeSource); }); } @@ -146,7 +150,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( @@ -155,7 +159,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { Assert.Equal(201, metadata.StatusCode); Assert.NotNull(metadata.Attribute); - Assert.Null(metadata.Convention); + Assert.Equal(method, metadata.AttributeSource); }); } @@ -169,7 +173,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( @@ -178,7 +182,6 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { Assert.Equal(201, metadata.StatusCode); Assert.NotNull(metadata.Attribute); - Assert.Null(metadata.Convention); }); } @@ -192,7 +195,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( @@ -206,6 +209,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers { Assert.Equal(404, metadata.StatusCode); Assert.NotNull(metadata.Attribute); + }, + metadata => + { + Assert.True(metadata.IsDefault); }); } @@ -219,7 +226,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( @@ -241,16 +248,18 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert - Assert.Empty(result); + Assert.Collection( + result, + metadata => Assert.True(metadata.IsImplicit)); } [Fact] public Task GetResponseMetadata_IgnoresAttributesWithIncorrectStatusCodeType() { - return GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues( + return GetResponseMetadata_WorksForInvalidOrUnsupportedAttribues( nameof(GetResponseMetadata_ControllerActionWithAttributes), nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseTypeWithIncorrectStatusCodeType)); } @@ -258,12 +267,12 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers [Fact] public Task GetResponseMetadata_IgnoresDerivedAttributesWithoutPropertyOnConstructorArguments() { - return GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues( + return GetResponseMetadata_WorksForInvalidOrUnsupportedAttribues( nameof(GetResponseMetadata_ControllerActionWithAttributes), nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithoutArguments)); } - private async Task GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues(string typeName, string methodName) + private async Task GetResponseMetadata_WorksForInvalidOrUnsupportedAttribues(string typeName, string methodName) { // Arrange var compilation = await GetResponseMetadataCompilation(); @@ -272,7 +281,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers var symbolCache = new ApiControllerSymbolCache(compilation); // Act - var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty()); + var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method); // Assert Assert.Collection( @@ -280,8 +289,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers metadata => { Assert.Equal(200, metadata.StatusCode); - Assert.NotNull(metadata.Attribute); - Assert.Null(metadata.Convention); + Assert.Same(method, metadata.AttributeSource); }); } diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Input.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Input.cs new file mode 100644 index 0000000000..e1b19d61a7 --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Input.cs @@ -0,0 +1,35 @@ + +[assembly: Microsoft.AspNetCore.Mvc.ApiConventionType(typeof(Microsoft.AspNetCore.Mvc.DefaultApiConventions))] + +namespace TestApp._INPUT_ +{ + using Microsoft.AspNetCore.Mvc; + + [ApiController] + [Route("[controller]/[action]")] + public class BaseController : ControllerBase + { + + } +} + +namespace TestApp._INPUT_ +{ + public class CodeFixAddsFullyQualifiedProducesResponseType : BaseController + { + public object GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + if (id == 1) + { + return BadRequest(); + } + + return Accepted(new object()); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Output.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Output.cs new file mode 100644 index 0000000000..67c467796b --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsFullyQualifiedProducesResponseType.Output.cs @@ -0,0 +1,39 @@ + +[assembly: Microsoft.AspNetCore.Mvc.ApiConventionType(typeof(Microsoft.AspNetCore.Mvc.DefaultApiConventions))] + +namespace TestApp._OUTPUT_ +{ + using Microsoft.AspNetCore.Mvc; + + [ApiController] + [Route("[controller]/[action]")] + public class BaseController : ControllerBase + { + + } +} + +namespace TestApp._OUTPUT_ +{ + public class CodeFixAddsFullyQualifiedProducesResponseType : BaseController + { + [Microsoft.AspNetCore.Mvc.ProducesResponseType(202)] + [Microsoft.AspNetCore.Mvc.ProducesResponseType(400)] + [Microsoft.AspNetCore.Mvc.ProducesResponseType(404)] + [Microsoft.AspNetCore.Mvc.ProducesDefaultResponseType] + public object GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + if (id == 1) + { + return BadRequest(); + } + + return Accepted(new object()); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Input.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Input.cs new file mode 100644 index 0000000000..708fd10db9 --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Input.cs @@ -0,0 +1,23 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsMissingStatusCodes : ControllerBase + { + [ProducesResponseType(404)] + public IActionResult GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + if (id == 1) + { + return BadRequest(); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Output.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Output.cs new file mode 100644 index 0000000000..b0f2b96dc6 --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsMissingStatusCodes.Output.cs @@ -0,0 +1,26 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsMissingStatusCodes : ControllerBase + { + [ProducesResponseType(404)] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + [ProducesDefaultResponseType] + public IActionResult GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + if (id == 1) + { + return BadRequest(); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Input.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Input.cs new file mode 100644 index 0000000000..39ae582dfd --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Input.cs @@ -0,0 +1,17 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsStatusCodesController : ControllerBase + { + public IActionResult GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Output.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Output.cs new file mode 100644 index 0000000000..690618c254 --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsStatusCodes.Output.cs @@ -0,0 +1,20 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsStatusCodesController : ControllerBase + { + [ProducesResponseType(200)] + [ProducesResponseType(404)] + [ProducesDefaultResponseType] + public IActionResult GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + return Ok(new object()); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Input.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Input.cs new file mode 100644 index 0000000000..63dc63e6c2 --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Input.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; + +[assembly: ApiConventionType(typeof(DefaultApiConventions))] + +namespace Microsoft.AspNetCore.Mvc.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsSuccessStatusCode : ControllerBase + { + public ActionResult GetItem(string id) + { + if (!int.TryParse(id, out var idInt)) + { + return BadRequest(); + } + + if (idInt == 0) + { + return NotFound(); + } + + return Created("url", new object()); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Output.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Output.cs new file mode 100644 index 0000000000..2bc2dae2af --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixAddsSuccessStatusCode.Output.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; + +[assembly: ApiConventionType(typeof(DefaultApiConventions))] + +namespace Microsoft.AspNetCore.Mvc.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixAddsSuccessStatusCode : ControllerBase + { + [ProducesResponseType(201)] + [ProducesResponseType(400)] + [ProducesResponseType(404)] + [ProducesDefaultResponseType] + public ActionResult GetItem(string id) + { + if (!int.TryParse(id, out var idInt)) + { + return BadRequest(); + } + + if (idInt == 0) + { + return NotFound(); + } + + return Created("url", new object()); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Input.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Input.cs new file mode 100644 index 0000000000..e907935bf4 --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Input.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; + +[assembly: ApiConventionType(typeof(DefaultApiConventions))] + +namespace Microsoft.AspNetCore.Mvc.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixWithConventionAddsMissingStatusCodes : ControllerBase + { + public ActionResult GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + return Accepted("Result"); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Output.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Output.cs new file mode 100644 index 0000000000..14c1a06e29 --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionAddsMissingStatusCodes.Output.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc; + +[assembly: ApiConventionType(typeof(DefaultApiConventions))] + +namespace Microsoft.AspNetCore.Mvc.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixWithConventionAddsMissingStatusCodes : ControllerBase + { + [ProducesResponseType(202)] + [ProducesResponseType(404)] + [ProducesDefaultResponseType] + public ActionResult GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + return Accepted("Result"); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Input.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Input.cs new file mode 100644 index 0000000000..960c9f8450 --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Input.cs @@ -0,0 +1,18 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers._INPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixWithConventionMethodAddsMissingStatusCodes : ControllerBase + { + [ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Find))] + public ActionResult GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + return Accepted("Result"); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Output.cs b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Output.cs new file mode 100644 index 0000000000..e5c384bf06 --- /dev/null +++ b/test/Mvc.Analyzers.Test/TestFiles/AddResponseTypeAttributeCodeFixProviderIntegrationTest/CodeFixWithConventionMethodAddsMissingStatusCodes.Output.cs @@ -0,0 +1,20 @@ +namespace Microsoft.AspNetCore.Mvc.Analyzers._OUTPUT_ +{ + [ApiController] + [Route("[controller]/[action]")] + public class CodeFixWithConventionMethodAddsMissingStatusCodes : ControllerBase + { + [ProducesResponseType(202)] + [ProducesResponseType(404)] + [ProducesDefaultResponseType] + public ActionResult GetItem(int id) + { + if (id == 0) + { + return NotFound(); + } + + return Accepted("Result"); + } + } +} diff --git a/test/Mvc.Analyzers.Test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCode.cs b/test/Mvc.Analyzers.Test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCode.cs index a338f55d1a..fa5b642f2e 100644 --- a/test/Mvc.Analyzers.Test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCode.cs +++ b/test/Mvc.Analyzers.Test/TestFiles/ApiConventionAnalyzerIntegrationTest/DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCode.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Analyzers; -[assembly: ApiConventionType(typeof(DefaultApiConventions))] +[assembly: ApiConventionType(typeof(DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCodeConvention))] namespace Microsoft.AspNetCore.Mvc.Analyzers { @@ -22,4 +23,11 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers return Ok(); } } + + public static class DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCodeConvention + { + [ProducesResponseType(200)] + [ProducesResponseType(404)] + public static void Get(int id) { } + } }