Skip to content

Commit bc064f5

Browse files
Implement analyzers for WebApplicationBuilder usage (dotnet#35884)
Add analyzers that warn on incorrect usage of UseStartup(), Configure() and ConfigureWebHost() when using WebApplicationBuilder. Resolves dotnet#35814.
1 parent fb75a95 commit bc064f5

8 files changed

+940
-1
lines changed

Diff for: src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs

+27
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,31 @@ internal static class DiagnosticDescriptors
5252
DiagnosticSeverity.Warning,
5353
isEnabledByDefault: true,
5454
helpLinkUri: "https://aka.ms/aspnet/analyzers");
55+
56+
internal static readonly DiagnosticDescriptor DoNotUseConfigureWebHostWithConfigureHostBuilder = new(
57+
"ASP0008",
58+
"Do not use ConfigureWebHost with WebApplicationBuilder.Host",
59+
"ConfigureWebHost cannot be used with WebApplicationBuilder.Host",
60+
"Usage",
61+
DiagnosticSeverity.Error,
62+
isEnabledByDefault: true,
63+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
64+
65+
internal static readonly DiagnosticDescriptor DoNotUseConfigureWithConfigureWebHostBuilder = new(
66+
"ASP0009",
67+
"Do not use Configure with WebApplicationBuilder.WebHost",
68+
"Configure cannot be used with WebApplicationBuilder.WebHost",
69+
"Usage",
70+
DiagnosticSeverity.Error,
71+
isEnabledByDefault: true,
72+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
73+
74+
internal static readonly DiagnosticDescriptor DoNotUseUseStartupWithConfigureWebHostBuilder = new(
75+
"ASP0010",
76+
"Do not use UseStartup with WebApplicationBuilder.WebHost",
77+
"UseStartup cannot be used with WebApplicationBuilder.WebHost",
78+
"Usage",
79+
DiagnosticSeverity.Error,
80+
isEnabledByDefault: true,
81+
helpLinkUri: "https://aka.ms/aspnet/analyzers");
5582
}

Diff for: src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectMisplacedLambdaAttribute.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ private static void DetectMisplacedLambdaAttribute(
6767
}
6868
}
6969

70-
bool IsInValidNamespace(INamespaceSymbol? @namespace)
70+
static bool IsInValidNamespace(INamespaceSymbol? @namespace)
7171
{
7272
if (@namespace != null && !@namespace.IsGlobalNamespace)
7373
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
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;
5+
using System.Collections.Immutable;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Diagnostics;
11+
using Microsoft.CodeAnalysis.Operations;
12+
using Microsoft.CodeAnalysis.Text;
13+
14+
namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder;
15+
16+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
17+
public class WebApplicationBuilderAnalyzer : DiagnosticAnalyzer
18+
{
19+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(new[]
20+
{
21+
DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder,
22+
DiagnosticDescriptors.DoNotUseConfigureWithConfigureWebHostBuilder,
23+
DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder,
24+
});
25+
26+
public override void Initialize(AnalysisContext context)
27+
{
28+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
29+
context.EnableConcurrentExecution();
30+
31+
context.RegisterCompilationStartAction(compilationStartAnalysisContext =>
32+
{
33+
var compilation = compilationStartAnalysisContext.Compilation;
34+
if (!WellKnownTypes.TryCreate(compilation, out var wellKnownTypes))
35+
{
36+
Debug.Fail("One or more types could not be found. This usually means you are bad at spelling C# type names.");
37+
return;
38+
}
39+
40+
INamedTypeSymbol[] configureTypes = { wellKnownTypes.WebHostBuilderExtensions };
41+
INamedTypeSymbol[] configureWebHostTypes = { wellKnownTypes.GenericHostWebHostBuilderExtensions };
42+
INamedTypeSymbol[] userStartupTypes =
43+
{
44+
wellKnownTypes.HostingAbstractionsWebHostBuilderExtensions,
45+
wellKnownTypes.WebHostBuilderExtensions,
46+
};
47+
48+
compilationStartAnalysisContext.RegisterOperationAction(operationAnalysisContext =>
49+
{
50+
var invocation = (IInvocationOperation)operationAnalysisContext.Operation;
51+
var targetMethod = invocation.TargetMethod;
52+
53+
// var builder = WebApplication.CreateBuilder();
54+
// builder.Host.ConfigureWebHost(x => {});
55+
if (IsDisallowedMethod(
56+
operationAnalysisContext,
57+
invocation,
58+
targetMethod,
59+
wellKnownTypes.ConfigureHostBuilder,
60+
"ConfigureWebHost",
61+
configureWebHostTypes))
62+
{
63+
operationAnalysisContext.ReportDiagnostic(
64+
CreateDiagnostic(
65+
DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder,
66+
invocation));
67+
}
68+
69+
// var builder = WebApplication.CreateBuilder();
70+
// builder.WebHost.Configure(x => {});
71+
if (IsDisallowedMethod(
72+
operationAnalysisContext,
73+
invocation,
74+
targetMethod,
75+
wellKnownTypes.ConfigureWebHostBuilder,
76+
"Configure",
77+
configureTypes))
78+
{
79+
operationAnalysisContext.ReportDiagnostic(
80+
CreateDiagnostic(
81+
DiagnosticDescriptors.DoNotUseConfigureWithConfigureWebHostBuilder,
82+
invocation));
83+
}
84+
85+
// var builder = WebApplication.CreateBuilder();
86+
// builder.WebHost.UseStartup<Startup>();
87+
if (IsDisallowedMethod(
88+
operationAnalysisContext,
89+
invocation,
90+
targetMethod,
91+
wellKnownTypes.ConfigureWebHostBuilder,
92+
"UseStartup",
93+
userStartupTypes))
94+
{
95+
operationAnalysisContext.ReportDiagnostic(
96+
CreateDiagnostic(
97+
DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder,
98+
invocation));
99+
}
100+
101+
static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, IInvocationOperation operation)
102+
{
103+
// Take the location for the whole invocation operation as a starting point.
104+
var location = operation.Syntax.GetLocation();
105+
106+
// As we're analyzing an extension method that might be chained off a number of
107+
// properties, we need the location to be where the invocation of the targeted
108+
// extension method is, not the beginning of the line where the chain begins.
109+
// So in the example `foo.bar.Baz(x => {})` we want the span to be for `Baz(x => {})`.
110+
// Otherwise the location can contain other unrelated bits of an invocation chain.
111+
// Take for example the below block of C#.
112+
//
113+
// builder.Host
114+
// .ConfigureWebHost(webHostBuilder => { })
115+
// .ConfigureSomethingElse()
116+
// .ConfigureYetAnotherThing(x => x());
117+
//
118+
// If we did not just select the method name, the location would end up including
119+
// the start of the chain and the leading trivia before the method invocation:
120+
//
121+
// builder.Host
122+
// .ConfigureWebHost(webHostBuilder => { })
123+
//
124+
// IdentifierNameSyntax finds non-generic methods (e.g. `Foo()`), whereas
125+
// GenericNameSyntax finds generic methods (e.g. `Foo<T>()`).
126+
var methodName = operation.Syntax
127+
.DescendantNodes()
128+
.OfType<SimpleNameSyntax>()
129+
.Where(node => node is IdentifierNameSyntax || node is GenericNameSyntax)
130+
.Where(node => string.Equals(node.Identifier.Value as string, operation.TargetMethod.Name, StringComparison.Ordinal))
131+
.FirstOrDefault();
132+
133+
if (methodName is not null)
134+
{
135+
// If we found the method's name, we can truncate the original location
136+
// of any leading chain and any trivia to leave the location as the method
137+
// invocation and its arguments: `ConfigureWebHost(webHostBuilder => { })`
138+
var methodLocation = methodName.GetLocation();
139+
140+
var fullSyntaxLength = location.SourceSpan.Length;
141+
var chainAndTriviaLength = methodLocation.SourceSpan.Start - location.SourceSpan.Start;
142+
143+
var targetSpan = new TextSpan(
144+
methodLocation.SourceSpan.Start,
145+
fullSyntaxLength - chainAndTriviaLength);
146+
147+
location = Location.Create(operation.Syntax.SyntaxTree, targetSpan);
148+
}
149+
150+
return Diagnostic.Create(descriptor, location);
151+
}
152+
153+
}, OperationKind.Invocation);
154+
});
155+
}
156+
157+
private static bool IsDisallowedMethod(
158+
in OperationAnalysisContext context,
159+
IInvocationOperation invocation,
160+
IMethodSymbol methodSymbol,
161+
INamedTypeSymbol disallowedReceiverType,
162+
string disallowedMethodName,
163+
INamedTypeSymbol[] disallowedMethodTypes)
164+
{
165+
if (!IsDisallowedMethod(methodSymbol, disallowedMethodName, disallowedMethodTypes))
166+
{
167+
return false;
168+
}
169+
170+
var receiverType = invocation.GetReceiverType(context.CancellationToken);
171+
172+
if (!SymbolEqualityComparer.Default.Equals(receiverType, disallowedReceiverType))
173+
{
174+
return false;
175+
}
176+
177+
return true;
178+
179+
static bool IsDisallowedMethod(
180+
IMethodSymbol methodSymbol,
181+
string disallowedMethodName,
182+
INamedTypeSymbol[] disallowedMethodTypes)
183+
{
184+
if (!string.Equals(methodSymbol?.Name, disallowedMethodName, StringComparison.Ordinal))
185+
{
186+
return false;
187+
}
188+
189+
var length = disallowedMethodTypes.Length;
190+
for (var i = 0; i < length; i++)
191+
{
192+
var type = disallowedMethodTypes[i];
193+
if (SymbolEqualityComparer.Default.Equals(type, methodSymbol.ContainingType))
194+
{
195+
return true;
196+
}
197+
}
198+
199+
return false;
200+
}
201+
}
202+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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.WebApplicationBuilder;
8+
9+
internal sealed class WellKnownTypes
10+
{
11+
public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out WellKnownTypes? wellKnownTypes)
12+
{
13+
wellKnownTypes = default;
14+
15+
const string ConfigureHostBuilder = "Microsoft.AspNetCore.Builder.ConfigureHostBuilder";
16+
if (compilation.GetTypeByMetadataName(ConfigureHostBuilder) is not { } configureHostBuilder)
17+
{
18+
return false;
19+
}
20+
21+
const string ConfigureWebHostBuilder = "Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder";
22+
if (compilation.GetTypeByMetadataName(ConfigureWebHostBuilder) is not { } configureWebHostBuilder)
23+
{
24+
return false;
25+
}
26+
27+
const string GenericHostWebHostBuilderExtensions = "Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions";
28+
if (compilation.GetTypeByMetadataName(GenericHostWebHostBuilderExtensions) is not { } genericHostWebHostBuilderExtensions)
29+
{
30+
return false;
31+
}
32+
33+
const string WebHostBuilderExtensions = "Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions";
34+
if (compilation.GetTypeByMetadataName(WebHostBuilderExtensions) is not { } webHostBuilderExtensions)
35+
{
36+
return false;
37+
}
38+
39+
const string HostingAbstractionsWebHostBuilderExtensions = "Microsoft.AspNetCore.Hosting.HostingAbstractionsWebHostBuilderExtensions";
40+
if (compilation.GetTypeByMetadataName(HostingAbstractionsWebHostBuilderExtensions) is not { } hostingAbstractionsWebHostBuilderExtensions)
41+
{
42+
return false;
43+
}
44+
45+
wellKnownTypes = new WellKnownTypes
46+
{
47+
ConfigureHostBuilder = configureHostBuilder,
48+
ConfigureWebHostBuilder = configureWebHostBuilder,
49+
GenericHostWebHostBuilderExtensions = genericHostWebHostBuilderExtensions,
50+
HostingAbstractionsWebHostBuilderExtensions = hostingAbstractionsWebHostBuilderExtensions,
51+
WebHostBuilderExtensions = webHostBuilderExtensions,
52+
};
53+
54+
return true;
55+
}
56+
57+
public INamedTypeSymbol ConfigureHostBuilder { get; private init; }
58+
public INamedTypeSymbol ConfigureWebHostBuilder { get; private init; }
59+
public INamedTypeSymbol GenericHostWebHostBuilderExtensions { get; private init; }
60+
public INamedTypeSymbol HostingAbstractionsWebHostBuilderExtensions { get; private init; }
61+
public INamedTypeSymbol WebHostBuilderExtensions { get; private init; }
62+
}

0 commit comments

Comments
 (0)