Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wouldn't fail if there's a single [FromBody] parameter and then another [AsParameter] parameter with a single [FromBody] property, right? Or multiple [AsParameter] parameters with a single [FromBody] property each? All of these should fail. Can we add test cases for these?

}

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