Refactoring for ApiConvention analyzers

This commit is contained in:
Pranav K 2018-06-28 09:49:52 -07:00
parent a87b8fa2af
commit 46189abda7
42 changed files with 3223 additions and 822 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
<PreserveCompilationContext>true</PreserveCompilationContext>
<RootNamespace>Microsoft.AspNetCore.Mvc.Analyzers</RootNamespace>
</PropertyGroup>
<ItemGroup>

View File

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

View File

@ -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(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[ApiConventionType(typeof(object))]
[ApiController]
[ApiConventionType(typeof(string))]
public class GetAttributes_OnTypeWithAttributes
{
}
}

View File

@ -0,0 +1,8 @@
using System;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class GetAttributes_OnTypeWithoutAttributesType
{
}
}

View File

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