Skip to content

Commit 00e49a6

Browse files
authored
Add RequestDelegate analyzer (dotnet#44048)
1 parent 31b8eea commit 00e49a6

File tree

4 files changed

+451
-0
lines changed

4 files changed

+451
-0
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,13 @@ internal static class DiagnosticDescriptors
124124
DiagnosticSeverity.Info,
125125
isEnabledByDefault: true,
126126
helpLinkUri: "https://aka.ms/aspnet/analyzers");
127+
128+
internal static readonly DiagnosticDescriptor DoNotReturnValueFromRequestDelegate = new(
129+
"ASP0016",
130+
"Do not return a value from RequestDelegate",
131+
"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.",
132+
"Usage",
133+
DiagnosticSeverity.Warning,
134+
isEnabledByDefault: true,
135+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
127136
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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 System.Diagnostics.CodeAnalysis;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.Diagnostics;
8+
using Microsoft.CodeAnalysis.Operations;
9+
10+
namespace Microsoft.AspNetCore.Analyzers.Http;
11+
12+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
13+
public partial class RequestDelegateReturnTypeAnalyzer : DiagnosticAnalyzer
14+
{
15+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptors.DoNotReturnValueFromRequestDelegate);
16+
17+
public override void Initialize(AnalysisContext context)
18+
{
19+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
20+
context.EnableConcurrentExecution();
21+
context.RegisterCompilationStartAction(context =>
22+
{
23+
var compilation = context.Compilation;
24+
25+
if (!WellKnownTypes.TryCreate(compilation, out var wellKnownTypes))
26+
{
27+
return;
28+
}
29+
context.RegisterOperationAction(context =>
30+
{
31+
var methodReference = (IMethodReferenceOperation)context.Operation;
32+
if (methodReference.Parent is { } parent &&
33+
parent.Kind == OperationKind.DelegateCreation &&
34+
SymbolEqualityComparer.Default.Equals(parent.Type, wellKnownTypes.RequestDelegate))
35+
{
36+
// Inspect return type of method signature for Task<T>.
37+
var returnType = methodReference.Method.ReturnType;
38+
39+
if (SymbolEqualityComparer.Default.Equals(returnType.OriginalDefinition, wellKnownTypes.TaskOfT))
40+
{
41+
AddDiagnosticWarning(context, methodReference.Syntax.GetLocation(), returnType);
42+
}
43+
}
44+
}, OperationKind.MethodReference);
45+
context.RegisterOperationAction(context =>
46+
{
47+
var anonymousFunction = (IAnonymousFunctionOperation)context.Operation;
48+
if (anonymousFunction.Parent is { } parent &&
49+
parent.Kind == OperationKind.DelegateCreation &&
50+
SymbolEqualityComparer.Default.Equals(parent.Type, wellKnownTypes.RequestDelegate))
51+
{
52+
// Inspect contents of anonymous function and search for return statements.
53+
// Return statement of Task<T> means a value was returned.
54+
foreach (var item in anonymousFunction.Body.Descendants())
55+
{
56+
if (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+
92+
internal sealed class WellKnownTypes
93+
{
94+
public static bool TryCreate(Compilation compilation, [NotNullWhen(returnValue: true)] out WellKnownTypes? wellKnownTypes)
95+
{
96+
wellKnownTypes = default;
97+
98+
const string RequestDelegate = "Microsoft.AspNetCore.Http.RequestDelegate";
99+
if (compilation.GetTypeByMetadataName(RequestDelegate) is not { } requestDelegate)
100+
{
101+
return false;
102+
}
103+
104+
const string TaskOfT = "System.Threading.Tasks.Task`1";
105+
if (compilation.GetTypeByMetadataName(TaskOfT) is not { } taskOfT)
106+
{
107+
return false;
108+
}
109+
110+
wellKnownTypes = new()
111+
{
112+
RequestDelegate = requestDelegate,
113+
TaskOfT = taskOfT
114+
};
115+
116+
return true;
117+
}
118+
119+
public INamedTypeSymbol RequestDelegate { get; private init; }
120+
public INamedTypeSymbol TaskOfT { get; private init; }
121+
}
122+
}

0 commit comments

Comments
 (0)