Add a code fix that applies ProducesResponseTypeAttributes
This commit is contained in:
parent
2b289d2f2c
commit
b7335ac768
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
ModelStateDictionary = compilation.GetTypeByMetadataName(SymbolNames.ModelStateDictionary);
|
ModelStateDictionary = compilation.GetTypeByMetadataName(SymbolNames.ModelStateDictionary);
|
||||||
NonActionAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonActionAttribute);
|
NonActionAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonActionAttribute);
|
||||||
NonControllerAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonControllerAttribute);
|
NonControllerAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonControllerAttribute);
|
||||||
|
ProducesDefaultResponseTypeAttribute = compilation.GetTypeByMetadataName(SymbolNames.ProducesDefaultResponseTypeAttribute);
|
||||||
ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(SymbolNames.ProducesResponseTypeAttribute);
|
ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(SymbolNames.ProducesResponseTypeAttribute);
|
||||||
|
|
||||||
var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable);
|
var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable);
|
||||||
|
|
@ -58,6 +59,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
|
|
||||||
public INamedTypeSymbol NonControllerAttribute { get; }
|
public INamedTypeSymbol NonControllerAttribute { get; }
|
||||||
|
|
||||||
|
public INamedTypeSymbol ProducesDefaultResponseTypeAttribute { get; }
|
||||||
|
|
||||||
public INamedTypeSymbol ProducesResponseTypeAttribute { get; }
|
public INamedTypeSymbol ProducesResponseTypeAttribute { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
|
@ -51,34 +50,32 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var conventionAttributes = GetConventionTypeAttributes(symbolCache, method);
|
var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
var declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, conventionAttributes);
|
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;
|
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(
|
syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
|
||||||
DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult,
|
DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult,
|
||||||
location));
|
location));
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
else if (!HasStatusCode(declaredResponseMetadata, item.StatusCode))
|
|
||||||
{
|
{
|
||||||
hasUndocumentedStatusCodes = true;
|
|
||||||
syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
|
syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
|
||||||
DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode,
|
DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode,
|
||||||
location,
|
location,
|
||||||
item.StatusCode));
|
actualMetadata.StatusCode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (hasUndocumentedStatusCodes || hasUnreadableStatusCodes)
|
if (hasUndocumentedStatusCodes || hasUnreadableStatusCodes)
|
||||||
{
|
{
|
||||||
|
|
@ -89,59 +86,24 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
|
|
||||||
for (var i = 0; i < declaredResponseMetadata.Count; i++)
|
for (var i = 0; i < declaredResponseMetadata.Count; i++)
|
||||||
{
|
{
|
||||||
var expectedStatusCode = declaredResponseMetadata[i].StatusCode;
|
var declaredMetadata = declaredResponseMetadata[i];
|
||||||
if (!HasStatusCode(actualResponseMetadata, expectedStatusCode))
|
if (!Contains(actualResponseMetadata, declaredMetadata))
|
||||||
{
|
{
|
||||||
syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
|
syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
|
||||||
DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode,
|
DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode,
|
||||||
methodSyntax.Identifier.GetLocation(),
|
methodSyntax.Identifier.GetLocation(),
|
||||||
expectedStatusCode));
|
declaredMetadata.StatusCode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, SyntaxKind.MethodDeclaration);
|
}, SyntaxKind.MethodDeclaration);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal IReadOnlyList<AttributeData> GetConventionTypeAttributes(ApiControllerSymbolCache symbolCache, IMethodSymbol method)
|
internal static bool Contains(IList<ActualApiResponseMetadata> actualResponseMetadata, DeclaredApiResponseMetadata declaredMetadata)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
for (var i = 0; i < actualResponseMetadata.Count; i++)
|
for (var i = 0; i < actualResponseMetadata.Count; i++)
|
||||||
{
|
{
|
||||||
if (actualResponseMetadata[i].IsDefaultResponse)
|
if (declaredMetadata.Matches(actualResponseMetadata[i]))
|
||||||
{
|
|
||||||
return statusCode == 200 || statusCode == 201;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(actualResponseMetadata[i].StatusCode == statusCode)
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,92 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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;
|
using Microsoft.CodeAnalysis;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
{
|
{
|
||||||
internal readonly struct DeclaredApiResponseMetadata
|
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;
|
StatusCode = statusCode;
|
||||||
Attribute = attributeData;
|
Attribute = attributeData;
|
||||||
Convention = convention;
|
AttributeSource = attributeSource;
|
||||||
|
IsImplicit = @implicit;
|
||||||
|
IsDefault = @default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int StatusCode { get; }
|
public int StatusCode { get; }
|
||||||
|
|
||||||
public AttributeData Attribute { 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
private const string StatusCodeProperty = "StatusCode";
|
private const string StatusCodeProperty = "StatusCode";
|
||||||
private const string StatusCodeConstructorParameter = "statusCode";
|
private const string StatusCodeConstructorParameter = "statusCode";
|
||||||
private static readonly Func<SyntaxNode, bool> _shouldDescendIntoChildren = ShouldDescendIntoChildren;
|
private static readonly Func<SyntaxNode, bool> _shouldDescendIntoChildren = ShouldDescendIntoChildren;
|
||||||
|
private static readonly IList<DeclaredApiResponseMetadata> DefaultResponseMetadatas = new[]
|
||||||
|
{
|
||||||
|
DeclaredApiResponseMetadata.ImplicitResponse,
|
||||||
|
};
|
||||||
|
|
||||||
internal static IList<DeclaredApiResponseMetadata> GetDeclaredResponseMetadata(
|
internal static IList<DeclaredApiResponseMetadata> GetDeclaredResponseMetadata(
|
||||||
ApiControllerSymbolCache symbolCache,
|
ApiControllerSymbolCache symbolCache,
|
||||||
IMethodSymbol method,
|
IMethodSymbol method)
|
||||||
IReadOnlyList<AttributeData> conventionTypeAttributes)
|
|
||||||
{
|
{
|
||||||
var metadataItems = GetResponseMetadataFromMethodAttributes(symbolCache, method);
|
var metadataItems = GetResponseMetadataFromMethodAttributes(symbolCache, method);
|
||||||
if (metadataItems.Count != 0)
|
if (metadataItems.Count != 0)
|
||||||
|
|
@ -28,19 +31,28 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
return metadataItems;
|
return metadataItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var conventionTypeAttributes = GetConventionTypes(symbolCache, method);
|
||||||
metadataItems = GetResponseMetadataFromConventions(symbolCache, method, conventionTypeAttributes);
|
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;
|
return metadataItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IList<DeclaredApiResponseMetadata> GetResponseMetadataFromConventions(
|
private static IList<DeclaredApiResponseMetadata> GetResponseMetadataFromConventions(
|
||||||
ApiControllerSymbolCache symbolCache,
|
ApiControllerSymbolCache symbolCache,
|
||||||
IMethodSymbol method,
|
IMethodSymbol method,
|
||||||
IReadOnlyList<AttributeData> attributes)
|
IReadOnlyList<ITypeSymbol> conventionTypes)
|
||||||
{
|
{
|
||||||
var conventionMethod = GetMethodFromConventionMethodAttribute(symbolCache, method);
|
var conventionMethod = GetMethodFromConventionMethodAttribute(symbolCache, method);
|
||||||
if (conventionMethod == null)
|
if (conventionMethod == null)
|
||||||
{
|
{
|
||||||
conventionMethod = MatchConventionMethod(symbolCache, method, attributes);
|
conventionMethod = MatchConventionMethod(symbolCache, method, conventionTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conventionMethod != null)
|
if (conventionMethod != null)
|
||||||
|
|
@ -49,8 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.Empty<DeclaredApiResponseMetadata>();
|
return Array.Empty<DeclaredApiResponseMetadata>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IMethodSymbol GetMethodFromConventionMethodAttribute(ApiControllerSymbolCache symbolCache, IMethodSymbol method)
|
private static IMethodSymbol GetMethodFromConventionMethodAttribute(ApiControllerSymbolCache symbolCache, IMethodSymbol method)
|
||||||
{
|
{
|
||||||
var attribute = method.GetAttributes(symbolCache.ApiConventionMethodAttribute, inherit: true)
|
var attribute = method.GetAttributes(symbolCache.ApiConventionMethodAttribute, inherit: true)
|
||||||
|
|
@ -87,17 +98,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
private static IMethodSymbol MatchConventionMethod(
|
private static IMethodSymbol MatchConventionMethod(
|
||||||
ApiControllerSymbolCache symbolCache,
|
ApiControllerSymbolCache symbolCache,
|
||||||
IMethodSymbol method,
|
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>())
|
foreach (var conventionMethod in conventionType.GetMembers().OfType<IMethodSymbol>())
|
||||||
{
|
{
|
||||||
if (!conventionMethod.IsStatic || conventionMethod.DeclaredAccessibility != Accessibility.Public)
|
if (!conventionMethod.IsStatic || conventionMethod.DeclaredAccessibility != Accessibility.Public)
|
||||||
|
|
@ -122,14 +126,45 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
foreach (var attribute in responseMetadataAttributes)
|
foreach (var attribute in responseMetadataAttributes)
|
||||||
{
|
{
|
||||||
var statusCode = GetStatusCode(attribute);
|
var statusCode = GetStatusCode(attribute);
|
||||||
var metadata = new DeclaredApiResponseMetadata(statusCode, attribute, convention: null);
|
var metadata = DeclaredApiResponseMetadata.ForProducesResponseType(statusCode, attribute, attributeSource: methodSymbol);
|
||||||
|
|
||||||
metadataItems.Add(metadata);
|
metadataItems.Add(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var producesDefaultResponse = methodSymbol.GetAttributes(symbolCache.ProducesDefaultResponseTypeAttribute, inherit: true).FirstOrDefault();
|
||||||
|
if (producesDefaultResponse != null)
|
||||||
|
{
|
||||||
|
metadataItems.Add(DeclaredApiResponseMetadata.ForProducesDefaultResponse(producesDefaultResponse, methodSymbol));
|
||||||
|
}
|
||||||
|
|
||||||
return metadataItems;
|
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)
|
internal static int GetStatusCode(AttributeData attribute)
|
||||||
{
|
{
|
||||||
const int DefaultStatusCode = 200;
|
const int DefaultStatusCode = 200;
|
||||||
|
|
@ -183,7 +218,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
{
|
{
|
||||||
actualResponseMetadata = new List<ActualApiResponseMetadata>();
|
actualResponseMetadata = new List<ActualApiResponseMetadata>();
|
||||||
|
|
||||||
var hasUnreadableReturnStatements = false;
|
var allReturnStatementsReadable = true;
|
||||||
|
|
||||||
foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>())
|
foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>())
|
||||||
{
|
{
|
||||||
|
|
@ -199,11 +234,11 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
hasUnreadableReturnStatements = true;
|
allReturnStatementsReadable = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasUnreadableReturnStatements;
|
return allReturnStatementsReadable;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static ActualApiResponseMetadata? InspectReturnStatementSyntax(
|
internal static ActualApiResponseMetadata? InspectReturnStatementSyntax(
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
|
|
||||||
public const string PartialMethod = "Partial";
|
public const string PartialMethod = "Partial";
|
||||||
|
|
||||||
|
public const string ProducesDefaultResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute";
|
||||||
|
|
||||||
public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute";
|
public const string ProducesResponseTypeAttribute = "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute";
|
||||||
|
|
||||||
public const string RenderPartialMethod = "RenderPartial";
|
public const string RenderPartialMethod = "RenderPartial";
|
||||||
|
|
|
||||||
|
|
@ -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_");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
@ -29,10 +27,12 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(result);
|
Assert.Collection(
|
||||||
|
result,
|
||||||
|
metadata => Assert.True(metadata.IsImplicit));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -45,10 +45,12 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(result);
|
Assert.Collection(
|
||||||
|
result,
|
||||||
|
metadata => Assert.True(metadata.IsImplicit));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -61,10 +63,12 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(result);
|
Assert.Collection(
|
||||||
|
result,
|
||||||
|
metadata => Assert.True(metadata.IsImplicit));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -77,7 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -86,7 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
{
|
{
|
||||||
Assert.Equal(201, metadata.StatusCode);
|
Assert.Equal(201, metadata.StatusCode);
|
||||||
Assert.NotNull(metadata.Attribute);
|
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);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -109,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
{
|
{
|
||||||
Assert.Equal(202, metadata.StatusCode);
|
Assert.Equal(202, metadata.StatusCode);
|
||||||
Assert.NotNull(metadata.Attribute);
|
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);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -132,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
{
|
{
|
||||||
Assert.Equal(203, metadata.StatusCode);
|
Assert.Equal(203, metadata.StatusCode);
|
||||||
Assert.NotNull(metadata.Attribute);
|
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);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -155,7 +159,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
{
|
{
|
||||||
Assert.Equal(201, metadata.StatusCode);
|
Assert.Equal(201, metadata.StatusCode);
|
||||||
Assert.NotNull(metadata.Attribute);
|
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);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -178,7 +182,6 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
{
|
{
|
||||||
Assert.Equal(201, metadata.StatusCode);
|
Assert.Equal(201, metadata.StatusCode);
|
||||||
Assert.NotNull(metadata.Attribute);
|
Assert.NotNull(metadata.Attribute);
|
||||||
Assert.Null(metadata.Convention);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,7 +195,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -206,6 +209,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
{
|
{
|
||||||
Assert.Equal(404, metadata.StatusCode);
|
Assert.Equal(404, metadata.StatusCode);
|
||||||
Assert.NotNull(metadata.Attribute);
|
Assert.NotNull(metadata.Attribute);
|
||||||
|
},
|
||||||
|
metadata =>
|
||||||
|
{
|
||||||
|
Assert.True(metadata.IsDefault);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,7 +226,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -241,16 +248,18 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(result);
|
Assert.Collection(
|
||||||
|
result,
|
||||||
|
metadata => Assert.True(metadata.IsImplicit));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task GetResponseMetadata_IgnoresAttributesWithIncorrectStatusCodeType()
|
public Task GetResponseMetadata_IgnoresAttributesWithIncorrectStatusCodeType()
|
||||||
{
|
{
|
||||||
return GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues(
|
return GetResponseMetadata_WorksForInvalidOrUnsupportedAttribues(
|
||||||
nameof(GetResponseMetadata_ControllerActionWithAttributes),
|
nameof(GetResponseMetadata_ControllerActionWithAttributes),
|
||||||
nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseTypeWithIncorrectStatusCodeType));
|
nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseTypeWithIncorrectStatusCodeType));
|
||||||
}
|
}
|
||||||
|
|
@ -258,12 +267,12 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
[Fact]
|
[Fact]
|
||||||
public Task GetResponseMetadata_IgnoresDerivedAttributesWithoutPropertyOnConstructorArguments()
|
public Task GetResponseMetadata_IgnoresDerivedAttributesWithoutPropertyOnConstructorArguments()
|
||||||
{
|
{
|
||||||
return GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues(
|
return GetResponseMetadata_WorksForInvalidOrUnsupportedAttribues(
|
||||||
nameof(GetResponseMetadata_ControllerActionWithAttributes),
|
nameof(GetResponseMetadata_ControllerActionWithAttributes),
|
||||||
nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithoutArguments));
|
nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithoutArguments));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task GetResponseMetadata_IgnoresInvalidOrUnsupportedAttribues(string typeName, string methodName)
|
private async Task GetResponseMetadata_WorksForInvalidOrUnsupportedAttribues(string typeName, string methodName)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var compilation = await GetResponseMetadataCompilation();
|
var compilation = await GetResponseMetadataCompilation();
|
||||||
|
|
@ -272,7 +281,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
|
|
@ -280,8 +289,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
metadata =>
|
metadata =>
|
||||||
{
|
{
|
||||||
Assert.Equal(200, metadata.StatusCode);
|
Assert.Equal(200, metadata.StatusCode);
|
||||||
Assert.NotNull(metadata.Attribute);
|
Assert.Same(method, metadata.AttributeSource);
|
||||||
Assert.Null(metadata.Convention);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Analyzers;
|
||||||
|
|
||||||
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
|
[assembly: ApiConventionType(typeof(DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCodeConvention))]
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
{
|
{
|
||||||
|
|
@ -22,4 +23,11 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCodeConvention
|
||||||
|
{
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
[ProducesResponseType(404)]
|
||||||
|
public static void Get(int id) { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue