diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/HeaderDictionaryAddAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/HeaderDictionaryAddAnalyzer.cs index 64c859541f7d..adf6e40cf0cb 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/HeaderDictionaryAddAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/HeaderDictionaryAddAnalyzer.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.Http; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class HeaderDictionaryAddAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/RequestDelegateReturnTypeAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/RequestDelegateReturnTypeAnalyzer.cs index fc710b8a4cce..9ce7c69a9ecb 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/RequestDelegateReturnTypeAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Http/RequestDelegateReturnTypeAnalyzer.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.Http; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public partial class RequestDelegateReturnTypeAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/ParsabilityHelper.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/ParsabilityHelper.cs index 654aa2a38707..124832884c96 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/ParsabilityHelper.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/ParsabilityHelper.cs @@ -12,6 +12,8 @@ namespace Microsoft.AspNetCore.Analyzers.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal static class ParsabilityHelper { private static bool IsTypeAlwaysParsableOrBindable(ITypeSymbol typeSymbol, WellKnownTypes wellKnownTypes) @@ -160,7 +162,7 @@ internal static Bindability GetBindability(INamedTypeSymbol typeSymbol, WellKnow { return Bindability.InvalidReturnType; } - + } return Bindability.NotBindable; diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypeData.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypeData.cs new file mode 100644 index 000000000000..d22c74e2d029 --- /dev/null +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypeData.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.App.Analyzers.Infrastructure; + +internal static class WellKnownTypeData +{ + public enum WellKnownType + { + Microsoft_AspNetCore_Components_Rendering_RenderTreeBuilder, + Microsoft_AspNetCore_Http_IHeaderDictionary, + Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata, + Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata, + Microsoft_AspNetCore_Http_HeaderDictionaryExtensions, + Microsoft_AspNetCore_Routing_IEndpointRouteBuilder, + Microsoft_AspNetCore_Mvc_ControllerAttribute, + Microsoft_AspNetCore_Mvc_NonControllerAttribute, + Microsoft_AspNetCore_Mvc_NonActionAttribute, + Microsoft_AspNetCore_Http_AsParametersAttribute, + System_Threading_CancellationToken, + Microsoft_AspNetCore_Http_HttpContext, + Microsoft_AspNetCore_Http_HttpRequest, + Microsoft_AspNetCore_Http_HttpResponse, + System_Security_Claims_ClaimsPrincipal, + Microsoft_AspNetCore_Http_IFormFileCollection, + Microsoft_AspNetCore_Http_IFormFile, + System_IO_Stream, + System_IO_Pipelines_PipeReader, + System_IFormatProvider, + System_Uri, + Microsoft_AspNetCore_Builder_ConfigureHostBuilder, + Microsoft_AspNetCore_Builder_ConfigureWebHostBuilder, + Microsoft_Extensions_Hosting_GenericHostWebHostBuilderExtensions, + Microsoft_AspNetCore_Hosting_WebHostBuilderExtensions, + Microsoft_AspNetCore_Hosting_HostingAbstractionsWebHostBuilderExtensions, + Microsoft_Extensions_Hosting_HostingHostBuilderExtensions, + Microsoft_AspNetCore_Builder_EndpointRoutingApplicationBuilderExtensions, + Microsoft_AspNetCore_Builder_WebApplication, + Microsoft_AspNetCore_Builder_EndpointRouteBuilderExtensions, + System_Delegate, + Microsoft_AspNetCore_Mvc_ModelBinding_IBinderTypeProviderMetadata, + Microsoft_AspNetCore_Mvc_BindAttribute, + Microsoft_AspNetCore_Http_IResult, + Microsoft_AspNetCore_Mvc_IActionResult, + Microsoft_AspNetCore_Mvc_Infrastructure_IConvertToActionResult, + Microsoft_AspNetCore_Http_RequestDelegate, + System_Threading_Tasks_Task_T, + System_Threading_Tasks_ValueTask_T, + System_Reflection_ParameterInfo, + Microsoft_AspNetCore_Http_IBindableFromHttpContext_T, + System_IParsable_T, + Microsoft_AspNetCore_Builder_AuthorizationEndpointConventionBuilderExtensions, + Microsoft_AspNetCore_Http_OpenApiRouteHandlerBuilderExtensions, + Microsoft_AspNetCore_Builder_CorsEndpointConventionBuilderExtensions, + Microsoft_Extensions_DependencyInjection_OutputCacheConventionBuilderExtensions, + Microsoft_AspNetCore_Builder_RateLimiterEndpointConventionBuilderExtensions, + Microsoft_AspNetCore_Builder_RoutingEndpointConventionBuilderExtensions, + Microsoft_AspNetCore_Mvc_RouteAttribute, + Microsoft_AspNetCore_Mvc_HttpDeleteAttribute, + Microsoft_AspNetCore_Mvc_HttpGetAttribute, + Microsoft_AspNetCore_Mvc_HttpHeadAttribute, + Microsoft_AspNetCore_Mvc_HttpOptionsAttribute, + Microsoft_AspNetCore_Mvc_HttpPatchAttribute, + Microsoft_AspNetCore_Mvc_HttpPostAttribute, + Microsoft_AspNetCore_Mvc_HttpPutAttribute, + Microsoft_AspNetCore_Http_EndpointDescriptionAttribute, + Microsoft_AspNetCore_Http_EndpointSummaryAttribute, + Microsoft_AspNetCore_Http_TagsAttribute, + Microsoft_AspNetCore_Routing_EndpointGroupNameAttribute, + Microsoft_AspNetCore_Routing_EndpointNameAttribute, + Microsoft_AspNetCore_Routing_ExcludeFromDescriptionAttribute, + Microsoft_AspNetCore_Cors_DisableCorsAttribute, + Microsoft_AspNetCore_Cors_EnableCorsAttribute, + Microsoft_AspNetCore_OutputCaching_OutputCacheAttribute, + Microsoft_AspNetCore_RateLimiting_DisableRateLimitingAttribute, + Microsoft_AspNetCore_RateLimiting_EnableRateLimitingAttribute, + Microsoft_AspNetCore_Mvc_ActionNameAttribute, + Microsoft_AspNetCore_Mvc_DisableRequestSizeLimitAttribute, + Microsoft_AspNetCore_Mvc_FormatFilterAttribute, + Microsoft_AspNetCore_Mvc_ProducesAttribute, + Microsoft_AspNetCore_Mvc_ProducesDefaultResponseTypeAttribute, + Microsoft_AspNetCore_Mvc_ProducesErrorResponseTypeAttribute, + Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute, + Microsoft_AspNetCore_Mvc_RequestFormLimitsAttribute, + Microsoft_AspNetCore_Mvc_RequestSizeLimitAttribute, + Microsoft_AspNetCore_Mvc_RequireHttpsAttribute, + Microsoft_AspNetCore_Mvc_ResponseCacheAttribute, + Microsoft_AspNetCore_Mvc_ServiceFilterAttribute, + Microsoft_AspNetCore_Mvc_TypeFilterAttribute, + Microsoft_AspNetCore_Mvc_ApiExplorer_ApiConventionNameMatchAttribute, + Microsoft_AspNetCore_Mvc_Filters_ResultFilterAttribute, + Microsoft_AspNetCore_Mvc_Infrastructure_DefaultStatusCodeAttribute, + Microsoft_AspNetCore_Mvc_AutoValidateAntiforgeryTokenAttribute, + Microsoft_AspNetCore_Mvc_IgnoreAntiforgeryTokenAttribute, + Microsoft_AspNetCore_Mvc_ViewFeatures_SaveTempDataAttribute, + Microsoft_AspNetCore_Mvc_SkipStatusCodePagesAttribute, + Microsoft_AspNetCore_Mvc_ValidateAntiForgeryTokenAttribute, + Microsoft_AspNetCore_Authorization_AllowAnonymousAttribute, + Microsoft_AspNetCore_Authorization_AuthorizeAttribute + } + + public static string[] WellKnownTypeNames = new[] + { + "Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder", + "Microsoft.AspNetCore.Http.IHeaderDictionary", + "Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata", + "Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata", + "Microsoft.AspNetCore.Http.HeaderDictionaryExtensions", + "Microsoft.AspNetCore.Routing.IEndpointRouteBuilder", + "Microsoft.AspNetCore.Mvc.ControllerAttribute", + "Microsoft.AspNetCore.Mvc.NonControllerAttribute", + "Microsoft.AspNetCore.Mvc.NonActionAttribute", + "Microsoft.AspNetCore.Http.AsParametersAttribute", + "System.Threading.CancellationToken", + "Microsoft.AspNetCore.Http.HttpContext", + "Microsoft.AspNetCore.Http.HttpRequest", + "Microsoft.AspNetCore.Http.HttpResponse", + "System.Security.Claims.ClaimsPrincipal", + "Microsoft.AspNetCore.Http.IFormFileCollection", + "Microsoft.AspNetCore.Http.IFormFile", + "System.IO.Stream", + "System.IO.Pipelines.PipeReader", + "System.IFormatProvider", + "System.Uri", + "Microsoft.AspNetCore.Builder.ConfigureHostBuilder", + "Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder", + "Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions", + "Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions", + "Microsoft.AspNetCore.Hosting.HostingAbstractionsWebHostBuilderExtensions", + "Microsoft.Extensions.Hosting.HostingHostBuilderExtensions", + "Microsoft.AspNetCore.Builder.EndpointRoutingApplicationBuilderExtensions", + "Microsoft.AspNetCore.Builder.WebApplication", + "Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions", + "System.Delegate", + "Microsoft.AspNetCore.Mvc.ModelBinding.IBinderTypeProviderMetadata", + "Microsoft.AspNetCore.Mvc.BindAttribute", + "Microsoft.AspNetCore.Http.IResult", + "Microsoft.AspNetCore.Mvc.IActionResult", + "Microsoft.AspNetCore.Mvc.Infrastructure.IConvertToActionResult", + "Microsoft.AspNetCore.Http.RequestDelegate", + "System.Threading.Tasks.Task`1", + "System.Threading.Tasks.ValueTask`1", + "System.Reflection.ParameterInfo", + "Microsoft.AspNetCore.Http.IBindableFromHttpContext`1", + "System.IParsable`1", + "Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions", + "Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions", + "Microsoft.AspNetCore.Builder.CorsEndpointConventionBuilderExtensions", + "Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions", + "Microsoft.AspNetCore.Builder.RateLimiterEndpointConventionBuilderExtensions", + "Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions", + "Microsoft.AspNetCore.Mvc.RouteAttribute", + "Microsoft.AspNetCore.Mvc.HttpDeleteAttribute", + "Microsoft.AspNetCore.Mvc.HttpGetAttribute", + "Microsoft.AspNetCore.Mvc.HttpHeadAttribute", + "Microsoft.AspNetCore.Mvc.HttpOptionsAttribute", + "Microsoft.AspNetCore.Mvc.HttpPatchAttribute", + "Microsoft.AspNetCore.Mvc.HttpPostAttribute", + "Microsoft.AspNetCore.Mvc.HttpPutAttribute", + "Microsoft.AspNetCore.Http.EndpointDescriptionAttribute", + "Microsoft.AspNetCore.Http.EndpointSummaryAttribute", + "Microsoft.AspNetCore.Http.TagsAttribute", + "Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute", + "Microsoft.AspNetCore.Routing.EndpointNameAttribute", + "Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute", + "Microsoft.AspNetCore.Cors.DisableCorsAttribute", + "Microsoft.AspNetCore.Cors.EnableCorsAttribute", + "Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute", + "Microsoft.AspNetCore.RateLimiting.DisableRateLimitingAttribute", + "Microsoft.AspNetCore.RateLimiting.EnableRateLimitingAttribute", + "Microsoft.AspNetCore.Mvc.ActionNameAttribute", + "Microsoft.AspNetCore.Mvc.DisableRequestSizeLimitAttribute", + "Microsoft.AspNetCore.Mvc.FormatFilterAttribute", + "Microsoft.AspNetCore.Mvc.ProducesAttribute", + "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute", + "Microsoft.AspNetCore.Mvc.ProducesErrorResponseTypeAttribute", + "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute", + "Microsoft.AspNetCore.Mvc.RequestFormLimitsAttribute", + "Microsoft.AspNetCore.Mvc.RequestSizeLimitAttribute", + "Microsoft.AspNetCore.Mvc.RequireHttpsAttribute", + "Microsoft.AspNetCore.Mvc.ResponseCacheAttribute", + "Microsoft.AspNetCore.Mvc.ServiceFilterAttribute", + "Microsoft.AspNetCore.Mvc.TypeFilterAttribute", + "Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchAttribute", + "Microsoft.AspNetCore.Mvc.Filters.ResultFilterAttribute", + "Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute", + "Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute", + "Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute", + "Microsoft.AspNetCore.Mvc.ViewFeatures.SaveTempDataAttribute", + "Microsoft.AspNetCore.Mvc.SkipStatusCodePagesAttribute", + "Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute", + "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute", + "Microsoft.AspNetCore.Authorization.AuthorizeAttribute" + }; +} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs deleted file mode 100644 index efa48b32700b..000000000000 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/WellKnownTypes.cs +++ /dev/null @@ -1,314 +0,0 @@ -// 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.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using Microsoft.AspNetCore.Analyzers.Infrastructure; -using Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.App.Analyzers.Infrastructure; - -internal enum WellKnownType -{ - Microsoft_AspNetCore_Components_Rendering_RenderTreeBuilder, - Microsoft_AspNetCore_Http_IHeaderDictionary, - Microsoft_AspNetCore_Http_Metadata_IFromBodyMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromFormMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromHeaderMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromQueryMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromServiceMetadata, - Microsoft_AspNetCore_Http_Metadata_IFromRouteMetadata, - Microsoft_AspNetCore_Http_HeaderDictionaryExtensions, - Microsoft_AspNetCore_Routing_IEndpointRouteBuilder, - Microsoft_AspNetCore_Mvc_ControllerAttribute, - Microsoft_AspNetCore_Mvc_NonControllerAttribute, - Microsoft_AspNetCore_Mvc_NonActionAttribute, - Microsoft_AspNetCore_Http_AsParametersAttribute, - System_Threading_CancellationToken, - Microsoft_AspNetCore_Http_HttpContext, - Microsoft_AspNetCore_Http_HttpRequest, - Microsoft_AspNetCore_Http_HttpResponse, - System_Security_Claims_ClaimsPrincipal, - Microsoft_AspNetCore_Http_IFormFileCollection, - Microsoft_AspNetCore_Http_IFormFile, - System_IO_Stream, - System_IO_Pipelines_PipeReader, - System_IFormatProvider, - System_Uri, - Microsoft_AspNetCore_Builder_ConfigureHostBuilder, - Microsoft_AspNetCore_Builder_ConfigureWebHostBuilder, - Microsoft_Extensions_Hosting_GenericHostWebHostBuilderExtensions, - Microsoft_AspNetCore_Hosting_WebHostBuilderExtensions, - Microsoft_AspNetCore_Hosting_HostingAbstractionsWebHostBuilderExtensions, - Microsoft_Extensions_Hosting_HostingHostBuilderExtensions, - Microsoft_AspNetCore_Builder_EndpointRoutingApplicationBuilderExtensions, - Microsoft_AspNetCore_Builder_WebApplication, - Microsoft_AspNetCore_Builder_EndpointRouteBuilderExtensions, - System_Delegate, - Microsoft_AspNetCore_Mvc_ModelBinding_IBinderTypeProviderMetadata, - Microsoft_AspNetCore_Mvc_BindAttribute, - Microsoft_AspNetCore_Http_IResult, - Microsoft_AspNetCore_Mvc_IActionResult, - Microsoft_AspNetCore_Mvc_Infrastructure_IConvertToActionResult, - Microsoft_AspNetCore_Http_RequestDelegate, - System_Threading_Tasks_Task_T, - System_Threading_Tasks_ValueTask_T, - System_Reflection_ParameterInfo, - Microsoft_AspNetCore_Http_IBindableFromHttpContext_T, - System_IParsable_T, - Microsoft_AspNetCore_Builder_AuthorizationEndpointConventionBuilderExtensions, - Microsoft_AspNetCore_Http_OpenApiRouteHandlerBuilderExtensions, - Microsoft_AspNetCore_Builder_CorsEndpointConventionBuilderExtensions, - Microsoft_Extensions_DependencyInjection_OutputCacheConventionBuilderExtensions, - Microsoft_AspNetCore_Builder_RateLimiterEndpointConventionBuilderExtensions, - Microsoft_AspNetCore_Builder_RoutingEndpointConventionBuilderExtensions, - Microsoft_AspNetCore_Mvc_RouteAttribute, - Microsoft_AspNetCore_Mvc_HttpDeleteAttribute, - Microsoft_AspNetCore_Mvc_HttpGetAttribute, - Microsoft_AspNetCore_Mvc_HttpHeadAttribute, - Microsoft_AspNetCore_Mvc_HttpOptionsAttribute, - Microsoft_AspNetCore_Mvc_HttpPatchAttribute, - Microsoft_AspNetCore_Mvc_HttpPostAttribute, - Microsoft_AspNetCore_Mvc_HttpPutAttribute, - Microsoft_AspNetCore_Http_EndpointDescriptionAttribute, - Microsoft_AspNetCore_Http_EndpointSummaryAttribute, - Microsoft_AspNetCore_Http_TagsAttribute, - Microsoft_AspNetCore_Routing_EndpointGroupNameAttribute, - Microsoft_AspNetCore_Routing_EndpointNameAttribute, - Microsoft_AspNetCore_Routing_ExcludeFromDescriptionAttribute, - Microsoft_AspNetCore_Cors_DisableCorsAttribute, - Microsoft_AspNetCore_Cors_EnableCorsAttribute, - Microsoft_AspNetCore_OutputCaching_OutputCacheAttribute, - Microsoft_AspNetCore_RateLimiting_DisableRateLimitingAttribute, - Microsoft_AspNetCore_RateLimiting_EnableRateLimitingAttribute, - Microsoft_AspNetCore_Mvc_ActionNameAttribute, - Microsoft_AspNetCore_Mvc_DisableRequestSizeLimitAttribute, - Microsoft_AspNetCore_Mvc_FormatFilterAttribute, - Microsoft_AspNetCore_Mvc_ProducesAttribute, - Microsoft_AspNetCore_Mvc_ProducesDefaultResponseTypeAttribute, - Microsoft_AspNetCore_Mvc_ProducesErrorResponseTypeAttribute, - Microsoft_AspNetCore_Mvc_ProducesResponseTypeAttribute, - Microsoft_AspNetCore_Mvc_RequestFormLimitsAttribute, - Microsoft_AspNetCore_Mvc_RequestSizeLimitAttribute, - Microsoft_AspNetCore_Mvc_RequireHttpsAttribute, - Microsoft_AspNetCore_Mvc_ResponseCacheAttribute, - Microsoft_AspNetCore_Mvc_ServiceFilterAttribute, - Microsoft_AspNetCore_Mvc_TypeFilterAttribute, - Microsoft_AspNetCore_Mvc_ApiExplorer_ApiConventionNameMatchAttribute, - Microsoft_AspNetCore_Mvc_Filters_ResultFilterAttribute, - Microsoft_AspNetCore_Mvc_Infrastructure_DefaultStatusCodeAttribute, - Microsoft_AspNetCore_Mvc_AutoValidateAntiforgeryTokenAttribute, - Microsoft_AspNetCore_Mvc_IgnoreAntiforgeryTokenAttribute, - Microsoft_AspNetCore_Mvc_ViewFeatures_SaveTempDataAttribute, - Microsoft_AspNetCore_Mvc_SkipStatusCodePagesAttribute, - Microsoft_AspNetCore_Mvc_ValidateAntiForgeryTokenAttribute, - Microsoft_AspNetCore_Authorization_AllowAnonymousAttribute, - Microsoft_AspNetCore_Authorization_AuthorizeAttribute -} - -internal sealed class WellKnownTypes -{ - private static readonly BoundedCacheWithFactory LazyWellKnownTypesCache = new(); - private static readonly string[] WellKnownTypeNames = new[] - { - "Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder", - "Microsoft.AspNetCore.Http.IHeaderDictionary", - "Microsoft.AspNetCore.Http.Metadata.IFromBodyMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromFormMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromHeaderMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromQueryMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromServiceMetadata", - "Microsoft.AspNetCore.Http.Metadata.IFromRouteMetadata", - "Microsoft.AspNetCore.Http.HeaderDictionaryExtensions", - "Microsoft.AspNetCore.Routing.IEndpointRouteBuilder", - "Microsoft.AspNetCore.Mvc.ControllerAttribute", - "Microsoft.AspNetCore.Mvc.NonControllerAttribute", - "Microsoft.AspNetCore.Mvc.NonActionAttribute", - "Microsoft.AspNetCore.Http.AsParametersAttribute", - "System.Threading.CancellationToken", - "Microsoft.AspNetCore.Http.HttpContext", - "Microsoft.AspNetCore.Http.HttpRequest", - "Microsoft.AspNetCore.Http.HttpResponse", - "System.Security.Claims.ClaimsPrincipal", - "Microsoft.AspNetCore.Http.IFormFileCollection", - "Microsoft.AspNetCore.Http.IFormFile", - "System.IO.Stream", - "System.IO.Pipelines.PipeReader", - "System.IFormatProvider", - "System.Uri", - "Microsoft.AspNetCore.Builder.ConfigureHostBuilder", - "Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder", - "Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions", - "Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions", - "Microsoft.AspNetCore.Hosting.HostingAbstractionsWebHostBuilderExtensions", - "Microsoft.Extensions.Hosting.HostingHostBuilderExtensions", - "Microsoft.AspNetCore.Builder.EndpointRoutingApplicationBuilderExtensions", - "Microsoft.AspNetCore.Builder.WebApplication", - "Microsoft.AspNetCore.Builder.EndpointRouteBuilderExtensions", - "System.Delegate", - "Microsoft.AspNetCore.Mvc.ModelBinding.IBinderTypeProviderMetadata", - "Microsoft.AspNetCore.Mvc.BindAttribute", - "Microsoft.AspNetCore.Http.IResult", - "Microsoft.AspNetCore.Mvc.IActionResult", - "Microsoft.AspNetCore.Mvc.Infrastructure.IConvertToActionResult", - "Microsoft.AspNetCore.Http.RequestDelegate", - "System.Threading.Tasks.Task`1", - "System.Threading.Tasks.ValueTask`1", - "System.Reflection.ParameterInfo", - "Microsoft.AspNetCore.Http.IBindableFromHttpContext`1", - "System.IParsable`1", - "Microsoft.AspNetCore.Builder.AuthorizationEndpointConventionBuilderExtensions", - "Microsoft.AspNetCore.Http.OpenApiRouteHandlerBuilderExtensions", - "Microsoft.AspNetCore.Builder.CorsEndpointConventionBuilderExtensions", - "Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions", - "Microsoft.AspNetCore.Builder.RateLimiterEndpointConventionBuilderExtensions", - "Microsoft.AspNetCore.Builder.RoutingEndpointConventionBuilderExtensions", - "Microsoft.AspNetCore.Mvc.RouteAttribute", - "Microsoft.AspNetCore.Mvc.HttpDeleteAttribute", - "Microsoft.AspNetCore.Mvc.HttpGetAttribute", - "Microsoft.AspNetCore.Mvc.HttpHeadAttribute", - "Microsoft.AspNetCore.Mvc.HttpOptionsAttribute", - "Microsoft.AspNetCore.Mvc.HttpPatchAttribute", - "Microsoft.AspNetCore.Mvc.HttpPostAttribute", - "Microsoft.AspNetCore.Mvc.HttpPutAttribute", - "Microsoft.AspNetCore.Http.EndpointDescriptionAttribute", - "Microsoft.AspNetCore.Http.EndpointSummaryAttribute", - "Microsoft.AspNetCore.Http.TagsAttribute", - "Microsoft.AspNetCore.Routing.EndpointGroupNameAttribute", - "Microsoft.AspNetCore.Routing.EndpointNameAttribute", - "Microsoft.AspNetCore.Routing.ExcludeFromDescriptionAttribute", - "Microsoft.AspNetCore.Cors.DisableCorsAttribute", - "Microsoft.AspNetCore.Cors.EnableCorsAttribute", - "Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute", - "Microsoft.AspNetCore.RateLimiting.DisableRateLimitingAttribute", - "Microsoft.AspNetCore.RateLimiting.EnableRateLimitingAttribute", - "Microsoft.AspNetCore.Mvc.ActionNameAttribute", - "Microsoft.AspNetCore.Mvc.DisableRequestSizeLimitAttribute", - "Microsoft.AspNetCore.Mvc.FormatFilterAttribute", - "Microsoft.AspNetCore.Mvc.ProducesAttribute", - "Microsoft.AspNetCore.Mvc.ProducesDefaultResponseTypeAttribute", - "Microsoft.AspNetCore.Mvc.ProducesErrorResponseTypeAttribute", - "Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute", - "Microsoft.AspNetCore.Mvc.RequestFormLimitsAttribute", - "Microsoft.AspNetCore.Mvc.RequestSizeLimitAttribute", - "Microsoft.AspNetCore.Mvc.RequireHttpsAttribute", - "Microsoft.AspNetCore.Mvc.ResponseCacheAttribute", - "Microsoft.AspNetCore.Mvc.ServiceFilterAttribute", - "Microsoft.AspNetCore.Mvc.TypeFilterAttribute", - "Microsoft.AspNetCore.Mvc.ApiExplorer.ApiConventionNameMatchAttribute", - "Microsoft.AspNetCore.Mvc.Filters.ResultFilterAttribute", - "Microsoft.AspNetCore.Mvc.Infrastructure.DefaultStatusCodeAttribute", - "Microsoft.AspNetCore.Mvc.AutoValidateAntiforgeryTokenAttribute", - "Microsoft.AspNetCore.Mvc.IgnoreAntiforgeryTokenAttribute", - "Microsoft.AspNetCore.Mvc.ViewFeatures.SaveTempDataAttribute", - "Microsoft.AspNetCore.Mvc.SkipStatusCodePagesAttribute", - "Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute", - "Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute", - "Microsoft.AspNetCore.Authorization.AuthorizeAttribute" - }; - - public static WellKnownTypes GetOrCreate(Compilation compilation) => - LazyWellKnownTypesCache.GetOrCreateValue(compilation, static c => new WellKnownTypes(c)); - - private readonly INamedTypeSymbol?[] _lazyWellKnownTypes; - private readonly Compilation _compilation; - - static WellKnownTypes() - { - AssertEnumAndTableInSync(); - } - - [Conditional("DEBUG")] - private static void AssertEnumAndTableInSync() - { - for (var i = 0; i < WellKnownTypeNames.Length; i++) - { - var name = WellKnownTypeNames[i]; - var typeId = (WellKnownType)i; - - var typeIdName = typeId.ToString().Replace("__", "+").Replace('_', '.'); - - var separator = name.IndexOf('`'); - if (separator >= 0) - { - // Ignore type parameter qualifier for generic types. - name = name.Substring(0, separator); - typeIdName = typeIdName.Substring(0, separator); - } - - Debug.Assert(name == typeIdName, $"Enum name ({typeIdName}) and type name ({name}) must match at {i}"); - } - } - - private WellKnownTypes(Compilation compilation) - { - _lazyWellKnownTypes = new INamedTypeSymbol?[WellKnownTypeNames.Length]; - _compilation = compilation; - } - - public INamedTypeSymbol Get(SpecialType type) - { - return _compilation.GetSpecialType(type); - } - - public INamedTypeSymbol Get(WellKnownType type) - { - var index = (int)type; - var symbol = _lazyWellKnownTypes[index]; - if (symbol is not null) - { - return symbol; - } - - // Symbol hasn't been added to the cache yet. - // Resolve symbol from name, cache, and return. - return GetAndCache(index); - } - - private INamedTypeSymbol GetAndCache(int index) - { - var result = _compilation.GetTypeByMetadataName(WellKnownTypeNames[index]); - if (result == null) - { - throw new InvalidOperationException($"Failed to resolve well-known type '{WellKnownTypeNames[index]}'."); - } - Interlocked.CompareExchange(ref _lazyWellKnownTypes[index], result, null); - - // GetTypeByMetadataName should always return the same instance for a name. - // To ensure we have a consistent value, for thread safety, return symbol set in the array. - return _lazyWellKnownTypes[index]!; - } - - public bool IsType(ITypeSymbol type, WellKnownType[] wellKnownTypes) => IsType(type, wellKnownTypes, out var _); - - public bool IsType(ITypeSymbol type, WellKnownType[] wellKnownTypes, [NotNullWhen(true)] out WellKnownType? match) - { - foreach (var wellKnownType in wellKnownTypes) - { - if (SymbolEqualityComparer.Default.Equals(type, Get(wellKnownType))) - { - match = wellKnownType; - return true; - } - } - - match = null; - return false; - } - - public bool Implements(ITypeSymbol type, WellKnownType[] interfaceWellKnownTypes) - { - foreach (var wellKnownType in interfaceWellKnownTypes) - { - if (type.Implements(Get(wellKnownType))) - { - return true; - } - } - - return false; - } -} diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj index 1481d546fbf6..5e6cbf836ff8 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Microsoft.AspNetCore.App.Analyzers.csproj @@ -23,6 +23,8 @@ + + diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/DetectAmbiguousActionRoutes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/DetectAmbiguousActionRoutes.cs index d16e9eff140e..37916d9514c2 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/DetectAmbiguousActionRoutes.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/DetectAmbiguousActionRoutes.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Analyzers.Mvc; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class MvcAnalyzer { private static void DetectAmbiguousActionRoutes(SymbolAnalysisContext context, WellKnownTypes wellKnownTypes, List actionRoutes) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/MvcAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/MvcAnalyzer.cs index 6a94f4480f73..e460f90f4686 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/MvcAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Mvc/MvcAnalyzer.cs @@ -14,6 +14,8 @@ namespace Microsoft.AspNetCore.Analyzers.Mvc; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public partial class MvcAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RenderTreeBuilder/RenderTreeBuilderAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RenderTreeBuilder/RenderTreeBuilderAnalyzer.cs index d09f4f6c0551..76c41aa8f066 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RenderTreeBuilder/RenderTreeBuilderAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RenderTreeBuilder/RenderTreeBuilderAnalyzer.cs @@ -10,6 +10,8 @@ namespace Microsoft.AspNetCore.Analyzers.RenderTreeBuilder; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public partial class RenderTreeBuilderAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs index 50d074b77c84..708c6f5baf4d 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/FrameworkParametersCompletionProvider.cs @@ -27,6 +27,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage; +using WellKnownType = WellKnownTypeData.WellKnownType; + [ExportCompletionProvider(nameof(RoutePatternCompletionProvider), LanguageNames.CSharp)] [Shared] public sealed class FrameworkParametersCompletionProvider : CompletionProvider diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs index a4b661ee8b9a..1f9b1eb041c6 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/MvcDetector.cs @@ -8,6 +8,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal static class MvcDetector { public static bool IsController(INamedTypeSymbol? typeSymbol, WellKnownTypes wellKnownTypes) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs index dabd6b18c90e..dfabd214ccbc 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RoutePatternParametersDetector.cs @@ -8,6 +8,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal static class RoutePatternParametersDetector { public static ImmutableArray ResolvedParameters(ISymbol symbol, WellKnownTypes wellKnownTypes) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs index 8e7be9d99f16..e5f47a84c0ca 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteUsageDetector.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal enum RouteUsageType { Other, diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteWellKnownTypes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteWellKnownTypes.cs index d070903f5bad..d11517ecacc1 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteWellKnownTypes.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteEmbeddedLanguage/Infrastructure/RouteWellKnownTypes.cs @@ -5,6 +5,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteEmbeddedLanguage.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + internal static class RouteWellKnownTypes { // Cache well known type keys rather than symbol instances. diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectAmbiguousRoutes.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectAmbiguousRoutes.cs index f76cf3fedd0d..bbab616e7f7e 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectAmbiguousRoutes.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DetectAmbiguousRoutes.cs @@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { private static void DetectAmbiguousRoutes(in OperationBlockAnalysisContext context, WellKnownTypes wellKnownTypes, ConcurrentDictionary mapOperations) diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowMvcBindArgumentsOnParameters.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowMvcBindArgumentsOnParameters.cs index b6a5fa10e287..ab8fc9d4c760 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowMvcBindArgumentsOnParameters.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowMvcBindArgumentsOnParameters.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { private static void DisallowMvcBindArgumentsOnParameters( diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowNonParsableComplexTypesOnParameters.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowNonParsableComplexTypesOnParameters.cs index 0e90cbc31b45..6f6d8c8cb71f 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowNonParsableComplexTypesOnParameters.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowNonParsableComplexTypesOnParameters.cs @@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { private static void DisallowNonParsableComplexTypesOnParameters( diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowReturningActionResultFromMapMethods.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowReturningActionResultFromMapMethods.cs index 96fe35f2cfd0..4cdc651d291d 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowReturningActionResultFromMapMethods.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/DisallowReturningActionResultFromMapMethods.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { private static void DisallowReturningActionResultFromMapMethods( diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs index a763446ee7b0..18eba86d37dc 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/RouteHandlers/RouteHandlerAnalyzer.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Analyzers.RouteHandlers; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public partial class RouteHandlerAnalyzer : DiagnosticAnalyzer { @@ -218,7 +220,7 @@ private record struct MapOperation(IOperation? Builder, IInvocationOperation Ope public static MapOperation Create(IInvocationOperation operation, RouteUsageModel routeUsageModel) { IOperation? builder = null; - + var builderArgument = operation.Arguments.SingleOrDefault(a => a.Parameter?.Ordinal == 0); if (builderArgument != null) { diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs index 1f296aa9cf46..95f596817c4a 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/Analyzers/WebApplicationBuilder/WebApplicationBuilderAnalyzer.cs @@ -13,6 +13,8 @@ namespace Microsoft.AspNetCore.Analyzers.WebApplicationBuilder; +using WellKnownType = WellKnownTypeData.WellKnownType; + [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class WebApplicationBuilderAnalyzer : DiagnosticAnalyzer { diff --git a/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Http/HeaderDictionaryAddFixer.cs b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Http/HeaderDictionaryAddFixer.cs index b21282c6c5eb..3d11de0e1f97 100644 --- a/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Http/HeaderDictionaryAddFixer.cs +++ b/src/Framework/AspNetCoreAnalyzers/src/CodeFixes/Http/HeaderDictionaryAddFixer.cs @@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Analyzers.Http.Fixers; +using WellKnownType = WellKnownTypeData.WellKnownType; + [ExportCodeFixProvider(LanguageNames.CSharp), Shared] public sealed class HeaderDictionaryAddFixer : CodeFixProvider { diff --git a/src/Framework/AspNetCoreAnalyzers/test/Infrastructure/WellKnownTypesTests.cs b/src/Framework/AspNetCoreAnalyzers/test/Infrastructure/WellKnownTypesTests.cs index 6e515b67fc37..8c02e10752d8 100644 --- a/src/Framework/AspNetCoreAnalyzers/test/Infrastructure/WellKnownTypesTests.cs +++ b/src/Framework/AspNetCoreAnalyzers/test/Infrastructure/WellKnownTypesTests.cs @@ -9,6 +9,8 @@ namespace Microsoft.AspNetCore.Analyzers.Infrastructure; +using WellKnownType = WellKnownTypeData.WellKnownType; + public partial class WellKnownTypesTests { private TestDiagnosticAnalyzerRunner Runner { get; } = new(new TestAnalyzer()); diff --git a/src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs b/src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs new file mode 100644 index 000000000000..d6a54e65fd00 --- /dev/null +++ b/src/Http/Http.Extensions/gen/DiagnosticDescriptors.cs @@ -0,0 +1,27 @@ +// 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; + +namespace Microsoft.AspNetCore.Http.Generators; + +internal static class DiagnosticDescriptors +{ + public static DiagnosticDescriptor UnableToResolveRoutePattern { get; } = new( + "RDG001", + new LocalizableResourceString(nameof(Resources.UnableToResolveRoutePattern_Title), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.UnableToResolveRoutePattern_Message), Resources.ResourceManager, typeof(Resources)), + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public static DiagnosticDescriptor UnableToResolveMethod { get; } = new( + "RDG002", + new LocalizableResourceString(nameof(Resources.UnableToResolveMethod_Title), Resources.ResourceManager, typeof(Resources)), + new LocalizableResourceString(nameof(Resources.UnableToResolveMethod_Message), Resources.ResourceManager, typeof(Resources)), + "Usage", + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); +} diff --git a/src/Http/Http.Extensions/gen/GeneratorSteps.cs b/src/Http/Http.Extensions/gen/GeneratorSteps.cs new file mode 100644 index 000000000000..3783ddfc7378 --- /dev/null +++ b/src/Http/Http.Extensions/gen/GeneratorSteps.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +namespace Microsoft.AspNetCore.Http.Generators; + +internal class GeneratorSteps +{ + internal const string EndpointModelStep = nameof(EndpointModelStep); + internal const string EndpointsWithoutDiagnosicsStep = nameof(EndpointsWithoutDiagnosicsStep); +} diff --git a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj index 171d742689ab..53ef4662cc6f 100644 --- a/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj +++ b/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.Generators.csproj @@ -9,20 +9,30 @@ + - - - + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + - + + + diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs index 529e7e6f4b4d..8fa3a9116366 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGenerator.cs @@ -1,8 +1,10 @@ // 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.Linq; using System.Text; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Operations; @@ -24,7 +26,7 @@ public sealed class RequestDelegateGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { - var endpoints = context.SyntaxProvider.CreateSyntaxProvider( + var endpointsWithDiagnostics = context.SyntaxProvider.CreateSyntaxProvider( predicate: (node, _) => node is InvocationExpressionSyntax { Expression: MemberAccessExpressionSyntax @@ -39,74 +41,112 @@ public void Initialize(IncrementalGeneratorInitializationContext context) transform: (context, token) => { var operation = context.SemanticModel.GetOperation(context.Node, token) as IInvocationOperation; - return StaticRouteHandlerModelParser.GetEndpointFromOperation(operation); + var wellKnownTypes = WellKnownTypes.GetOrCreate(context.SemanticModel.Compilation); + return new Endpoint(operation, wellKnownTypes); }) - .Where(endpoint => endpoint.Response.ResponseType == "string") - .WithTrackingName("EndpointModel"); + .WithTrackingName(GeneratorSteps.EndpointModelStep); - var thunks = endpoints.Select((endpoint, _) => $$""" -[{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}] = ( - (methodInfo, options) => + context.RegisterSourceOutput(endpointsWithDiagnostics, (context, endpoint) => + { + var (filePath, _) = endpoint.Location; + foreach (var diagnostic in endpoint.Diagnostics) { - if (options == null) - { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } - options.EndpointBuilder.Metadata.Add(new SourceKey{{StaticRouteHandlerModelEmitter.EmitSourceKey(endpoint)}}); - return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; - }, - (del, options, inferredMetadataResult) => + context.ReportDiagnostic(Diagnostic.Create(diagnostic, endpoint.Operation.Syntax.GetLocation(), filePath)); + } + foreach (var diagnostic in endpoint.Response.Diagnostics) + { + context.ReportDiagnostic(Diagnostic.Create(diagnostic, endpoint.Operation.Syntax.GetLocation(), filePath)); + } + foreach (var diagnostic in endpoint.Route.Diagnostics) { - var handler = ({{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}})del; - EndpointFilterDelegate? filteredInvocation = null; + context.ReportDiagnostic(Diagnostic.Create(diagnostic, endpoint.Operation.Syntax.GetLocation(), filePath)); + } + }); - if (options.EndpointBuilder.FilterFactories.Count > 0) + var endpoints = endpointsWithDiagnostics + .Where(endpoint => endpoint.Diagnostics.Count == 0 && + endpoint.Response.Diagnostics.Count == 0 && + endpoint.Route.Diagnostics.Count == 0) + .WithTrackingName(GeneratorSteps.EndpointsWithoutDiagnosicsStep); + + var thunks = endpoints.Select((endpoint, _) => $$""" + [{{endpoint.EmitSourceKey()}}] = ( + (methodInfo, options) => + { + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new SourceKey{{endpoint.EmitSourceKey()}}); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => { - filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + var handler = ({{endpoint.EmitHandlerDelegateType()}})del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) { - if (ic.HttpContext.Response.StatusCode == 400) + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => { - return ValueTask.FromResult(Results.Empty); - } - {{StaticRouteHandlerModelEmitter.EmitFilteredInvocation()}} - }, - options.EndpointBuilder, - handler.Method); - } + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + {{endpoint.EmitFilteredInvocation()}} + }, + options.EndpointBuilder, + handler.Method); + } - {{StaticRouteHandlerModelEmitter.EmitRequestHandler()}} - {{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}} +{{endpoint.EmitRequestHandler()}} +{{StaticRouteHandlerModelEmitter.EmitFilteredRequestHandler()}} - RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; - var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; - return new RequestDelegateResult(targetDelegate, metadata); - }), + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), """); - var stronglyTypedEndpointDefinitions = endpoints.Select((endpoint, _) => $$""" + var stronglyTypedEndpointDefinitions = endpoints + .Collect() + .Select((endpoints, _) => + { + var dedupedByDelegate = endpoints.Distinct(EndpointDelegateComparer.Instance); + var code = new StringBuilder(); + foreach (var endpoint in dedupedByDelegate) + { + code.AppendLine($$""" internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder {{endpoint.HttpMethod}}( this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, - global::{{StaticRouteHandlerModelEmitter.EmitHandlerDelegateType(endpoint)}} handler, + global::{{endpoint.EmitHandlerDelegateType()}} handler, [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) { - return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, {{StaticRouteHandlerModelEmitter.EmitVerb(endpoint)}}, filePath, lineNumber); + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + {{endpoint.EmitVerb()}}, + filePath, + lineNumber); } """); + } + + return code.ToString(); + }); - var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions.Collect()); + var thunksAndEndpoints = thunks.Collect().Combine(stronglyTypedEndpointDefinitions); context.RegisterSourceOutput(thunksAndEndpoints, (context, sources) => { - var (thunks, endpoints) = sources; + var (thunks, endpointsCode) = sources; - var endpointsCode = new StringBuilder(); - var thunksCode = new StringBuilder(); - foreach (var endpoint in endpoints) + if (thunks.IsDefaultOrEmpty || string.IsNullOrEmpty(endpointsCode)) { - endpointsCode.AppendLine(endpoint); + return; } + + var thunksCode = new StringBuilder(); foreach (var thunk in thunks) { thunksCode.AppendLine(thunk); @@ -115,7 +155,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var code = RequestDelegateGeneratorSources.GetGeneratedRouteBuilderExtensionsSource( genericThunks: string.Empty, thunks: thunksCode.ToString(), - endpoints: endpointsCode.ToString()); + endpoints: endpointsCode); + context.AddSource("GeneratedRouteBuilderExtensions.g.cs", code); }); } diff --git a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs index 6cb436e4d9e9..0183e9d0b9f6 100644 --- a/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs +++ b/src/Http/Http.Extensions/gen/RequestDelegateGeneratorSources.cs @@ -85,16 +85,6 @@ private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate return filteredInvocation; } - private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) where T : IEndpointMetadataProvider - { - T.PopulateMetadata(method, builder); - } - - private static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) where T : IEndpointParameterMetadataProvider - { - T.PopulateMetadata(parameter, builder); - } - private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) { if (obj is IResult r) @@ -115,10 +105,10 @@ private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) """; private static string GetGenericThunks(string genericThunks) => genericThunks != string.Empty ? $$""" private static class GenericThunks + { + public static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { - public static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() - { - {{genericThunks}} + {{genericThunks}} }; } @@ -138,7 +128,7 @@ internal static RouteHandlerBuilder MapCore( private static string GetThunks(string thunks) => thunks != string.Empty ? $$""" private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { - {{thunks}} +{{thunks}} }; internal static RouteHandlerBuilder MapCore( diff --git a/src/Http/Http.Extensions/gen/Resources.resx b/src/Http/Http.Extensions/gen/Resources.resx new file mode 100644 index 000000000000..6880e701b101 --- /dev/null +++ b/src/Http/Http.Extensions/gen/Resources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unable to resolve route pattern + + + Unable to statically resolve route pattern for endpoint. Compile-time endpoint generation will skip this endpoint. + + + Unable to resolve endpoint handler + + + Unable to statically resolve endpoint handler method. Only method groups, lambda expressions or readonly fields/variables are allowed. Compile-time endpoint generation will skip this endpoint. + + diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs new file mode 100644 index 000000000000..b5a694770f80 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/Endpoint.cs @@ -0,0 +1,72 @@ +// 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.Linq; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +internal class Endpoint +{ + public string HttpMethod { get; } + public EndpointRoute Route { get; } + public EndpointResponse Response { get; } + public List Diagnostics { get; } = new List(); + public (string, int) Location { get; } + public IInvocationOperation Operation { get; } + + private WellKnownTypes WellKnownTypes { get; } + + public Endpoint(IInvocationOperation operation, WellKnownTypes wellKnownTypes) + { + Operation = operation; + WellKnownTypes = wellKnownTypes; + Location = GetLocation(); + HttpMethod = GetHttpMethod(); + Response = new EndpointResponse(Operation, wellKnownTypes); + Route = new EndpointRoute(Operation); + } + + private (string, int) GetLocation() + { + var filePath = Operation.Syntax.SyntaxTree.FilePath; + var span = Operation.Syntax.SyntaxTree.GetLineSpan(Operation.Syntax.Span); + var lineNumber = span.EndLinePosition.Line + 1; + return (filePath, lineNumber); + } + + private string GetHttpMethod() + { + var syntax = (InvocationExpressionSyntax)Operation.Syntax; + var expression = (MemberAccessExpressionSyntax)syntax.Expression; + var name = (IdentifierNameSyntax)expression.Name; + var identifier = name.Identifier; + return identifier.ValueText; + } + + public override bool Equals(object o) + { + if (o is null) + { + return false; + } + + if (o is Endpoint endpoint) + { + return endpoint.HttpMethod.Equals(HttpMethod, StringComparison.OrdinalIgnoreCase) && + endpoint.Location.Item1.Equals(Location.Item1, StringComparison.OrdinalIgnoreCase) && + endpoint.Location.Item2.Equals(Location.Item2) && + endpoint.Response.Equals(Response) && + endpoint.Diagnostics.SequenceEqual(Diagnostics); + } + + return false; + } + + public override int GetHashCode() => + HashCode.Combine(HttpMethod, Route, Location, Response, Diagnostics); +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointDelegateComparer.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointDelegateComparer.cs new file mode 100644 index 000000000000..7d159ebd7176 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointDelegateComparer.cs @@ -0,0 +1,31 @@ +// 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; +using System.Collections.Generic; +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +internal sealed class EndpointDelegateComparer : IEqualityComparer, IComparer +{ + public static readonly EndpointDelegateComparer Instance = new EndpointDelegateComparer(); + + public bool Equals(Endpoint a, Endpoint b) => Compare(a, b) == 0; + + public int GetHashCode(Endpoint endpoint) => HashCode.Combine( + endpoint.Response.WrappedResponseType, + endpoint.Response.IsVoid, + endpoint.Response.IsAwaitable, + endpoint.HttpMethod); + + public int Compare(Endpoint a, Endpoint b) + { + if (a.Response.IsAwaitable == b.Response.IsAwaitable && + a.Response.IsVoid == b.Response.IsVoid && + a.Response.WrappedResponseType.Equals(b.Response.WrappedResponseType, StringComparison.Ordinal) && + a.HttpMethod.Equals(b.HttpMethod, StringComparison.Ordinal)) + { + return 0; + } + return -1; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs new file mode 100644 index 000000000000..4f27b0700ce2 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointResponse.cs @@ -0,0 +1,135 @@ +// 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.Linq; +using Microsoft.AspNetCore.App.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +using WellKnownType = WellKnownTypeData.WellKnownType; + +public class EndpointResponse +{ + public ITypeSymbol ResponseType { get; set; } + public string WrappedResponseType { get; set; } + public string ContentType { get; set; } + public bool IsAwaitable { get; set; } + public bool IsVoid { get; set; } + public bool IsIResult { get; set; } + + private WellKnownTypes WellKnownTypes { get; init; } + + public List Diagnostics { get; init; } = new List(); + + internal EndpointResponse(IInvocationOperation operation, WellKnownTypes wellKnownTypes) + { + if (!operation.TryGetRouteHandlerMethod(out var method)) + { + Diagnostics.Add(DiagnosticDescriptors.UnableToResolveMethod); + return; + } + + WellKnownTypes = wellKnownTypes; + ResponseType = UnwrapResponseType(method); + WrappedResponseType = method.ReturnType.ToString(); + IsAwaitable = GetIsAwaitable(method); + IsVoid = method.ReturnsVoid; + IsIResult = GetIsIResult(); + ContentType = GetContentType(method); + } + + private ITypeSymbol UnwrapResponseType(IMethodSymbol method) + { + var returnType = method.ReturnType; + var task = WellKnownTypes.Get(WellKnownType.System_Threading_Tasks_Task); + var taskOfT = WellKnownTypes.Get(WellKnownType.System_Threading_Tasks_Task_T); + var valueTask = WellKnownTypes.Get(WellKnownType.System_Threading_Tasks_ValueTask); + var valueTaskOfT = WellKnownTypes.Get(WellKnownType.System_Threading_Tasks_ValueTask_T); + if (returnType.OriginalDefinition.Equals(taskOfT, SymbolEqualityComparer.Default) || + returnType.OriginalDefinition.Equals(valueTaskOfT, SymbolEqualityComparer.Default)) + { + return ((INamedTypeSymbol)returnType).TypeArguments[0]; + } + + if (returnType.OriginalDefinition.Equals(task, SymbolEqualityComparer.Default) || + returnType.OriginalDefinition.Equals(valueTask, SymbolEqualityComparer.Default)) + { + return null; + } + + return returnType; + } + + private bool GetIsIResult() + { + var resultType = WellKnownTypes.Get(WellKnownType.Microsoft_AspNetCore_Http_IResult); + return WellKnownTypes.Implements(ResponseType, resultType) || + SymbolEqualityComparer.Default.Equals(ResponseType, resultType); + } + + private static bool GetIsAwaitable(IMethodSymbol method) + { + var potentialGetAwaiters = method.ReturnType.OriginalDefinition.GetMembers(WellKnownMemberNames.GetAwaiter); + var getAwaiters = potentialGetAwaiters.OfType().Where(x => !x.Parameters.Any()); + return getAwaiters.Any(symbol => symbol.Name == WellKnownMemberNames.GetAwaiter && VerifyGetAwaiter(symbol)); + + static bool VerifyGetAwaiter(IMethodSymbol getAwaiter) + { + var returnType = getAwaiter.ReturnType; + + // bool IsCompleted { get } + if (!returnType.GetMembers() + .OfType() + .Any(p => p.Name == WellKnownMemberNames.IsCompleted && + p.Type.SpecialType == SpecialType.System_Boolean && p.GetMethod != null)) + { + return false; + } + + var methods = returnType.GetMembers().OfType(); + + if (!methods.Any(x => x.Name == WellKnownMemberNames.OnCompleted && + x.ReturnsVoid && + x.Parameters.Length == 1 && + x.Parameters.First().Type.TypeKind == TypeKind.Delegate)) + { + return false; + } + + // void GetResult() || T GetResult() + return methods.Any(m => m.Name == WellKnownMemberNames.GetResult && !m.Parameters.Any()); + } + } + + private string? GetContentType(IMethodSymbol method) + { + // `void` returning methods do not have a Content-Type. + // We don't have a strategy for resolving a Content-Type + // from an IResult. Typically, this would be done via an + // IEndpointMetadataProvider so we don't need to set a + // Content-Type here. + if (method.ReturnsVoid || IsIResult) + { + return null; + } + return method.ReturnType.SpecialType is SpecialType.System_String ? "text/plain" : "application/json"; + } + + public override bool Equals(object obj) + { + return obj is EndpointResponse otherEndpointResponse && + SymbolEqualityComparer.Default.Equals(otherEndpointResponse.ResponseType, ResponseType) && + otherEndpointResponse.WrappedResponseType.Equals(WrappedResponseType, StringComparison.Ordinal) && + otherEndpointResponse.IsAwaitable == IsAwaitable && + otherEndpointResponse.IsVoid == IsVoid && + otherEndpointResponse.IsIResult == IsIResult && + otherEndpointResponse.ContentType.Equals(ContentType, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() => + HashCode.Combine(SymbolEqualityComparer.Default.GetHashCode(ResponseType), WrappedResponseType, IsAwaitable, IsVoid, IsIResult, ContentType); +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointRoute.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointRoute.cs new file mode 100644 index 000000000000..66de4f9d88f6 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/EndpointRoute.cs @@ -0,0 +1,48 @@ +// 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 Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +public class EndpointRoute +{ + private const int RoutePatternArgumentOrdinal = 1; + + public string RoutePattern { get; init; } + + public List Diagnostics { get; init; } = new List(); + + public EndpointRoute(IInvocationOperation operation) + { + if (!TryGetRouteHandlerPattern(operation, out var routeToken)) + { + Diagnostics.Add(DiagnosticDescriptors.UnableToResolveRoutePattern); + } + + RoutePattern = routeToken.ValueText; + } + + private static bool TryGetRouteHandlerPattern(IInvocationOperation invocation, out SyntaxToken token) + { + IArgumentOperation? argumentOperation = null; + foreach (var argument in invocation.Arguments) + { + if (argument.Parameter?.Ordinal == RoutePatternArgumentOrdinal) + { + argumentOperation = argument; + } + } + if (argumentOperation?.Syntax is not ArgumentSyntax routePatternArgumentSyntax || + routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax) + { + token = default; + return false; + } + token = routePatternArgumentLiteralSyntax.Token; + return true; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs new file mode 100644 index 000000000000..6a0ee6783a49 --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/InvocationOperationExtensions.cs @@ -0,0 +1,68 @@ +// 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; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; + +namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; + +public static class InvocationOperationExtensions +{ + private const int RouteHandlerArgumentOrdinal = 2; + + public static bool TryGetRouteHandlerMethod(this IInvocationOperation invocation, out IMethodSymbol method) + { + foreach (var argument in invocation.Arguments) + { + if (argument.Parameter?.Ordinal == RouteHandlerArgumentOrdinal) + { + method = ResolveMethodFromOperation(argument); + return true; + } + } + method = null; + return false; + } + + private static IMethodSymbol ResolveMethodFromOperation(IOperation operation) => operation switch + { + IArgumentOperation argument => ResolveMethodFromOperation(argument.Value), + IConversionOperation conv => ResolveMethodFromOperation(conv.Operand), + IDelegateCreationOperation del => ResolveMethodFromOperation(del.Target), + IFieldReferenceOperation { Field.IsReadOnly: true } f when ResolveDeclarationOperation(f.Field, operation.SemanticModel) is IOperation op => + ResolveMethodFromOperation(op), + IAnonymousFunctionOperation anon => anon.Symbol, + ILocalFunctionOperation local => local.Symbol, + IMethodReferenceOperation method => method.Method, + IParenthesizedOperation parenthesized => ResolveMethodFromOperation(parenthesized.Operand), + _ => null + }; + + private static IOperation ResolveDeclarationOperation(ISymbol symbol, SemanticModel semanticModel) + { + foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) + { + var syn = syntaxReference.GetSyntax(); + + if (syn is VariableDeclaratorSyntax + { + Initializer: + { + Value: var expr + } + }) + { + // Use the correct semantic model based on the syntax tree + var operation = semanticModel.GetOperation(expr); + + if (operation is not null) + { + return operation; + } + } + } + + return null; + } +} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs index 996cec75e731..a167d600bb62 100644 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Emitter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using Microsoft.CodeAnalysis; namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; @@ -20,17 +21,25 @@ internal static class StaticRouteHandlerModelEmitter * that do return a value, `System.Func` will * be emitted to indicate a `string`return type. */ - public static string EmitHandlerDelegateType(Endpoint endpoint) + public static string EmitHandlerDelegateType(this Endpoint endpoint) { + if (endpoint.Response.IsVoid) + { + return $"System.Action"; + } + if (endpoint.Response.IsAwaitable) + { + return $"System.Func<{endpoint.Response.WrappedResponseType}>"; + } return $"System.Func<{endpoint.Response.ResponseType}>"; } - public static string EmitSourceKey(Endpoint endpoint) + public static string EmitSourceKey(this Endpoint endpoint) { return $@"(@""{endpoint.Location.Item1}"", {endpoint.Location.Item2})"; } - public static string EmitVerb(Endpoint endpoint) + public static string EmitVerb(this Endpoint endpoint) { return endpoint.HttpMethod switch { @@ -49,17 +58,52 @@ public static string EmitVerb(Endpoint endpoint) * their validity (optionality), invoke the underlying handler with * the arguments bound from HTTP context, and write out the response. */ - public static string EmitRequestHandler() + public static string EmitRequestHandler(this Endpoint endpoint) { - return """ -Task RequestHandler(HttpContext httpContext) - { - var result = handler(); - return httpContext.Response.WriteAsync(result); - } + var handlerSignature = endpoint.Response.IsAwaitable ? "async Task RequestHandler(HttpContext httpContext)" : "Task RequestHandler(HttpContext httpContext)"; + var resultAssignment = endpoint.Response.IsVoid ? string.Empty : "var result = "; + var awaitHandler = endpoint.Response.IsAwaitable ? "await " : string.Empty; + var setContentType = endpoint.Response.IsVoid ? string.Empty : $@"httpContext.Response.ContentType ??= ""{endpoint.Response.ContentType}"";"; + return $$""" + {{handlerSignature}} + { + {{setContentType}} + {{resultAssignment}}{{awaitHandler}}handler(); + {{(endpoint.Response.IsVoid ? "return Task.CompletedTask;" : endpoint.EmitResponseWritingCall())}} + } """; } + private static string EmitResponseWritingCall(this Endpoint endpoint) + { + var returnOrAwait = endpoint.Response.IsAwaitable ? "await" : "return"; + + if (endpoint.Response.IsIResult) + { + return $"{returnOrAwait} result.ExecuteAsync(httpContext);"; + } + else if (endpoint.Response.ResponseType.SpecialType == SpecialType.System_String) + { + return $"{returnOrAwait} httpContext.Response.WriteAsync(result);"; + } + else if (endpoint.Response.ResponseType.SpecialType == SpecialType.System_Object) + { + return $"{returnOrAwait} GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext);"; + } + else if (!endpoint.Response.IsVoid) + { + return $"{returnOrAwait} httpContext.Response.WriteAsJsonAsync(result);"; + } + else if (!endpoint.Response.IsAwaitable && endpoint.Response.IsVoid) + { + return $"{returnOrAwait} Task.CompletedTask;"; + } + else + { + return $"{returnOrAwait} httpContext.Response.WriteAsync(result);"; + } + } + /* * TODO: Emit invocation to the `filteredInvocation` pipeline by constructing * the `EndpointFilterInvocationContext` using the bound arguments for the handler. @@ -70,11 +114,11 @@ Task RequestHandler(HttpContext httpContext) public static string EmitFilteredRequestHandler() { return """ -async Task RequestHandlerFiltered(HttpContext httpContext) - { - var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); - await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); - } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } """; } @@ -83,7 +127,7 @@ async Task RequestHandlerFiltered(HttpContext httpContext) * the appropriate arguments processed via the parameter binding. * * ``` - * return System.Threading.Tasks.ValueTask.FromResult(handler(name, age)); + * return ValueTask.FromResult(handler(name, age)); * ``` * * If the handler returns void, it will be invoked and an `EmptyHttpResult` @@ -91,11 +135,18 @@ async Task RequestHandlerFiltered(HttpContext httpContext) * * ``` * handler(name, age); - * return System.Threading.Tasks.ValueTask.FromResult(Results.Empty); + * return ValueTask.FromResult(Results.Empty); * ``` */ - public static string EmitFilteredInvocation() + public static string EmitFilteredInvocation(this Endpoint endpoint) { - return "return ValueTask.FromResult(handler());"; + // Note: This string does not need indentation since it is + // handled when we generate the output string in the `thunks` pipeline. + return endpoint.Response.IsVoid ? """ +handler(); +return ValueTask.FromResult(Results.Empty); +""" : """ +return ValueTask.FromResult(handler()); +"""; } } diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs deleted file mode 100644 index b7a53d418fca..000000000000 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.Parser.cs +++ /dev/null @@ -1,121 +0,0 @@ -// 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; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Operations; - -namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; - -internal static class StaticRouteHandlerModelParser -{ - private const int RoutePatternArgumentOrdinal = 1; - private const int RouteHandlerArgumentOrdinal = 2; - - private static EndpointRoute GetEndpointRouteFromArgument(SyntaxToken routePattern) - { - return new EndpointRoute(routePattern.ValueText); - } - - private static EndpointResponse GetEndpointResponseFromMethod(IMethodSymbol method) - { - return new EndpointResponse(method.ReturnType.ToString(), "plain/text"); - } - - public static Endpoint GetEndpointFromOperation(IInvocationOperation operation) - { - if (!TryGetRouteHandlerPattern(operation, out var routeToken)) - { - return null; - } - if (!TryGetRouteHandlerMethod(operation, out var method)) - { - return null; - } - var filePath = operation.Syntax.SyntaxTree.FilePath; - var span = operation.Syntax.SyntaxTree.GetLineSpan(operation.Syntax.Span); - - var invocationExpression = (InvocationExpressionSyntax)operation.Syntax; - var httpMethod = ((IdentifierNameSyntax)((MemberAccessExpressionSyntax)invocationExpression.Expression).Name).Identifier.ValueText; - - return new Endpoint(httpMethod, - GetEndpointRouteFromArgument(routeToken), - GetEndpointResponseFromMethod(method), - (filePath, span.EndLinePosition.Line + 1)); - } - - private static bool TryGetRouteHandlerPattern(IInvocationOperation invocation, out SyntaxToken token) - { - IArgumentOperation? argumentOperation = null; - foreach (var argument in invocation.Arguments) - { - if (argument.Parameter?.Ordinal == RoutePatternArgumentOrdinal) - { - argumentOperation = argument; - } - } - if (argumentOperation?.Syntax is not ArgumentSyntax routePatternArgumentSyntax || - routePatternArgumentSyntax.Expression is not LiteralExpressionSyntax routePatternArgumentLiteralSyntax) - { - token = default; - return false; - } - token = routePatternArgumentLiteralSyntax.Token; - return true; - } - - private static bool TryGetRouteHandlerMethod(IInvocationOperation invocation, out IMethodSymbol method) - { - foreach (var argument in invocation.Arguments) - { - if (argument.Parameter?.Ordinal == RouteHandlerArgumentOrdinal) - { - method = ResolveMethodFromOperation(argument); - return true; - } - } - method = null; - return false; - } - - private static IMethodSymbol ResolveMethodFromOperation(IOperation operation) => operation switch - { - IArgumentOperation argument => ResolveMethodFromOperation(argument.Value), - IConversionOperation conv => ResolveMethodFromOperation(conv.Operand), - IDelegateCreationOperation del => ResolveMethodFromOperation(del.Target), - IFieldReferenceOperation { Field.IsReadOnly: true } f when ResolveDeclarationOperation(f.Field, operation.SemanticModel) is IOperation op => - ResolveMethodFromOperation(op), - IAnonymousFunctionOperation anon => anon.Symbol, - ILocalFunctionOperation local => local.Symbol, - IMethodReferenceOperation method => method.Method, - IParenthesizedOperation parenthesized => ResolveMethodFromOperation(parenthesized.Operand), - _ => null - }; - - private static IOperation ResolveDeclarationOperation(ISymbol symbol, SemanticModel semanticModel) - { - foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) - { - var syn = syntaxReference.GetSyntax(); - - if (syn is VariableDeclaratorSyntax - { - Initializer: - { - Value: var expr - } - }) - { - // Use the correct semantic model based on the syntax tree - var operation = semanticModel.GetOperation(expr); - - if (operation is not null) - { - return operation; - } - } - } - - return null; - } -} diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs deleted file mode 100644 index fb3a60c63bbf..000000000000 --- a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/StaticRouteHandlerModel.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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; -namespace Microsoft.AspNetCore.Http.Generators.StaticRouteHandlerModel; - -internal enum RequestParameterSource -{ - Query, - Route, - Header, - Form, - Service, - BodyOrService, -} - -internal record RequestParameter(string Name, string Type, RequestParameterSource Source, bool IsOptional, object? DefaultValue); -internal record EndpointRoute(string RoutePattern); -internal record EndpointResponse(string ResponseType, string ContentType); -internal record Endpoint(string HttpMethod, EndpointRoute Route, EndpointResponse Response, (string, int) Location); diff --git a/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs new file mode 100644 index 000000000000..72657181113a --- /dev/null +++ b/src/Http/Http.Extensions/gen/StaticRouteHandlerModel/WellKnownTypeData.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.App.Analyzers.Infrastructure; + +internal static class WellKnownTypeData +{ + public enum WellKnownType + { + Microsoft_AspNetCore_Http_IResult, + System_Threading_Tasks_Task, + System_Threading_Tasks_Task_T, + System_Threading_Tasks_ValueTask, + System_Threading_Tasks_ValueTask_T + } + + public static readonly string[] WellKnownTypeNames = new[] + { + "Microsoft.AspNetCore.Http.IResult", + "System.Threading.Tasks.Task", + "System.Threading.Tasks.Task`1", + "System.Threading.Tasks.ValueTask", + "System.Threading.Tasks.ValueTask`1" + }; +} diff --git a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj index 95942fd9ed66..42b8310af5da 100644 --- a/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj +++ b/src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj @@ -20,11 +20,11 @@ - + - + PreserveNewest diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt index 8309dc890cee..7f9a7f9be477 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/MapGet_NoParam_StringReturn_WithFilter.generated.txt @@ -44,7 +44,13 @@ namespace Microsoft.AspNetCore.Builder [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) { - return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore(endpoints, pattern, handler, GetVerb, filePath, lineNumber); + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); } } @@ -79,49 +85,47 @@ namespace Microsoft.AspNetCore.Http.Generated private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() { [(@"TestMapActions.cs", 15)] = ( - (methodInfo, options) => - { - if (options == null) + (methodInfo, options) => { - return new RequestDelegateMetadataResult { EndpointMetadata = ReadOnlyCollection.Empty }; - } - options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); - return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; - }, - (del, options, inferredMetadataResult) => - { - var handler = (System.Func)del; - EndpointFilterDelegate? filteredInvocation = null; - - if (options.EndpointBuilder.FilterFactories.Count > 0) + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => { - filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) { - if (ic.HttpContext.Response.StatusCode == 400) + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => { - return ValueTask.FromResult(Results.Empty); - } - return ValueTask.FromResult(handler()); - }, - options.EndpointBuilder, - handler.Method); - } - - Task RequestHandler(HttpContext httpContext) - { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "text/plain"; var result = handler(); return httpContext.Response.WriteAsync(result); - } - async Task RequestHandlerFiltered(HttpContext httpContext) - { - var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); - await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); - } + } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } - RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; - var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; - return new RequestDelegateResult(targetDelegate, metadata); - }), + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), }; @@ -154,16 +158,6 @@ namespace Microsoft.AspNetCore.Http.Generated return filteredInvocation; } - private static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) where T : IEndpointMetadataProvider - { - T.PopulateMetadata(method, builder); - } - - private static void PopulateMetadata(ParameterInfo parameter, EndpointBuilder builder) where T : IEndpointParameterMetadataProvider - { - T.PopulateMetadata(parameter, builder); - } - private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) { if (obj is IResult r) diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt new file mode 100644 index 000000000000..35f739c6d74d --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/Baselines/Multiple_MapAction_NoParam_StringReturn.generated.txt @@ -0,0 +1,333 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +#nullable enable + +namespace Microsoft.AspNetCore.Builder +{ + %GENERATEDCODEATTRIBUTE% + internal class SourceKey + { + public string Path { get; init; } + public int Line { get; init; } + + public SourceKey(string path, int line) + { + Path = path; + Line = line; + } + } + + // This class needs to be internal so that the compiled application + // has access to the strongly-typed endpoint definitions that are + // generated by the compiler so that they will be favored by + // overload resolution and opt the runtime in to the code generated + // implementation produced here. + %GENERATEDCODEATTRIBUTE% + internal static class GenerateRouteBuilderEndpoints + { + private static readonly string[] GetVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Get }; + private static readonly string[] PostVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Post }; + private static readonly string[] PutVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Put }; + private static readonly string[] DeleteVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Delete }; + private static readonly string[] PatchVerb = new[] { global::Microsoft.AspNetCore.Http.HttpMethods.Patch }; + + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); + } + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func> handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); + } + internal static global::Microsoft.AspNetCore.Builder.RouteHandlerBuilder MapGet( + this global::Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, + [global::System.Diagnostics.CodeAnalysis.StringSyntax("Route")] string pattern, + global::System.Func> handler, + [global::System.Runtime.CompilerServices.CallerFilePath] string filePath = "", + [global::System.Runtime.CompilerServices.CallerLineNumber]int lineNumber = 0) + { + return global::Microsoft.AspNetCore.Http.Generated.GeneratedRouteBuilderExtensionsCore.MapCore( + endpoints, + pattern, + handler, + GetVerb, + filePath, + lineNumber); + } + + } +} + +namespace Microsoft.AspNetCore.Http.Generated +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + using System.IO; + using Microsoft.AspNetCore.Routing; + using Microsoft.AspNetCore.Routing.Patterns; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Http; + using Microsoft.AspNetCore.Http.Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.FileProviders; + using Microsoft.Extensions.Primitives; + + using MetadataPopulator = System.Func; + using RequestDelegateFactoryFunc = System.Func; + + file static class GeneratedRouteBuilderExtensionsCore + { + + private static readonly Dictionary<(string, int), (MetadataPopulator, RequestDelegateFactoryFunc)> map = new() + { + [(@"TestMapActions.cs", 15)] = ( + (methodInfo, options) => + { + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 15)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "text/plain"; + var result = handler(); + return httpContext.Response.WriteAsync(result); + } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + [(@"TestMapActions.cs", 16)] = ( + (methodInfo, options) => + { + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 16)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + }, + options.EndpointBuilder, + handler.Method); + } + + Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "text/plain"; + var result = handler(); + return httpContext.Response.WriteAsync(result); + } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + [(@"TestMapActions.cs", 17)] = ( + (methodInfo, options) => + { + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 17)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func>)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + }, + options.EndpointBuilder, + handler.Method); + } + + async Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "application/json"; + var result = await handler(); + await httpContext.Response.WriteAsync(result); + } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + [(@"TestMapActions.cs", 18)] = ( + (methodInfo, options) => + { + Debug.Assert(options?.EndpointBuilder != null, "EndpointBuilder not found."); + options.EndpointBuilder.Metadata.Add(new SourceKey(@"TestMapActions.cs", 18)); + return new RequestDelegateMetadataResult { EndpointMetadata = options.EndpointBuilder.Metadata.AsReadOnly() }; + }, + (del, options, inferredMetadataResult) => + { + var handler = (System.Func>)del; + EndpointFilterDelegate? filteredInvocation = null; + + if (options?.EndpointBuilder?.FilterFactories.Count > 0) + { + filteredInvocation = GeneratedRouteBuilderExtensionsCore.BuildFilterDelegate(ic => + { + if (ic.HttpContext.Response.StatusCode == 400) + { + return ValueTask.FromResult(Results.Empty); + } + return ValueTask.FromResult(handler()); + }, + options.EndpointBuilder, + handler.Method); + } + + async Task RequestHandler(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "application/json"; + var result = await handler(); + await httpContext.Response.WriteAsync(result); + } + async Task RequestHandlerFiltered(HttpContext httpContext) + { + var result = await filteredInvocation(new DefaultEndpointFilterInvocationContext(httpContext)); + await GeneratedRouteBuilderExtensionsCore.ExecuteObjectResult(result, httpContext); + } + + RequestDelegate targetDelegate = filteredInvocation is null ? RequestHandler : RequestHandlerFiltered; + var metadata = inferredMetadataResult?.EndpointMetadata ?? ReadOnlyCollection.Empty; + return new RequestDelegateResult(targetDelegate, metadata); + }), + + }; + + internal static RouteHandlerBuilder MapCore( + this IEndpointRouteBuilder routes, + string pattern, + Delegate handler, + IEnumerable httpMethods, + string filePath, + int lineNumber) + { + var (populateMetadata, createRequestDelegate) = map[(filePath, lineNumber)]; + return RouteHandlerServices.Map(routes, pattern, handler, httpMethods, populateMetadata, createRequestDelegate); + } + + private static EndpointFilterDelegate BuildFilterDelegate(EndpointFilterDelegate filteredInvocation, EndpointBuilder builder, MethodInfo mi) + { + var routeHandlerFilters = builder.FilterFactories; + var context0 = new EndpointFilterFactoryContext + { + MethodInfo = mi, + ApplicationServices = builder.ApplicationServices, + }; + var initialFilteredInvocation = filteredInvocation; + for (var i = routeHandlerFilters.Count - 1; i >= 0; i--) + { + var filterFactory = routeHandlerFilters[i]; + filteredInvocation = filterFactory(context0, filteredInvocation); + } + return filteredInvocation; + } + + private static Task ExecuteObjectResult(object? obj, HttpContext httpContext) + { + if (obj is IResult r) + { + return r.ExecuteAsync(httpContext); + } + else if (obj is string s) + { + return httpContext.Response.WriteAsync(s); + } + else + { + return httpContext.Response.WriteAsJsonAsync(obj); + } + } + } +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorIncrementalityTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorIncrementalityTests.cs new file mode 100644 index 000000000000..111442bea3a3 --- /dev/null +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorIncrementalityTests.cs @@ -0,0 +1,45 @@ +// 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; +namespace Microsoft.AspNetCore.Http.Generators.Tests; + +public class RequestDelegateGeneratorIncrementalityTests : RequestDelegateGeneratorTestBase +{ + [Fact] + public void MapAction_SameReturnType_DoesNotTriggerUpdate() + { + var source = @"app.MapGet(""/hello"", () => ""Hello world!"");"; + var updatedSource = @"app.MapGet(""/hello"", () => ""Bye world!"");"; + + var (result, compilation) = RunGenerator(source, updatedSource); + var outputSteps = GetRunStepOutputs(result); + + Assert.All(outputSteps, (value) => Assert.Equal(IncrementalStepRunReason.Cached, value.Reason)); + } + + [Fact] + public void MapAction_DifferentRoutePattern_DoesNotTriggerUpdate() + { + var source = @"app.MapGet(""/hello"", () => ""Hello world!"");"; + var updatedSource = @"app.MapGet(""/hello-2"", () => ""Hello world!"");"; + + var (result, compilation) = RunGenerator(source, updatedSource); + var outputSteps = GetRunStepOutputs(result); + + Assert.All(outputSteps, (value) => Assert.Equal(IncrementalStepRunReason.Cached, value.Reason)); + } + + [Fact] + public void MapAction_ChangeReturnType_TriggersUpdate() + { + var source = @"app.MapGet(""/hello"", () => ""Hello world!"");"; + var updatedSource = @"app.MapGet(""/hello"", () => Task.FromResult(""Hello world!""));"; + + var (result, compilation) = RunGenerator(source, updatedSource); + var outputSteps = GetRunStepOutputs(result); + + Assert.All(outputSteps, (value) => Assert.Equal(IncrementalStepRunReason.New, value.Reason)); + } + + private static IEnumerable<(object Value, IncrementalStepRunReason Reason)> GetRunStepOutputs(GeneratorRunResult result) => result.TrackedOutputSteps.SelectMany(step => step.Value).SelectMany(value => value.Outputs); +} diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs index 87fe135e619b..0a192576a1e1 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTestBase.cs @@ -1,16 +1,15 @@ // 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.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; using System.Text; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Generators; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Testing; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.DependencyInjection; @@ -19,9 +18,9 @@ namespace Microsoft.AspNetCore.Http.Generators.Tests; -public class RequestDelegateGeneratorTestBase +public class RequestDelegateGeneratorTestBase : LoggedTest { - internal static (ImmutableArray, Compilation) RunGenerator(string sources) + internal static (GeneratorRunResult, Compilation) RunGenerator(string sources, params string[] updatedSources) { var compilation = CreateCompilation(sources); var generator = new RequestDelegateGenerator().AsSourceGenerator(); @@ -36,18 +35,25 @@ internal static (ImmutableArray, Compilation) RunGenerator(s // Run the source generator driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var updatedCompilation, out var _); + foreach (var updatedSource in updatedSources) + { + var syntaxTree = CSharpSyntaxTree.ParseText(GetMapActionString(updatedSource), path: $"TestMapActions.cs"); + compilation = compilation + .ReplaceSyntaxTree(compilation.SyntaxTrees.First(), syntaxTree); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out updatedCompilation, + out var _); + } var diagnostics = updatedCompilation.GetDiagnostics(); Assert.Empty(diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning)); var runResult = driver.GetRunResult(); - return (runResult.Results, updatedCompilation); + return (Assert.Single(runResult.Results), updatedCompilation); } - internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(ImmutableArray results, string stepName) + internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(GeneratorRunResult result, string stepName) { // We only invoke the generator once in our test scenarios - var firstGeneratorPass = results[0]; - if (firstGeneratorPass.TrackedSteps.TryGetValue(stepName, out var staticEndpointSteps)) + if (result.TrackedSteps.TryGetValue(stepName, out var staticEndpointSteps)) { var staticEndpointStep = staticEndpointSteps.Single(); var staticEndpointOutput = staticEndpointStep.Outputs.Single(); @@ -58,7 +64,7 @@ internal static StaticRouteHandlerModel.Endpoint GetStaticEndpoint(ImmutableArra return null; } - internal static Endpoint GetEndpointFromCompilation(Compilation compilation) + internal static Endpoint GetEndpointFromCompilation(Compilation compilation, bool checkSourceKey = true) { var assemblyName = compilation.AssemblyName!; var symbolsName = Path.ChangeExtension(assemblyName, "pdb"); @@ -100,7 +106,6 @@ internal static Endpoint GetEndpointFromCompilation(Compilation compilation) var handler = assembly.GetType("TestMapActions") ?.GetMethod("MapTestEndpoints", BindingFlags.Public | BindingFlags.Static) ?.CreateDelegate>(); - var sourceKeyType = assembly.GetType("Microsoft.AspNetCore.Builder.SourceKey"); Assert.NotNull(handler); @@ -111,15 +116,31 @@ internal static Endpoint GetEndpointFromCompilation(Compilation compilation) // Trigger Endpoint build by calling getter. var endpoint = Assert.Single(dataSource.Endpoints); - var sourceKeyMetadata = endpoint.Metadata.Single(metadata => metadata.GetType() == sourceKeyType); - Assert.NotNull(sourceKeyMetadata); + if (checkSourceKey) + { + var sourceKeyType = assembly.GetType("Microsoft.AspNetCore.Builder.SourceKey"); + var sourceKeyMetadata = endpoint.Metadata.Single(metadata => metadata.GetType() == sourceKeyType); + Assert.NotNull(sourceKeyMetadata); + } return endpoint; } - private static Compilation CreateCompilation(string sources) + internal HttpContext CreateHttpContext() { - var source = $$""" + var httpContext = new DefaultHttpContext(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + return httpContext; + } + + private static string GetMapActionString(string sources) => $$""" #nullable enable using System; using System.Collections.Generic; @@ -137,8 +158,25 @@ public static IEndpointRouteBuilder MapTestEndpoints(this IEndpointRouteBuilder {{sources}} return app; } + + public interface ITodo + { + public int Id { get; } + public string? Name { get; } + public bool IsComplete { get; } + } + + public class Todo + { + public int Id { get; set; } + public string? Name { get; set; } = "Todo"; + public bool IsComplete { get; set; } + } } """; + private static Compilation CreateCompilation(string sources) + { + var source = GetMapActionString(sources); var syntaxTrees = new[] { diff --git a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs index 18db6f48d6bf..dca59e23a360 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateGenerator/RequestDelegateGeneratorTests.cs @@ -6,23 +6,24 @@ namespace Microsoft.AspNetCore.Http.Generators.Tests; public class RequestDelegateGeneratorTests : RequestDelegateGeneratorTestBase { [Theory] - [InlineData(@"app.MapGet(""/hello"", () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapPost(""/hello"", () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapDelete(""/hello"", () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapPut(""/hello"", () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapGet(pattern: ""/hello"", handler: () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapPost(handler: () => ""Hello world!"", pattern: ""/hello"");", "Hello world!")] - [InlineData(@"app.MapDelete(pattern: ""/hello"", handler: () => ""Hello world!"");", "Hello world!")] - [InlineData(@"app.MapPut(handler: () => ""Hello world!"", pattern: ""/hello"");", "Hello world!")] - public async Task MapAction_NoParam_StringReturn(string source, string expectedBody) + [InlineData(@"app.MapGet(""/hello"", () => ""Hello world!"");", "MapGet", "Hello world!")] + [InlineData(@"app.MapPost(""/hello"", () => ""Hello world!"");", "MapPost", "Hello world!")] + [InlineData(@"app.MapDelete(""/hello"", () => ""Hello world!"");", "MapDelete", "Hello world!")] + [InlineData(@"app.MapPut(""/hello"", () => ""Hello world!"");", "MapPut", "Hello world!")] + [InlineData(@"app.MapGet(pattern: ""/hello"", handler: () => ""Hello world!"");", "MapGet", "Hello world!")] + [InlineData(@"app.MapPost(handler: () => ""Hello world!"", pattern: ""/hello"");", "MapPost", "Hello world!")] + [InlineData(@"app.MapDelete(pattern: ""/hello"", handler: () => ""Hello world!"");", "MapDelete", "Hello world!")] + [InlineData(@"app.MapPut(handler: () => ""Hello world!"", pattern: ""/hello"");", "MapPut", "Hello world!")] + public async Task MapAction_NoParam_StringReturn(string source, string httpMethod, string expectedBody) { - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; Assert.Equal("/hello", endpointModel.Route.RoutePattern); + Assert.Equal(httpMethod, endpointModel.HttpMethod); var httpContext = new DefaultHttpContext(); @@ -50,11 +51,11 @@ public async Task MapGet_NoParam_StringReturn_WithFilter() }); """; var expectedBody = "Filtered: Hello world!"; - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); await VerifyAgainstBaselineUsingFile(compilation); - var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); var endpoint = GetEndpointFromCompilation(compilation); var requestDelegate = endpoint.RequestDelegate; @@ -76,13 +77,226 @@ public async Task MapGet_NoParam_StringReturn_WithFilter() } [Theory] - [InlineData("""app.MapGet("/hello", () => 2);""")] - [InlineData("""app.MapGet("/hello", () => new System.DateTime());""")] - public void MapGet_UnsupportedSignature_DoesNotEmit(string source) + [InlineData(@"app.MapGet(""/"", () => 123456);", "123456")] + [InlineData(@"app.MapGet(""/"", () => true);", "true")] + [InlineData(@"app.MapGet(""/"", () => new DateTime(2023, 1, 1));", @"""2023-01-01T00:00:00""")] + public async Task MapAction_NoParam_AnyReturn(string source, string expectedBody) { - var (results, compilation) = RunGenerator(source); + var (result, compilation) = RunGenerator(source); - var endpointModel = GetStaticEndpoint(results, "EndpointModel"); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + + var httpContext = new DefaultHttpContext(); + + var outStream = new MemoryStream(); + httpContext.Response.Body = outStream; + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => new Todo() { Name = ""Test Item""});")] + [InlineData(""" +object GetTodo() => new Todo() { Name = "Test Item"}; +app.MapGet("/", GetTodo); +""")] + [InlineData(@"app.MapGet(""/"", () => TypedResults.Ok(new Todo() { Name = ""Test Item""}));")] + public async Task MapAction_NoParam_ComplexReturn(string source) + { + var expectedBody = """{"id":0,"name":"Test Item","isComplete":false}"""; + var (result, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => Console.WriteLine(""Returns void""));", null)] + [InlineData(@"app.MapGet(""/"", () => TypedResults.Ok(""Alright!""));", null)] + [InlineData(@"app.MapGet(""/"", () => Results.NotFound(""Oops!""));", null)] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(new Todo() { Name = ""Test Item""}));", "application/json")] + [InlineData(@"app.MapGet(""/"", () => ""Hello world!"");", "text/plain")] + public void MapAction_ProducesCorrectContentType(string source, string expectedContentType) + { + var (result, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.Equal(expectedContentType, endpointModel.Response.ContentType); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(""Hello world!""));", "Hello world!")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] + public async Task MapAction_NoParam_TaskOfTReturn(string source, string expectedBody) + { + var (result, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => ValueTask.FromResult(""Hello world!""));", "Hello world!")] + [InlineData(@"app.MapGet(""/"", () => ValueTask.FromResult(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => ValueTask.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] + public async Task MapAction_NoParam_ValueTaskOfTReturn(string source, string expectedBody) + { + var (result, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Theory] + [InlineData(@"app.MapGet(""/"", () => new ValueTask(""Hello world!""));", "Hello world!")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(""Hello world!""));", "Hello world!")] + [InlineData(@"app.MapGet(""/"", () => new ValueTask(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(new Todo() { Name = ""Test Item""}));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => new ValueTask(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] + [InlineData(@"app.MapGet(""/"", () => Task.FromResult(TypedResults.Ok(new Todo() { Name = ""Test Item""})));", """{"id":0,"name":"Test Item","isComplete":false}""")] + public async Task MapAction_NoParam_TaskLikeOfObjectReturn(string source, string expectedBody) + { + var (result, compilation) = RunGenerator(source); + + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); + var endpoint = GetEndpointFromCompilation(compilation); + var requestDelegate = endpoint.RequestDelegate; + + Assert.Equal("/", endpointModel.Route.RoutePattern); + Assert.Equal("MapGet", endpointModel.HttpMethod); + Assert.True(endpointModel.Response.IsAwaitable); + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Fact] + public async Task Multiple_MapAction_NoParam_StringReturn() + { + var source = """ +app.MapGet("/en", () => "Hello world!"); +app.MapGet("/es", () => "Hola mundo!"); +app.MapGet("/en-task", () => Task.FromResult("Hello world!")); +app.MapGet("/es-task", () => new ValueTask("Hola mundo!")); +"""; + var (_, compilation) = RunGenerator(source); + + await VerifyAgainstBaselineUsingFile(compilation); + } + + [Fact] + public async Task MapAction_VariableRoutePattern_EmitsDiagnostic_NoSource() + { + var expectedBody = "Hello world!"; + var source = """ +var route = "/en"; +app.MapGet(route, () => "Hello world!"); +"""; + var (result, compilation) = RunGenerator(source); + + // Emits diagnostic but generates no source + var diagnostic = Assert.Single(result.Diagnostics); + Assert.Equal(DiagnosticDescriptors.UnableToResolveRoutePattern.Id,diagnostic.Id); + Assert.Empty(result.GeneratedSources); + + // Falls back to runtime-generated endpoint + var endpoint = GetEndpointFromCompilation(compilation, checkSourceKey: false); + var requestDelegate = endpoint.RequestDelegate; + + var httpContext = CreateHttpContext(); + + await requestDelegate(httpContext); + + var httpResponse = httpContext.Response; + httpResponse.Body.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(httpResponse.Body); + var body = await streamReader.ReadToEndAsync(); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(expectedBody, body); + } + + [Fact] + public void MapAction_RequestDelegateHandler_DoesNotEmit() + { + var source = """ +app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello world")); +"""; + var (result, _) = RunGenerator(source); + var endpointModel = GetStaticEndpoint(result, GeneratorSteps.EndpointModelStep); + + // Endpoint model is null because we don't pass transform Assert.Null(endpointModel); + Assert.Empty(result.GeneratedSources); } } diff --git a/src/Http/samples/MinimalSample/MinimalSample.csproj b/src/Http/samples/MinimalSample/MinimalSample.csproj index 8c5e9e6306e3..f057142bca29 100644 --- a/src/Http/samples/MinimalSample/MinimalSample.csproj +++ b/src/Http/samples/MinimalSample/MinimalSample.csproj @@ -3,6 +3,8 @@ $(DefaultNetCoreTargetFramework) enable + true + true @@ -15,4 +17,8 @@ + + + + diff --git a/src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/BoundedCacheWithFactory.cs b/src/Shared/RoslynUtils/BoundedCacheWithFactory.cs similarity index 100% rename from src/Framework/AspNetCoreAnalyzers/src/Analyzers/Infrastructure/BoundedCacheWithFactory.cs rename to src/Shared/RoslynUtils/BoundedCacheWithFactory.cs diff --git a/src/Shared/RoslynUtils/WellKnownTypes.cs b/src/Shared/RoslynUtils/WellKnownTypes.cs new file mode 100644 index 000000000000..3d8c22d17b10 --- /dev/null +++ b/src/Shared/RoslynUtils/WellKnownTypes.cs @@ -0,0 +1,130 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.AspNetCore.Analyzers.Infrastructure; +using Microsoft.CodeAnalysis; + +namespace Microsoft.AspNetCore.App.Analyzers.Infrastructure; + +internal class WellKnownTypes +{ + private static readonly BoundedCacheWithFactory LazyWellKnownTypesCache = new(); + + public static WellKnownTypes GetOrCreate(Compilation compilation) => + LazyWellKnownTypesCache.GetOrCreateValue(compilation, static c => new WellKnownTypes(c)); + + private readonly INamedTypeSymbol?[] _lazyWellKnownTypes; + private readonly Compilation _compilation; + + static WellKnownTypes() + { + AssertEnumAndTableInSync(); + } + + [Conditional("DEBUG")] + private static void AssertEnumAndTableInSync() + { + for (var i = 0; i < WellKnownTypeData.WellKnownTypeNames.Length; i++) + { + var name = WellKnownTypeData.WellKnownTypeNames[i]; + var typeId = (WellKnownTypeData.WellKnownType)i; + + var typeIdName = typeId.ToString().Replace("__", "+").Replace('_', '.'); + + var separator = name.IndexOf('`'); + if (separator >= 0) + { + // Ignore type parameter qualifier for generic types. + name = name.Substring(0, separator); + typeIdName = typeIdName.Substring(0, separator); + } + + Debug.Assert(name == typeIdName, $"Enum name ({typeIdName}) and type name ({name}) must match at {i}"); + } + } + + private WellKnownTypes(Compilation compilation) + { + _lazyWellKnownTypes = new INamedTypeSymbol?[WellKnownTypeData.WellKnownTypeNames.Length]; + _compilation = compilation; + } + + public INamedTypeSymbol Get(SpecialType type) + { + return _compilation.GetSpecialType(type); + } + + public INamedTypeSymbol Get(WellKnownTypeData.WellKnownType type) + { + var index = (int)type; + var symbol = _lazyWellKnownTypes[index]; + if (symbol is not null) + { + return symbol; + } + + // Symbol hasn't been added to the cache yet. + // Resolve symbol from name, cache, and return. + return GetAndCache(index); + } + + private INamedTypeSymbol GetAndCache(int index) + { + var result = _compilation.GetTypeByMetadataName(WellKnownTypeData.WellKnownTypeNames[index]); + if (result == null) + { + throw new InvalidOperationException($"Failed to resolve well-known type '{WellKnownTypeData.WellKnownTypeNames[index]}'."); + } + Interlocked.CompareExchange(ref _lazyWellKnownTypes[index], result, null); + + // GetTypeByMetadataName should always return the same instance for a name. + // To ensure we have a consistent value, for thread safety, return symbol set in the array. + return _lazyWellKnownTypes[index]!; + } + + public bool IsType(ITypeSymbol type, WellKnownTypeData.WellKnownType[] wellKnownTypes) => IsType(type, wellKnownTypes, out var _); + + public bool IsType(ITypeSymbol type, WellKnownTypeData.WellKnownType[] wellKnownTypes, [NotNullWhen(true)] out WellKnownTypeData.WellKnownType? match) + { + foreach (var wellKnownType in wellKnownTypes) + { + if (SymbolEqualityComparer.Default.Equals(type, Get(wellKnownType))) + { + match = wellKnownType; + return true; + } + } + + match = null; + return false; + } + + public bool Implements(ITypeSymbol type, WellKnownTypeData.WellKnownType[] interfaceWellKnownTypes) + { + foreach (var wellKnownType in interfaceWellKnownTypes) + { + if (Implements(type, Get(wellKnownType))) + { + return true; + } + } + + return false; + } + + public static bool Implements(ITypeSymbol type, ITypeSymbol interfaceType) + { + foreach (var t in type.AllInterfaces) + { + if (SymbolEqualityComparer.Default.Equals(t, interfaceType)) + { + return true; + } + } + return false; + } +}