From 79f725139eff7194a5de7087bf3718cc6d4a1a49 Mon Sep 17 00:00:00 2001 From: martincostello Date: Sat, 6 Nov 2021 10:01:09 +0000 Subject: [PATCH] Implement analyzers for WebApplicationBuilder usage Add analyzers that warn on incorrect usage of UseStartup(), Configure() and ConfigureWebHost() when using WebApplicationBuilder. Resolves #35814. --- .../src/Analyzers/DiagnosticDescriptors.cs | 27 ++ .../DetectMisplacedLambdaAttribute.cs | 4 +- .../WebApplicationBuilderAnalyzer.cs | 202 +++++++++++++++ .../WebApplicationBuilder/WellKnownTypes.cs | 62 +++++ ...onfigureHostBuilderConfigureWebHostTest.cs | 240 ++++++++++++++++++ ...lowConfigureWebHostBuilderConfigureTest.cs | 162 ++++++++++++ ...owConfigureWebHostBuilderUseStartupTest.cs | 209 +++++++++++++++ src/Shared/Roslyn/CodeAnalysisExtensions.cs | 37 +++ 8 files changed, 941 insertions(+), 2 deletions(-) create mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs create mode 100644 src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WellKnownTypes.cs create mode 100644 src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureHostBuilderConfigureWebHostTest.cs create mode 100644 src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureWebHostBuilderConfigureTest.cs create mode 100644 src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureWebHostBuilderUseStartupTest.cs diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs index b27bf54204f5..c01db137deaa 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs @@ -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"); } diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectMisplacedLambdaAttribute.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectMisplacedLambdaAttribute.cs index f4754c9a00d9..96ecd8297224 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectMisplacedLambdaAttribute.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectMisplacedLambdaAttribute.cs @@ -67,7 +67,7 @@ private static void DetectMisplacedLambdaAttribute( } } - bool IsInValidNamespace(INamespaceSymbol? @namespace) + static bool IsInValidNamespace(INamespaceSymbol? @namespace) { if (@namespace != null && !@namespace.IsGlobalNamespace) { @@ -83,4 +83,4 @@ bool IsInValidNamespace(INamespaceSymbol? @namespace) return false; } } -} \ No newline at end of file +} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs new file mode 100644 index 000000000000..01cfdc829a71 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs @@ -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 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(); + 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()`). + var methodName = operation.Syntax + .DescendantNodes() + .OfType() + .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; + } + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WellKnownTypes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WellKnownTypes.cs new file mode 100644 index 000000000000..d4c43aa2a843 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WellKnownTypes.cs @@ -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; } +} diff --git a/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureHostBuilderConfigureWebHostTest.cs b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureHostBuilderConfigureWebHostTest.cs new file mode 100644 index 000000000000..569960684c2e --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureHostBuilderConfigureWebHostTest.cs @@ -0,0 +1,240 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.AspNetCore.Analyzer.Testing; + +namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder; + +public partial class DisallowConfigureHostBuilderConfigureWebHostTest +{ + private TestDiagnosticAnalyzerRunner Runner { get; } = new(new WebApplicationBuilderAnalyzer()); + + [Fact] + public async Task WebApplicationBuilder_HostWithoutConfigureWebHost_Works() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.Host.ConfigureServices(services => { }); +"; + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + // Assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WebApplicationBuilder_HostWithConfigureWebHost_ProducesDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.Host./*MM*/ConfigureWebHost(webHostBuilder => { }); +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("ConfigureWebHost cannot be used with WebApplicationBuilder.Host", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_HostWithConfigureWebHost_ProducesDiagnostics_OnDifferentLine() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.Host. + /*MM*/ConfigureWebHost(webHostBuilder => { }); +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("ConfigureWebHost cannot be used with WebApplicationBuilder.Host", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_HostWithConfigureWebHost_ProducesDiagnostics_WhenChained() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.Host + ./*MM*/ConfigureWebHost(webHostBuilder => { }) + .ConfigureServices(services => { }); +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("ConfigureWebHost cannot be used with WebApplicationBuilder.Host", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_HostWithConfigureWebHost_DoesNotProduceDiagnostics_WhenChained() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.Host + .ConfigureServices(services => { }) // Because ConfigureServices() returns IHostBuilder, the type gets erased + .ConfigureWebHost(webHostBuilder => { }); +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WebApplicationBuilder_HostWithConfigureWebHostWithOptions_ProducesDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.Host./*MM*/ConfigureWebHost(webHostBuilder => { }, optionsBuilder => { }); +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("ConfigureWebHost cannot be used with WebApplicationBuilder.Host", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithConfigureWebHostOnProperty_ProducesDiagnostics_In_Program_Main() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Host./*MM*/ConfigureWebHost(webHostBuilder => { }); + } +} +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("ConfigureWebHost cannot be used with WebApplicationBuilder.Host", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithConfigureWebHostOnBuilder_ProducesDiagnostics_In_Program_Main() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var host = builder.Host; + host./*MM*/ConfigureWebHost(webHostBuilder => { }); + } +} +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("ConfigureWebHost cannot be used with WebApplicationBuilder.Host", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithConfigureWebHostInsideOtherMethod_ProducesDiagnostics_In_Program_Main() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + ConfigureHost(builder.Host); + } + + private static void ConfigureHost(ConfigureHostBuilder hostBuilder) + { + hostBuilder + ./*MM*/ConfigureWebHost(webHostBuilder => { }); + } +} +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWebHostWithConfigureHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("ConfigureWebHost cannot be used with WebApplicationBuilder.Host", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task HostBuilder_ConfigureWebHost_DoesNotProduceDiagnostic() + { + // Arrange + var source = @" +using Microsoft.Extensions.Hosting; +var builder = Host.CreateDefaultBuilder(); +builder.ConfigureWebHost(webHostBuilder => { }); +builder.ConfigureWebHost(webHostBuilder => { }, optionsBuilder => { }); +"; + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + // Assert + Assert.Empty(diagnostics); + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureWebHostBuilderConfigureTest.cs b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureWebHostBuilderConfigureTest.cs new file mode 100644 index 000000000000..0e4276745b00 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureWebHostBuilderConfigureTest.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.AspNetCore.Analyzer.Testing; + +namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder; + +public partial class DisallowConfigureWebHostBuilderConfigureTest +{ + private TestDiagnosticAnalyzerRunner Runner { get; } = new(new WebApplicationBuilderAnalyzer()); + + [Fact] + public async Task WebApplicationBuilder_WebHostWithoutConfigure_Works() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.WebHost.ConfigureKestrel(options => { }); +"; + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + // Assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithConfigure_ProducesDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.WebHost./*MM*/Configure(webHostBuilder => { }); +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("Configure cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithConfigureWithContext_ProducesDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.WebHost./*MM*/Configure((context, webHostBuilder) => { }); +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("Configure cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithConfigureOnProperty_ProducesDiagnostics_In_Program_Main() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.WebHost./*MM*/Configure((context, webHostBuilder) => { }); + } +} +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("Configure cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithConfigureOnBuilder_ProducesDiagnostics_In_Program_Main() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var webHost = builder.WebHost; + webHost./*MM*/Configure((context, webHostBuilder) => { }); + } +} +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseConfigureWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("Configure cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task HostBuilder_WebHostBuilder_Configure_DoesNotProduceDiagnostic() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +var builder = Host.CreateDefaultBuilder(); +builder.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.Configure(configure => { })); +"; + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + // Assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WebHostBuilder_Configure_DoesNotProduceDiagnostic() + { + // Arrange + var source = @" +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +var builder = WebHost.CreateDefaultBuilder(); +builder.Configure(configure => { }); +builder.Configure((context, webHostBuilder) => { }); +"; + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + // Assert + Assert.Empty(diagnostics); + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureWebHostBuilderUseStartupTest.cs b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureWebHostBuilderUseStartupTest.cs new file mode 100644 index 000000000000..7ba830a05fcc --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/DisallowConfigureWebHostBuilderUseStartupTest.cs @@ -0,0 +1,209 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using Microsoft.AspNetCore.Analyzer.Testing; + +namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder; + +public partial class DisallowConfigureWebHostBuilderUseStartupTest +{ + private TestDiagnosticAnalyzerRunner Runner { get; } = new(new WebApplicationBuilderAnalyzer()); + + [Fact] + public async Task WebApplicationBuilder_WebHostWithoutUseStartup_Works() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.WebHost.ConfigureKestrel(options => { }); +"; + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + // Assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithoutUseStartupGenericType_ProducesDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.WebHost./*MM*/UseStartup(); +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("UseStartup cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithoutUseStartupType_ProducesDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.WebHost./*MM*/UseStartup(typeof(Startup)); +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("UseStartup cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithoutUseStartupString_ProducesDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.WebHost./*MM*/UseStartup(""Startup""); +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("UseStartup cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithoutUseStartupGenericTypeWithContext_ProducesDiagnostics() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +var builder = WebApplication.CreateBuilder(); +builder.WebHost./*MM*/UseStartup(context => new Startup()); +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("UseStartup cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithUseStartupOnProperty_ProducesDiagnostics_In_Program_Main() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.WebHost./*MM*/UseStartup(); + } +} +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("UseStartup cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task WebApplicationBuilder_WebHostWithUseStartupOnBuilder_ProducesDiagnostics_In_Program_Main() + { + // Arrange + var source = TestSource.Read(@" +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +public static class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + var webHost = builder.WebHost; + webHost./*MM*/UseStartup(); + } +} +public class Startup { } +"); + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source.Source); + + // Assert + var diagnostic = Assert.Single(diagnostics); + Assert.Same(DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder, diagnostic.Descriptor); + AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location); + Assert.Equal("UseStartup cannot be used with WebApplicationBuilder.WebHost", diagnostic.GetMessage(CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task HostBuilder_WebHostBuilder_UseStartup_DoesNotProduceDiagnostic() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +var builder = Host.CreateDefaultBuilder(); +builder.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.UseStartup()); +builder.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.UseStartup(typeof(Startup))); +builder.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.UseStartup(""Startup"")); +builder.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.UseStartup(context => new Startup())); +public class Startup { } +"; + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + // Assert + Assert.Empty(diagnostics); + } + + [Fact] + public async Task WebHostBuilder_UseStartup_DoesNotProduceDiagnostic() + { + // Arrange + var source = @" +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +var builder = WebHost.CreateDefaultBuilder(); +builder.UseStartup(); +builder.UseStartup(typeof(Startup)); +builder.UseStartup(""Startup""); +builder.UseStartup(context => new Startup()); +public class Startup { } +"; + // Act + var diagnostics = await Runner.GetDiagnosticsAsync(source); + + // Assert + Assert.Empty(diagnostics); + } +} diff --git a/src/Shared/Roslyn/CodeAnalysisExtensions.cs b/src/Shared/Roslyn/CodeAnalysisExtensions.cs index 453138c08cbb..2dd3a6cfb66d 100644 --- a/src/Shared/Roslyn/CodeAnalysisExtensions.cs +++ b/src/Shared/Roslyn/CodeAnalysisExtensions.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis.Operations; namespace Microsoft.CodeAnalysis; @@ -169,4 +171,39 @@ public static IEnumerable GetAllMethodSymbolsOfPartialParts(this yield return method; } } + + // Adapted from IOperationExtensions.GetReceiverType in dotnet/roslyn-analyzers. + // See https://github.com/dotnet/roslyn-analyzers/blob/762b08948cdcc1d94352fba681296be7bf474dd7/src/Utilities/Compiler/Extensions/IOperationExtensions.cs#L22-L51 + public static INamedTypeSymbol? GetReceiverType( + this IInvocationOperation invocation, + CancellationToken cancellationToken) + { + if (invocation.Instance != null) + { + return GetReceiverType(invocation.Instance.Syntax, invocation.SemanticModel, cancellationToken); + } + else if (invocation.TargetMethod.IsExtensionMethod && !invocation.TargetMethod.Parameters.IsEmpty) + { + var firstArg = invocation.Arguments.FirstOrDefault(); + if (firstArg != null) + { + return GetReceiverType(firstArg.Value.Syntax, invocation.SemanticModel, cancellationToken); + } + else if (invocation.TargetMethod.Parameters[0].IsParams) + { + return invocation.TargetMethod.Parameters[0].Type as INamedTypeSymbol; + } + } + + return null; + + static INamedTypeSymbol? GetReceiverType( + SyntaxNode receiverSyntax, + SemanticModel? model, + CancellationToken cancellationToken) + { + var typeInfo = model?.GetTypeInfo(receiverSyntax, cancellationToken); + return typeInfo?.Type as INamedTypeSymbol; + } + } }