Skip to content

Commit c382b1c

Browse files
committed
Add RequestDelegate analyzer
1 parent 1ab188a commit c382b1c

File tree

5 files changed

+581
-3
lines changed

5 files changed

+581
-3
lines changed

eng/Versions.props

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,9 @@
207207
-->
208208
<Analyzer_MicrosoftCodeAnalysisCSharpVersion>3.3.1</Analyzer_MicrosoftCodeAnalysisCSharpVersion>
209209
<Analyzer_MicrosoftCodeAnalysisCSharpWorkspacesVersion>3.3.1</Analyzer_MicrosoftCodeAnalysisCSharpWorkspacesVersion>
210-
<MicrosoftCodeAnalysisCommonVersion>4.2.0-2.22128.1</MicrosoftCodeAnalysisCommonVersion>
211-
<MicrosoftCodeAnalysisCSharpVersion>4.2.0-2.22128.1</MicrosoftCodeAnalysisCSharpVersion>
212-
<MicrosoftCodeAnalysisCSharpWorkspacesVersion>4.2.0-2.22128.1</MicrosoftCodeAnalysisCSharpWorkspacesVersion>
210+
<MicrosoftCodeAnalysisCommonVersion>4.4.0-2.22458.3</MicrosoftCodeAnalysisCommonVersion>
211+
<MicrosoftCodeAnalysisCSharpVersion>4.4.0-2.22458.3</MicrosoftCodeAnalysisCSharpVersion>
212+
<MicrosoftCodeAnalysisCSharpWorkspacesVersion>4.4.0-2.22458.3</MicrosoftCodeAnalysisCSharpWorkspacesVersion>
213213
<MicrosoftCodeAnalysisPublicApiAnalyzersVersion>3.3.3</MicrosoftCodeAnalysisPublicApiAnalyzersVersion>
214214
<MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion>1.1.2-beta1.22276.1</MicrosoftCodeAnalysisCSharpAnalyzerTestingXUnitVersion>
215215
<MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion>1.1.2-beta1.22276.1</MicrosoftCodeAnalysisCSharpCodeFixTestingXUnitVersion>

src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,13 @@ internal static class DiagnosticDescriptors
115115
DiagnosticSeverity.Warning,
116116
isEnabledByDefault: true,
117117
helpLinkUri: "https://aka.ms/aspnet/analyzers");
118+
119+
internal static readonly DiagnosticDescriptor DoNotReturnValueFromRequestDelegate = new(
120+
"ASP0015",
121+
"Do not return a value from RequestDelegate",
122+
"The method used to create a RequestDelegate returns Task<{0}>. RequestDelegate discards this value. If this isn't intended then don't return a value or change the method signature to not match RequestDelegate.",
123+
"Usage",
124+
DiagnosticSeverity.Warning,
125+
isEnabledByDefault: true,
126+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
118127
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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.Collections.Immutable;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.Diagnostics;
7+
using Microsoft.CodeAnalysis.Operations;
8+
9+
namespace Microsoft.AspNetCore.Analyzers.Http;
10+
11+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
12+
public partial class RequestDelegateReturnTypeAnalyzer : DiagnosticAnalyzer
13+
{
14+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.DoNotReturnValueFromRequestDelegate);
15+
16+
public override void Initialize(AnalysisContext context)
17+
{
18+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
19+
context.EnableConcurrentExecution();
20+
context.RegisterCompilationStartAction(context =>
21+
{
22+
var compilation = context.Compilation;
23+
24+
if (!WellKnownTypes.TryCreate(compilation, out var wellKnownTypes))
25+
{
26+
return;
27+
}
28+
context.RegisterOperationAction(context =>
29+
{
30+
var methodReference = (IMethodReferenceOperation)context.Operation;
31+
if (methodReference.Parent is { } parent &&
32+
parent.Kind == OperationKind.DelegateCreation &&
33+
SymbolEqualityComparer.Default.Equals(parent.Type, wellKnownTypes.RequestDelegate))
34+
{
35+
// Inspect return type of method signature for Task<T>.
36+
var returnType = methodReference.Method.ReturnType;
37+
38+
if (SymbolEqualityComparer.Default.Equals(returnType.OriginalDefinition, wellKnownTypes.TaskOfT))
39+
{
40+
AddDiagnosticWarning(context, methodReference.Syntax.GetLocation(), returnType);
41+
}
42+
}
43+
}, OperationKind.MethodReference);
44+
context.RegisterOperationAction(context =>
45+
{
46+
var anonymousFunction = (IAnonymousFunctionOperation)context.Operation;
47+
if (anonymousFunction.Parent is { } parent &&
48+
parent.Kind == OperationKind.DelegateCreation &&
49+
SymbolEqualityComparer.Default.Equals(parent.Type, wellKnownTypes.RequestDelegate))
50+
{
51+
// Inspect contents of anonymous function and search for return statements.
52+
// Return statement of Task<T> means a value was returned.
53+
foreach (var item in anonymousFunction.Body.Descendants())
54+
{
55+
if (item.Kind == OperationKind.Return &&
56+
item is IReturnOperation returnOperation &&
57+
returnOperation.ReturnedValue is { } returnedValue)
58+
{
59+
var resolvedOperation = WalkDownConversion(returnedValue);
60+
var returnType = resolvedOperation.Type;
61+
62+
if (SymbolEqualityComparer.Default.Equals(returnType.OriginalDefinition, wellKnownTypes.TaskOfT))
63+
{
64+
AddDiagnosticWarning(context, anonymousFunction.Syntax.GetLocation(), returnType);
65+
return;
66+
}
67+
}
68+
}
69+
}
70+
}, OperationKind.AnonymousFunction);
71+
});
72+
}
73+
74+
private static void AddDiagnosticWarning(OperationAnalysisContext context, Location location, ITypeSymbol returnType)
75+
{
76+
context.ReportDiagnostic(Diagnostic.Create(
77+
DiagnosticDescriptors.DoNotReturnValueFromRequestDelegate,
78+
location,
79+
((INamedTypeSymbol)returnType).TypeArguments[0].ToString()));
80+
}
81+
82+
private static IOperation WalkDownConversion(IOperation operation)
83+
{
84+
while (operation is IConversionOperation conversionOperation)
85+
{
86+
operation = conversionOperation.Operand;
87+
}
88+
89+
return operation;
90+
}
91+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.Diagnostics.CodeAnalysis;
5+
using Microsoft.CodeAnalysis;
6+
7+
namespace Microsoft.AspNetCore.Analyzers.Http;
8+
9+
internal sealed class WellKnownTypes
10+
{
11+
public static bool TryCreate(Compilation compilation, [NotNullWhen(returnValue: true)] out WellKnownTypes? wellKnownTypes)
12+
{
13+
wellKnownTypes = default;
14+
15+
const string RequestDelegate = "Microsoft.AspNetCore.Http.RequestDelegate";
16+
if (compilation.GetTypeByMetadataName(RequestDelegate) is not { } requestDelegate)
17+
{
18+
return false;
19+
}
20+
21+
const string TaskOfT = "System.Threading.Tasks.Task`1";
22+
if (compilation.GetTypeByMetadataName(TaskOfT) is not { } taskOfT)
23+
{
24+
return false;
25+
}
26+
27+
wellKnownTypes = new()
28+
{
29+
RequestDelegate = requestDelegate,
30+
TaskOfT = taskOfT
31+
};
32+
33+
return true;
34+
}
35+
36+
public INamedTypeSymbol RequestDelegate { get; private init; }
37+
public INamedTypeSymbol TaskOfT { get; private init; }
38+
}

0 commit comments

Comments
 (0)