Add a code fix that applies ProducesResponseTypeAttributes

This commit is contained in:
Pranav K 2018-07-10 10:17:42 -07:00
parent 2b289d2f2c
commit b7335ac768
No known key found for this signature in database
GPG Key ID: 1963DA6D96C3057A
22 changed files with 752 additions and 104 deletions

View File

@ -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<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<CodeActionContext> 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<MethodDeclarationSyntax>();
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<int> CalculateStatusCodesToApply(CodeActionContext context, IList<DeclaredApiResponseMetadata> 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<int>();
}
var statusCodes = new HashSet<int>();
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; }
}
}
}

View File

@ -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<string> 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;
}
}
}

View File

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

View File

@ -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<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 HasStatusCode(IList<DeclaredApiResponseMetadata> 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<ActualApiResponseMetadata> actualResponseMetadata, int statusCode)
internal static bool Contains(IList<ActualApiResponseMetadata> 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;
}

View File

@ -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> declaredApiResponseMetadata, ActualApiResponseMetadata actualMetadata)
{
return TryGetDeclaredMetadata(declaredApiResponseMetadata, actualMetadata, out _);
}
internal static bool TryGetDeclaredMetadata(
IList<DeclaredApiResponseMetadata> 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;
}
}
}

View File

@ -16,11 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
private const string StatusCodeProperty = "StatusCode";
private const string StatusCodeConstructorParameter = "statusCode";
private static readonly Func<SyntaxNode, bool> _shouldDescendIntoChildren = ShouldDescendIntoChildren;
private static readonly IList<DeclaredApiResponseMetadata> DefaultResponseMetadatas = new[]
{
DeclaredApiResponseMetadata.ImplicitResponse,
};
internal static IList<DeclaredApiResponseMetadata> GetDeclaredResponseMetadata(
ApiControllerSymbolCache symbolCache,
IMethodSymbol method,
IReadOnlyList<AttributeData> 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<DeclaredApiResponseMetadata> GetResponseMetadataFromConventions(
ApiControllerSymbolCache symbolCache,
IMethodSymbol method,
IReadOnlyList<AttributeData> attributes)
IReadOnlyList<ITypeSymbol> 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<DeclaredApiResponseMetadata>();
}
}
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<AttributeData> attributes)
IReadOnlyList<ITypeSymbol> 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<IMethodSymbol>())
{
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<ITypeSymbol> 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<ITypeSymbol>();
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<ActualApiResponseMetadata>();
var hasUnreadableReturnStatements = false;
var allReturnStatementsReadable = true;
foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>())
{
@ -199,11 +234,11 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
}
else
{
hasUnreadableReturnStatements = true;
allReturnStatementsReadable = false;
}
}
return hasUnreadableReturnStatements;
return allReturnStatementsReadable;
}
internal static ActualApiResponseMetadata? InspectReturnStatementSyntax(

View File

@ -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";

View File

@ -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_");
}
}
}

View File

@ -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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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<AttributeData>());
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);
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<object> GetItem(string id)
{
if (!int.TryParse(id, out var idInt))
{
return BadRequest();
}
if (idInt == 0)
{
return NotFound();
}
return Created("url", new object());
}
}
}

View File

@ -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<object> GetItem(string id)
{
if (!int.TryParse(id, out var idInt))
{
return BadRequest();
}
if (idInt == 0)
{
return NotFound();
}
return Created("url", new object());
}
}
}

View File

@ -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<string> GetItem(int id)
{
if (id == 0)
{
return NotFound();
}
return Accepted("Result");
}
}
}

View File

@ -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<string> GetItem(int id)
{
if (id == 0)
{
return NotFound();
}
return Accepted("Result");
}
}
}

View File

@ -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<string> GetItem(int id)
{
if (id == 0)
{
return NotFound();
}
return Accepted("Result");
}
}
}

View File

@ -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<string> GetItem(int id)
{
if (id == 0)
{
return NotFound();
}
return Accepted("Result");
}
}
}

View File

@ -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) { }
}
}