Merge pull request #8081 from dotnet-maestro-bot/merge/release/2.2-to-master

[automated] Merge branch 'release/2.2' => 'master'
This commit is contained in:
Pranav K 2018-07-13 13:48:09 -07:00 committed by GitHub
commit 0038ccbaa3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 554 additions and 409 deletions

View File

@ -0,0 +1,30 @@
// 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.CSharp.Syntax;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
internal readonly struct ActualApiResponseMetadata
{
private readonly int? _statusCode;
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement)
{
ReturnStatement = returnStatement;
_statusCode = null;
}
public ActualApiResponseMetadata(ReturnStatementSyntax returnStatement, int statusCode)
{
ReturnStatement = returnStatement;
_statusCode = statusCode;
}
public ReturnStatementSyntax ReturnStatement { get; }
public int StatusCode => _statusCode.Value;
public bool IsDefaultResponse => _statusCode == null;
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
@ -15,8 +14,6 @@ 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,
@ -44,6 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
{
compilationStartAnalysisContext.RegisterSyntaxNodeAction(syntaxNodeContext =>
{
var cancellationToken = syntaxNodeContext.CancellationToken;
var methodSyntax = (MethodDeclarationSyntax)syntaxNodeContext.Node;
var semanticModel = syntaxNodeContext.SemanticModel;
var method = semanticModel.GetDeclaredSymbol(methodSyntax, syntaxNodeContext.CancellationToken);
@ -54,34 +52,47 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
}
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 declaredResponseMetadata = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, conventionAttributes);
var hasUnreadableStatusCodes = SymbolApiResponseMetadataProvider.TryGetActualResponseMetadata(symbolCache, semanticModel, methodSyntax, cancellationToken, out var actualResponseMetadata);
var hasUndocumentedStatusCodes = false;
foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>())
foreach (var item in actualResponseMetadata)
{
hasUndocumentedStatusCodes |= VisitReturnStatementSyntax(context, returnStatementSyntax);
var location = item.ReturnStatement.GetLocation();
if (item.IsDefaultResponse)
{
if (!(HasStatusCode(declaredResponseMetadata, 200) || HasStatusCode(declaredResponseMetadata, 201)))
{
hasUndocumentedStatusCodes = true;
syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.MVC1005_ActionReturnsUndocumentedSuccessResult,
location));
}
}
else if (!HasStatusCode(declaredResponseMetadata, item.StatusCode))
{
hasUndocumentedStatusCodes = true;
syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.MVC1004_ActionReturnsUndocumentedStatusCode,
location,
item.StatusCode));
}
}
if (hasUndocumentedStatusCodes)
if (hasUndocumentedStatusCodes || hasUnreadableStatusCodes)
{
// 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++)
for (var i = 0; i < declaredResponseMetadata.Count; i++)
{
var expectedStatusCode = expectedResponseMetadata[i].StatusCode;
if (!actualResponseMetadata.Contains(expectedStatusCode))
var expectedStatusCode = declaredResponseMetadata[i].StatusCode;
if (!HasStatusCode(actualResponseMetadata, expectedStatusCode))
{
context.SyntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
syntaxNodeContext.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode,
methodSyntax.Identifier.GetLocation(),
expectedStatusCode));
@ -102,86 +113,6 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
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)
@ -212,7 +143,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
return true;
}
internal static bool HasStatusCode(IList<ApiResponseMetadata> declaredApiResponseMetadata, int statusCode)
internal static bool HasStatusCode(IList<DeclaredApiResponseMetadata> declaredApiResponseMetadata, int statusCode)
{
if (declaredApiResponseMetadata.Count == 0)
{
@ -231,47 +162,22 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
return false;
}
private static bool ShouldDescendIntoChildren(SyntaxNode syntaxNode)
internal static bool HasStatusCode(IList<ActualApiResponseMetadata> actualResponseMetadata, int statusCode)
{
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)
for (var i = 0; i < actualResponseMetadata.Count; i++)
{
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)
if (actualResponseMetadata[i].IsDefaultResponse)
{
ReportDiagnosticAction(diagnostic);
return statusCode == 200 || statusCode == 201;
}
SyntaxNodeContext.ReportDiagnostic(diagnostic);
else if(actualResponseMetadata[i].StatusCode == statusCode)
{
return true;
}
}
}
return false;
}
}
}

View File

@ -5,9 +5,9 @@ using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
internal readonly struct ApiResponseMetadata
internal readonly struct DeclaredApiResponseMetadata
{
public ApiResponseMetadata(int statusCode, AttributeData attributeData, IMethodSymbol convention)
public DeclaredApiResponseMetadata(int statusCode, AttributeData attributeData, IMethodSymbol convention)
{
StatusCode = statusCode;
Attribute = attributeData;

View File

@ -4,16 +4,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
internal class SymbolApiResponseMetadataProvider
internal static class SymbolApiResponseMetadataProvider
{
private const string StatusCodeProperty = "StatusCode";
private const string StatusCodeConstructorParameter = "statusCode";
private static readonly Func<SyntaxNode, bool> _shouldDescendIntoChildren = ShouldDescendIntoChildren;
internal static IList<ApiResponseMetadata> GetResponseMetadata(
internal static IList<DeclaredApiResponseMetadata> GetDeclaredResponseMetadata(
ApiControllerSymbolCache symbolCache,
IMethodSymbol method,
IReadOnlyList<AttributeData> conventionTypeAttributes)
@ -28,7 +32,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
return metadataItems;
}
private static IList<ApiResponseMetadata> GetResponseMetadataFromConventions(
private static IList<DeclaredApiResponseMetadata> GetResponseMetadataFromConventions(
ApiControllerSymbolCache symbolCache,
IMethodSymbol method,
IReadOnlyList<AttributeData> attributes)
@ -58,17 +62,17 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
}
}
return Array.Empty<ApiResponseMetadata>();
return Array.Empty<DeclaredApiResponseMetadata>();
}
private static IList<ApiResponseMetadata> GetResponseMetadataFromMethodAttributes(ApiControllerSymbolCache symbolCache, IMethodSymbol methodSymbol)
private static IList<DeclaredApiResponseMetadata> GetResponseMetadataFromMethodAttributes(ApiControllerSymbolCache symbolCache, IMethodSymbol methodSymbol)
{
var metadataItems = new List<ApiResponseMetadata>();
var metadataItems = new List<DeclaredApiResponseMetadata>();
var responseMetadataAttributes = methodSymbol.GetAttributes(symbolCache.ProducesResponseTypeAttribute, inherit: true);
foreach (var attribute in responseMetadataAttributes)
{
var statusCode = GetStatusCode(attribute);
var metadata = new ApiResponseMetadata(statusCode, attribute, convention: null);
var metadata = new DeclaredApiResponseMetadata(statusCode, attribute, convention: null);
metadataItems.Add(metadata);
}
@ -119,5 +123,103 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
return DefaultStatusCode;
}
internal static bool TryGetActualResponseMetadata(
in ApiControllerSymbolCache symbolCache,
SemanticModel semanticModel,
MethodDeclarationSyntax methodSyntax,
CancellationToken cancellationToken,
out IList<ActualApiResponseMetadata> actualResponseMetadata)
{
actualResponseMetadata = new List<ActualApiResponseMetadata>();
var hasUnreadableReturnStatements = false;
foreach (var returnStatementSyntax in methodSyntax.DescendantNodes(_shouldDescendIntoChildren).OfType<ReturnStatementSyntax>())
{
var responseMetadata = InspectReturnStatementSyntax(
symbolCache,
semanticModel,
returnStatementSyntax,
cancellationToken);
if (responseMetadata != null)
{
actualResponseMetadata.Add(responseMetadata.Value);
}
else
{
hasUnreadableReturnStatements = true;
}
}
return hasUnreadableReturnStatements;
}
internal static ActualApiResponseMetadata? InspectReturnStatementSyntax(
in ApiControllerSymbolCache symbolCache,
SemanticModel semanticModel,
ReturnStatementSyntax returnStatementSyntax,
CancellationToken cancellationToken)
{
var returnExpression = returnStatementSyntax.Expression;
if (returnExpression.IsMissing)
{
return null;
}
var typeInfo = semanticModel.GetTypeInfo(returnExpression, cancellationToken);
if (typeInfo.Type.TypeKind == TypeKind.Error)
{
return null;
}
var statementReturnType = typeInfo.Type;
var defaultStatusCodeAttribute = statementReturnType
.GetAttributes(symbolCache.DefaultStatusCodeAttribute, inherit: true)
.FirstOrDefault();
if (defaultStatusCodeAttribute != null)
{
var statusCode = GetDefaultStatusCode(defaultStatusCodeAttribute);
if (statusCode == null)
{
// Unable to read the status code even though the attribute exists.
return null;
}
return new ActualApiResponseMetadata(returnStatementSyntax, statusCode.Value);
}
else if (!symbolCache.IActionResult.IsAssignableFrom(statementReturnType))
{
// Return expression does not have a DefaultStatusCodeAttribute and it is not
// an instance of IActionResult. Must be returning the "model".
return new ActualApiResponseMetadata(returnStatementSyntax);
}
return null;
}
private static bool ShouldDescendIntoChildren(SyntaxNode syntaxNode)
{
return !syntaxNode.IsKind(SyntaxKind.LocalFunctionStatement) &&
!syntaxNode.IsKind(SyntaxKind.ParenthesizedLambdaExpression) &&
!syntaxNode.IsKind(SyntaxKind.SimpleLambdaExpression) &&
!syntaxNode.IsKind(SyntaxKind.AnonymousMethodExpression);
}
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;
}
}
}

View File

@ -296,7 +296,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
var entries = ModelState.FindKeysWithPrefix(key);
foreach (var entry in entries)
{
entry.Value.ValidationState = ModelValidationState.Skipped;
if (entry.Value.ValidationState != ModelValidationState.Invalid)
{
entry.Value.ValidationState = ModelValidationState.Skipped;
}
}
}

View File

@ -133,6 +133,27 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Empty(entry.Errors);
}
// More like how product code does suppressions than Validate_SimpleType_SuppressValidation()
[Fact]
public void Validate_SimpleType_SuppressValidationWithNullKey()
{
// Arrange
var actionContext = new ActionContext();
var modelState = actionContext.ModelState;
var validator = CreateValidator();
var model = "test";
var validationState = new ValidationStateDictionary
{
{ model, new ValidationStateEntry { SuppressValidation = true } }
};
// Act
validator.Validate(actionContext, validationState, "parameter", model);
// Assert
Assert.True(modelState.IsValid);
Assert.Empty(modelState);
}
[Fact]
public void Validate_ComplexValueType_Valid()
@ -1195,6 +1216,48 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Empty(entry.Value.Errors);
}
// Regression test for aspnet/Mvc#7992
[Fact]
public void Validate_SuppressValidation_AfterHasReachedMaxErrors_Invalid()
{
// Arrange
var actionContext = new ActionContext();
var modelState = actionContext.ModelState;
modelState.MaxAllowedErrors = 2;
modelState.AddModelError(key: "one", errorMessage: "1");
modelState.AddModelError(key: "two", errorMessage: "2");
var validator = CreateValidator();
var model = (object)23; // Box ASAP
var validationState = new ValidationStateDictionary
{
{ model, new ValidationStateEntry { SuppressValidation = true } }
};
// Act
validator.Validate(actionContext, validationState, prefix: string.Empty, model);
// Assert
Assert.False(modelState.IsValid);
Assert.True(modelState.HasReachedMaxErrors);
Assert.Collection(
modelState,
kvp =>
{
Assert.Empty(kvp.Key);
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
var error = Assert.Single(kvp.Value.Errors);
Assert.IsType<TooManyModelErrorsException>(error.Exception);
},
kvp =>
{
Assert.Equal("one", kvp.Key);
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
var error = Assert.Single(kvp.Value.Errors);
Assert.Equal("1", error.ErrorMessage);
});
}
private static DefaultObjectValidator CreateValidator(Type excludedType)
{
var excludeFilters = new List<SuppressChildValidationMetadataProvider>();

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
@ -706,6 +707,131 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
});
}
// Regression test 1 for aspnet/Mvc#7963. ModelState should never be valid.
[Fact]
public async Task BindModelAsync_ForOverlappingParametersWithSuppressions_InValid_WithValidSecondParameter()
{
// Arrange
var parameterDescriptor = new ParameterDescriptor
{
Name = "patchDocument",
ParameterType = typeof(IJsonPatchDocument),
};
var actionContext = GetControllerContext();
var modelState = actionContext.ModelState;
// First ModelState key is not empty to match SimpleTypeModelBinder.
modelState.SetModelValue("id", "notAGuid", "notAGuid");
modelState.AddModelError("id", "This is not valid.");
var modelMetadataProvider = new TestModelMetadataProvider();
modelMetadataProvider.ForType<IJsonPatchDocument>().ValidationDetails(v => v.ValidateChildren = false);
var modelMetadata = modelMetadataProvider.GetMetadataForType(typeof(IJsonPatchDocument));
var parameterBinder = new ParameterBinder(
modelMetadataProvider,
Mock.Of<IModelBinderFactory>(),
new DefaultObjectValidator(
modelMetadataProvider,
new[] { TestModelValidatorProvider.CreateDefaultProvider() }),
_optionsAccessor,
NullLoggerFactory.Instance);
// BodyModelBinder does not update ModelState in success case.
var modelBindingResult = ModelBindingResult.Success(new JsonPatchDocument());
var modelBinder = CreateMockModelBinder(modelBindingResult);
// Act
var result = await parameterBinder.BindModelAsync(
actionContext,
modelBinder,
new SimpleValueProvider(),
parameterDescriptor,
modelMetadata,
value: null);
// Assert
Assert.True(result.IsModelSet);
Assert.False(modelState.IsValid);
Assert.Collection(
modelState,
kvp =>
{
Assert.Equal("id", kvp.Key);
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
var error = Assert.Single(kvp.Value.Errors);
Assert.Equal("This is not valid.", error.ErrorMessage);
});
}
// Regression test 2 for aspnet/Mvc#7963. ModelState should never be valid.
[Fact]
public async Task BindModelAsync_ForOverlappingParametersWithSuppressions_InValid_WithInValidSecondParameter()
{
// Arrange
var parameterDescriptor = new ParameterDescriptor
{
Name = "patchDocument",
ParameterType = typeof(IJsonPatchDocument),
};
var actionContext = GetControllerContext();
var modelState = actionContext.ModelState;
// First ModelState key is not empty to match SimpleTypeModelBinder.
modelState.SetModelValue("id", "notAGuid", "notAGuid");
modelState.AddModelError("id", "This is not valid.");
// Second ModelState key is empty to match BodyModelBinder.
modelState.AddModelError(string.Empty, "This is also not valid.");
var modelMetadataProvider = new TestModelMetadataProvider();
modelMetadataProvider.ForType<IJsonPatchDocument>().ValidationDetails(v => v.ValidateChildren = false);
var modelMetadata = modelMetadataProvider.GetMetadataForType(typeof(IJsonPatchDocument));
var parameterBinder = new ParameterBinder(
modelMetadataProvider,
Mock.Of<IModelBinderFactory>(),
new DefaultObjectValidator(
modelMetadataProvider,
new[] { TestModelValidatorProvider.CreateDefaultProvider() }),
_optionsAccessor,
NullLoggerFactory.Instance);
var modelBindingResult = ModelBindingResult.Failed();
var modelBinder = CreateMockModelBinder(modelBindingResult);
// Act
var result = await parameterBinder.BindModelAsync(
actionContext,
modelBinder,
new SimpleValueProvider(),
parameterDescriptor,
modelMetadata,
value: null);
// Assert
Assert.False(result.IsModelSet);
Assert.False(modelState.IsValid);
Assert.Collection(
modelState,
kvp =>
{
Assert.Empty(kvp.Key);
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
var error = Assert.Single(kvp.Value.Errors);
Assert.Equal("This is also not valid.", error.ErrorMessage);
},
kvp =>
{
Assert.Equal("id", kvp.Key);
Assert.Equal(ModelValidationState.Invalid, kvp.Value.ValidationState);
var error = Assert.Single(kvp.Value.Errors);
Assert.Equal("This is not valid.", error.ErrorMessage);
});
}
private static ControllerContext GetControllerContext()
{
var services = new ServiceCollection();
@ -813,25 +939,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return mockValueProvider.Object;
}
private static IModelValidatorProvider CreateMockValidatorProvider(IModelValidator validator = null)
{
var mockValidator = new Mock<IModelValidatorProvider>();
mockValidator
.Setup(o => o.CreateValidators(
It.IsAny<ModelValidatorProviderContext>()))
.Callback<ModelValidatorProviderContext>(context =>
{
if (validator != null)
{
foreach (var result in context.Results)
{
result.Validator = validator;
}
}
});
return mockValidator.Object;
}
private class Person : IEquatable<Person>, IEquatable<object>
{
public string Name { get; set; }
@ -862,9 +969,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
public string DerivedProperty { get; set; }
}
[Required]
private Person PersonProperty { get; set; }
public abstract class FakeModelMetadata : ModelMetadata
{
public FakeModelMetadata()

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class ApiConventionAnalyzerIntegrationTest
{
private MvcDiagnosticAnalyzerRunner Executor { get; } = new MvcDiagnosticAnalyzerRunner(new ApiConventionAnalyzer());
private MvcDiagnosticAnalyzerRunner Executor { get; } = new ApiCoventionWith1006DiagnosticEnabledRunner();
[Fact]
public Task NoDiagnosticsAreReturned_ForNonApiController()
@ -77,11 +77,15 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
[Fact]
public Task DiagnosticsAreReturned_IfMethodWithProducesResponseTypeAttribute_DoesNotReturnDocumentedStatusCode()
=> RunTestFor1006(400);
=> RunTest(DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode, 400);
[Fact]
public Task DiagnosticsAreReturned_IfMethodWithConvention_DoesNotReturnDocumentedStatusCode()
=> RunTestFor1006(404);
=> RunTest(DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode, 404);
[Fact]
public Task DiagnosticsAreReturned_IfMethodWithProducesResponseTypeAttribute_DoesNotDocumentSuccessStatusCode()
=> RunTest(DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode, 200);
private async Task RunNoDiagnosticsAreReturned([CallerMemberName] string testMethod = "")
{
@ -122,30 +126,6 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
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())
@ -155,6 +135,9 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
protected override CompilationOptions ConfigureCompilationOptions(CompilationOptions options)
{
var compilationOptions = base.ConfigureCompilationOptions(options);
// 10006 is disabled by default. Explicitly enable it so we can correctly validate no diagnostics
// are returned scenarios.
var specificDiagnosticOptions = compilationOptions.SpecificDiagnosticOptions.Add(
DiagnosticDescriptors.MVC1006_ActionDoesNotReturnDocumentedStatusCode.Id,
ReportDiagnostic.Info);

View File

@ -1,181 +1,17 @@
// 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()
{
@ -275,17 +111,6 @@ namespace TestNamespace
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");

View File

@ -2,11 +2,15 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Analyzer.Testing;
using Microsoft.AspNetCore.Mvc.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Analyzers
@ -25,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Empty(result);
@ -41,7 +45,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Empty(result);
@ -57,7 +61,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Empty(result);
@ -73,7 +77,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Collection(
@ -96,7 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Collection(
@ -119,7 +123,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Collection(
@ -142,7 +146,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Collection(
@ -165,7 +169,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Collection(
@ -188,7 +192,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Empty(result);
@ -219,7 +223,7 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
var symbolCache = new ApiControllerSymbolCache(compilation);
// Act
var result = SymbolApiResponseMetadataProvider.GetResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
var result = SymbolApiResponseMetadataProvider.GetDeclaredResponseMetadata(symbolCache, method, Array.Empty<AttributeData>());
// Assert
Assert.Collection(
@ -299,6 +303,107 @@ namespace Microsoft.AspNetCore.Mvc.Analyzers
Assert.Equal(expected, statusCode);
}
[Fact]
public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingStatusCodeConstants()
{
// Arrange
var compilation = await GetCompilation("GetDefaultStatusCodeTest");
var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingStatusCodesConstants).FullName).GetAttributes()[0];
// Act
var actual = SymbolApiResponseMetadataProvider.GetDefaultStatusCode(attribute);
// Assert
Assert.Equal(412, actual);
}
[Fact]
public async Task GetDefaultStatusCode_ReturnsValueDefinedUsingHttpStatusCast()
{
// Arrange
var compilation = await GetCompilation("GetDefaultStatusCodeTest");
var attribute = compilation.GetTypeByMetadataName(typeof(TestActionResultUsingHttpStatusCodeCast).FullName).GetAttributes()[0];
// Act
var actual = SymbolApiResponseMetadataProvider.GetDefaultStatusCode(attribute);
// Assert
Assert.Equal(302, actual);
}
[Fact]
public async Task InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound()
{
// Arrange & Act
var source = @"
using Microsoft.AspNetCore.Mvc;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[ApiController]
public class InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound : ControllerBase
{
public IActionResult Get(int id)
{
return new DoesNotExist(id);
}
}
}";
var actualResponseMetadata = await RunInspectReturnStatementSyntax(source, nameof(InspectReturnExpression_ReturnsNull_IfReturnExpressionCannotBeFound));
// Assert
Assert.Null(actualResponseMetadata);
}
[Fact]
public async Task InspectReturnExpression_ReturnsStatusCodeFromDefaultStatusCodeAttributeOnActionResult()
{
// Arrange & Act
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
// Assert
Assert.NotNull(actualResponseMetadata);
Assert.Equal(401, actualResponseMetadata.Value.StatusCode);
}
[Fact]
public async Task InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResult()
{
// Arrange & Act
var actualResponseMetadata = await RunInspectReturnStatementSyntax();
// Assert
Assert.NotNull(actualResponseMetadata);
Assert.True(actualResponseMetadata.Value.IsDefaultResponse);
}
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax([CallerMemberName]string test = null)
{
// Arrange
var testSource = MvcTestSource.Read(GetType().Name, test);
return await RunInspectReturnStatementSyntax(testSource.Source, test);
}
private async Task<ActualApiResponseMetadata?> RunInspectReturnStatementSyntax(string source, string test)
{
var project = DiagnosticProject.Create(GetType().Assembly, new[] { source });
var compilation = await project.GetCompilationAsync();
var symbolCache = new ApiControllerSymbolCache(compilation);
var returnType = compilation.GetTypeByMetadataName($"{Namespace}.{test}");
var syntaxTree = returnType.DeclaringSyntaxReferences[0].SyntaxTree;
var method = (IMethodSymbol)returnType.GetMembers().First();
var methodSyntax = syntaxTree.GetRoot().FindNode(method.Locations[0].SourceSpan);
var returnStatement = methodSyntax.DescendantNodes().OfType<ReturnStatementSyntax>().First();
return SymbolApiResponseMetadataProvider.InspectReturnStatementSyntax(
symbolCache,
compilation.GetSemanticModel(syntaxTree),
returnStatement,
CancellationToken.None);
}
private Task<Compilation> GetResponseMetadataCompilation() => GetCompilation("GetResponseMetadataTests");
private Task<Compilation> GetCompilation(string test)

View File

@ -0,0 +1,20 @@
using System;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[ApiController]
public class DiagnosticsAreReturned_IfMethodWithProducesResponseTypeAttribute_DoesNotDocumentSuccessStatusCode : ControllerBase
{
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public ActionResult<string> /*MM*/Method(int id)
{
if (id == 0)
{
return NotFound();
}
throw new NotImplementedException();
}
}
}

View File

@ -1,37 +1,7 @@
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;
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;

View File

@ -0,0 +1,12 @@
using System.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
[DefaultStatusCode(StatusCodes.Status412PreconditionFailed)]
public class TestActionResultUsingStatusCodesConstants { }
[DefaultStatusCode((int)HttpStatusCode.Redirect)]
public class TestActionResultUsingHttpStatusCodeCast { }
}

View File

@ -0,0 +1,12 @@
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResult : ControllerBase
{
public object Get()
{
return new InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResultModel();
}
}
public class InspectReturnExpression_ReturnsDefaultResponseMetadata_IfReturnedTypeIsNotActionResultModel { }
}

View File

@ -0,0 +1,10 @@
namespace Microsoft.AspNetCore.Mvc.Analyzers
{
public class InspectReturnExpression_ReturnsStatusCodeFromDefaultStatusCodeAttributeOnActionResult : ControllerBase
{
public IActionResult Get()
{
return Unauthorized();
}
}
}