Skip to content

Warn that UseStartup/Configure/ConfigureWebHost not supported on WebApplicationBuilder #35884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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 @@ -52,4 +52,31 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");

internal static readonly DiagnosticDescriptor DoNotUseConfigureWebHostWithConfigureHostBuilder = new(
"ASP0008",
"Do not use ConfigureWebHost with WebApplicationBuilder.Host",
"ConfigureWebHost cannot be used with WebApplicationBuilder.Host",
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");

internal static readonly DiagnosticDescriptor DoNotUseConfigureWithConfigureWebHostBuilder = new(
"ASP0009",
"Do not use Configure with WebApplicationBuilder.WebHost",
"Configure cannot be used with WebApplicationBuilder.WebHost",
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");

internal static readonly DiagnosticDescriptor DoNotUseUseStartupWithConfigureWebHostBuilder = new(
"ASP0010",
"Do not use UseStartup with WebApplicationBuilder.WebHost",
"UseStartup cannot be used with WebApplicationBuilder.WebHost",
"Usage",
DiagnosticSeverity.Error,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ private static void DetectMisplacedLambdaAttribute(
}
}

bool IsInValidNamespace(INamespaceSymbol? @namespace)
static bool IsInValidNamespace(INamespaceSymbol? @namespace)
{
if (@namespace != null && [email protected])
{
Expand All @@ -83,4 +83,4 @@ bool IsInValidNamespace(INamespaceSymbol? @namespace)
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class WebApplicationBuilderAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(new[]
{
DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder,
DiagnosticDescriptors.DoNotUseConfigureWithConfigureWebHostBuilder,
DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder,
});

public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

context.RegisterCompilationStartAction(compilationStartAnalysisContext =>
{
var compilation = compilationStartAnalysisContext.Compilation;
if (!WellKnownTypes.TryCreate(compilation, out var wellKnownTypes))
{
Debug.Fail("One or more types could not be found. This usually means you are bad at spelling C# type names.");
return;
}

INamedTypeSymbol[] configureTypes = { wellKnownTypes.WebHostBuilderExtensions };
INamedTypeSymbol[] configureWebHostTypes = { wellKnownTypes.GenericHostWebHostBuilderExtensions };
INamedTypeSymbol[] userStartupTypes =
{
wellKnownTypes.HostingAbstractionsWebHostBuilderExtensions,
wellKnownTypes.WebHostBuilderExtensions,
};

compilationStartAnalysisContext.RegisterOperationAction(operationAnalysisContext =>
{
var invocation = (IInvocationOperation)operationAnalysisContext.Operation;
var targetMethod = invocation.TargetMethod;

// var builder = WebApplication.CreateBuilder();
// builder.Host.ConfigureWebHost(x => {});
if (IsDisallowedMethod(
operationAnalysisContext,
invocation,
targetMethod,
wellKnownTypes.ConfigureHostBuilder,
"ConfigureWebHost",
configureWebHostTypes))
{
operationAnalysisContext.ReportDiagnostic(
CreateDiagnostic(
DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder,
invocation));
}

// var builder = WebApplication.CreateBuilder();
// builder.WebHost.Configure(x => {});
if (IsDisallowedMethod(
operationAnalysisContext,
invocation,
targetMethod,
wellKnownTypes.ConfigureWebHostBuilder,
"Configure",
configureTypes))
{
operationAnalysisContext.ReportDiagnostic(
CreateDiagnostic(
DiagnosticDescriptors.DoNotUseConfigureWithConfigureWebHostBuilder,
invocation));
}

// var builder = WebApplication.CreateBuilder();
// builder.WebHost.UseStartup<Startup>();
if (IsDisallowedMethod(
operationAnalysisContext,
invocation,
targetMethod,
wellKnownTypes.ConfigureWebHostBuilder,
"UseStartup",
userStartupTypes))
{
operationAnalysisContext.ReportDiagnostic(
CreateDiagnostic(
DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder,
invocation));
}

static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, IInvocationOperation operation)
{
// Take the location for the whole invocation operation as a starting point.
var location = operation.Syntax.GetLocation();

// As we're analyzing an extension method that might be chained off a number of
// properties, we need the location to be where the invocation of the targeted
// extension method is, not the beginning of the line where the chain begins.
// So in the example `foo.bar.Baz(x => {})` we want the span to be for `Baz(x => {})`.
// Otherwise the location can contain other unrelated bits of an invocation chain.
// Take for example the below block of C#.
//
// builder.Host
// .ConfigureWebHost(webHostBuilder => { })
// .ConfigureSomethingElse()
// .ConfigureYetAnotherThing(x => x());
//
// If we did not just select the method name, the location would end up including
// the start of the chain and the leading trivia before the method invocation:
//
// builder.Host
// .ConfigureWebHost(webHostBuilder => { })
//
// IdentifierNameSyntax finds non-generic methods (e.g. `Foo()`), whereas
// GenericNameSyntax finds generic methods (e.g. `Foo<T>()`).
var methodName = operation.Syntax
.DescendantNodes()
.OfType<SimpleNameSyntax>()
.Where(node => node is IdentifierNameSyntax || node is GenericNameSyntax)
.Where(node => string.Equals(node.Identifier.Value as string, operation.TargetMethod.Name, StringComparison.Ordinal))
.FirstOrDefault();

if (methodName is not null)
{
// If we found the method's name, we can truncate the original location
// of any leading chain and any trivia to leave the location as the method
// invocation and its arguments: `ConfigureWebHost(webHostBuilder => { })`
var methodLocation = methodName.GetLocation();

var fullSyntaxLength = location.SourceSpan.Length;
var chainAndTriviaLength = methodLocation.SourceSpan.Start - location.SourceSpan.Start;

var targetSpan = new TextSpan(
methodLocation.SourceSpan.Start,
fullSyntaxLength - chainAndTriviaLength);

location = Location.Create(operation.Syntax.SyntaxTree, targetSpan);
}

return Diagnostic.Create(descriptor, location);
}

}, OperationKind.Invocation);
});
}

private static bool IsDisallowedMethod(
in OperationAnalysisContext context,
IInvocationOperation invocation,
IMethodSymbol methodSymbol,
INamedTypeSymbol disallowedReceiverType,
string disallowedMethodName,
INamedTypeSymbol[] disallowedMethodTypes)
{
if (!IsDisallowedMethod(methodSymbol, disallowedMethodName, disallowedMethodTypes))
{
return false;
}

var receiverType = invocation.GetReceiverType(context.CancellationToken);

if (!SymbolEqualityComparer.Default.Equals(receiverType, disallowedReceiverType))
{
return false;
}

return true;

static bool IsDisallowedMethod(
IMethodSymbol methodSymbol,
string disallowedMethodName,
INamedTypeSymbol[] disallowedMethodTypes)
{
if (!string.Equals(methodSymbol?.Name, disallowedMethodName, StringComparison.Ordinal))
{
return false;
}

var length = disallowedMethodTypes.Length;
for (var i = 0; i < length; i++)
{
var type = disallowedMethodTypes[i];
if (SymbolEqualityComparer.Default.Equals(type, methodSymbol.ContainingType))
{
return true;
}
}

return false;
}
}
}
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.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;

namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder;

internal sealed class WellKnownTypes
{
public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out WellKnownTypes? wellKnownTypes)
{
wellKnownTypes = default;

const string ConfigureHostBuilder = "Microsoft.AspNetCore.Builder.ConfigureHostBuilder";
if (compilation.GetTypeByMetadataName(ConfigureHostBuilder) is not { } configureHostBuilder)
{
return false;
}

const string ConfigureWebHostBuilder = "Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder";
if (compilation.GetTypeByMetadataName(ConfigureWebHostBuilder) is not { } configureWebHostBuilder)
{
return false;
}

const string GenericHostWebHostBuilderExtensions = "Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions";
if (compilation.GetTypeByMetadataName(GenericHostWebHostBuilderExtensions) is not { } genericHostWebHostBuilderExtensions)
{
return false;
}

const string WebHostBuilderExtensions = "Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions";
if (compilation.GetTypeByMetadataName(WebHostBuilderExtensions) is not { } webHostBuilderExtensions)
{
return false;
}

const string HostingAbstractionsWebHostBuilderExtensions = "Microsoft.AspNetCore.Hosting.HostingAbstractionsWebHostBuilderExtensions";
if (compilation.GetTypeByMetadataName(HostingAbstractionsWebHostBuilderExtensions) is not { } hostingAbstractionsWebHostBuilderExtensions)
{
return false;
}

wellKnownTypes = new WellKnownTypes
{
ConfigureHostBuilder = configureHostBuilder,
ConfigureWebHostBuilder = configureWebHostBuilder,
GenericHostWebHostBuilderExtensions = genericHostWebHostBuilderExtensions,
HostingAbstractionsWebHostBuilderExtensions = hostingAbstractionsWebHostBuilderExtensions,
WebHostBuilderExtensions = webHostBuilderExtensions,
};

return true;
}

public INamedTypeSymbol ConfigureHostBuilder { get; private init; }
public INamedTypeSymbol ConfigureWebHostBuilder { get; private init; }
public INamedTypeSymbol GenericHostWebHostBuilderExtensions { get; private init; }
public INamedTypeSymbol HostingAbstractionsWebHostBuilderExtensions { get; private init; }
public INamedTypeSymbol WebHostBuilderExtensions { get; private init; }
}
Loading