Skip to content

Commit 8b72a2b

Browse files
[release/6.0] Complain when returning IActionResult from MinimalAction (#35710)
* Complain when returning IActionResult from MinimalAction * Update src/Framework/Analyzer/test/MinimalActions/DisallowReturningActionResultsFromMapMethodsTest.cs Co-authored-by: Pranav K <[email protected]>
1 parent 707ab6c commit 8b72a2b

9 files changed

+436
-8
lines changed

src/Framework/Analyzer/src/DelegateEndpoints/DelegateEndpointAnalyzer.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Immutable;
6+
using System.Diagnostics;
67
using System.Linq;
78
using Microsoft.CodeAnalysis;
89
using Microsoft.CodeAnalysis.Diagnostics;
@@ -16,6 +17,7 @@ public partial class DelegateEndpointAnalyzer : DiagnosticAnalyzer
1617
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(new[]
1718
{
1819
DiagnosticDescriptors.DoNotUseModelBindingAttributesOnDelegateEndpointParameters,
20+
DiagnosticDescriptors.DoNotReturnActionResultsFromMapActions,
1921
});
2022

2123
public override void Initialize(AnalysisContext context)
@@ -28,6 +30,7 @@ public override void Initialize(AnalysisContext context)
2830
var compilation = compilationStartAnalysisContext.Compilation;
2931
if (!WellKnownTypes.TryCreate(compilation, out var wellKnownTypes))
3032
{
33+
Debug.Fail("One or more types could not be found. This usually means you are bad at spelling C# type names.");
3134
return;
3235
}
3336

@@ -50,11 +53,52 @@ public override void Initialize(AnalysisContext context)
5053
{
5154
var lambda = ((IAnonymousFunctionOperation)delegateCreation.Target);
5255
DisallowMvcBindArgumentsOnParameters(in operationAnalysisContext, wellKnownTypes, invocation, lambda.Symbol);
56+
DisallowReturningActionResultFromMapMethods(in operationAnalysisContext, wellKnownTypes, invocation, lambda);
5357
}
5458
else if (delegateCreation.Target.Kind == OperationKind.MethodReference)
5559
{
5660
var methodReference = (IMethodReferenceOperation)delegateCreation.Target;
5761
DisallowMvcBindArgumentsOnParameters(in operationAnalysisContext, wellKnownTypes, invocation, methodReference.Method);
62+
63+
var foundMethodReferenceBody = false;
64+
if (!methodReference.Method.DeclaringSyntaxReferences.IsEmpty)
65+
{
66+
var syntaxReference = methodReference.Method.DeclaringSyntaxReferences[0];
67+
var methodOperation = invocation.SemanticModel.GetOperation(syntaxReference.GetSyntax(operationAnalysisContext.CancellationToken));
68+
if (methodOperation is ILocalFunctionOperation { Body: not null } localFunction)
69+
{
70+
foundMethodReferenceBody = true;
71+
DisallowReturningActionResultFromMapMethods(
72+
in operationAnalysisContext,
73+
wellKnownTypes,
74+
invocation,
75+
methodReference.Method,
76+
localFunction.Body);
77+
}
78+
else if (methodOperation is IMethodBodyOperation methodBody)
79+
{
80+
foundMethodReferenceBody = true;
81+
DisallowReturningActionResultFromMapMethods(
82+
in operationAnalysisContext,
83+
wellKnownTypes,
84+
invocation,
85+
methodReference.Method,
86+
methodBody.BlockBody ?? methodBody.ExpressionBody);
87+
}
88+
}
89+
90+
if (!foundMethodReferenceBody)
91+
{
92+
// it's possible we couldn't find the operation for the method reference. In this case,
93+
// try and provide less detailed diagnostics to the extent we can
94+
DisallowReturningActionResultFromMapMethods(
95+
in operationAnalysisContext,
96+
wellKnownTypes,
97+
invocation,
98+
methodReference.Method,
99+
methodBody: null);
100+
101+
}
58102
}
59103
}, OperationKind.Invocation);
60104
});

src/Framework/Analyzer/src/DelegateEndpoints/DiagnosticDescriptors.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,14 @@ internal static class DiagnosticDescriptors
1616
DiagnosticSeverity.Warning,
1717
isEnabledByDefault: true,
1818
helpLinkUri: "https://aka.ms/aspnet/analyzers");
19+
20+
internal static readonly DiagnosticDescriptor DoNotReturnActionResultsFromMapActions = new(
21+
"ASP0004",
22+
"Do not use action results with Map actions",
23+
"IActionResult instances should not be returned from a {0} Delegate parameter. Consider returning an equivalent result from Microsoft.AspNetCore.Http.Results.",
24+
"Usage",
25+
DiagnosticSeverity.Warning,
26+
isEnabledByDefault: true,
27+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
1928
}
2029
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
using Microsoft.CodeAnalysis.Operations;
8+
9+
namespace Microsoft.AspNetCore.Analyzers.DelegateEndpoints;
10+
11+
public partial class DelegateEndpointAnalyzer : DiagnosticAnalyzer
12+
{
13+
private static void DisallowReturningActionResultFromMapMethods(
14+
in OperationAnalysisContext context,
15+
WellKnownTypes wellKnownTypes,
16+
IInvocationOperation invocationOperation,
17+
IAnonymousFunctionOperation anonymousFunction)
18+
{
19+
DisallowReturningActionResultFromMapMethods(in context, wellKnownTypes, invocationOperation, anonymousFunction.Symbol, anonymousFunction.Body);
20+
}
21+
22+
private static void DisallowReturningActionResultFromMapMethods(
23+
in OperationAnalysisContext context,
24+
WellKnownTypes wellKnownTypes,
25+
IInvocationOperation invocationOperation,
26+
IMethodSymbol methodSymbol,
27+
IBlockOperation? methodBody)
28+
{
29+
var returnType = UnwrapPossibleAsyncReturnType(methodSymbol.ReturnType);
30+
31+
if (wellKnownTypes.IResult.IsAssignableFrom(returnType))
32+
{
33+
// This type returns some form of IResult. Nothing to do here.
34+
return;
35+
}
36+
37+
if (methodBody is null &&
38+
(wellKnownTypes.IActionResult.IsAssignableFrom(returnType) ||
39+
wellKnownTypes.IConvertToActionResult.IsAssignableFrom(returnType)))
40+
{
41+
// if we don't have a method body, and the action is IResult or ActionResult<T> returning, produce diagnostics for the entire method.
42+
context.ReportDiagnostic(Diagnostic.Create(
43+
DiagnosticDescriptors.DoNotReturnActionResultsFromMapActions,
44+
invocationOperation.Arguments[2].Syntax.GetLocation(),
45+
invocationOperation.TargetMethod.Name));
46+
return;
47+
}
48+
49+
foreach (var returnOperation in methodBody.Descendants().OfType<IReturnOperation>())
50+
{
51+
if (returnOperation.ReturnedValue is null or IInvalidOperation)
52+
{
53+
continue;
54+
}
55+
56+
var returnedValue = returnOperation.ReturnedValue;
57+
if (returnedValue is IConversionOperation conversionOperation)
58+
{
59+
returnedValue = conversionOperation.Operand;
60+
}
61+
62+
var type = returnedValue.Type;
63+
64+
if (type is null)
65+
{
66+
continue;
67+
}
68+
69+
if (wellKnownTypes.IResult.IsAssignableFrom(type))
70+
{
71+
continue;
72+
}
73+
74+
if (wellKnownTypes.IActionResult.IsAssignableFrom(type))
75+
{
76+
context.ReportDiagnostic(Diagnostic.Create(
77+
DiagnosticDescriptors.DoNotReturnActionResultsFromMapActions,
78+
returnOperation.Syntax.GetLocation(),
79+
invocationOperation.TargetMethod.Name));
80+
}
81+
}
82+
}
83+
84+
private static ITypeSymbol UnwrapPossibleAsyncReturnType(ITypeSymbol returnType)
85+
{
86+
if (returnType is not INamedTypeSymbol { Name: "Task" or "ValueTask", IsGenericType: true, TypeArguments: { Length: 1 } } taskLike)
87+
{
88+
return returnType;
89+
}
90+
91+
return taskLike.TypeArguments[0];
92+
}
93+
}

src/Framework/Analyzer/src/DelegateEndpoints/WellKnownTypes.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,32 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out We
3131
return false;
3232
}
3333

34+
const string IResult = "Microsoft.AspNetCore.Http.IResult";
35+
if (compilation.GetTypeByMetadataName(IResult) is not { } iResult)
36+
{
37+
return false;
38+
}
39+
40+
const string IActionResult = "Microsoft.AspNetCore.Mvc.IActionResult";
41+
if (compilation.GetTypeByMetadataName(IActionResult) is not { } iActionResult)
42+
{
43+
return false;
44+
}
45+
46+
const string IConvertToActionResult = "Microsoft.AspNetCore.Mvc.Infrastructure.IConvertToActionResult";
47+
if (compilation.GetTypeByMetadataName(IConvertToActionResult) is not { } iConvertToActionResult)
48+
{
49+
return false;
50+
}
51+
3452
wellKnownTypes = new WellKnownTypes
3553
{
3654
DelegateEndpointRouteBuilderExtensions = delegateEndpointRouteBuilderExtensions,
3755
IBinderTypeProviderMetadata = ibinderTypeProviderMetadata,
3856
BindAttribute = bindAttribute,
57+
IResult = iResult,
58+
IActionResult = iActionResult,
59+
IConvertToActionResult = iConvertToActionResult,
3960
};
4061

4162
return true;
@@ -44,4 +65,7 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out We
4465
public ITypeSymbol DelegateEndpointRouteBuilderExtensions { get; private init; }
4566
public INamedTypeSymbol IBinderTypeProviderMetadata { get; private init; }
4667
public INamedTypeSymbol BindAttribute { get; private init; }
68+
public INamedTypeSymbol IResult { get; private init; }
69+
public INamedTypeSymbol IActionResult { get; private init; }
70+
public INamedTypeSymbol IConvertToActionResult { get; private init; }
4771
}

src/Framework/Analyzer/test/Microsoft.AspNetCore.App.Analyzers.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<ProjectReference Include="$(RepoRoot)src\Analyzers\Microsoft.AspNetCore.Analyzer.Testing\src\Microsoft.AspNetCore.Analyzer.Testing.csproj" />
1616
<Reference Include="Microsoft.AspNetCore" />
1717
<Reference Include="Microsoft.AspNetCore.Mvc" />
18+
<Reference Include="Microsoft.AspNetCore.Http.Results" />
1819
</ItemGroup>
1920

2021
</Project>

src/Framework/Analyzer/test/MinimalActions/DisallowMvcBindArgumentsOnParametersTest.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
using Microsoft.AspNetCore.Analyzer.Testing;
66
using Xunit;
77

8-
namespace Microsoft.AspNetCore.Analyzers.MinimalActions;
8+
namespace Microsoft.AspNetCore.Analyzers.DelegateEndpoints;
99

1010
public partial class DisallowMvcBindArgumentsOnParametersTest
1111
{
12-
private TestDiagnosticAnalyzerRunner Runner { get; } = new(new MinimalActionAnalyzer());
12+
private TestDiagnosticAnalyzerRunner Runner { get; } = new(new DelegateEndpointAnalyzer());
1313

1414
[Fact]
1515
public async Task MinimalAction_WithoutBindAttributes_Works()
@@ -59,9 +59,9 @@ public async Task MinimalAction_Lambda_WithBindAttributes_ProducesDiagnostics()
5959

6060
// Assert
6161
var diagnostic = Assert.Single(diagnostics);
62-
Assert.Same(DiagnosticDescriptors.DoNotUseModelBindingAttributesOnMinimalActionParameters, diagnostic.Descriptor);
62+
Assert.Same(DiagnosticDescriptors.DoNotUseModelBindingAttributesOnDelegateEndpointParameters, diagnostic.Descriptor);
6363
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
64-
Assert.Equal("BindAttribute should not be specified for a MapGet delegate parameter", diagnostic.GetMessage(CultureInfo.InvariantCulture));
64+
Assert.Equal("BindAttribute should not be specified for a MapGet Delegate parameter", diagnostic.GetMessage(CultureInfo.InvariantCulture));
6565
}
6666

6767
[Fact]
@@ -81,9 +81,9 @@ static void PostWithBind(/*MM*/[ModelBinder] string name) {}
8181

8282
// Assert
8383
var diagnostic = Assert.Single(diagnostics);
84-
Assert.Same(DiagnosticDescriptors.DoNotUseModelBindingAttributesOnMinimalActionParameters, diagnostic.Descriptor);
84+
Assert.Same(DiagnosticDescriptors.DoNotUseModelBindingAttributesOnDelegateEndpointParameters, diagnostic.Descriptor);
8585
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
86-
Assert.Equal("ModelBinderAttribute should not be specified for a MapPost delegate parameter", diagnostic.GetMessage(CultureInfo.InvariantCulture));
86+
Assert.Equal("ModelBinderAttribute should not be specified for a MapPost Delegate parameter", diagnostic.GetMessage(CultureInfo.InvariantCulture));
8787
}
8888
}
8989

0 commit comments

Comments
 (0)