Skip to content

Add analyzer to suggest top level route registration #42937

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
merged 6 commits into from
Aug 9, 2022
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 @@ -106,4 +106,13 @@ internal static class DiagnosticDescriptors
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");

internal static readonly DiagnosticDescriptor UseTopLevelRouteRegistrationsInsteadOfUseEndpoints = new(
"ASP0014",
"Suggest using top level route registrations",
"Suggest using top level route registrations instead of {0}",
"Usage",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
helpLinkUri: "https://aka.ms/aspnet/analyzers");
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class WebApplicationBuilderAnalyzer : DiagnosticAnalyzer
DiagnosticDescriptors.DoNotUseUseStartupWithConfigureWebHostBuilder,
DiagnosticDescriptors.DoNotUseHostConfigureLogging,
DiagnosticDescriptors.DoNotUseHostConfigureServices,
DiagnosticDescriptors.DisallowConfigureAppConfigureHostBuilder
DiagnosticDescriptors.DisallowConfigureAppConfigureHostBuilder,
DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints
);

public override void Initialize(AnalysisContext context)
Expand Down Expand Up @@ -64,6 +65,11 @@ public override void Initialize(AnalysisContext context)
wellKnownTypes.HostingHostBuilderExtensions,
};
INamedTypeSymbol[] configureHostTypes = { wellKnownTypes.ConfigureHostBuilder };
INamedTypeSymbol[] useEndpointTypes =
{
wellKnownTypes.EndpointRoutingApplicationBuilderExtensions,
wellKnownTypes.WebApplicationBuilder
};

context.RegisterOperationAction(context =>
{
Expand Down Expand Up @@ -230,6 +236,24 @@ public override void Initialize(AnalysisContext context)
invocation));
}

//var builder = WebApplication.CreateBuilder(args);
//var app= builder.Build();
//app.UseRouting();
//app.UseEndpoints(x => {})
if (IsDisallowedMethod(
context,
invocation,
targetMethod,
wellKnownTypes.WebApplicationBuilder,
"UseEndpoints",
useEndpointTypes))
{
context.ReportDiagnostic(
CreateDiagnostic(
DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints,
invocation));
}

static Diagnostic CreateDiagnostic(DiagnosticDescriptor descriptor, IInvocationOperation operation)
{
// Take the location for the whole invocation operation as a starting point.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,28 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out We
return false;
}

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

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

wellKnownTypes = new WellKnownTypes
{
ConfigureHostBuilder = configureHostBuilder,
ConfigureWebHostBuilder = configureWebHostBuilder,
GenericHostWebHostBuilderExtensions = genericHostWebHostBuilderExtensions,
HostingAbstractionsWebHostBuilderExtensions = hostingAbstractionsWebHostBuilderExtensions,
WebHostBuilderExtensions = webHostBuilderExtensions,
HostingHostBuilderExtensions = hostingHostBuilderExtensions
HostingHostBuilderExtensions = hostingHostBuilderExtensions,
EndpointRoutingApplicationBuilderExtensions = endpointRoutingApplicationBuilderExtensions,
WebApplicationBuilder = webApplicationBuilder
};

return true;
Expand All @@ -67,4 +81,6 @@ public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out We
public INamedTypeSymbol HostingAbstractionsWebHostBuilderExtensions { get; private init; }
public INamedTypeSymbol WebHostBuilderExtensions { get; private init; }
public INamedTypeSymbol HostingHostBuilderExtensions { get; private init; }
public INamedTypeSymbol EndpointRoutingApplicationBuilderExtensions { get; private init; }
public INamedTypeSymbol WebApplicationBuilder { get; private init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// 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.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Analyzer.Testing;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder;
public partial class UseTopLevelRouteRegistrationsInsteadOfUseEndpointsTest
{
private TestDiagnosticAnalyzerRunner Runner { get; } = new(new WebApplicationBuilderAnalyzer());

[Fact]
public async Task DoesNotWarnWhenEndpointRegistrationIsTopLevel()
{
//arrange
var source = @"
using Microsoft.AspNetCore.Builder;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseRouting();
app.MapGet(""/"", () => ""Hello World!"");
";
//act
var diagnostics = await Runner.GetDiagnosticsAsync(source);

//assert
Assert.Empty(diagnostics);
}

[Fact]
public async Task DoesNotWarnWhenEnpointRegistrationIsTopLevel_InMain()
{
//arrange
var source = @"
using Microsoft.AspNetCore.Builder;
public static class Program
{
public static void Main (string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseRouting();
app.MapGet(""/"", () => ""Hello World!"");
}
}
";
//act
var diagnostics = await Runner.GetDiagnosticsAsync(source);

//assert
Assert.Empty(diagnostics);
}

[Fact]
public async Task WarnsWhenEndpointRegistrationIsNotTopLevel()
{
//arrange
var source = TestSource.Read(@"
using Microsoft.AspNetCore.Builder;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseRouting();
app./*MM*/UseEndpoints(endpoints =>
{
endpoints.MapGet(""/"", () => ""Hello World!"");
});
");
//act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);

//assert
var diagnostic = Assert.Single(diagnostics);
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic.GetMessage(CultureInfo.InvariantCulture));
}

[Fact]
public async Task WarnsWhenEndpointRegistrationIsNotTopLevel_OtherMapMethods()
{
//arrange
var source = TestSource.Read(@"
using Microsoft.AspNetCore.Builder;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseRouting();
app./*MM1*/UseEndpoints(endpoints =>
{
endpoints.MapGet(""/"", () => ""This is a GET"");
});
app./*MM2*/UseEndpoints(endpoints =>
{
endpoints.MapPost(""/"", () => ""This is a POST"");
});
app./*MM3*/UseEndpoints(endpoints =>
{
endpoints.MapPut(""/"", () => ""This is a PUT"");
});
app./*MM4*/UseEndpoints(endpoints =>
{
endpoints.MapDelete(""/"", () => ""This is a DELETE"");
});
");
//act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);

//assert
Assert.Equal(4, diagnostics.Length);
var diagnostic1 = diagnostics[0];
var diagnostic2 = diagnostics[1];
var diagnostic3 = diagnostics[2];
var diagnostic4 = diagnostics[3];

Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic1.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic1.Location);
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic1.GetMessage(CultureInfo.InvariantCulture));

Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic2.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM2"], diagnostic2.Location);
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic2.GetMessage(CultureInfo.InvariantCulture));

Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic3.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM3"], diagnostic3.Location);
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic3.GetMessage(CultureInfo.InvariantCulture));

Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic2.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM4"], diagnostic4.Location);
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic2.GetMessage(CultureInfo.InvariantCulture));
}

[Fact]
public async Task WarnsWhenEndpointRegistrationIsNotTopLevel_InMain_MapControllers()
{
//arrange
var source = TestSource.Read(@"
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
public static class Program
{
public static void Main (string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseRouting();
app./*MM*/UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
");
//act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);

//assert
var diagnostic = Assert.Single(diagnostics);
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic.GetMessage(CultureInfo.InvariantCulture));
}

[Fact]
public async Task WarnsWhenEndpointRegistrationIsNotTopLevel_OnDifferentLine_WithRouteParameters()
{
//arrange
var source = TestSource.Read(@"
using Microsoft.AspNetCore.Builder;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseRouting();
app.
/*MM*/UseEndpoints(endpoints =>
{
endpoints.MapGet(""/users/{userId}/books/{bookId}"",
(int userId, int bookId) => $""The user id is {userId} and book id is {bookId}"");
});
");
//act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);

//assert
var diagnostic = Assert.Single(diagnostics);
Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.DefaultMarkerLocation, diagnostic.Location);
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic.GetMessage(CultureInfo.InvariantCulture));
}

[Fact]
public async Task WarnsTwiceWhenEndpointRegistrationIsNotTopLevel_OnDifferentLine()
{
//arrange
var source = TestSource.Read(@"
using Microsoft.AspNetCore.Builder;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseRouting();
app./*MM1*/UseEndpoints(endpoints =>
{
endpoints.MapGet(""/"", () => ""Hello World!"");
});
app./*MM2*/UseEndpoints(endpoints =>
{
endpoints.MapGet(""/"", () => ""Hello World!"");
});
");
//act
var diagnostics = await Runner.GetDiagnosticsAsync(source.Source);
//assert
Assert.Equal(2, diagnostics.Length);
var diagnostic1 = diagnostics[0];
var diagnostic2 = diagnostics[1];

Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic1.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM1"], diagnostic1.Location);
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic1.GetMessage(CultureInfo.InvariantCulture));

Assert.Same(DiagnosticDescriptors.UseTopLevelRouteRegistrationsInsteadOfUseEndpoints, diagnostic2.Descriptor);
AnalyzerAssert.DiagnosticLocation(source.MarkerLocations["MM2"], diagnostic2.Location);
Assert.Equal("Suggest using top level route registrations instead of UseEndpoints", diagnostic2.GetMessage(CultureInfo.InvariantCulture));
}
}