diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index e6195634513e..34dacca5a243 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -35,6 +35,7 @@ | __`ASP0027`__ | Unnecessary public Program class declaration | | __`ASP0028`__ | Consider using ListenAnyIP() instead of Listen(IPAddress.Any) | | __`ASP0029`__ | Experimental warning for validations resolver APIs | +| __`ASP0030`__ | Use Host.CreateDefaultBuilder instead of WebHost.CreateDefaultBuilder | ### API (`API1000-API1003`) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs index e48dd9445bfc..c2cd9b184e79 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/DiagnosticDescriptors.cs @@ -248,4 +248,13 @@ internal static class DiagnosticDescriptors DiagnosticSeverity.Info, isEnabledByDefault: true, helpLinkUri: AnalyzersLink); + + internal static readonly DiagnosticDescriptor UseCreateHostBuilderInsteadOfCreateWebHostBuilder = new( + "ASP0030", + CreateLocalizableResourceString(nameof(Resources.Analyzer_UseCreateHostBuilderInsteadOfCreateWebHostBuilder_Title)), + CreateLocalizableResourceString(nameof(Resources.Analyzer_UseCreateHostBuilderInsteadOfCreateWebHostBuilder_Message)), + Usage, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: AnalyzersLink); } diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx index 8c9397f5be64..fea73d77333e 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Resources.resx @@ -333,4 +333,10 @@ If the server does not specifically reject IPv6, IPAddress.IPv6Any is preferred over IPAddress.Any usage for safety and performance reasons. See https://aka.ms/aspnetcore-warnings/ASP0028 for more details. + + Use Host.CreateDefaultBuilder instead of WebHost.CreateDefaultBuilder + + + WebHost is deprecated. Use Host.CreateDefaultBuilder and ConfigureWebHostDefaults instead. + diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/UseCreateHostBuilderInsteadOfCreateWebHostBuilderAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/UseCreateHostBuilderInsteadOfCreateWebHostBuilderAnalyzer.cs new file mode 100644 index 000000000000..9e0fea9013c2 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/UseCreateHostBuilderInsteadOfCreateWebHostBuilderAnalyzer.cs @@ -0,0 +1,109 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +using WellKnownType = Microsoft.AspNetCore.App.Analyzers.Infrastructure.WellKnownTypeData.WellKnownType; + +namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class UseCreateHostBuilderInsteadOfCreateWebHostBuilderAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + DiagnosticDescriptors.UseCreateHostBuilderInsteadOfCreateWebHostBuilder + ); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterCompilationStartAction(context => + { + var compilation = context.Compilation; + var wellKnownTypes = WellKnownTypes.GetOrCreate(compilation); + + context.RegisterOperationAction(context => + { + var invocation = (IInvocationOperation)context.Operation; + var targetMethod = invocation.TargetMethod; + + // Check if this is WebHost.CreateDefaultBuilder + if (IsWebHostCreateDefaultBuilderCall(targetMethod, wellKnownTypes)) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptors.UseCreateHostBuilderInsteadOfCreateWebHostBuilder, + invocation.Syntax.GetLocation() + ); + context.ReportDiagnostic(diagnostic); + } + }, OperationKind.Invocation); + + context.RegisterSyntaxNodeAction(context => + { + var methodDeclaration = (MethodDeclarationSyntax)context.Node; + var semantic = context.SemanticModel; + var symbol = semantic.GetDeclaredSymbol(methodDeclaration); + + // Check if this method returns IWebHostBuilder + if (symbol != null && IsWebHostBuilderReturnType(symbol, wellKnownTypes)) + { + // Check if the method body contains WebHost.CreateDefaultBuilder + if (ContainsWebHostCreateDefaultBuilder(methodDeclaration)) + { + var diagnostic = Diagnostic.Create( + DiagnosticDescriptors.UseCreateHostBuilderInsteadOfCreateWebHostBuilder, + methodDeclaration.ReturnType.GetLocation() + ); + context.ReportDiagnostic(diagnostic); + } + } + }, SyntaxKind.MethodDeclaration); + }); + } + + private static bool IsWebHostCreateDefaultBuilderCall(IMethodSymbol method, WellKnownTypes wellKnownTypes) + { + // Check if this is WebHost.CreateDefaultBuilder (not other WebHost methods) + if (method.Name == "CreateDefaultBuilder" && + SymbolEqualityComparer.Default.Equals(method.ContainingType, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_WebHost))) + { + return true; + } + + return false; + } + + private static bool IsWebHostBuilderReturnType(IMethodSymbol method, WellKnownTypes wellKnownTypes) + { + // Check if the return type is IWebHostBuilder + var returnType = method.ReturnType; + return SymbolEqualityComparer.Default.Equals(returnType, wellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Hosting_IWebHostBuilder)); + } + + private static bool ContainsWebHostCreateDefaultBuilder(MethodDeclarationSyntax methodDeclaration) + { + // Check if the method contains WebHost.CreateDefaultBuilder calls + var descendants = methodDeclaration.DescendantNodes().OfType(); + + foreach (var invocation in descendants) + { + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.ValueText == "WebHost" && + memberAccess.Name.Identifier.ValueText == "CreateDefaultBuilder") + { + return true; + } + } + + return false; + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/UseCreateHostBuilderInsteadOfCreateWebHostBuilderFixer.cs b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/UseCreateHostBuilderInsteadOfCreateWebHostBuilderFixer.cs new file mode 100644 index 000000000000..96e6c40d7b6d --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/UseCreateHostBuilderInsteadOfCreateWebHostBuilderFixer.cs @@ -0,0 +1,472 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder.Fixers; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseCreateHostBuilderInsteadOfCreateWebHostBuilderFixer)), Shared] +public sealed class UseCreateHostBuilderInsteadOfCreateWebHostBuilderFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + DiagnosticDescriptors.UseCreateHostBuilderInsteadOfCreateWebHostBuilder.Id + ); + + public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root == null) + { + return; + } + + foreach (var diagnostic in context.Diagnostics) + { + var node = root.FindNode(diagnostic.Location.SourceSpan); + + // Handle method return type case (IWebHostBuilder return type) + if (node is TypeSyntax returnType && + returnType.Parent is MethodDeclarationSyntax methodDeclaration) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Convert to IHostBuilder and use Host.CreateDefaultBuilder", + createChangedDocument: c => ConvertWebHostBuilderMethod(context.Document, methodDeclaration, c), + equivalenceKey: "ConvertWebHostBuilderMethod"), + diagnostic); + } + + // Handle invocation case (WebHost.CreateDefaultBuilder) + else if (node.FirstAncestorOrSelf() is InvocationExpressionSyntax invocation) + { + if (IsWebHostCreateDefaultBuilderInvocation(invocation)) + { + context.RegisterCodeFix( + CodeAction.Create( + title: "Replace with Host.CreateDefaultBuilder", + createChangedDocument: c => ConvertWebHostCreateDefaultBuilder(context.Document, invocation, c), + equivalenceKey: "ConvertWebHostCreateDefaultBuilder"), + diagnostic); + } + } + } + } + + private static bool IsWebHostCreateDefaultBuilderInvocation(InvocationExpressionSyntax invocation) + { + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + return memberAccess.Expression is IdentifierNameSyntax { Identifier.ValueText: "WebHost" } && + memberAccess.Name.Identifier.ValueText == "CreateDefaultBuilder"; + } + return false; + } + + private static async Task ConvertWebHostBuilderMethod(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root == null) + { + return document; + } + + var newMethodDeclaration = methodDeclaration; + + // Change return type from IWebHostBuilder to IHostBuilder + if (IsWebHostBuilderType(methodDeclaration.ReturnType)) + { + var newReturnType = SyntaxFactory.IdentifierName("IHostBuilder"); + newMethodDeclaration = newMethodDeclaration.WithReturnType(newReturnType); + } + + // Transform the method body to use Host.CreateDefaultBuilder and ConfigureWebHostDefaults + if (methodDeclaration.Body != null) + { + var transformedBody = TransformMethodBody(methodDeclaration.Body); + newMethodDeclaration = newMethodDeclaration.WithBody(transformedBody); + } + else if (methodDeclaration.ExpressionBody != null) + { + var transformedExpressionBody = TransformExpressionBody(methodDeclaration.ExpressionBody); + newMethodDeclaration = newMethodDeclaration.WithExpressionBody(transformedExpressionBody); + } + + var newRoot = root.ReplaceNode(methodDeclaration, newMethodDeclaration.WithLeadingTrivia(methodDeclaration.GetLeadingTrivia())); + + // Add the required using statement if not already present + if (root is CompilationUnitSyntax compilationUnit) + { + var hasHostingUsing = compilationUnit.Usings.Any(u => + u.Name?.ToString() == "Microsoft.Extensions.Hosting"); + + if (!hasHostingUsing) + { + var hostingUsing = SyntaxFactory.UsingDirective( + SyntaxFactory.QualifiedName( + SyntaxFactory.QualifiedName( + SyntaxFactory.IdentifierName("Microsoft"), + SyntaxFactory.IdentifierName("Extensions")), + SyntaxFactory.IdentifierName("Hosting"))); + + newRoot = ((CompilationUnitSyntax)newRoot).AddUsings(hostingUsing); + } + } + + return document.WithSyntaxRoot(newRoot); + } + + private static bool IsWebHostBuilderType(TypeSyntax typeSyntax) + { + return typeSyntax.ToString().Equals("IWebHostBuilder"); + } + + private static async Task ConvertWebHostCreateDefaultBuilder(Document document, InvocationExpressionSyntax invocation, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root == null) + { + return document; + } + + // Find the full expression chain that contains this invocation + var fullExpression = GetFullExpressionChain(invocation); + + SyntaxTriviaList leadingTrivia = fullExpression.GetLeadingTrivia(); + if (!fullExpression.HasLeadingTrivia) + { + // Try to find some leading trivia from a parent + // e.g. using (var host = WebHost.CreateDefaultBuilder(args)) would not have leading trivia on the WebHost call + var parent = fullExpression.Parent; + while (parent != null && !parent.HasLeadingTrivia) + { + parent = parent.Parent; + } + leadingTrivia = parent?.GetLeadingTrivia() ?? SyntaxFactory.TriviaList(); + } + + // Transform the entire expression + var transformedExpression = TransformExpression(fullExpression, leadingTrivia); + + var newRoot = root.ReplaceNode(fullExpression, transformedExpression); + + // Add the required using statement if not already present + if (root is CompilationUnitSyntax compilationUnit) + { + var hasHostingUsing = compilationUnit.Usings.Any(u => + u.Name?.ToString() == "Microsoft.Extensions.Hosting"); + + if (!hasHostingUsing) + { + var hostingUsing = SyntaxFactory.UsingDirective( + SyntaxFactory.QualifiedName( + SyntaxFactory.QualifiedName( + SyntaxFactory.IdentifierName("Microsoft"), + SyntaxFactory.IdentifierName("Extensions")), + SyntaxFactory.IdentifierName("Hosting"))); + + newRoot = ((CompilationUnitSyntax)newRoot).AddUsings(hostingUsing); + } + } + + return document.WithSyntaxRoot(newRoot); + } + + private static ExpressionSyntax GetFullExpressionChain(ExpressionSyntax expression) + { + // Walk up the tree to find the root of the method call chain + // The input expression is WebHost.CreateDefaultBuilder(), but we need to find the full chain + var current = expression; + + // Walk up the parent hierarchy to find the outermost invocation in the chain + while (current.Parent != null) + { + if (current.Parent is MemberAccessExpressionSyntax memberAccess && memberAccess.Expression == current) + { + // This current expression is the left side of a member access, so there's more to the chain + if (memberAccess.Parent is InvocationExpressionSyntax parentInvocation) + { + current = parentInvocation; + } + else + { + break; + } + } + else + { + // We've reached the end of the chain + break; + } + } + + return current; + } + + private static BlockSyntax TransformMethodBody(BlockSyntax body) + { + var transformedStatements = body.Statements.Select(TransformStatement).ToArray(); + return body.WithStatements(SyntaxFactory.List(transformedStatements)); + } + + private static ArrowExpressionClauseSyntax TransformExpressionBody(ArrowExpressionClauseSyntax expressionBody) + { + var transformedExpression = TransformExpression(expressionBody.Expression, expressionBody.Expression.GetLeadingTrivia()); + return expressionBody.WithExpression(transformedExpression); + } + + private static StatementSyntax TransformStatement(StatementSyntax statement) + { + if (statement is ReturnStatementSyntax returnStatement && returnStatement.Expression != null) + { + var transformedExpression = TransformExpression(returnStatement.Expression, + new SyntaxTriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed).AddRange(returnStatement.GetLeadingTrivia()).Add(SyntaxFactory.ElasticTab)); + return returnStatement.WithExpression(transformedExpression); + } + return statement; + } + + private static ExpressionSyntax TransformExpression(ExpressionSyntax expression, SyntaxTriviaList leadingTrivia) + { + // Transform WebHost.CreateDefaultBuilder(args).ConfigureServices(...).UseStartup() + // to Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => webBuilder.ConfigureServices(...).UseStartup()) + + // Find the WebHost.CreateDefaultBuilder call and extract everything after it + var (webHostCreateCall, chainedCalls, remainingChain) = ExtractWebHostBuilderChain(expression); + + if (webHostCreateCall != null && chainedCalls.Count > 0) + { + // Create Host.CreateDefaultBuilder + var hostCreateCall = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Host"), + SyntaxFactory.IdentifierName("CreateDefaultBuilder"))) + .WithArgumentList(webHostCreateCall.ArgumentList); + + // Create the webBuilder expression chain + var webBuilderChain = CreateWebBuilderChain(chainedCalls, leadingTrivia); + + // Create the ConfigureWebHostDefaults lambda with proper formatting + var lambda = SyntaxFactory.SimpleLambdaExpression( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("webBuilder")), + webBuilderChain); + + // Create Host.CreateDefaultBuilder().ConfigureWebHostDefaults(...) + var configureCall = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + hostCreateCall.WithLeadingTrivia(leadingTrivia), + SyntaxFactory.IdentifierName("ConfigureWebHostDefaults")) + .WithOperatorToken(SyntaxFactory.Token(SyntaxKind.DotToken) + .WithLeadingTrivia(leadingTrivia) + )) + .WithArgumentList(SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument(lambda)))) + // Adds new line and indentation for remaining chain calls e.g. Build(), Run(), etc. + .WithTrailingTrivia(new SyntaxTriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed).AddRange(leadingTrivia)); + + // If there's a remaining chain (like .Build()), append it + ExpressionSyntax result = configureCall; + if (remainingChain != null) + { + var placeHolder = remainingChain.DescendantNodes().OfType() + .FirstOrDefault(n => n.Identifier.ValueText == "HOST_PLACEHOLDER"); + if (placeHolder != null) + { + // Replace the placeholder with the actual configure call + result = remainingChain.ReplaceNode(placeHolder, configureCall); + } + } + + return result; + } + + // Handle standalone WebHost.CreateDefaultBuilder without chaining + if (expression is InvocationExpressionSyntax invocation && + invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Expression is IdentifierNameSyntax { Identifier.ValueText: "WebHost" } && + memberAccess.Name.Identifier.ValueText == "CreateDefaultBuilder") + { + return SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Host"), + SyntaxFactory.IdentifierName("CreateDefaultBuilder"))) + .WithArgumentList(invocation.ArgumentList) + .WithTrailingTrivia(invocation.GetTrailingTrivia()) + .WithLeadingTrivia(invocation.GetLeadingTrivia()); + } + + return expression; + } + + private static readonly HashSet WebHostBuilderMethods = new HashSet + { + "UseStartup", + "ConfigureServices", + "ConfigureKestrel", + "Configure", + "UseUrls", + "UseContentRoot", + "UseEnvironment", + "UseWebRoot", + "ConfigureLogging", + "ConfigureAppConfiguration", + "UseIISIntegration", + "UseIIS", + "UseKestrel", + "UseSockets", + "UseQuic", + "UseHttpSys", + "UseDefaultServiceProvider" + }; + + private static (InvocationExpressionSyntax? webHostCreateCall, List<(SimpleNameSyntax methodName, ArgumentListSyntax arguments, SyntaxTriviaList leadingTrivia)> chainedCalls, ExpressionSyntax? remainingChain) ExtractWebHostBuilderChain(ExpressionSyntax expression) + { + var chainedCalls = new List<(SimpleNameSyntax methodName, ArgumentListSyntax arguments, SyntaxTriviaList leadingTrivia)>(); + InvocationExpressionSyntax? webHostCreateCall = null; + ExpressionSyntax? remainingChain = null; + + // Walk the expression chain from the top level down to find all method calls + var currentExpr = expression; + var methodCalls = new Stack<(SimpleNameSyntax methodName, ArgumentListSyntax arguments, InvocationExpressionSyntax invocation)>(); + + // Traverse the chain by following invocation expressions + while (currentExpr is InvocationExpressionSyntax invocation) + { + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + var methodName = memberAccess.Name; // This preserves generic arguments + methodCalls.Push((methodName, invocation.ArgumentList, invocation)); + + // Move to the next expression in the chain + currentExpr = memberAccess.Expression; + } + else + { + // This could be the WebHost.CreateDefaultBuilder call itself + break; + } + } + + // Now process the stack to find WebHost.CreateDefaultBuilder and everything after it + bool foundWebHostCall = false; + var nonWebHostMethods = new List<(SimpleNameSyntax methodName, ArgumentListSyntax arguments, InvocationExpressionSyntax invocation)>(); + + while (methodCalls.Count > 0) + { + var (methodName, arguments, invocationExpr) = methodCalls.Pop(); + + if (!foundWebHostCall && methodName.Identifier.ValueText == "CreateDefaultBuilder") + { + // Check if this is WebHost.CreateDefaultBuilder + if (invocationExpr.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Expression is IdentifierNameSyntax identifier && + identifier.Identifier.ValueText == "WebHost") + { + webHostCreateCall = invocationExpr; + foundWebHostCall = true; + } + } + else if (foundWebHostCall) + { + // This is a method chained after WebHost.CreateDefaultBuilder + // Check if it's a WebHostBuilder method that should go inside ConfigureWebHostDefaults + if (WebHostBuilderMethods.Contains(methodName.Identifier.ValueText)) + { + chainedCalls.Add((methodName, arguments, invocationExpr.GetLeadingTrivia())); + } + else + { + // This method should remain outside the lambda (like Build(), Run(), etc.) + nonWebHostMethods.Add((methodName, arguments, invocationExpr)); + // Add any remaining methods to the non-WebHostBuilder list + nonWebHostMethods.AddRange(methodCalls); + methodCalls.Clear(); + // Stop processing once we hit a non-WebHostBuilder method + break; + } + } + } + + // Build the remaining chain from non-WebHostBuilder methods + if (nonWebHostMethods.Count > 0) + { + // Create a placeholder for the Host.CreateDefaultBuilder().ConfigureWebHostDefaults(...) call + // This will be replaced later, but we need something to chain the remaining methods to + var placeholder = SyntaxFactory.IdentifierName("HOST_PLACEHOLDER"); + + // Chain the remaining methods + ExpressionSyntax current = placeholder; + foreach (var (methodName, arguments, invocation) in nonWebHostMethods) + { + SyntaxTriviaList leadingTrivia = default; + if (invocation.Expression is MemberAccessExpressionSyntax memberAccessExpr) + { + leadingTrivia = memberAccessExpr.Expression.GetLeadingTrivia(); + } + + var memberAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + // Since we're appending method calls, + // we need to add trailing trivia after each one to affect the new method calls formatting + current.WithTrailingTrivia(current.GetTrailingTrivia().AddRange(leadingTrivia)), + methodName); + + current = SyntaxFactory.InvocationExpression(memberAccess, arguments); + } + + remainingChain = current; + } + + return (webHostCreateCall, chainedCalls, remainingChain); + } + + private static ExpressionSyntax CreateWebBuilderChain(List<(SimpleNameSyntax methodName, ArgumentListSyntax arguments, SyntaxTriviaList leadingTrivia)> chainedCalls, SyntaxTriviaList leadingTrivia2) + { + if (chainedCalls.Count == 0) + { + return SyntaxFactory.IdentifierName("webBuilder"); + } + + // Start with webBuilder + ExpressionSyntax current = SyntaxFactory.IdentifierName("webBuilder"); + + // Chain all the method calls with proper formatting + for (int i = 0; i < chainedCalls.Count; i++) + { + var (methodName, arguments, leadingTrivia) = chainedCalls[i]; + + var memberAccess = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + current, + methodName); // Use the SimpleNameSyntax directly to preserve generics + + current = SyntaxFactory.InvocationExpression(memberAccess, arguments); + current = current.WithTrailingTrivia(); + + if (i < chainedCalls.Count - 1) + { + // Add a line break and indentation for all but the last method call + var triviaList = new SyntaxTriviaList(SyntaxFactory.ElasticCarriageReturnLineFeed).AddRange(leadingTrivia); + triviaList = triviaList.Add(SyntaxFactory.ElasticTab); + current = current.WithTrailingTrivia(triviaList); + } + } + + return current; + } +} diff --git a/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/UseCreateHostBuilderInsteadOfCreateWebHostBuilderTest.cs b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/UseCreateHostBuilderInsteadOfCreateWebHostBuilderTest.cs new file mode 100644 index 000000000000..49166404d1cf --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/test/WebApplicationBuilder/UseCreateHostBuilderInsteadOfCreateWebHostBuilderTest.cs @@ -0,0 +1,450 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.CodeAnalysis.Testing; +using VerifyAnalyzer = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpAnalyzerVerifier< + Microsoft.AspNetCore.Analyzers.WebApplicationBuilder.UseCreateHostBuilderInsteadOfCreateWebHostBuilderAnalyzer>; +using VerifyCS = Microsoft.AspNetCore.Analyzers.Verifiers.CSharpCodeFixVerifier< + Microsoft.AspNetCore.Analyzers.WebApplicationBuilder.UseCreateHostBuilderInsteadOfCreateWebHostBuilderAnalyzer, + Microsoft.AspNetCore.Analyzers.WebApplicationBuilder.Fixers.UseCreateHostBuilderInsteadOfCreateWebHostBuilderFixer>; + +namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder; + +public class UseCreateHostBuilderInsteadOfCreateWebHostBuilderTest +{ + [Fact] + public async Task DoesNotWarnWhenUsingHostCreateDefaultBuilder() + { + // Arrange + var source = @" +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Builder; +public static class Program +{ + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { }); +} +"; + // Assert + await VerifyCS.VerifyCodeFixAsync(source, source); + } + + [Fact] + public async Task WarnsWhenUsingWebHostCreateDefaultBuilder() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +public static class Program +{ + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static {|#0:IWebHostBuilder|} CreateWebHostBuilder(string[] args) => + {|#1:WebHost.CreateDefaultBuilder(args)|} + .UseStartup(); +} +public class Startup { } +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +using Microsoft.Extensions.Hosting; + +public static class Program +{ + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateWebHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); +} +public class Startup { } +"; + + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseCreateHostBuilderInsteadOfCreateWebHostBuilder) + .WithMessage(Resources.Analyzer_UseCreateHostBuilderInsteadOfCreateWebHostBuilder_Message); + + var expectedDiagnostics = new[] + { + diagnostic.WithLocation(0), + diagnostic.WithLocation(1), + }; + + // Assert + await VerifyCS.VerifyCodeFixAsync(source, expectedDiagnostics, fixedSource); + } + + [Fact] + public async Task WarnsWhenUsingCreateWebHostBuilderWithBlockBody() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +public static class Program +{ + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static {|#0:IWebHostBuilder|} CreateWebHostBuilder(string[] args) + { + return {|#1:WebHost.CreateDefaultBuilder(args)|}.UseStartup(); + } +} +public class Startup { } +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +using Microsoft.Extensions.Hosting; + +public static class Program +{ + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateWebHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); + } +} +public class Startup { } +"; + + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseCreateHostBuilderInsteadOfCreateWebHostBuilder) + .WithMessage(Resources.Analyzer_UseCreateHostBuilderInsteadOfCreateWebHostBuilder_Message); + + var expectedDiagnostics = new[] + { + diagnostic.WithLocation(0), + diagnostic.WithLocation(1), + }; + + // Assert + await VerifyCS.VerifyCodeFixAsync(source, expectedDiagnostics, fixedSource); + } + + [Fact] + public async Task WarnsOnlyForWebHostCreateDefaultBuilder() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +public static class Program +{ + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static {|#0:IWebHostBuilder|} CreateWebHostBuilder(string[] args) => + {|#1:WebHost.CreateDefaultBuilder(args)|}.UseStartup(); + + public static void StartWeb() => + WebHost.Start(""http://localhost:5000"", (c) => { }); +} +public class Startup { } +"; + + var expectedDiagnostics = new[] + { + VerifyAnalyzer.Diagnostic().WithLocation(0), + VerifyAnalyzer.Diagnostic().WithLocation(1) + }; + + await VerifyAnalyzer.VerifyAnalyzerAsync(source, expectedDiagnostics); + } + + [Fact] + public async Task WarnsForMultipleMethods() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +public static class Program +{ + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static {|#0:IWebHostBuilder|} CreateWebHostBuilder(string[] args) => + {|#1:WebHost.CreateDefaultBuilder(args)|}.UseStartup(); + + public static {|#2:IWebHostBuilder|} CreateWebHostBuilderForTesting(string[] args) => + {|#3:WebHost.CreateDefaultBuilder(args)|}.UseStartup(); +} +public class Startup { } +"; + + var expectedDiagnostics = new[] + { + VerifyAnalyzer.Diagnostic().WithLocation(0), + VerifyAnalyzer.Diagnostic().WithLocation(1), + VerifyAnalyzer.Diagnostic().WithLocation(2), + VerifyAnalyzer.Diagnostic().WithLocation(3) + }; + + await VerifyAnalyzer.VerifyAnalyzerAsync(source, expectedDiagnostics); + } + + [Fact] + public async Task WarnsForInstanceMethod() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +public class Program +{ + public static void Main(string[] args) + { + } + + public {|#0:IWebHostBuilder|} CreateWebHostBuilder(string[] args) => + {|#1:WebHost.CreateDefaultBuilder(args)|}.UseStartup(); +} +public class Startup { } +"; + + var expectedDiagnostics = new[] + { + VerifyAnalyzer.Diagnostic().WithLocation(0), + VerifyAnalyzer.Diagnostic().WithLocation(1) + }; + + // No diagnostics expected + await VerifyAnalyzer.VerifyAnalyzerAsync(source, expectedDiagnostics); + } + + [Fact] + public async Task WarnsForPrivateMethods() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +public static class Program +{ + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + private static {|#0:IWebHostBuilder|} CreateWebHostBuilder(string[] args) => + {|#1:WebHost.CreateDefaultBuilder(args)|}.UseStartup(); +} +public class Startup { } +"; + + var expectedDiagnostics = new[] + { + VerifyAnalyzer.Diagnostic().WithLocation(0), + VerifyAnalyzer.Diagnostic().WithLocation(1) + }; + + // No diagnostics expected + await VerifyAnalyzer.VerifyAnalyzerAsync(source, expectedDiagnostics); + } + + [Fact] + public async Task CodeFixWorksInsideUsingStatement() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +public static class Program +{ + public static void Main(string[] args) + { + using (var host = {|#0:WebHost.CreateDefaultBuilder(args)|} + .UseStartup() + .Build()) + { + host.Run(); + } + } +} +public class Startup { } +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +using Microsoft.Extensions.Hosting; + +public static class Program +{ + public static void Main(string[] args) + { + using (var host = Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) + .Build()) + { + host.Run(); + } + } +} +public class Startup { } +"; + + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseCreateHostBuilderInsteadOfCreateWebHostBuilder) + .WithMessage(Resources.Analyzer_UseCreateHostBuilderInsteadOfCreateWebHostBuilder_Message); + + var expectedDiagnostics = new[] + { + diagnostic.WithLocation(0), + }; + + // Assert + await VerifyCS.VerifyCodeFixAsync(source, expectedDiagnostics, fixedSource); + } + + [Fact] + public async Task FixerWorksWithMultipleNonWebHostChainedMethods() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +public static class Program +{ + public static void Main(string[] args) + { + {|#0:WebHost.CreateDefaultBuilder(args)|} + .UseStartup() + .Build() + .RunAsync(default); + } +} +public class Startup { } +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +using Microsoft.Extensions.Hosting; + +public static class Program +{ + public static void Main(string[] args) + { + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) + .Build() + .RunAsync(default); + } +} +public class Startup { } +"; + + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseCreateHostBuilderInsteadOfCreateWebHostBuilder) + .WithMessage(Resources.Analyzer_UseCreateHostBuilderInsteadOfCreateWebHostBuilder_Message); + + var expectedDiagnostics = new[] + { + diagnostic.WithLocation(0), + }; + + // Assert + await VerifyCS.VerifyCodeFixAsync(source, expectedDiagnostics, fixedSource); + } + + [Fact] + public async Task CodeFixWorksWithManyChainedCalls() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +public static class Program +{ + public static void Main(string[] args) + { + {|#0:WebHost.CreateDefaultBuilder(new[] { ""--cliKey"", ""cliValue"" })|} + .ConfigureServices((context, service) => { }) + .ConfigureKestrel(options => + options.Configure(options.ConfigurationLoader.Configuration)) + .Configure(app => + { + }); + } +} +"; + + var fixedSource = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore; +using Microsoft.Extensions.Hosting; + +public static class Program +{ + public static void Main(string[] args) + { + Host.CreateDefaultBuilder(new[] { ""--cliKey"", ""cliValue"" }) + .ConfigureWebHostDefaults(webBuilder => webBuilder.ConfigureServices((context, service) => { }) + .ConfigureKestrel(options => + options.Configure(options.ConfigurationLoader.Configuration)) + .Configure(app => + { + })); + } +} +"; + + var diagnostic = new DiagnosticResult(DiagnosticDescriptors.UseCreateHostBuilderInsteadOfCreateWebHostBuilder) + .WithMessage(Resources.Analyzer_UseCreateHostBuilderInsteadOfCreateWebHostBuilder_Message); + + var expectedDiagnostics = new[] + { + diagnostic.WithLocation(0), + }; + + // Assert + await VerifyCS.VerifyCodeFixAsync(source, expectedDiagnostics, fixedSource); + } + + [Fact] + public async Task DoesNotWarnForIWebHostBuilderMethodWithoutWebHostUsage() + { + // Arrange + var source = @" +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Builder; +public static class Program +{ + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => null!; +} +"; + // Assert + await VerifyCS.VerifyCodeFixAsync(source, source); + } +} diff --git a/src/Shared/RoslynUtils/WellKnownTypeData.cs b/src/Shared/RoslynUtils/WellKnownTypeData.cs index 1afb045fc713..c2fa022bc37f 100644 --- a/src/Shared/RoslynUtils/WellKnownTypeData.cs +++ b/src/Shared/RoslynUtils/WellKnownTypeData.cs @@ -124,6 +124,8 @@ public enum WellKnownType System_ComponentModel_DataAnnotations_RequiredAttribute, System_ComponentModel_DataAnnotations_CustomValidationAttribute, System_Type, + Microsoft_AspNetCore_Hosting_IWebHostBuilder, + Microsoft_AspNetCore_WebHost, } public static string[] WellKnownTypeNames = @@ -245,5 +247,7 @@ public enum WellKnownType "System.ComponentModel.DataAnnotations.RequiredAttribute", "System.ComponentModel.DataAnnotations.CustomValidationAttribute", "System.Type", + "Microsoft.AspNetCore.Hosting.IWebHostBuilder", + "Microsoft.AspNetCore.WebHost", ]; }