Skip to content

Commit

Permalink
Greater than 1 FromBody analyzer. (#46494)
Browse files Browse the repository at this point in the history
Report an error daignostic when a minimal API ```Map...``` method contains multiple `[FromBody]` attributes or a type referenced with an `[AsPraameters]` attribute with multiple `[FromBody]` members is present.
  • Loading branch information
mitchdenny authored Feb 21, 2023
1 parent 0134f61 commit 86e3a4b
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,13 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");

internal static readonly DiagnosticDescriptor AtMostOneFromBodyAttribute = new(
"ASP0024",
new LocalizableResourceString(nameof(Resources.Analyzer_MultipleFromBody_Title), Resources.ResourceManager, typeof(Resources)),
new LocalizableResourceString(nameof(Resources.Analyzer_MultipleFromBody_Message), Resources.ResourceManager, typeof(Resources)),
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,10 @@
<data name="Analyzer_HeaderDictionaryAdd_Title" xml:space="preserve">
<value>Suggest using IHeaderDictionary.Append or the indexer</value>
</data>
</root>
<data name="Analyzer_MultipleFromBody_Message" xml:space="preserve">
<value>Route handler has multiple parameters with the [FromBody] attribute or a parameter with an [AsParameters] attribute where the parameter type contains multiple members with [FromBody] attributes. Only one parameter can have a [FromBody] attribute.</value>
</data>
<data name="Analyzer_MultipleFromBody_Title" xml:space="preserve">
<value>Route handler has multiple parameters with the [FromBody] attribute.</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure;
using Microsoft.AspNetCore.App.Analyzers.Infrastructure;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.AspNetCore.Analyzers.RouteHandlers;

using WellKnownType = WellKnownTypeData.WellKnownType;

public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer
{
private static void AtMostOneFromBodyAttribute(
in OperationAnalysisContext context,
WellKnownTypes wellKnownTypes,
IMethodSymbol methodSymbol)
{
var fromBodyMetadataInterfaceType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata);
var asParametersAttributeType = wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_AsParametersAttribute);

var asParametersDecoratedParameters = methodSymbol.Parameters.Where(p => p.HasAttribute(asParametersAttributeType));

foreach (var asParameterDecoratedParameter in asParametersDecoratedParameters)
{
var fromBodyMetadataInterfaceMembers = asParameterDecoratedParameter.Type.GetMembers().Where(
m => m.HasAttributeImplementingInterface(fromBodyMetadataInterfaceType)
);

if (fromBodyMetadataInterfaceMembers.Count() >= 2)
{
ReportDiagnostics(context, fromBodyMetadataInterfaceMembers);
}
}

var fromBodyMetadataInterfaceParameters = methodSymbol.Parameters.Where(p => p.HasAttributeImplementingInterface(fromBodyMetadataInterfaceType));

if (fromBodyMetadataInterfaceParameters.Count() >= 2)
{
ReportDiagnostics(context, fromBodyMetadataInterfaceParameters);
}

static void ReportDiagnostics(OperationAnalysisContext context, IEnumerable<ISymbol> symbols)
{
foreach (var symbol in symbols)
{
if (symbol.DeclaringSyntaxReferences.Length > 0)
{
var syntax = symbol.DeclaringSyntaxReferences[0].GetSyntax(context.CancellationToken);
var location = syntax.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.AtMostOneFromBodyAttribute,
location
));
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer
{
private static void DisallowNonParsableComplexTypesOnParameters(
in OperationAnalysisContext context,
WellKnownTypes wellKnownTypes,
RouteUsageModel routeUsage,
IMethodSymbol methodSymbol)
{
var wellKnownTypes = WellKnownTypes.GetOrCreate(context.Compilation);

foreach (var handlerDelegateParameter in methodSymbol.Parameters)
{
// If the parameter is decorated with a FromServices attribute then we can skip it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer
DiagnosticDescriptors.DetectMismatchedParameterOptionality,
DiagnosticDescriptors.RouteParameterComplexTypeIsNotParsableOrBindable,
DiagnosticDescriptors.BindAsyncSignatureMustReturnValueTaskOfT,
DiagnosticDescriptors.AmbiguousRouteHandlerRoute
DiagnosticDescriptors.AmbiguousRouteHandlerRoute,
DiagnosticDescriptors.AtMostOneFromBodyAttribute
);

public override void Initialize(AnalysisContext context)
Expand Down Expand Up @@ -105,17 +106,19 @@ void DoOperationAnalysis(OperationAnalysisContext context, ConcurrentDictionary<
{
var lambda = (IAnonymousFunctionOperation)delegateCreation.Target;
DisallowMvcBindArgumentsOnParameters(in context, wellKnownTypes, invocation, lambda.Symbol);
DisallowNonParsableComplexTypesOnParameters(in context, routeUsage, lambda.Symbol);
DisallowNonParsableComplexTypesOnParameters(in context, wellKnownTypes, routeUsage, lambda.Symbol);
DisallowReturningActionResultFromMapMethods(in context, wellKnownTypes, invocation, lambda, delegateCreation.Syntax);
DetectMisplacedLambdaAttribute(context, lambda);
DetectMismatchedParameterOptionality(in context, routeUsage, lambda.Symbol);
AtMostOneFromBodyAttribute(in context, wellKnownTypes, lambda.Symbol);
}
else if (delegateCreation.Target.Kind == OperationKind.MethodReference)
{
var methodReference = (IMethodReferenceOperation)delegateCreation.Target;
DisallowMvcBindArgumentsOnParameters(in context, wellKnownTypes, invocation, methodReference.Method);
DisallowNonParsableComplexTypesOnParameters(in context, routeUsage, methodReference.Method);
DisallowNonParsableComplexTypesOnParameters(in context, wellKnownTypes, routeUsage, methodReference.Method);
DetectMismatchedParameterOptionality(in context, routeUsage, methodReference.Method);
AtMostOneFromBodyAttribute(in context, wellKnownTypes, methodReference.Method);
var foundMethodReferenceBody = false;
if (!methodReference.Method.DeclaringSyntaxReferences.IsEmpty)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Security.Policy;
using Microsoft.CodeAnalysis.Testing;
using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpAnalyzerVerifier<Microsoft.AspNetCore.Analyzers.RouteHandlers.RouteHandlerAnalyzer>;

namespace Microsoft.AspNetCore.Analyzers.RouteHandlers;

public partial class AtMostOneFromBodyAttributeTest
{
private TestDiagnosticAnalyzerRunner Runner { get; } = new(new RouteHandlerAnalyzer());

[Fact]
public async Task Handler_With_No_FromBody_Attributes_Works()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Builder;
var webApp = WebApplication.Create();
webApp.MapPost(""/products/{productId}"", (string productId, Product product) => {});
public class Product
{
}
";

// Act
await VerifyCS.VerifyAnalyzerAsync(source);
}

[Fact]
public async Task Handler_With_One_FromBody_Attributes_Works()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
var webApp = WebApplication.Create();
webApp.MapPost(""/products/{productId}"", (string productId, [FromBody]Product product) => {});
public class Product
{
}
";

// Act
await VerifyCS.VerifyAnalyzerAsync(source);
}

[Fact]
public async Task Handler_With_Two_FromBody_Attributes_Fails()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
var webApp = WebApplication.Create();
webApp.MapPost(""/products/{productId}"", (string productId, {|#0:[FromBody]Product product1|}, {|#1:[FromBody]Product product2|}) => {});
public class Product
{
}
";

var expectedDiagnostic1 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(0);
var expectedDiagnostic2 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(1);

// Act
await VerifyCS.VerifyAnalyzerAsync(
source,
expectedDiagnostic1,
expectedDiagnostic2
);
}

[Fact]
public async Task MethodGroup_Handler_With_Two_FromBody_Attributes_Fails()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
var webApp = WebApplication.Create();
webApp.MapPost(""/products/{productId}"", MyHandlers.ProcessRequest);
public static class MyHandlers
{
public static void ProcessRequest(string productId, {|#0:[FromBody]Product product1|}, {|#1:[FromBody]Product product2|})
{
}
}
public class Product
{
}
";

var expectedDiagnostic1 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(0);
var expectedDiagnostic2 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(1);

// Act
await VerifyCS.VerifyAnalyzerAsync(
source,
expectedDiagnostic1,
expectedDiagnostic2
);
}

[Fact]
public async Task Handler_Handler_With_AsParameters_Argument_With_TwoFromBody_Attributes_Fails()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
var webApp = WebApplication.Create();
webApp.MapPost(""/products/{productId}"", ([AsParameters]GetProductRequest request) => {});
public class GetProductRequest
{
{|#0:[FromBody]
public Product Product1 { get; set; }|}
{|#1:[FromBody]
public Product Product2 { get; set; }|}
}
public class Product
{
}
";

var expectedDiagnostic1 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(0);
var expectedDiagnostic2 = new DiagnosticResult(DiagnosticDescriptors.AtMostOneFromBodyAttribute).WithLocation(1);

// Act
await VerifyCS.VerifyAnalyzerAsync(
source,
expectedDiagnostic1,
expectedDiagnostic2
);
}

[Fact]
public async Task Handler_Handler_With_AsParameters_Argument_With_OneFromBody_Attributes_Works()
{
// Arrange
var source = @"
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
var webApp = WebApplication.Create();
webApp.MapPost(""/products/{productId}"", ([AsParameters]GetProductRequest request) => {});
public class GetProductRequest
{
{|#0:[FromBody]
public Product Product1 { get; set; }|}
public Product Product2 { get; set; }
}
public class Product
{
}
";

// Act
await VerifyCS.VerifyAnalyzerAsync(source);
}
}

0 comments on commit 86e3a4b

Please sign in to comment.