Refactoring for ApiConvention analyzers
This commit is contained in:
parent
a87b8fa2af
commit
46189abda7
|
|
@ -0,0 +1,57 @@
|
|||
// 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 Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
internal readonly struct ApiControllerSymbolCache
|
||||
{
|
||||
public ApiControllerSymbolCache(Compilation compilation)
|
||||
{
|
||||
ActionResultOfT = compilation.GetTypeByMetadataName(SymbolNames.ActionResultOfT);
|
||||
ApiConventionNameMatchAttribute = compilation.GetTypeByMetadataName(SymbolNames.ApiConventionNameMatchAttribute);
|
||||
ApiConventionTypeAttribute = compilation.GetTypeByMetadataName(SymbolNames.ApiConventionTypeAttribute);
|
||||
ApiConventionTypeMatchAttribute = compilation.GetTypeByMetadataName(SymbolNames.ApiConventionTypeMatchAttribute);
|
||||
ControllerAttribute = compilation.GetTypeByMetadataName(SymbolNames.ControllerAttribute);
|
||||
DefaultStatusCodeAttribute = compilation.GetTypeByMetadataName(SymbolNames.DefaultStatusCodeAttribute);
|
||||
IActionResult = compilation.GetTypeByMetadataName(SymbolNames.IActionResult);
|
||||
IApiBehaviorMetadata = compilation.GetTypeByMetadataName(SymbolNames.IApiBehaviorMetadata);
|
||||
IConvertToActionResult = compilation.GetTypeByMetadataName(SymbolNames.IConvertToActionResult);
|
||||
NonActionAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonActionAttribute);
|
||||
NonControllerAttribute = compilation.GetTypeByMetadataName(SymbolNames.NonControllerAttribute);
|
||||
ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(SymbolNames.ProducesResponseTypeAttribute);
|
||||
|
||||
var disposable = compilation.GetSpecialType(SpecialType.System_IDisposable);
|
||||
var members = disposable.GetMembers(nameof(IDisposable.Dispose));
|
||||
IDisposableDispose = members.Length == 1 ? (IMethodSymbol)members[0] : null;
|
||||
}
|
||||
|
||||
public INamedTypeSymbol ActionResultOfT { get; }
|
||||
|
||||
public INamedTypeSymbol ApiConventionNameMatchAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol ApiConventionTypeAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol ApiConventionTypeMatchAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol ControllerAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol DefaultStatusCodeAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol IActionResult { get; }
|
||||
|
||||
public INamedTypeSymbol IApiBehaviorMetadata { get; }
|
||||
|
||||
public INamedTypeSymbol IConvertToActionResult { get; }
|
||||
|
||||
public IMethodSymbol IDisposableDispose { get; }
|
||||
|
||||
public INamedTypeSymbol NonActionAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol NonControllerAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol ProducesResponseTypeAttribute { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// 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 Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
internal readonly struct ApiControllerTypeCache
|
||||
{
|
||||
public ApiControllerTypeCache(Compilation compilation)
|
||||
{
|
||||
ApiConventionAttribute = compilation.GetTypeByMetadataName(SymbolNames.ApiConventionAttribute);
|
||||
ProducesResponseTypeAttribute = compilation.GetTypeByMetadataName(SymbolNames.ProducesResponseTypeAttribute);
|
||||
}
|
||||
|
||||
public INamedTypeSymbol ApiConventionAttribute { get; }
|
||||
|
||||
public INamedTypeSymbol ProducesResponseTypeAttribute { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
// 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.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||
public class ApiConventionAnalyzer : DiagnosticAnalyzer
|
||||
{
|
||||
private static readonly Func<SyntaxNode, bool> _shouldDescendIntoChildren = ShouldDescendIntoChildren;
|
||||
|
||||
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
|
||||
DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode,
|
||||
DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult,
|
||||
DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode);
|
||||
|
||||
public override void Initialize(AnalysisContext context)
|
||||
{
|
||||
context.EnableConcurrentExecution();
|
||||
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
|
||||
|
||||
context.RegisterCompilationStartAction(compilationStartAnalysisContext =>
|
||||
{
|
||||
var symbolCache = new ApiControllerSymbolCache(compilationStartAnalysisContext.Compilation);
|
||||
if (symbolCache.ApiConventionTypeAttribute == null || symbolCache.ApiConventionTypeAttribute.TypeKind == TypeKind.Error)
|
||||
{
|
||||
// No-op if we can't find types we care about.
|
||||
return;
|
||||
}
|
||||
|
||||
InitializeWorker(compilationStartAnalysisContext, symbolCache);
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeWorker(CompilationStartAnalysisContext compilationStartAnalysisContext, ApiControllerSymbolCache symbolCache)
|
||||
{
|
||||
compilationStartAnalysisContext.RegisterSyntaxNodeAction(syntaxNodeContext =>
|
||||
{
|
||||
var methodSyntax = (MethodDeclarationSyntax)syntaxNodeContext.Node;
|
||||
var semanticModel = syntaxNodeContext.SemanticModel;
|
||||
var method = semanticModel.GetDeclaredSymbol(methodSyntax, syntaxNodeContext.CancellationToken);
|
||||
|
||||
if (!ShouldEvaluateMethod(symbolCache, method))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var conventionAttributes = GetConventionTypeAttributes(symbolCache, method);
|
||||
var expectedResponseMetadata = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, conventionAttributes);
|
||||
var actualResponseMetadata = new HashSet<int>();
|
||||
|
||||
var context = new ApiConventionContext(
|
||||
symbolCache,
|
||||
syntaxNodeContext,
|
||||
expectedResponseMetadata,
|
||||
actualResponseMetadata);
|
||||
|
||||
var hasUndocumentedStatusCodes = false;
|
||||
foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>())
|
||||
{
|
||||
hasUndocumentedStatusCodes |= VisitReturnStatementSyntax(context, returnStatementSyntax);
|
||||
}
|
||||
|
||||
if (hasUndocumentedStatusCodes)
|
||||
{
|
||||
// If we produced analyzer warnings about undocumented status codes, don't attempt to determine
|
||||
// if there are documented status codes that are missing from the method body.
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < expectedResponseMetadata.Count; i++)
|
||||
{
|
||||
var expectedStatusCode = expectedResponseMetadata[i].StatusCode;
|
||||
if (!actualResponseMetadata.Contains(expectedStatusCode))
|
||||
{
|
||||
context.SyntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
|
||||
DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode,
|
||||
methodSyntax.Identifier.GetLocation(),
|
||||
expectedStatusCode));
|
||||
}
|
||||
}
|
||||
|
||||
}, SyntaxKind.MethodDeclaration);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Returns true if the return statement returns an undocumented status code.
|
||||
private static bool VisitReturnStatementSyntax(
|
||||
in ApiConventionContext context,
|
||||
ReturnStatementSyntax returnStatementSyntax)
|
||||
{
|
||||
var returnExpression = returnStatementSyntax.Expression;
|
||||
if (returnExpression.IsMissing)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var syntaxNodeContext = context.SyntaxNodeContext;
|
||||
|
||||
var typeInfo = syntaxNodeContext.SemanticModel.GetTypeInfo(returnExpression, syntaxNodeContext.CancellationToken);
|
||||
if (typeInfo.Type.TypeKind == TypeKind.Error)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var location = returnStatementSyntax.GetLocation();
|
||||
var diagnostic = InspectReturnExpression(context, typeInfo.Type, location);
|
||||
if (diagnostic != null)
|
||||
{
|
||||
context.SyntaxNodeContext.ReportDiagnostic(diagnostic);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static Diagnostic InspectReturnExpression(in ApiConventionContext context, ITypeSymbol type, Location location)
|
||||
{
|
||||
var defaultStatusCodeAttribute = type
|
||||
.GetAttributes(context.SymbolCache.DefaultStatusCodeAttribute, inherit: true)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (defaultStatusCodeAttribute != null)
|
||||
{
|
||||
var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute);
|
||||
if (statusCode == null)
|
||||
{
|
||||
// Unable to read the status code. Treat this as valid.
|
||||
return null;
|
||||
}
|
||||
|
||||
context.ActualResponseMetadata.Add(statusCode.Value);
|
||||
if (!HasStatusCode(context.ExpectedResponseMetadata, statusCode.Value))
|
||||
{
|
||||
return Diagnostic.Create(
|
||||
DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode,
|
||||
location,
|
||||
statusCode);
|
||||
}
|
||||
}
|
||||
else if (!context.SymbolCache.IActionResult.IsAssignableFrom(type))
|
||||
{
|
||||
if (!HasStatusCode(context.ExpectedResponseMetadata, 200) && !HasStatusCode(context.ExpectedResponseMetadata, 201))
|
||||
{
|
||||
return Diagnostic.Create(
|
||||
DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult,
|
||||
location);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static int? GetDefaultStatusCode(AttributeData attribute)
|
||||
{
|
||||
if (attribute != null &&
|
||||
attribute.ConstructorArguments.Length == 1 &&
|
||||
attribute.ConstructorArguments[0].Kind == TypedConstantKind.Primitive &&
|
||||
attribute.ConstructorArguments[0].Value is int statusCode)
|
||||
{
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool ShouldEvaluateMethod(ApiControllerSymbolCache symbolCache, IMethodSymbol method)
|
||||
{
|
||||
if (method == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (method.ReturnsVoid || method.ReturnType.TypeKind == TypeKind.Error)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MvcFacts.IsController(method.ContainingType, symbolCache.ControllerAttribute, symbolCache.NonControllerAttribute))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!method.ContainingType.HasAttribute(symbolCache.IApiBehaviorMetadata, inherit: true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MvcFacts.IsControllerAction(method, symbolCache.NonActionAttribute, symbolCache.IDisposableDispose))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool HasStatusCode(IList<ApiResponseMetadata> 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;
|
||||
}
|
||||
|
||||
private static bool ShouldDescendIntoChildren(SyntaxNode syntaxNode)
|
||||
{
|
||||
return !syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement) &&
|
||||
!syntaxNode.IsKind(SyntaxKind.ParenthesizedLambdaExpression) &&
|
||||
!syntaxNode.IsKind(SyntaxKind.SimpleLambdaExpression) &&
|
||||
!syntaxNode.IsKind(SyntaxKind.AnonymousMethodExpression);
|
||||
}
|
||||
|
||||
|
||||
internal readonly struct ApiConventionContext
|
||||
{
|
||||
public ApiConventionContext(
|
||||
ApiControllerSymbolCache symbolCache,
|
||||
SyntaxNodeAnalysisContext syntaxNodeContext,
|
||||
IList<ApiResponseMetadata> expectedResponseMetadata,
|
||||
HashSet<int> actualResponseMetadata,
|
||||
Action<Diagnostic> reportDiagnostic = null)
|
||||
{
|
||||
SymbolCache = symbolCache;
|
||||
SyntaxNodeContext = syntaxNodeContext;
|
||||
ExpectedResponseMetadata = expectedResponseMetadata;
|
||||
ActualResponseMetadata = actualResponseMetadata;
|
||||
ReportDiagnosticAction = reportDiagnostic;
|
||||
}
|
||||
|
||||
public ApiControllerSymbolCache SymbolCache { get; }
|
||||
public SyntaxNodeAnalysisContext SyntaxNodeContext { get; }
|
||||
public IList<ApiResponseMetadata> ExpectedResponseMetadata { get; }
|
||||
public HashSet<int> ActualResponseMetadata { get; }
|
||||
private Action<Diagnostic> ReportDiagnosticAction { get; }
|
||||
|
||||
public void ReportDiagnostic(Diagnostic diagnostic)
|
||||
{
|
||||
if (ReportDiagnosticAction != null)
|
||||
{
|
||||
ReportDiagnosticAction(diagnostic);
|
||||
}
|
||||
|
||||
SyntaxNodeContext.ReportDiagnostic(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// 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 Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
internal readonly struct ApiResponseMetadata
|
||||
{
|
||||
public ApiResponseMetadata(int statusCode, AttributeData attributeData, IMethodSymbol convention)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
Attribute = attributeData;
|
||||
Convention = convention;
|
||||
}
|
||||
|
||||
public int StatusCode { get; }
|
||||
|
||||
public AttributeData Attribute { get; }
|
||||
|
||||
public IMethodSymbol Convention { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -11,29 +11,22 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
internal static class CodeAnalysisExtensions
|
||||
{
|
||||
public static bool HasAttribute(this ITypeSymbol typeSymbol, ITypeSymbol attribute, bool inherit)
|
||||
{
|
||||
Debug.Assert(typeSymbol != null);
|
||||
Debug.Assert(attribute != null);
|
||||
|
||||
if (!inherit)
|
||||
{
|
||||
return HasAttribute(typeSymbol, attribute);
|
||||
}
|
||||
|
||||
foreach (var type in typeSymbol.GetTypeHierarchy())
|
||||
{
|
||||
if (type.HasAttribute(attribute))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
=> GetAttributes(typeSymbol, attribute, inherit).Any();
|
||||
|
||||
public static bool HasAttribute(this IMethodSymbol methodSymbol, ITypeSymbol attribute, bool inherit)
|
||||
=> GetAttributes(methodSymbol, attribute, inherit).Any();
|
||||
|
||||
public static IEnumerable<AttributeData> GetAttributes(this ISymbol symbol, ITypeSymbol attribute)
|
||||
{
|
||||
foreach (var declaredAttribute in symbol.GetAttributes())
|
||||
{
|
||||
if (attribute.IsAssignableFrom(declaredAttribute.AttributeClass))
|
||||
{
|
||||
yield return declaredAttribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<AttributeData> GetAttributes(this IMethodSymbol methodSymbol, ITypeSymbol attribute, bool inherit)
|
||||
{
|
||||
Debug.Assert(methodSymbol != null);
|
||||
|
|
@ -55,6 +48,25 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<AttributeData> GetAttributes(this ITypeSymbol typeSymbol, ITypeSymbol attribute, bool inherit)
|
||||
{
|
||||
Debug.Assert(typeSymbol != null);
|
||||
Debug.Assert(attribute != null);
|
||||
|
||||
foreach (var type in GetTypeHierarchy(typeSymbol))
|
||||
{
|
||||
foreach (var attributeData in GetAttributes(type, attribute))
|
||||
{
|
||||
yield return attributeData;
|
||||
}
|
||||
|
||||
if (!inherit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool HasAttribute(this IPropertySymbol propertySymbol, ITypeSymbol attribute, bool inherit)
|
||||
{
|
||||
Debug.Assert(propertySymbol != null);
|
||||
|
|
@ -78,11 +90,16 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
return false;
|
||||
}
|
||||
|
||||
public static bool IsAssignableFrom(this ITypeSymbol source, INamedTypeSymbol target)
|
||||
public static bool IsAssignableFrom(this ITypeSymbol source, ITypeSymbol target)
|
||||
{
|
||||
Debug.Assert(source != null);
|
||||
Debug.Assert(target != null);
|
||||
|
||||
if (source == target)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (source.TypeKind == TypeKind.Interface)
|
||||
{
|
||||
foreach (var @interface in target.AllInterfaces)
|
||||
|
|
@ -120,17 +137,6 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerable<AttributeData> GetAttributes(this ISymbol symbol, ITypeSymbol attribute)
|
||||
{
|
||||
foreach (var declaredAttribute in symbol.GetAttributes())
|
||||
{
|
||||
if (attribute.IsAssignableFrom(declaredAttribute.AttributeClass))
|
||||
{
|
||||
yield return declaredAttribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ITypeSymbol> GetTypeHierarchy(this ITypeSymbol typeSymbol)
|
||||
{
|
||||
while (typeSymbol != null)
|
||||
|
|
|
|||
|
|
@ -42,5 +42,32 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor MVC1004_ActionReturnsUndocumentedStatusCode =
|
||||
new DiagnosticDescriptor(
|
||||
"MVC1004",
|
||||
"Action returns undeclared status code.",
|
||||
"Action method returns undeclared status code '{0}'.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor MVC1005_ActionReturnsUndocumentedSuccessResult =
|
||||
new DiagnosticDescriptor(
|
||||
"MVC1005",
|
||||
"Action returns undeclared success result.",
|
||||
"Action method returns a success result without a corresponding ProducesResponseType.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Warning,
|
||||
isEnabledByDefault: true);
|
||||
|
||||
public static readonly DiagnosticDescriptor MVC1006_ActionDoesNotReturnDocumentedStatusCode =
|
||||
new DiagnosticDescriptor(
|
||||
"MVC1006",
|
||||
"Action documents status code that is not returned.",
|
||||
"Action method documents status code '{0}' without a corresponding return type.",
|
||||
"Usage",
|
||||
DiagnosticSeverity.Info,
|
||||
isEnabledByDefault: false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
internal static class SymbolApiConventionMatcher
|
||||
{
|
||||
internal static bool IsMatch(ApiControllerSymbolCache symbolCache, IMethodSymbol method, IMethodSymbol conventionMethod)
|
||||
{
|
||||
return MethodMatches() && ParametersMatch();
|
||||
|
||||
bool MethodMatches()
|
||||
{
|
||||
var methodNameMatchBehavior = GetNameMatchBehavior(symbolCache, conventionMethod);
|
||||
if (!IsNameMatch(method.Name, conventionMethod.Name, methodNameMatchBehavior))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParametersMatch()
|
||||
{
|
||||
var methodParameters = method.Parameters;
|
||||
var conventionMethodParameters = conventionMethod.Parameters;
|
||||
|
||||
for (var i = 0; i < conventionMethodParameters.Length; i++)
|
||||
{
|
||||
var conventionParameter = conventionMethodParameters[i];
|
||||
if (conventionParameter.IsParams)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (methodParameters.Length <= i)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nameMatchBehavior = GetNameMatchBehavior(symbolCache, conventionParameter);
|
||||
var typeMatchBehavior = GetTypeMatchBehavior(symbolCache, conventionParameter);
|
||||
|
||||
if (!IsTypeMatch(methodParameters[i].Type, conventionParameter.Type, typeMatchBehavior) ||
|
||||
!IsNameMatch(methodParameters[i].Name, conventionParameter.Name, nameMatchBehavior))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure convention has at least as many parameters as the method. params convention argument are handled
|
||||
// inside the for loop.
|
||||
return methodParameters.Length == conventionMethodParameters.Length;
|
||||
}
|
||||
}
|
||||
|
||||
internal static SymbolApiConventionNameMatchBehavior GetNameMatchBehavior(ApiControllerSymbolCache symbolCache, ISymbol symbol)
|
||||
{
|
||||
var attribute = symbol.GetAttributes(symbolCache.ApiConventionNameMatchAttribute).FirstOrDefault();
|
||||
if (attribute == null ||
|
||||
attribute.ConstructorArguments.Length != 1 ||
|
||||
attribute.ConstructorArguments[0].Kind != TypedConstantKind.Enum)
|
||||
{
|
||||
return SymbolApiConventionNameMatchBehavior.Exact;
|
||||
}
|
||||
|
||||
var intValue = (int)attribute.ConstructorArguments[0].Value;
|
||||
return (SymbolApiConventionNameMatchBehavior)intValue;
|
||||
}
|
||||
|
||||
internal static SymbolApiConventionTypeMatchBehavior GetTypeMatchBehavior(ApiControllerSymbolCache symbolCache, ISymbol symbol)
|
||||
{
|
||||
var attribute = symbol.GetAttributes(symbolCache.ApiConventionTypeMatchAttribute).FirstOrDefault();
|
||||
if (attribute == null ||
|
||||
attribute.ConstructorArguments.Length != 1 ||
|
||||
attribute.ConstructorArguments[0].Kind != TypedConstantKind.Enum)
|
||||
{
|
||||
return SymbolApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
}
|
||||
|
||||
var intValue = (int)attribute.ConstructorArguments[0].Value;
|
||||
return (SymbolApiConventionTypeMatchBehavior)intValue;
|
||||
}
|
||||
|
||||
internal static bool IsNameMatch(string name, string conventionName, SymbolApiConventionNameMatchBehavior nameMatchBehavior)
|
||||
{
|
||||
switch (nameMatchBehavior)
|
||||
{
|
||||
case SymbolApiConventionNameMatchBehavior.Any:
|
||||
return true;
|
||||
|
||||
case SymbolApiConventionNameMatchBehavior.Exact:
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
|
||||
case SymbolApiConventionNameMatchBehavior.Prefix:
|
||||
return IsNameMatchPrefix();
|
||||
|
||||
case SymbolApiConventionNameMatchBehavior.Suffix:
|
||||
return IsNameMatchSuffix();
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsNameMatchPrefix()
|
||||
{
|
||||
if (name.Length < conventionName.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Length == conventionName.Length)
|
||||
{
|
||||
// name = "Post", conventionName = "Post"
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (!name.StartsWith(conventionName, StringComparison.Ordinal))
|
||||
{
|
||||
// name = "GetPerson", conventionName = "Post"
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for name = "PostPerson", conventionName = "Post"
|
||||
// Verify the first letter after the convention name is upper case. In this case 'P' from "Person"
|
||||
return char.IsUpper(name[conventionName.Length]);
|
||||
}
|
||||
|
||||
bool IsNameMatchSuffix()
|
||||
{
|
||||
if (name.Length < conventionName.Length)
|
||||
{
|
||||
// name = "person", conventionName = "personName"
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Length == conventionName.Length)
|
||||
{
|
||||
// name = id, conventionName = id
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// Check for name = personName, conventionName = name
|
||||
var index = name.Length - conventionName.Length - 1;
|
||||
if (!char.IsLower(name[index]))
|
||||
{
|
||||
// Verify letter before "name" is lowercase. In this case the letter 'n' at the end of "person"
|
||||
return false;
|
||||
}
|
||||
|
||||
index++;
|
||||
if (name[index] != char.ToUpper(conventionName[0]))
|
||||
{
|
||||
// Verify the first letter from convention is upper case. In this case 'n' from "name"
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match the remaining letters with exact case. i.e. match "ame" from "personName", "name"
|
||||
index++;
|
||||
return string.Compare(name, index, conventionName, 1, conventionName.Length - 1, StringComparison.Ordinal) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsTypeMatch(ITypeSymbol type, ITypeSymbol conventionType, SymbolApiConventionTypeMatchBehavior typeMatchBehavior)
|
||||
{
|
||||
switch (typeMatchBehavior)
|
||||
{
|
||||
case SymbolApiConventionTypeMatchBehavior.Any:
|
||||
return true;
|
||||
|
||||
case SymbolApiConventionTypeMatchBehavior.AssignableFrom:
|
||||
return conventionType.IsAssignableFrom(type);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal enum SymbolApiConventionTypeMatchBehavior
|
||||
{
|
||||
Any,
|
||||
AssignableFrom
|
||||
}
|
||||
|
||||
internal enum SymbolApiConventionNameMatchBehavior
|
||||
{
|
||||
Any,
|
||||
Exact,
|
||||
Prefix,
|
||||
Suffix,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
|
|
@ -12,10 +13,58 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
private const string StatusCodeProperty = "StatusCode";
|
||||
private const string StatusCodeConstructorParameter = "statusCode";
|
||||
|
||||
internal static IList<ApiResponseMetadata> GetResponseMetadata(ApiControllerTypeCache typeCache, IMethodSymbol methodSymbol)
|
||||
internal static IList<ApiResponseMetadata> GetResponseMetadata(
|
||||
ApiControllerSymbolCache symbolCache,
|
||||
IMethodSymbol method,
|
||||
IReadOnlyList<AttributeData> conventionTypeAttributes)
|
||||
{
|
||||
var metadataItems = GetResponseMetadataFromMethodAttributes(symbolCache, method);
|
||||
if (metadataItems.Count != 0)
|
||||
{
|
||||
return metadataItems;
|
||||
}
|
||||
|
||||
metadataItems = GetResponseMetadataFromConventions(symbolCache, method, conventionTypeAttributes);
|
||||
return metadataItems;
|
||||
}
|
||||
|
||||
private static IList<ApiResponseMetadata> GetResponseMetadataFromConventions(
|
||||
ApiControllerSymbolCache symbolCache,
|
||||
IMethodSymbol method,
|
||||
IReadOnlyList<AttributeData> attributes)
|
||||
{
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
if (attribute.ConstructorArguments.Length != 1 ||
|
||||
attribute.ConstructorArguments[0].Kind != TypedConstantKind.Type ||
|
||||
!(attribute.ConstructorArguments[0].Value is ITypeSymbol conventionType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var conventionMethod in conventionType.GetMembers().OfType<IMethodSymbol>())
|
||||
{
|
||||
if (!conventionMethod.IsStatic || conventionMethod.DeclaredAccessibility != Accessibility.Public)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!SymbolApiConventionMatcher.IsMatch(symbolCache, method, conventionMethod))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return GetResponseMetadataFromMethodAttributes(symbolCache, conventionMethod);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<ApiResponseMetadata>();
|
||||
}
|
||||
|
||||
private static IList<ApiResponseMetadata> GetResponseMetadataFromMethodAttributes(ApiControllerSymbolCache symbolCache, IMethodSymbol methodSymbol)
|
||||
{
|
||||
var responseMetadataAttributes = methodSymbol.GetAttributes(typeCache.ProducesResponseTypeAttribute, inherit: true);
|
||||
var metadataItems = new List<ApiResponseMetadata>();
|
||||
var responseMetadataAttributes = methodSymbol.GetAttributes(symbolCache.ProducesResponseTypeAttribute, inherit: true);
|
||||
foreach (var attribute in responseMetadataAttributes)
|
||||
{
|
||||
var statusCode = GetStatusCode(attribute);
|
||||
|
|
@ -34,7 +83,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
{
|
||||
var namedArgument = attribute.NamedArguments[i];
|
||||
var namedArgumentValue = namedArgument.Value;
|
||||
if (string.Equals(namedArgument.Key, StatusCodeProperty, StringComparison.Ordinal) &&
|
||||
if (string.Equals(namedArgument.Key, StatusCodeProperty, StringComparison.Ordinal) &&
|
||||
namedArgumentValue.Kind == TypedConstantKind.Primitive &&
|
||||
(namedArgumentValue.Type.SpecialType & SpecialType.System_Int32) == SpecialType.System_Int32 &&
|
||||
namedArgumentValue.Value is int statusCode)
|
||||
|
|
@ -71,20 +120,4 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
return DefaultStatusCode;
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct ApiResponseMetadata
|
||||
{
|
||||
public ApiResponseMetadata(int statusCode, AttributeData attributeData, IMethodSymbol convention)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
Attribute = attributeData;
|
||||
Convention = convention;
|
||||
}
|
||||
|
||||
public int StatusCode { get; }
|
||||
|
||||
public AttributeData Attribute { get; }
|
||||
|
||||
public IMethodSymbol Convention { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,26 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
{
|
||||
public const string AllowAnonymousAttribute = "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute";
|
||||
|
||||
public const string ApiConventionAttribute = "Microsoft.AspNetCore.Mvc.ApiConventionAttribute";
|
||||
public const string ApiConventionNameMatchAttribute = "Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchAttribute";
|
||||
|
||||
public const string ApiConventionTypeMatchAttribute = "Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionTypeMatchAttribute";
|
||||
|
||||
public const string ApiConventionTypeAttribute = "Microsoft.AspNetCore.Mvc.ApiConventionTypeAttribute";
|
||||
|
||||
public const string ActionResultOfT = "Microsoft.AspNetCore.Mvc.ActionResult`1";
|
||||
|
||||
public const string AuthorizeAttribute = "Microsoft.AspNetCore.Authorization.AuthorizeAttribute";
|
||||
|
||||
public const string ControllerAttribute = "Microsoft.AspNetCore.Mvc.ControllerAttribute";
|
||||
|
||||
public const string DefaultStatusCodeAttribute = "Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute";
|
||||
|
||||
public const string IApiBehaviorMetadata = "Microsoft.AspNetCore.Mvc.Internal.IApiBehaviorMetadata";
|
||||
|
||||
public const string IActionResult = "Microsoft.AspNetCore.Mvc.IActionResult";
|
||||
|
||||
public const string IConvertToActionResult = "Microsoft.AspNetCore.Mvc.IConvertToActionResult";
|
||||
|
||||
public const string IFilterMetadataType = "Microsoft.AspNetCore.Mvc.Filters.IFilterMetadata";
|
||||
|
||||
public const string HtmlHelperPartialExtensionsType = "Microsoft.AspNetCore.Mvc.Rendering.HtmlHelperPartialExtensions";
|
||||
|
|
@ -19,6 +35,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
|
||||
public const string IRouteTemplateProvider = "Microsoft.AspNetCore.Mvc.Routing.IRouteTemplateProvider";
|
||||
|
||||
public const string NonActionAttribute = "Microsoft.AspNetCore.Mvc.NonActionAttribute";
|
||||
|
||||
public const string NonControllerAttribute = "Microsoft.AspNetCore.Mvc.NonControllerAttribute";
|
||||
|
||||
public const string PageModelAttributeType = "Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageModelAttribute";
|
||||
|
||||
public const string PartialMethod = "Partial";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
// 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.Reflection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
internal static class ApiConventionMatcher
|
||||
{
|
||||
internal static bool IsMatch(MethodInfo methodInfo, MethodInfo conventionMethod)
|
||||
{
|
||||
return MethodMatches() && ParametersMatch();
|
||||
|
||||
bool MethodMatches()
|
||||
{
|
||||
var methodNameMatchBehavior = GetNameMatchBehavior(conventionMethod);
|
||||
if (!IsNameMatch(methodInfo.Name, conventionMethod.Name, methodNameMatchBehavior))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParametersMatch()
|
||||
{
|
||||
var methodParameters = methodInfo.GetParameters();
|
||||
var conventionMethodParameters = conventionMethod.GetParameters();
|
||||
|
||||
for (var i = 0; i < conventionMethodParameters.Length; i++)
|
||||
{
|
||||
var conventionParameter = conventionMethodParameters[i];
|
||||
if (conventionParameter.IsDefined(typeof(ParamArrayAttribute)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (methodParameters.Length <= i)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nameMatchBehavior = GetNameMatchBehavior(conventionParameter);
|
||||
var typeMatchBehavior = GetTypeMatchBehavior(conventionParameter);
|
||||
|
||||
if (!IsTypeMatch(methodParameters[i].ParameterType, conventionParameter.ParameterType, typeMatchBehavior) ||
|
||||
!IsNameMatch(methodParameters[i].Name, conventionParameter.Name, nameMatchBehavior))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure convention has at least as many parameters as the method. params convention argument are handled
|
||||
// inside the for loop.
|
||||
return methodParameters.Length == conventionMethodParameters.Length;
|
||||
}
|
||||
}
|
||||
|
||||
internal static ApiConventionNameMatchBehavior GetNameMatchBehavior(ICustomAttributeProvider attributeProvider)
|
||||
{
|
||||
var attribute = GetCustomAttribute<ApiConventionNameMatchAttribute>(attributeProvider);
|
||||
return attribute?.MatchBehavior ?? ApiConventionNameMatchBehavior.Exact;
|
||||
}
|
||||
|
||||
internal static ApiConventionTypeMatchBehavior GetTypeMatchBehavior(ICustomAttributeProvider attributeProvider)
|
||||
{
|
||||
var attribute = GetCustomAttribute<ApiConventionTypeMatchAttribute>(attributeProvider);
|
||||
return attribute?.MatchBehavior ?? ApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
}
|
||||
|
||||
private static TAttribute GetCustomAttribute<TAttribute>(ICustomAttributeProvider attributeProvider)
|
||||
{
|
||||
var attributes = attributeProvider.GetCustomAttributes(inherit: false);
|
||||
for (var i = 0; i < attributes.Length; i++)
|
||||
{
|
||||
if (attributes[i] is TAttribute attribute)
|
||||
{
|
||||
return attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
internal static bool IsNameMatch(string name, string conventionName, ApiConventionNameMatchBehavior nameMatchBehavior)
|
||||
{
|
||||
switch (nameMatchBehavior)
|
||||
{
|
||||
case ApiConventionNameMatchBehavior.Any:
|
||||
return true;
|
||||
|
||||
case ApiConventionNameMatchBehavior.Exact:
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
|
||||
case ApiConventionNameMatchBehavior.Prefix:
|
||||
return IsNameMatchPrefix();
|
||||
|
||||
case ApiConventionNameMatchBehavior.Suffix:
|
||||
return IsNameMatchSuffix();
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsNameMatchPrefix()
|
||||
{
|
||||
if (name.Length < conventionName.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Length == conventionName.Length)
|
||||
{
|
||||
// name = "Post", conventionName = "Post"
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (!name.StartsWith(conventionName, StringComparison.Ordinal))
|
||||
{
|
||||
// name = "GetPerson", conventionName = "Post"
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for name = "PostPerson", conventionName = "Post"
|
||||
// Verify the first letter after the convention name is upper case. In this case 'P' from "Person"
|
||||
return char.IsUpper(name[conventionName.Length]);
|
||||
}
|
||||
|
||||
bool IsNameMatchSuffix()
|
||||
{
|
||||
if (name.Length < conventionName.Length)
|
||||
{
|
||||
// name = "person", conventionName = "personName"
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Length == conventionName.Length)
|
||||
{
|
||||
// name = id, conventionName = id
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// Check for name = personName, conventionName = name
|
||||
var index = name.Length - conventionName.Length - 1;
|
||||
if (!char.IsLower(name[index]))
|
||||
{
|
||||
// Verify letter before "name" is lowercase. In this case the letter 'n' at the end of "person"
|
||||
return false;
|
||||
}
|
||||
|
||||
index++;
|
||||
if (name[index] != char.ToUpper(conventionName[0]))
|
||||
{
|
||||
// Verify the first letter from convention is upper case. In this case 'n' from "name"
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match the remaining letters with exact case. i.e. match "ame" from "personName", "name"
|
||||
index++;
|
||||
return string.Compare(name, index, conventionName, 1, conventionName.Length - 1, StringComparison.Ordinal) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsTypeMatch(Type type, Type conventionType, ApiConventionTypeMatchBehavior typeMatchBehavior)
|
||||
{
|
||||
switch (typeMatchBehavior)
|
||||
{
|
||||
case ApiConventionTypeMatchBehavior.Any:
|
||||
return true;
|
||||
|
||||
case ApiConventionTypeMatchBehavior.AssignableFrom:
|
||||
return conventionType.IsAssignableFrom(type);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,9 +46,9 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
|
||||
private static MethodInfo GetConventionMethod(MethodInfo method, Type conventionType)
|
||||
{
|
||||
foreach (var conventionMethod in conventionType.GetMethods())
|
||||
foreach (var conventionMethod in conventionType.GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
if (IsMatch(method, conventionMethod))
|
||||
if (ApiConventionMatcher.IsMatch(method, conventionMethod))
|
||||
{
|
||||
return conventionMethod;
|
||||
}
|
||||
|
|
@ -56,174 +56,5 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool IsMatch(MethodInfo methodInfo, MethodInfo conventionMethod)
|
||||
{
|
||||
return MethodMatches() && ParametersMatch();
|
||||
|
||||
bool MethodMatches()
|
||||
{
|
||||
var methodNameMatchBehavior = GetNameMatchBehavior(conventionMethod);
|
||||
if (!IsNameMatch(methodInfo.Name, conventionMethod.Name, methodNameMatchBehavior))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParametersMatch()
|
||||
{
|
||||
var methodParameters = methodInfo.GetParameters();
|
||||
var conventionMethodParameters = conventionMethod.GetParameters();
|
||||
|
||||
for (var i = 0; i < conventionMethodParameters.Length; i++)
|
||||
{
|
||||
var conventionParameter = conventionMethodParameters[i];
|
||||
if (conventionParameter.IsDefined(typeof(ParamArrayAttribute)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (methodParameters.Length <= i)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nameMatchBehavior = GetNameMatchBehavior(conventionParameter);
|
||||
var typeMatchBehavior = GetTypeMatchBehavior(conventionParameter);
|
||||
|
||||
if (!IsTypeMatch(methodParameters[i].ParameterType, conventionParameter.ParameterType, typeMatchBehavior) ||
|
||||
!IsNameMatch(methodParameters[i].Name, conventionParameter.Name, nameMatchBehavior))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure convention has at least as many parameters as the method. params convention argument are handled
|
||||
// inside the for loop.
|
||||
return methodParameters.Length == conventionMethodParameters.Length;
|
||||
}
|
||||
}
|
||||
|
||||
internal static ApiConventionNameMatchBehavior GetNameMatchBehavior(ICustomAttributeProvider attributeProvider)
|
||||
{
|
||||
var attribute = GetCustomAttribute<ApiConventionNameMatchAttribute>(attributeProvider);
|
||||
return attribute?.MatchBehavior ?? ApiConventionNameMatchBehavior.Exact;
|
||||
}
|
||||
|
||||
internal static ApiConventionTypeMatchBehavior GetTypeMatchBehavior(ICustomAttributeProvider attributeProvider)
|
||||
{
|
||||
var attribute = GetCustomAttribute<ApiConventionTypeMatchAttribute>(attributeProvider);
|
||||
return attribute?.MatchBehavior ?? ApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
}
|
||||
|
||||
private static TAttribute GetCustomAttribute<TAttribute>(ICustomAttributeProvider attributeProvider)
|
||||
{
|
||||
var attributes = attributeProvider.GetCustomAttributes(inherit: false);
|
||||
for (var i = 0; i < attributes.Length; i++)
|
||||
{
|
||||
if (attributes[i] is TAttribute attribute)
|
||||
{
|
||||
return attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
internal static bool IsNameMatch(string name, string conventionName, ApiConventionNameMatchBehavior nameMatchBehavior)
|
||||
{
|
||||
switch (nameMatchBehavior)
|
||||
{
|
||||
case ApiConventionNameMatchBehavior.Any:
|
||||
return true;
|
||||
|
||||
case ApiConventionNameMatchBehavior.Exact:
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
|
||||
case ApiConventionNameMatchBehavior.Prefix:
|
||||
return IsNameMatchPrefix();
|
||||
|
||||
case ApiConventionNameMatchBehavior.Suffix:
|
||||
return IsNameMatchSuffix();
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsNameMatchPrefix()
|
||||
{
|
||||
if (name.Length < conventionName.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Length == conventionName.Length)
|
||||
{
|
||||
// name = "Post", conventionName = "Post"
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
if (!name.StartsWith(conventionName, StringComparison.Ordinal))
|
||||
{
|
||||
// name = "GetPerson", conventionName = "Post"
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for name = "PostPerson", conventionName = "Post"
|
||||
// Verify the first letter after the convention name is upper case. In this case 'P' from "Person"
|
||||
return char.IsUpper(name[conventionName.Length]);
|
||||
}
|
||||
|
||||
bool IsNameMatchSuffix()
|
||||
{
|
||||
if (name.Length < conventionName.Length)
|
||||
{
|
||||
// name = "person", conventionName = "personName"
|
||||
return false;
|
||||
}
|
||||
|
||||
if (name.Length == conventionName.Length)
|
||||
{
|
||||
// name = id, conventionName = id
|
||||
return string.Equals(name, conventionName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// Check for name = personName, conventionName = name
|
||||
var index = name.Length - conventionName.Length - 1;
|
||||
if (!char.IsLower(name[index]))
|
||||
{
|
||||
// Verify letter before "name" is lowercase. In this case the letter 'n' at the end of "person"
|
||||
return false;
|
||||
}
|
||||
|
||||
index++;
|
||||
if (name[index] != char.ToUpper(conventionName[0]))
|
||||
{
|
||||
// Verify the first letter from convention is upper case. In this case 'n' from "name"
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match the remaining letters with exact case. i.e. match "ame" from "personName", "name"
|
||||
index++;
|
||||
return string.Compare(name, index, conventionName, 1, conventionName.Length - 1, StringComparison.Ordinal) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsTypeMatch(Type type, Type conventionType, ApiConventionTypeMatchBehavior typeMatchBehavior)
|
||||
{
|
||||
switch (typeMatchBehavior)
|
||||
{
|
||||
case ApiConventionTypeMatchBehavior.Any:
|
||||
return true;
|
||||
|
||||
case ApiConventionTypeMatchBehavior.AssignableFrom:
|
||||
return conventionType.IsAssignableFrom(type);
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,571 @@
|
|||
// 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.Reflection;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
{
|
||||
public class ApiConventionMatcherTest
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Method", "method")]
|
||||
[InlineData("Method", "ConventionMethod")]
|
||||
[InlineData("p", "model")]
|
||||
[InlineData("person", "model")]
|
||||
public void IsNameMatch_WithAny_AlwaysReturnsTrue(string name, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Any);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfNamesDifferInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Name";
|
||||
var conventionName = "name";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Name";
|
||||
var conventionName = "Different";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSubString()
|
||||
{
|
||||
// Arrange
|
||||
var name = "RegularName";
|
||||
var conventionName = "Regular";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSuperString()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Regular";
|
||||
var conventionName = "RegularName";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsTrue_IfExactMatch()
|
||||
{
|
||||
// Arrange
|
||||
var name = "parameterName";
|
||||
var conventionName = "parameterName";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsTrue_IfNamesAreExact()
|
||||
{
|
||||
// Arrange
|
||||
var name = "PostPerson";
|
||||
var conventionName = "PostPerson";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsTrue_IfNameIsProperPrefix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "PostPerson";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GetPerson";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesDifferInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GetPerson";
|
||||
var conventionName = "post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsNotProperPrfix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Postman";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GoPost";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "name";
|
||||
var conventionName = "diff";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnsFalse_IfNameIsNotSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "personId";
|
||||
var conventionName = "idx";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsExact()
|
||||
{
|
||||
// Arrange
|
||||
var name = "test";
|
||||
var conventionName = "test";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnFalse_IfNameDiffersInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "test";
|
||||
var conventionName = "Test";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsProperSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "personId";
|
||||
var conventionName = "id";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("candid", "id")]
|
||||
[InlineData("canDid", "id")]
|
||||
public void IsNameMatch_WithSuffix_ReturnFalse_IfNameIsNotProperSuffix(string name, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(object), typeof(object))]
|
||||
[InlineData(typeof(int), typeof(void))]
|
||||
[InlineData(typeof(string), typeof(DateTime))]
|
||||
public void IsTypeMatch_WithAny_ReturnsTrue(Type type, Type conventionType)
|
||||
{
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.Any);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssignableFrom_ReturnsTrueForExact()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Base);
|
||||
var conventionType = typeof(Base);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssignableFrom_ReturnsTrueForDerived()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Derived);
|
||||
var conventionType = typeof(Base);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssignableFrom_ReturnsFalseForBaseTypes()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Base);
|
||||
var conventionType = typeof(Derived);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssignableFrom_ReturnsFalseForUnrelated()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(string);
|
||||
var conventionType = typeof(Derived);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfMethodNamesDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Post));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IMethodHasMoreParametersThanConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetNoArgs));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfMethodHasFewerParametersThanConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetTwoArgs));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfParametersDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetParameterNotMatching));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsTrue_IfMethodNameAndParametersMatchs()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Get));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsTrue_IfParamsArrayMatchesRemainingArguments()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Search));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Search));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_WithEmpty_MatchesMethodWithNoParameters()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.SearchEmpty));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.SearchWithParams));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNameMatchBehavior_ReturnsExact_WhenNoAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionNameMatchBehavior.Exact;
|
||||
var attributes = new object[0];
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.GetNameMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNameMatchBehavior_ReturnsExact_WhenNoNameMatchBehaviorAttributeIsSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionNameMatchBehavior.Exact;
|
||||
var attributes = new object[] { new CLSCompliantAttribute(false), new ProducesResponseTypeAttribute(200) };
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.GetNameMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNameMatchBehavior_ReturnsValueFromAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionNameMatchBehavior.Prefix;
|
||||
var attributes = new object[]
|
||||
{
|
||||
new CLSCompliantAttribute(false),
|
||||
new ApiConventionNameMatchAttribute(expected),
|
||||
new ProducesResponseTypeAttribute(200) }
|
||||
;
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.GetNameMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
var attributes = new object[0];
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.GetTypeMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoMatchingAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
var attributes = new object[] { new CLSCompliantAttribute(false), new ProducesResponseTypeAttribute(200) };
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.GetTypeMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTypeMatchBehavior_ReturnsValueFromAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionTypeMatchBehavior.Any;
|
||||
var attributes = new object[]
|
||||
{
|
||||
new CLSCompliantAttribute(false),
|
||||
new ApiConventionTypeMatchAttribute(expected),
|
||||
new ProducesResponseTypeAttribute(200) }
|
||||
;
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionMatcher.GetTypeMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
public class Base { }
|
||||
|
||||
public class Derived : Base { }
|
||||
|
||||
public class TestController
|
||||
{
|
||||
public IActionResult Get(int id) => null;
|
||||
|
||||
public IActionResult Search(string searchTerm, bool sortDescending, int page) => null;
|
||||
|
||||
public IActionResult SearchEmpty() => null;
|
||||
}
|
||||
|
||||
public static class TestConvention
|
||||
{
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Get(int id) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void GetNoArgs() { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void GetTwoArgs(int id, string name) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Post(Derived model) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void GetParameterNotMatching([ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.AssignableFrom)] Derived model) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void Search(
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Exact)]
|
||||
string searchTerm,
|
||||
params object[] others)
|
||||
{ }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void SearchWithParams(params object[] others) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
||||
|
|
@ -196,563 +194,5 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
|
|||
}
|
||||
|
||||
public class User { }
|
||||
|
||||
[Theory]
|
||||
[InlineData("Method", "method")]
|
||||
[InlineData("Method", "ConventionMethod")]
|
||||
[InlineData("p", "model")]
|
||||
[InlineData("person", "model")]
|
||||
public void IsNameMatch_WithAny_AlwaysReturnsTrue(string name, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Any);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfNamesDifferInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Name";
|
||||
var conventionName = "name";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Name";
|
||||
var conventionName = "Different";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSubString()
|
||||
{
|
||||
// Arrange
|
||||
var name = "RegularName";
|
||||
var conventionName = "Regular";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSuperString()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Regular";
|
||||
var conventionName = "RegularName";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsTrue_IfExactMatch()
|
||||
{
|
||||
// Arrange
|
||||
var name = "parameterName";
|
||||
var conventionName = "parameterName";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsTrue_IfNamesAreExact()
|
||||
{
|
||||
// Arrange
|
||||
var name = "PostPerson";
|
||||
var conventionName = "PostPerson";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsTrue_IfNameIsProperPrefix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "PostPerson";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GetPerson";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesDifferInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GetPerson";
|
||||
var conventionName = "post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsNotProperPrfix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Postman";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GoPost";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "name";
|
||||
var conventionName = "diff";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnsFalse_IfNameIsNotSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "personId";
|
||||
var conventionName = "idx";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsExact()
|
||||
{
|
||||
// Arrange
|
||||
var name = "test";
|
||||
var conventionName = "test";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnFalse_IfNameDiffersInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "test";
|
||||
var conventionName = "Test";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsProperSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "personId";
|
||||
var conventionName = "id";
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("candid", "id")]
|
||||
[InlineData("canDid", "id")]
|
||||
public void IsNameMatch_WithSuffix_ReturnFalse_IfNameIsNotProperSuffix(string name, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = ApiConventionResult.IsNameMatch(name, conventionName, ApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(object), typeof(object))]
|
||||
[InlineData(typeof(int), typeof(void))]
|
||||
[InlineData(typeof(string), typeof(DateTime))]
|
||||
public void IsTypeMatch_WithAny_ReturnsTrue(Type type, Type conventionType)
|
||||
{
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.Any);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssinableFrom_ReturnsTrueForExact()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Base);
|
||||
var conventionType = typeof(Base);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssinableFrom_ReturnsTrueForDerived()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Derived);
|
||||
var conventionType = typeof(Base);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssinableFrom_ReturnsFalseForBaseTypes()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(Base);
|
||||
var conventionType = typeof(Derived);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsTypeMatch_WithAssinableFrom_ReturnsFalseForUnrelated()
|
||||
{
|
||||
// Arrange
|
||||
var type = typeof(string);
|
||||
var conventionType = typeof(Derived);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsTypeMatch(type, conventionType, ApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfMethodNamesDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Post));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IMethodHasMoreParametersThanConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetNoArgs));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfMethodHasFewerParametersThanConvention()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetTwoArgs));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsFalse_IfParametersDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.GetParameterNotMatching));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsTrue_IfMethodNameAndParametersMatchs()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Get));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Get));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_ReturnsTrue_IfParamsArrayMatchesRemainingArguments()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.Search));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.Search));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMatch_WithEmpty_MatchesMethodWithNoParameters()
|
||||
{
|
||||
// Arrange
|
||||
var method = typeof(TestController).GetMethod(nameof(TestController.SearchEmpty));
|
||||
var conventionMethod = typeof(TestConvention).GetMethod(nameof(TestConvention.SearchWithParams));
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.IsMatch(method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNameMatchBehavior_ReturnsExact_WhenNoAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionNameMatchBehavior.Exact;
|
||||
var attributes = new object[0];
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetNameMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNameMatchBehavior_ReturnsExact_WhenNoNameMatchBehaviorAttributeIsSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionNameMatchBehavior.Exact;
|
||||
var attributes = new object[] { new CLSCompliantAttribute(false), new ProducesResponseTypeAttribute(200) };
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetNameMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetNameMatchBehavior_ReturnsValueFromAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionNameMatchBehavior.Prefix;
|
||||
var attributes = new object[]
|
||||
{
|
||||
new CLSCompliantAttribute(false),
|
||||
new ApiConventionNameMatchAttribute(expected),
|
||||
new ProducesResponseTypeAttribute(200) }
|
||||
;
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetNameMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
var attributes = new object[0];
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetTypeMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoMatchingAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
var attributes = new object[] { new CLSCompliantAttribute(false), new ProducesResponseTypeAttribute(200) };
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetTypeMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetTypeMatchBehavior_ReturnsValueFromAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = ApiConventionTypeMatchBehavior.Any;
|
||||
var attributes = new object[]
|
||||
{
|
||||
new CLSCompliantAttribute(false),
|
||||
new ApiConventionTypeMatchAttribute(expected),
|
||||
new ProducesResponseTypeAttribute(200) }
|
||||
;
|
||||
var provider = Mock.Of<ICustomAttributeProvider>(p => p.GetCustomAttributes(false) == attributes);
|
||||
|
||||
// Act
|
||||
var result = ApiConventionResult.GetTypeMatchBehavior(provider);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
public class Base { }
|
||||
|
||||
public class Derived : Base { }
|
||||
|
||||
public class TestController
|
||||
{
|
||||
public IActionResult Get(int id) => null;
|
||||
|
||||
public IActionResult Search(string searchTerm, bool sortDescending, int page) => null;
|
||||
|
||||
public IActionResult SearchEmpty() => null;
|
||||
}
|
||||
|
||||
public static class TestConvention
|
||||
{
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Get(int id) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void GetNoArgs() { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void GetTwoArgs(int id, string name) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Post(Derived model) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void GetParameterNotMatching([ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.AssignableFrom)] Derived model) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void Search(
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Exact)]
|
||||
string searchTerm,
|
||||
params object[] others)
|
||||
{ }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void SearchWithParams(params object[] others) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,166 @@
|
|||
// 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 ApiConventionAnalyzerIntegrationTest
|
||||
{
|
||||
private MvcDiagnosticAnalyzerRunner Executor { get; } = new MvcDiagnosticAnalyzerRunner(new ApiConventionAnalyzer());
|
||||
|
||||
[Fact]
|
||||
public Task NoDiagnosticsAreReturned_ForNonApiController()
|
||||
=> RunNoDiagnosticsAreReturned();
|
||||
|
||||
[Fact]
|
||||
public Task NoDiagnosticsAreReturned_ForRazorPageModels()
|
||||
=> RunNoDiagnosticsAreReturned();
|
||||
|
||||
[Fact]
|
||||
public Task NoDiagnosticsAreReturned_ForApiController_WithAllDocumentedStatusCodes()
|
||||
=> RunNoDiagnosticsAreReturned();
|
||||
|
||||
[Fact]
|
||||
public Task NoDiagnosticsAreReturned_ForApiController_IfStatusCodesCannotBeInferred()
|
||||
=> RunNoDiagnosticsAreReturned();
|
||||
|
||||
[Fact]
|
||||
public Task NoDiagnosticsAreReturned_ForReturnStatementsInLambdas()
|
||||
=> RunNoDiagnosticsAreReturned();
|
||||
|
||||
[Fact]
|
||||
public Task NoDiagnosticsAreReturned_ForReturnStatementsInLocalFunctions()
|
||||
=> RunNoDiagnosticsAreReturned();
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfMethodWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode()
|
||||
=> RunTest(DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode, 404);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfAsyncMethodWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode()
|
||||
=> RunTest(DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode, 404);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfAsyncMethodReturningValueTaskWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode()
|
||||
=> RunTest(DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode, 200);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutAnyAttributes()
|
||||
=> RunTest(DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode, 404);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutSomeAttributes()
|
||||
=> RunTest(DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode, 422);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCode()
|
||||
=> RunTest(DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode, 400);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfMethodWithAttributeReturnsValue_WithoutDocumentation()
|
||||
=> RunTest(DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfMethodWithAttributeAsynchronouslyReturnsValue_WithoutDocumentation()
|
||||
=> RunTest(DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfMethodWithAttribute_ReturnsDerivedType()
|
||||
=> RunTest(DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfMethodWithProducesResponseTypeAttribute_DoesNotReturnDocumentedStatusCode()
|
||||
=> RunTestFor1006(400);
|
||||
|
||||
[Fact]
|
||||
public Task DiagnosticsAreReturned_IfMethodWithConvention_DoesNotReturnDocumentedStatusCode()
|
||||
=> RunTestFor1006(404);
|
||||
|
||||
private async Task RunNoDiagnosticsAreReturned([CallerMemberName] string testMethod = "")
|
||||
{
|
||||
// Arrange
|
||||
var testSource = MvcTestSource.Read(GetType().Name, testMethod);
|
||||
var expectedLocation = testSource.DefaultMarkerLocation;
|
||||
|
||||
// Act
|
||||
var result = await Executor.GetDiagnosticsAsync(testSource.Source);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
private Task RunTest(DiagnosticDescriptor descriptor, [CallerMemberName] string testMethod = "")
|
||||
=> RunTest(descriptor, Array.Empty<object>(), testMethod);
|
||||
|
||||
private Task RunTest(DiagnosticDescriptor descriptor, int statusCode, [CallerMemberName] string testMethod = "")
|
||||
=> RunTest(descriptor, new[] { statusCode.ToString() }, testMethod);
|
||||
|
||||
private async Task RunTest(DiagnosticDescriptor descriptor, object[] args, [CallerMemberName] string testMethod = "")
|
||||
{
|
||||
// Arrange
|
||||
var testSource = MvcTestSource.Read(GetType().Name, testMethod);
|
||||
var expectedLocation = testSource.DefaultMarkerLocation;
|
||||
|
||||
// Act
|
||||
var result = await Executor.GetDiagnosticsAsync(testSource.Source);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
result,
|
||||
diagnostic =>
|
||||
{
|
||||
Assert.Equal(descriptor.Id, diagnostic.Id);
|
||||
Assert.Same(descriptor, diagnostic.Descriptor);
|
||||
AnalyzerAssert.DiagnosticLocation(expectedLocation, diagnostic.Location);
|
||||
Assert.Equal(string.Format(descriptor.MessageFormat.ToString(), args), diagnostic.GetMessage());
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RunTestFor1006(int statusCode, [CallerMemberName] string testMethod = "")
|
||||
{
|
||||
// Arrange
|
||||
var descriptor = DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode;
|
||||
var testSource = MvcTestSource.Read(GetType().Name, testMethod);
|
||||
var expectedLocation = testSource.DefaultMarkerLocation;
|
||||
var executor = new ApiCoventionWith1006DiagnosticEnabledRunner();
|
||||
|
||||
// Act
|
||||
var result = await executor.GetDiagnosticsAsync(testSource.Source);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
result,
|
||||
diagnostic =>
|
||||
{
|
||||
Assert.Equal(descriptor.Id, diagnostic.Id);
|
||||
Assert.Same(descriptor, diagnostic.Descriptor);
|
||||
AnalyzerAssert.DiagnosticLocation(expectedLocation, diagnostic.Location);
|
||||
Assert.Equal(string.Format(descriptor.MessageFormat.ToString(), new[] { statusCode.ToString() }), diagnostic.GetMessage());
|
||||
});
|
||||
}
|
||||
|
||||
private class ApiCoventionWith1006DiagnosticEnabledRunner : MvcDiagnosticAnalyzerRunner
|
||||
{
|
||||
public ApiCoventionWith1006DiagnosticEnabledRunner() : base(new ApiConventionAnalyzer())
|
||||
{
|
||||
}
|
||||
|
||||
protected override CompilationOptions ConfigureCompilationOptions(CompilationOptions options)
|
||||
{
|
||||
var compilationOptions = base.ConfigureCompilationOptions(options);
|
||||
var specificDiagnosticOptions = compilationOptions.SpecificDiagnosticOptions.Add(
|
||||
DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode.Id,
|
||||
ReportDiagnostic.Info);
|
||||
|
||||
return compilationOptions.WithSpecificDiagnosticOptions(specificDiagnosticOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
// 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.Tasks;
|
||||
using Microsoft.AspNetCore.Analyzer.Testing;
|
||||
using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Xunit;
|
||||
using static Microsoft.AspNetCore.Mvc.Analyzers.ApiConventionAnalyzer;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
public class ApiConventionAnalyzerTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingStatusCodeConstants()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingStatusCodesConstants).FullName).GetAttributes()[0];
|
||||
|
||||
// Act
|
||||
var actual = ApiConventionAnalyzer.GetDefaultStatusCode(attribute);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(412, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingHttpStatusCast()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingHttpStatusCodeCast).FullName).GetAttributes()[0];
|
||||
|
||||
// Act
|
||||
var actual = ApiConventionAnalyzer.GetDefaultStatusCode(attribute);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(302, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsNull_ForReturnTypeIf200StatusCodeIsDeclared()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
|
||||
var returnType = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerBaseModel).FullName);
|
||||
var context = GetContext(compilation, new[] { 200 });
|
||||
|
||||
// Act
|
||||
var diagnostic = ApiConventionAnalyzer.InspectReturnExpression(context, returnType, Location.None);
|
||||
|
||||
// Assert
|
||||
Assert.Null(diagnostic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsNull_ForReturnTypeIf201StatusCodeIsDeclared()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
|
||||
var returnType = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerBaseModel).FullName);
|
||||
var context = GetContext(compilation, new[] { 201 });
|
||||
|
||||
// Act
|
||||
var diagnostic = ApiConventionAnalyzer.InspectReturnExpression(context, returnType, Location.None);
|
||||
|
||||
// Assert
|
||||
Assert.Null(diagnostic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsNull_ForDerivedReturnTypeIf200StatusCodeIsDeclared()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
|
||||
var declaredReturnType = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerBaseModel).FullName);
|
||||
var context = GetContext(compilation, new[] { 201 });
|
||||
var actualReturnType = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerDerivedModel).FullName);
|
||||
|
||||
// Act
|
||||
var diagnostic = ApiConventionAnalyzer.InspectReturnExpression(context, actualReturnType, Location.None);
|
||||
|
||||
// Assert
|
||||
Assert.Null(diagnostic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsDiagnostic_If200IsNotDocumented()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
|
||||
var context = GetContext(compilation, new[] { 404 });
|
||||
var actualReturnType = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerDerivedModel).FullName);
|
||||
|
||||
// Act
|
||||
var diagnostic = ApiConventionAnalyzer.InspectReturnExpression(context, actualReturnType, Location.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(diagnostic);
|
||||
Assert.Same(DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult, diagnostic.Descriptor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_ReturnsDiagnostic_IfReturnTypeIsActionResultReturningUndocumentedStatusCode()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
|
||||
var declaredReturnType = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerBaseModel).FullName);
|
||||
var context = GetContext(compilation, new[] { 200, 404 });
|
||||
var actualReturnType = compilation.GetTypeByMetadataName(typeof(BadRequestObjectResult).FullName);
|
||||
|
||||
// Act
|
||||
var diagnostic = ApiConventionAnalyzer.InspectReturnExpression(context, actualReturnType, Location.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(diagnostic);
|
||||
Assert.Same(DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode, diagnostic.Descriptor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_DoesNotReturnDiagnostic_IfReturnTypeDoesNotHaveStatusCodeAttribute()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
|
||||
var context = GetContext(compilation, new[] { 200, 404 });
|
||||
var actualReturnType = compilation.GetTypeByMetadataName(typeof(EmptyResult).FullName);
|
||||
|
||||
// Act
|
||||
var diagnostic = ApiConventionAnalyzer.InspectReturnExpression(context, actualReturnType, Location.None);
|
||||
|
||||
// Assert
|
||||
Assert.Null(diagnostic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_DoesNotReturnDiagnostic_IfDeclaredAndActualReturnTypeAreIActionResultInstances()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
|
||||
var declaredReturnType = compilation.GetTypeByMetadataName(typeof(IActionResult).FullName);
|
||||
var context = GetContext(compilation, new[] { 404 });
|
||||
var actualReturnType = compilation.GetTypeByMetadataName(typeof(EmptyResult).FullName);
|
||||
|
||||
// Act
|
||||
var diagnostic = ApiConventionAnalyzer.InspectReturnExpression(context, actualReturnType, Location.None);
|
||||
|
||||
// Assert
|
||||
Assert.Null(diagnostic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InspectReturnExpression_DoesNotReturnDiagnostic_IfDeclaredAndActualReturnTypeAreIActionResult()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
|
||||
var context = GetContext(compilation, new[] { 404 });
|
||||
var actualReturnType = compilation.GetTypeByMetadataName(typeof(IActionResult).FullName);
|
||||
|
||||
// Act
|
||||
var diagnostic = ApiConventionAnalyzer.InspectReturnExpression(context, actualReturnType, Location.None);
|
||||
|
||||
// Assert
|
||||
Assert.Null(diagnostic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShouldEvaluateMethod_ReturnsFalse_IfMethodReturnTypeIsInvalid()
|
||||
{
|
||||
// Arrange
|
||||
var source = @"
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace TestNamespace
|
||||
{
|
||||
[ApiController]
|
||||
public class TestController : ControllerBase
|
||||
{
|
||||
public DoesNotExist Get(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return new DoesNotExist(id);
|
||||
}
|
||||
}
|
||||
}";
|
||||
var project = DiagnosticProject.Create(GetType().Assembly, new[] { source });
|
||||
var compilation = await project.GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
var method = (IMethodSymbol)compilation.GetTypeByMetadataName("TestNamespace.TestController").GetMembers("Get").First();
|
||||
|
||||
// Act
|
||||
var result = ApiConventionAnalyzer.ShouldEvaluateMethod(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShouldEvaluateMethod_ReturnsFalse_IfContainingTypeIsNotController()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
var type = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_IndexModel).FullName);
|
||||
var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_IndexModel.OnGet)).First();
|
||||
|
||||
// Act
|
||||
var result = ApiConventionAnalyzer.ShouldEvaluateMethod(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShouldEvaluateMethod_ReturnsFalse_IfContainingTypeIsNotApiController()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
var type = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_NotApiController).FullName);
|
||||
var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_NotApiController.Index)).First();
|
||||
|
||||
// Act
|
||||
var result = ApiConventionAnalyzer.ShouldEvaluateMethod(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShouldEvaluateMethod_ReturnsFalse_IfContainingTypeIsNotAction()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
var type = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_NotAction).FullName);
|
||||
var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_NotAction.Index)).First();
|
||||
|
||||
// Act
|
||||
var result = ApiConventionAnalyzer.ShouldEvaluateMethod(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ShouldEvaluateMethod_ReturnsTrue_ForValidActionMethods()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
var type = compilation.GetTypeByMetadataName(typeof(ApiConventionAnalyzerTest_Valid).FullName);
|
||||
var method = (IMethodSymbol)type.GetMembers(nameof(ApiConventionAnalyzerTest_Valid.Index)).First();
|
||||
|
||||
// Act
|
||||
var result = ApiConventionAnalyzer.ShouldEvaluateMethod(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
private static ApiConventionContext GetContext(Compilation compilation, int[] expectedStatusCodes)
|
||||
{
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
var context = new ApiConventionContext(
|
||||
symbolCache,
|
||||
default,
|
||||
expectedStatusCodes.Select(s => new ApiResponseMetadata(s, null, null)).ToArray(),
|
||||
new HashSet<int>());
|
||||
return context;
|
||||
}
|
||||
|
||||
private Task<Compilation> GetCompilation()
|
||||
{
|
||||
var testSource = MvcTestSource.Read(GetType().Name, "ApiConventionAnalyzerTestFile");
|
||||
var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source });
|
||||
|
||||
return project.GetCompilationAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,24 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
attributeData => Assert.Equal(400, attributeData.ConstructorArguments[0].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAttributesSymbolOverload_OnMethodSymbol()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation("GetAttributes_WithMethodOverridding");
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(ProducesResponseTypeAttribute).FullName);
|
||||
var testClass = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentActionClass)}");
|
||||
var method = (IMethodSymbol)testClass.GetMembers(nameof(GetAttributes_WithInheritFalse_ReturnsAllAttributesOnCurrentActionClass.Method)).First();
|
||||
|
||||
// Act
|
||||
var attributes = CodeAnalysisExtensions.GetAttributes(symbol: method, attribute: attribute);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
attributes,
|
||||
attributeData => Assert.Equal(400, attributeData.ConstructorArguments[0].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAttributes_WithInheritTrue_ReturnsAllAttributesOnCurrentActionAndOverridingMethod()
|
||||
{
|
||||
|
|
@ -123,6 +141,120 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
attributeData => Assert.Equal(401, attributeData.ConstructorArguments[0].Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAttributes_OnTypeWithoutAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(ApiConventionTypeAttribute).FullName);
|
||||
var testClass = compilation.GetTypeByMetadataName(typeof(GetAttributes_OnTypeWithoutAttributesType).FullName);
|
||||
|
||||
// Act
|
||||
var attributes = CodeAnalysisExtensions.GetAttributes(testClass, attribute, inherit: true);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(attributes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAttributes_OnTypeWithAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(ApiConventionTypeAttribute).FullName);
|
||||
var testClass = compilation.GetTypeByMetadataName(typeof(GetAttributes_OnTypeWithAttributes).FullName);
|
||||
|
||||
// Act
|
||||
var attributes = CodeAnalysisExtensions.GetAttributes(testClass, attribute, inherit: true);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
attributes,
|
||||
attributeData =>
|
||||
{
|
||||
Assert.Same(attribute, attributeData.AttributeClass);
|
||||
Assert.Equal(attributeData.ConstructorArguments[0].Value, compilation.GetSpecialType(SpecialType.System_Object));
|
||||
},
|
||||
attributeData =>
|
||||
{
|
||||
Assert.Same(attribute, attributeData.AttributeClass);
|
||||
Assert.Equal(attributeData.ConstructorArguments[0].Value, compilation.GetSpecialType(SpecialType.System_String));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAttributes_BaseTypeWithAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation();
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(ApiConventionTypeAttribute).FullName);
|
||||
var testClass = compilation.GetTypeByMetadataName(typeof(GetAttributes_BaseTypeWithAttributesDerived).FullName);
|
||||
|
||||
// Act
|
||||
var attributes = CodeAnalysisExtensions.GetAttributes(testClass, attribute, inherit: true);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
attributes,
|
||||
attributeData =>
|
||||
{
|
||||
Assert.Same(attribute, attributeData.AttributeClass);
|
||||
Assert.Equal(attributeData.ConstructorArguments[0].Value, compilation.GetSpecialType(SpecialType.System_Int32));
|
||||
},
|
||||
attributeData =>
|
||||
{
|
||||
Assert.Same(attribute, attributeData.AttributeClass);
|
||||
Assert.Equal(attributeData.ConstructorArguments[0].Value, compilation.GetSpecialType(SpecialType.System_Object));
|
||||
},
|
||||
attributeData =>
|
||||
{
|
||||
Assert.Same(attribute, attributeData.AttributeClass);
|
||||
Assert.Equal(attributeData.ConstructorArguments[0].Value, compilation.GetSpecialType(SpecialType.System_String));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAttributes_OnDerivedTypeWithInheritFalse()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation(nameof(GetAttributes_BaseTypeWithAttributes));
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(ApiConventionTypeAttribute).FullName);
|
||||
var testClass = compilation.GetTypeByMetadataName(typeof(GetAttributes_BaseTypeWithAttributesDerived).FullName);
|
||||
|
||||
// Act
|
||||
var attributes = CodeAnalysisExtensions.GetAttributes(testClass, attribute, inherit: false);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
attributes,
|
||||
attributeData =>
|
||||
{
|
||||
Assert.Same(attribute, attributeData.AttributeClass);
|
||||
Assert.Equal(attributeData.ConstructorArguments[0].Value, compilation.GetSpecialType(SpecialType.System_Int32));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAttributesSymbolOverload_OnTypeSymbol()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation(nameof(GetAttributes_BaseTypeWithAttributes));
|
||||
var attribute = compilation.GetTypeByMetadataName(typeof(ApiConventionTypeAttribute).FullName);
|
||||
var testClass = compilation.GetTypeByMetadataName(typeof(GetAttributes_BaseTypeWithAttributesDerived).FullName);
|
||||
|
||||
// Act
|
||||
var attributes = CodeAnalysisExtensions.GetAttributes(symbol: testClass, attribute: attribute);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
attributes,
|
||||
attributeData =>
|
||||
{
|
||||
Assert.Same(attribute, attributeData.AttributeClass);
|
||||
Assert.Equal(attributeData.ConstructorArguments[0].Value, compilation.GetSpecialType(SpecialType.System_Int32));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasAttribute_ReturnsFalseIfSymbolDoesNotHaveAttribute()
|
||||
{
|
||||
|
|
@ -325,6 +457,21 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
Assert.False(isAssignableFromDerived); // Inverse shouldn't be true
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsAssignable_ReturnsTrue_IfSourceAndDestinationAreTheSameInterface()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilation(nameof(IsAssignable_ReturnsTrueIfTypeImplementsInterface));
|
||||
var source = compilation.GetTypeByMetadataName(typeof(IsAssignable_ReturnsTrueIfTypeImplementsInterface).FullName);
|
||||
var target = compilation.GetTypeByMetadataName(typeof(IsAssignable_ReturnsTrueIfTypeImplementsInterface).FullName);
|
||||
|
||||
// Act
|
||||
var isAssignableFrom = CodeAnalysisExtensions.IsAssignableFrom(source, target);
|
||||
|
||||
// Assert
|
||||
Assert.True(isAssignableFrom);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsAssignable_ReturnsTrueIfAncestorTypeImplementsInterface()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
<PropertyGroup>
|
||||
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
<RootNamespace>Microsoft.AspNetCore.Mvc.Analyzers</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,568 @@
|
|||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Analyzer.Testing;
|
||||
using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Xunit;
|
||||
using static Microsoft.AspNetCore.Mvc.Analyzers.SymbolApiConventionMatcher;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
public class SymbolApiConventionMatcherTest
|
||||
{
|
||||
private static readonly string BaseTypeName = typeof(Base).FullName;
|
||||
private static readonly string DerivedTypeName = typeof(Derived).FullName;
|
||||
private static readonly string TestControllerName = typeof(TestController).FullName;
|
||||
private static readonly string TestConventionName = typeof(TestConvention).FullName;
|
||||
|
||||
[Theory]
|
||||
[InlineData("Method", "method")]
|
||||
[InlineData("Method", "ConventionMethod")]
|
||||
[InlineData("p", "model")]
|
||||
[InlineData("person", "model")]
|
||||
public void IsNameMatch_WithAny_AlwaysReturnsTrue(string name, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Any);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfNamesDifferInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Name";
|
||||
var conventionName = "name";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Name";
|
||||
var conventionName = "Different";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSubString()
|
||||
{
|
||||
// Arrange
|
||||
var name = "RegularName";
|
||||
var conventionName = "Regular";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsFalse_IfConventionNameIsSuperString()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Regular";
|
||||
var conventionName = "RegularName";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithExact_ReturnsTrue_IfExactMatch()
|
||||
{
|
||||
// Arrange
|
||||
var name = "parameterName";
|
||||
var conventionName = "parameterName";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Exact);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsTrue_IfNamesAreExact()
|
||||
{
|
||||
// Arrange
|
||||
var name = "PostPerson";
|
||||
var conventionName = "PostPerson";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsTrue_IfNameIsProperPrefix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "PostPerson";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GetPerson";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNamesDifferInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GetPerson";
|
||||
var conventionName = "post";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsNotProperPrfix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "Postman";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithPrefix_ReturnsFalse_IfNameIsSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "GoPost";
|
||||
var conventionName = "Post";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Prefix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnsFalse_IfNamesAreDifferent()
|
||||
{
|
||||
// Arrange
|
||||
var name = "name";
|
||||
var conventionName = "diff";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnsFalse_IfNameIsNotSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "personId";
|
||||
var conventionName = "idx";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsExact()
|
||||
{
|
||||
// Arrange
|
||||
var name = "test";
|
||||
var conventionName = "test";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnFalse_IfNameDiffersInCase()
|
||||
{
|
||||
// Arrange
|
||||
var name = "test";
|
||||
var conventionName = "Test";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNameMatch_WithSuffix_ReturnTrue_IfNameIsProperSuffix()
|
||||
{
|
||||
// Arrange
|
||||
var name = "personId";
|
||||
var conventionName = "id";
|
||||
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("candid", "id")]
|
||||
[InlineData("canDid", "id")]
|
||||
public void IsNameMatch_WithSuffix_ReturnFalse_IfNameIsNotProperSuffix(string name, string conventionName)
|
||||
{
|
||||
// Act
|
||||
var result = IsNameMatch(name, conventionName, SymbolApiConventionNameMatchBehavior.Suffix);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(object), typeof(object))]
|
||||
[InlineData(typeof(int), typeof(void))]
|
||||
[InlineData(typeof(string), typeof(DateTime))]
|
||||
public async Task IsTypeMatch_WithAny_ReturnsTrue(Type type, Type conventionType)
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilationAsync();
|
||||
var typeSymbol = compilation.GetTypeByMetadataName(type.FullName);
|
||||
var conventionTypeSymbol = compilation.GetTypeByMetadataName(conventionType.FullName);
|
||||
|
||||
// Act
|
||||
var result = IsTypeMatch(typeSymbol, conventionTypeSymbol, SymbolApiConventionTypeMatchBehavior.Any);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsTypeMatch_WithAssignableFrom_ReturnsTrueForExact()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilationAsync();
|
||||
|
||||
var type = compilation.GetTypeByMetadataName(BaseTypeName);
|
||||
var conventionType = compilation.GetTypeByMetadataName(BaseTypeName);
|
||||
|
||||
// Act
|
||||
var result = IsTypeMatch(type, conventionType, SymbolApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsTypeMatch_WithAssinableFrom_ReturnsTrueForDerived()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilationAsync();
|
||||
|
||||
var type = compilation.GetTypeByMetadataName(DerivedTypeName);
|
||||
var conventionType = compilation.GetTypeByMetadataName(BaseTypeName);
|
||||
|
||||
|
||||
// Act
|
||||
var result = IsTypeMatch(type, conventionType, SymbolApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsTypeMatch_WithAssinableFrom_ReturnsFalseForBaseTypes()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilationAsync();
|
||||
|
||||
var type = compilation.GetTypeByMetadataName(BaseTypeName);
|
||||
var conventionType = compilation.GetTypeByMetadataName(DerivedTypeName);
|
||||
|
||||
// Act
|
||||
var result = IsTypeMatch(type, conventionType, SymbolApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsTypeMatch_WithAssinableFrom_ReturnsFalseForUnrelated()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = await GetCompilationAsync();
|
||||
|
||||
var type = compilation.GetSpecialType(SpecialType.System_String);
|
||||
var conventionType = compilation.GetTypeByMetadataName(BaseTypeName);
|
||||
|
||||
// Act
|
||||
var result = IsTypeMatch(type, conventionType, SymbolApiConventionTypeMatchBehavior.AssignableFrom);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task IsMatch_ReturnsFalse_IfMethodNamesDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = nameof(TestController.Get);
|
||||
var conventionMethodName = nameof(TestConvention.Post);
|
||||
var expected = false;
|
||||
|
||||
return RunMatchTest(methodName, conventionMethodName, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task IsMatch_ReturnsFalse_IMethodHasMoreParametersThanConvention()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = nameof(TestController.Get);
|
||||
var conventionMethodName = nameof(TestConvention.GetNoArgs);
|
||||
var expected = false;
|
||||
|
||||
return RunMatchTest(methodName, conventionMethodName, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task IsMatch_ReturnsFalse_IfMethodHasFewerParametersThanConvention()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = nameof(TestController.Get);
|
||||
var conventionMethodName = nameof(TestConvention.GetTwoArgs);
|
||||
var expected = false;
|
||||
|
||||
return RunMatchTest(methodName, conventionMethodName, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task IsMatch_ReturnsFalse_IfParametersDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = nameof(TestController.Get);
|
||||
var conventionMethodName = nameof(TestConvention.GetParameterNotMatching);
|
||||
var expected = false;
|
||||
|
||||
return RunMatchTest(methodName, conventionMethodName, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task IsMatch_ReturnsTrue_IfMethodNameAndParametersMatchs()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = nameof(TestController.Get);
|
||||
var conventionMethodName = nameof(TestConvention.Get);
|
||||
var expected = true;
|
||||
|
||||
return RunMatchTest(methodName, conventionMethodName, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task IsMatch_ReturnsTrue_IfParamsArrayMatchesRemainingArguments()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = nameof(TestController.Search);
|
||||
var conventionMethodName = nameof(TestConvention.Search);
|
||||
var expected = true;
|
||||
|
||||
return RunMatchTest(methodName, conventionMethodName, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public Task IsMatch_WithEmpty_MatchesMethodWithNoParameters()
|
||||
{
|
||||
// Arrange
|
||||
var methodName = nameof(TestController.SearchEmpty);
|
||||
var conventionMethodName = nameof(TestConvention.SearchWithParams);
|
||||
var expected = true;
|
||||
|
||||
return RunMatchTest(methodName, conventionMethodName, expected);
|
||||
}
|
||||
|
||||
private async Task RunMatchTest(string methodName, string conventionMethodName, bool expected)
|
||||
{
|
||||
var compilation = await GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var testController = compilation.GetTypeByMetadataName(TestControllerName);
|
||||
var testConvention = compilation.GetTypeByMetadataName(TestConventionName);
|
||||
var method = (IMethodSymbol)testController.GetMembers(methodName).First();
|
||||
var conventionMethod = (IMethodSymbol)testConvention.GetMembers(conventionMethodName).First();
|
||||
|
||||
// Act
|
||||
var result = IsMatch(symbolCache, method, conventionMethod);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNameMatchBehavior_ReturnsExact_WhenNoAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = SymbolApiConventionNameMatchBehavior.Exact;
|
||||
var compilation = await GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var testConvention = compilation.GetTypeByMetadataName(TestConventionName);
|
||||
var method = testConvention.GetMembers(nameof(TestConvention.MethodWithoutMatchBehavior)).First();
|
||||
|
||||
// Act
|
||||
var result = GetNameMatchBehavior(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNameMatchBehavior_ReturnsExact_WhenNoNameMatchBehaviorAttributeIsSpecified()
|
||||
{
|
||||
// Arrange
|
||||
var expected = SymbolApiConventionNameMatchBehavior.Exact;
|
||||
var compilation = await GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var testConvention = compilation.GetTypeByMetadataName(TestConventionName);
|
||||
var method = testConvention.GetMembers(nameof(TestConvention.MethodWithRandomAttributes)).First();
|
||||
|
||||
// Act
|
||||
var result = GetNameMatchBehavior(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetNameMatchBehavior_ReturnsValueFromAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = SymbolApiConventionNameMatchBehavior.Prefix;
|
||||
var compilation = await GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var testConvention = compilation.GetTypeByMetadataName(TestConventionName);
|
||||
var method = testConvention.GetMembers(nameof(TestConvention.Get)).First();
|
||||
|
||||
// Act
|
||||
var result = GetNameMatchBehavior(symbolCache, method);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = SymbolApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
var compilation = await GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var testConvention = compilation.GetTypeByMetadataName(TestConventionName);
|
||||
var method = (IMethodSymbol)testConvention.GetMembers(nameof(TestConvention.Get)).First();
|
||||
var parameter = method.Parameters[0];
|
||||
|
||||
// Act
|
||||
var result = GetTypeMatchBehavior(symbolCache, parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTypeMatchBehavior_ReturnsIsAssignableFrom_WhenNoMatchingAttributesArePresent()
|
||||
{
|
||||
// Arrange
|
||||
var expected = SymbolApiConventionTypeMatchBehavior.AssignableFrom;
|
||||
var compilation = await GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var testConvention = compilation.GetTypeByMetadataName(TestConventionName);
|
||||
var method = (IMethodSymbol)testConvention.GetMembers(nameof(TestConvention.MethodParameterWithRandomAttributes)).First();
|
||||
var parameter = method.Parameters[0];
|
||||
|
||||
// Act
|
||||
var result = GetTypeMatchBehavior(symbolCache, parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetTypeMatchBehavior_ReturnsValueFromAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var expected = SymbolApiConventionTypeMatchBehavior.Any;
|
||||
var compilation = await GetCompilationAsync();
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
var testConvention = compilation.GetTypeByMetadataName(TestConventionName);
|
||||
var method = (IMethodSymbol)testConvention.GetMembers(nameof(TestConvention.MethodWithAnyTypeMatchBehaviorParameter)).First();
|
||||
var parameter = method.Parameters[0];
|
||||
|
||||
// Act
|
||||
var result = GetTypeMatchBehavior(symbolCache, parameter);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
private Task<Compilation> GetCompilationAsync(string test = "SymbolApiConventionMatcherTestFile")
|
||||
{
|
||||
var testSource = MvcTestSource.Read(GetType().Name, test);
|
||||
var project = DiagnosticProject.Create(GetType().Assembly, new[] { testSource.Source });
|
||||
|
||||
return project.GetCompilationAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Analyzer.Testing;
|
||||
|
|
@ -21,10 +22,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerWithoutConvention)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerWithoutConvention.GetPerson)).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
|
|
@ -37,10 +38,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerWithoutConvention)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerWithoutConvention.PostPerson)).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
|
|
@ -53,10 +54,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesAttribute)).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
|
|
@ -69,10 +70,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructor)).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
|
|
@ -92,10 +93,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructor)).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
|
|
@ -115,10 +116,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeInConstructorAndProperty)).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
|
|
@ -138,10 +139,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithProducesResponseType_StatusCodeAndTypeInConstructorAndProperty)).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
|
|
@ -161,10 +162,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomProducesResponseTypeAttributeWithArguments)).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
|
|
@ -184,10 +185,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{nameof(GetResponseMetadata_ControllerActionWithAttributes)}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(nameof(GetResponseMetadata_ControllerActionWithAttributes.ActionWithCustomApiResponseMetadataProvider)).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Empty(result);
|
||||
|
|
@ -215,10 +216,10 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
|
|||
var compilation = await GetResponseMetadataCompilation();
|
||||
var controller = compilation.GetTypeByMetadataName($"{Namespace}.{typeName}");
|
||||
var method = (IMethodSymbol)controller.GetMembers(methodName).First();
|
||||
var typeCache = new ApiControllerTypeCache(compilation);
|
||||
var symbolCache = new ApiControllerSymbolCache(compilation);
|
||||
|
||||
// Act
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(typeCache, method);
|
||||
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
|
||||
|
||||
// Assert
|
||||
Assert.Collection(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutAnyAttributes : ControllerBase
|
||||
{
|
||||
public ActionResult<string> Method(Guid? id)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
/*MM*/return NotFound();
|
||||
}
|
||||
|
||||
return "Hello world";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_ForActionResultOfTReturningMethodWithoutSomeAttributes : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(typeof(string), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
public IActionResult Put(int id, object model)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
/*MM*/return UnprocessableEntity();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Analyzers;
|
||||
|
||||
[assembly: ApiConventionType(typeof(DiagnosticsAreReturned_ForControllerWithCustomConvention))]
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_ForControllerWithCustomConventionController : ControllerBase
|
||||
{
|
||||
public async Task<IActionResult> Update(int id, Product product)
|
||||
{
|
||||
if (id < 0)
|
||||
{
|
||||
/*MM*/return BadRequest();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await product.Update();
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Conflict();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
public static class DiagnosticsAreReturned_ForControllerWithCustomConvention
|
||||
{
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
|
||||
public static void Update(int id, Product product)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class Product
|
||||
{
|
||||
public Task Update() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_IfAsyncMethodReturningValueTaskWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
public async ValueTask<IActionResult> Method(int id)
|
||||
{
|
||||
await Task.Yield();
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
/*MM*/return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_IfAsyncMethodWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(typeof(string), 200)]
|
||||
public async Task<IActionResult> Method(int id)
|
||||
{
|
||||
await Task.Yield();
|
||||
if (id == 0)
|
||||
{
|
||||
/*MM*/return NotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_IfMethodWithAttributeAsynchronouslyReturnsValue_WithoutDocumentation : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<ActionResult<DiagnosticsAreReturned_IfMethodWithAttributeAsynchronouslyReturnsValue_WithoutDocumentationModel>> Method(int id)
|
||||
{
|
||||
await Task.Yield();
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
/*MM*/return new DiagnosticsAreReturned_IfMethodWithAttributeAsynchronouslyReturnsValue_WithoutDocumentationModel();
|
||||
}
|
||||
}
|
||||
|
||||
public class DiagnosticsAreReturned_IfMethodWithAttributeAsynchronouslyReturnsValue_WithoutDocumentationModel { }
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_IfMethodWithAttributeReturnsValue_WithoutDocumentation : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(404)]
|
||||
public ActionResult<DiagnosticsAreReturned_IfMethodWithAttributeReturnsValue_WithoutDocumentationModel> Method(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
/*MM*/return new DiagnosticsAreReturned_IfMethodWithAttributeReturnsValue_WithoutDocumentationModel();
|
||||
}
|
||||
}
|
||||
|
||||
public class DiagnosticsAreReturned_IfMethodWithAttributeReturnsValue_WithoutDocumentationModel { }
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_IfMethodWithAttribute_ReturnsDerivedType : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(404)]
|
||||
public ActionResult<DiagnosticsAreReturned_IfMethodWithAttribute_ReturnsDerivedTypeBaseModel> Method(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
/*MM*/return new DiagnosticsAreReturned_IfMethodWithAttribute_ReturnsDerivedTypeDerived();
|
||||
}
|
||||
}
|
||||
|
||||
public class DiagnosticsAreReturned_IfMethodWithAttribute_ReturnsDerivedTypeBaseModel { }
|
||||
|
||||
public class DiagnosticsAreReturned_IfMethodWithAttribute_ReturnsDerivedTypeDerived : DiagnosticsAreReturned_IfMethodWithAttribute_ReturnsDerivedTypeBaseModel { }
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_IfMethodWithConvention_DoesNotReturnDocumentedStatusCode : ControllerBase
|
||||
{
|
||||
public IActionResult /*MM*/Delete(int id)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_IfMethodWithConvention_ReturnsUndocumentedStatusCode : ControllerBase
|
||||
{
|
||||
public IActionResult Get(int id)
|
||||
{
|
||||
if (id < 0)
|
||||
{
|
||||
/*MM*/return BadRequest();
|
||||
}
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_IfMethodWithProducesResponseTypeAttribute_DoesNotReturnDocumentedStatusCode : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(400)]
|
||||
[ProducesResponseType(404)]
|
||||
public IActionResult /*MM*/Method(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class DiagnosticsAreReturned_IfMethodWithProducesResponseTypeAttribute_ReturnsUndocumentedStatusCode : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(typeof(string), 200)]
|
||||
public IActionResult Method(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
/*MM*/return NotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class NoDiagnosticsAreReturned_ForApiController_IfStatusCodesCannotBeInferred : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(201)]
|
||||
public IActionResult Method(int id)
|
||||
{
|
||||
return id == 0 ? (IActionResult)NotFound() : Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class NoDiagnosticsAreReturned_ForApiController_WithAllDocumentedStatusCodes : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(typeof(string), 200)]
|
||||
[ProducesResponseType(typeof(string), 400)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
public IActionResult Put(int id, object model)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
public class NoDiagnosticsAreReturned_ForNonApiController : Controller
|
||||
{
|
||||
[ProducesResponseType(typeof(string), 200)]
|
||||
public IActionResult Method(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
public class Home : PageModel
|
||||
{
|
||||
[ProducesResponseType(302)]
|
||||
public IActionResult OnPost(int id)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Page();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
[assembly: ApiConventionType(typeof(DefaultApiConventions))]
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class NoDiagnosticsAreReturned_ForReturnStatementsInLambdas : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(typeof(string), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
public IActionResult Put(int id, object model)
|
||||
{
|
||||
Func<IActionResult> someLambda = () =>
|
||||
{
|
||||
if (id < -1)
|
||||
{
|
||||
// We should not process this.
|
||||
return UnprocessableEntity();
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
|
||||
if (id == 1)
|
||||
{
|
||||
return someLambda();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiController]
|
||||
public class NoDiagnosticsAreReturned_ForReturnStatementsInLocalFunctions : ControllerBase
|
||||
{
|
||||
[ProducesResponseType(typeof(string), 200)]
|
||||
[ProducesResponseType(typeof(string), 404)]
|
||||
public IActionResult Put(int id, object model)
|
||||
{
|
||||
if (id == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (id == 1)
|
||||
{
|
||||
return LocalFunction();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
|
||||
IActionResult LocalFunction()
|
||||
{
|
||||
if (id < -1)
|
||||
{
|
||||
// We should not process this.
|
||||
return UnprocessableEntity();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
public class UnwrapMethodReturnType
|
||||
{
|
||||
public ApiConventionAnalyzerBaseModel ReturnsBaseModel() => null;
|
||||
|
||||
public ActionResult<ApiConventionAnalyzerBaseModel> ReturnsActionResultOfBaseModel() => null;
|
||||
|
||||
public Task<ActionResult<ApiConventionAnalyzerBaseModel>> ReturnsTaskOfActionResultOfBaseModel() => null;
|
||||
|
||||
public ValueTask<ActionResult<ApiConventionAnalyzerBaseModel>> ReturnsValueTaskOfActionResultOfBaseModel() => default(ValueTask<ActionResult<ApiConventionAnalyzerBaseModel>>);
|
||||
|
||||
public ActionResult<IEnumerable<ApiConventionAnalyzerBaseModel>> ReturnsActionResultOfIEnumerableOfBaseModel() => null;
|
||||
|
||||
public IEnumerable<ApiConventionAnalyzerBaseModel> ReturnsIEnumerableOfBaseModel() => null;
|
||||
}
|
||||
|
||||
[DefaultStatusCode(StatusCodes.Status412PreconditionFailed)]
|
||||
public class TestActionResultUsingStatusCodesConstants { }
|
||||
|
||||
[DefaultStatusCode((int)HttpStatusCode.Found)]
|
||||
public class TestActionResultUsingHttpStatusCodeCast { }
|
||||
|
||||
public class ApiConventionAnalyzerBaseModel { }
|
||||
|
||||
public class ApiConventionAnalyzerDerivedModel : ApiConventionAnalyzerBaseModel { }
|
||||
|
||||
public class ApiConventionAnalyzerTest_IndexModel : PageModel
|
||||
{
|
||||
public IActionResult OnGet() => null;
|
||||
}
|
||||
|
||||
public class ApiConventionAnalyzerTest_NotApiController : Controller
|
||||
{
|
||||
public IActionResult Index() => null;
|
||||
}
|
||||
|
||||
public class ApiConventionAnalyzerTest_NotAction : Controller
|
||||
{
|
||||
[NonAction]
|
||||
public IActionResult Index() => null;
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
public class ApiConventionAnalyzerTest_Valid : Controller
|
||||
{
|
||||
public IActionResult Index() => null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiConventionType(typeof(object))]
|
||||
[ApiController]
|
||||
[ApiConventionType(typeof(string))]
|
||||
public class GetAttributes_BaseTypeWithAttributesBase
|
||||
{
|
||||
}
|
||||
|
||||
[ApiConventionType(typeof(int))]
|
||||
public class GetAttributes_BaseTypeWithAttributesDerived : GetAttributes_BaseTypeWithAttributesBase
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
[ApiConventionType(typeof(object))]
|
||||
[ApiController]
|
||||
[ApiConventionType(typeof(string))]
|
||||
public class GetAttributes_OnTypeWithAttributes
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
public class GetAttributes_OnTypeWithoutAttributesType
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
|
||||
namespace Microsoft.AspNetCore.Mvc.Analyzers
|
||||
{
|
||||
public class Base { }
|
||||
|
||||
public class Derived : Base { }
|
||||
|
||||
public class TestController
|
||||
{
|
||||
public IActionResult Get(int id) => null;
|
||||
|
||||
public IActionResult Search(string searchTerm, bool sortDescending, int page) => null;
|
||||
|
||||
public IActionResult SearchEmpty() => null;
|
||||
}
|
||||
|
||||
public static class TestConvention
|
||||
{
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Get(int id) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void GetNoArgs() { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void GetTwoArgs(int id, string name) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void Post(Derived model) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
|
||||
public static void GetParameterNotMatching([ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.AssignableFrom)] Derived model) { }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void Search(
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Exact)]
|
||||
string searchTerm,
|
||||
params object[] others)
|
||||
{ }
|
||||
|
||||
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
|
||||
public static void SearchWithParams(params object[] others) { }
|
||||
|
||||
public static void MethodWithoutMatchBehavior() { }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void MethodWithRandomAttributes() { }
|
||||
|
||||
public static void MethodParameterWithRandomAttributes([FromRoute] int value) { }
|
||||
|
||||
public static void MethodWithAnyTypeMatchBehaviorParameter([ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)] int value) { }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue