From 9ff9267096425b5caf39df8dcf134e33faaaba26 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 5 Aug 2021 09:15:58 -0500 Subject: [PATCH 1/3] Set EndpointName automatically from method name --- ...malActionEndpointRouteBuilderExtensions.cs | 9 ++++ .../src/Microsoft.AspNetCore.Routing.csproj | 1 + ...ctionEndpointRouteBuilderExtensionsTest.cs | 49 +++++++++++++++++++ .../EndpointMetadataApiDescriptionProvider.cs | 8 +-- ...icrosoft.AspNetCore.Mvc.ApiExplorer.csproj | 1 + src/Shared/TypeHelper.cs | 28 +++++++++++ 6 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 src/Shared/TypeHelper.cs diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index 02addb8e468b..422378eeddff 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; @@ -184,6 +185,14 @@ public static MinimalActionEndpointConventionBuilder Map( // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint. builder.Metadata.Add(action.Method); + // Methods defined in a top-level program are generated as statics so the delegate + // target will be null. Inline lambdas are compiler generated properties so they can + // be filtered that way. + if (action.Target == null || !TypeHelper.IsCompilerGenerated(action.Method.Name)) + { + builder.Metadata.Add(new EndpointNameMetadata(action.Method.Name)); + } + // Add delegate attributes as metadata var attributes = action.Method.GetCustomAttributes(); diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index 4c0c0a1a5443..b992da41da2c 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -24,6 +24,7 @@ Microsoft.AspNetCore.Routing.RouteCollection + diff --git a/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs index 4dd5bbaa4569..ecbe22a7b813 100644 --- a/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs @@ -359,6 +359,55 @@ public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder() Assert.Equal(int.MaxValue, routeEndpointBuilder.Order); } + [Fact] + // This test scenario simulates methods defined in a top-level program + // which are compiler generated. + public void MapMethod_SetsEndpointNameForInnerMethod() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + string InnerGetString() => "TestString"; + _ = builder.MapDelete("/", InnerGetString); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var endpointName = endpoint.Metadata.GetMetadata(); + Assert.NotNull(endpointName); + Assert.Equal("InnerGetString", endpointName?.EndpointName); + } + + [Fact] + public void MapMethod_SetsEndpointNameForMethodGroup() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + _ = builder.MapDelete("/", GetString); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var endpointName = endpoint.Metadata.GetMetadata(); + Assert.NotNull(endpointName); + Assert.Equal("GetString", endpointName?.EndpointName); + } + + private string GetString() => "TestString"; + + [Fact] + public void MapMethod_DoesNotSetEndpointNameForLambda() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + _ = builder.MapDelete("/", () => { }); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var endpointName = endpoint.Metadata.GetMetadata(); + Assert.Null(endpointName); + } + class FromRoute : Attribute, IFromRouteMetadata { public string? Name { get; set; } diff --git a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs index fc3f69701a1e..d6a93763e6e4 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs +++ b/src/Mvc/Mvc.ApiExplorer/src/EndpointMetadataApiDescriptionProvider.cs @@ -78,7 +78,7 @@ private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string // For now, put all methods defined the same declaring type together. string controllerName; - if (methodInfo.DeclaringType is not null && !IsCompilerGenerated(methodInfo.DeclaringType)) + if (methodInfo.DeclaringType is not null && !TypeHelper.IsCompilerGenerated(methodInfo.DeclaringType.Name, methodInfo.DeclaringType)) { controllerName = methodInfo.DeclaringType.Name; } @@ -363,11 +363,5 @@ private static void AddActionDescriptorEndpointMetadata( actionDescriptor.EndpointMetadata = new List(endpointMetadata); } } - - // The CompilerGeneratedAttribute doesn't always get added so we also check if the type name starts with "<" - // For example, "<>c" is a "declaring" type the C# compiler will generate without the attribute for a top-level lambda - // REVIEW: Is there a better way to do this? - private static bool IsCompilerGenerated(Type type) => - Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute)) || type.Name.StartsWith('<'); } } diff --git a/src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj b/src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj index 4b86355d1087..a9748b7bc86b 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj +++ b/src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Shared/TypeHelper.cs b/src/Shared/TypeHelper.cs new file mode 100644 index 000000000000..1f81ee50526f --- /dev/null +++ b/src/Shared/TypeHelper.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + internal static class TypeHelper + { + /// + /// Checks to see if a given type is compiler generated. + /// + /// The compiler doesn't always annotate every time it generates with the + /// CompilerGeneratedAttribute so sometimes we have to check if the type's + /// identifier represents a generated type. Follows the same heuristics seen + /// in https://github.com/dotnet/roslyn/blob/b57c1f89c1483da8704cde7b535a20fd029748db/src/ExpressionEvaluator/Core/Source/ResultProvider/Helpers/GeneratedMetadataNames.cs#L19 + /// + /// + /// The type to evaluate. Can be null if evaluating only on name. + /// The identifier associated wit the type. + /// if is compiler generated + /// or represents a compiler generated identifier. + internal static bool IsCompilerGenerated(string name, Type? type = null) + { + return (type is Type && Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute))) + || name.StartsWith("<", StringComparison.Ordinal) + || (name.IndexOf('$') >= 0); + } + } +} \ No newline at end of file From 704673f0fe5613bb0ccde399511e6ccb0bae696c Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 17 Aug 2021 15:28:20 +0000 Subject: [PATCH 2/3] Only support adding names from local functions --- .../MinimalActionEndpointRouteBuilderExtensions.cs | 12 ++++++++---- ...inimalActionEndpointRouteBuilderExtensionsTest.cs | 8 ++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index 422378eeddff..aa278dbdf63e 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -185,10 +185,14 @@ public static MinimalActionEndpointConventionBuilder Map( // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint. builder.Metadata.Add(action.Method); - // Methods defined in a top-level program are generated as statics so the delegate - // target will be null. Inline lambdas are compiler generated properties so they can - // be filtered that way. - if (action.Target == null || !TypeHelper.IsCompilerGenerated(action.Method.Name)) + // We only add endpoint names for types that are not compiler generated since + // compiler generated types are mangled by default. This logic can be changed once + // https://github.com/dotnet/roslyn/issues/55651 is addressed. For now, this will + // not set the endpoint name metadata for: + // - Local functions + // - Inline lambdas + // - Static functions + if (!TypeHelper.IsCompilerGenerated(action.Method.Name)) { builder.Metadata.Add(new EndpointNameMetadata(action.Method.Name)); } diff --git a/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs index ecbe22a7b813..ec8480c072cf 100644 --- a/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs @@ -361,8 +361,9 @@ public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder() [Fact] // This test scenario simulates methods defined in a top-level program - // which are compiler generated. - public void MapMethod_SetsEndpointNameForInnerMethod() + // which are compiler generated. This can be re-examined once + // https://github.com/dotnet/roslyn/issues/55651 is addressed. + public void MapMethod_DoesNotEndpointNameForInnerMethod() { var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); string InnerGetString() => "TestString"; @@ -373,8 +374,7 @@ public void MapMethod_SetsEndpointNameForInnerMethod() var endpoint = Assert.Single(dataSource.Endpoints); var endpointName = endpoint.Metadata.GetMetadata(); - Assert.NotNull(endpointName); - Assert.Equal("InnerGetString", endpointName?.EndpointName); + Assert.Null(endpointName); } [Fact] From c5fa42fd44200738e7117ea8254af0a21205f303 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Tue, 17 Aug 2021 18:53:05 +0000 Subject: [PATCH 3/3] Supoort local functions via Roslyn parsing logic --- ...malActionEndpointRouteBuilderExtensions.cs | 24 ++- ...tingEndpointConventionBuilderExtensions.cs | 1 + .../src/Microsoft.AspNetCore.Routing.csproj | 5 +- ...ctionEndpointRouteBuilderExtensionsTest.cs | 22 ++- ...icrosoft.AspNetCore.Mvc.ApiExplorer.csproj | 2 +- .../RoslynUtils/GeneratedNameConstants.cs | 17 ++ src/Shared/RoslynUtils/GeneratedNameKind.cs | 70 ++++++++ src/Shared/RoslynUtils/GeneratedNameParser.cs | 170 ++++++++++++++++++ src/Shared/{ => RoslynUtils}/TypeHelper.cs | 0 9 files changed, 297 insertions(+), 14 deletions(-) create mode 100644 src/Shared/RoslynUtils/GeneratedNameConstants.cs create mode 100644 src/Shared/RoslynUtils/GeneratedNameKind.cs create mode 100644 src/Shared/RoslynUtils/GeneratedNameParser.cs rename src/Shared/{ => RoslynUtils}/TypeHelper.cs (100%) diff --git a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs index aa278dbdf63e..f34254be0816 100644 --- a/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/MinimalActionEndpointRouteBuilderExtensions.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; +using Microsoft.CodeAnalysis.CSharp.Symbols; namespace Microsoft.AspNetCore.Builder { @@ -185,16 +186,21 @@ public static MinimalActionEndpointConventionBuilder Map( // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint. builder.Metadata.Add(action.Method); - // We only add endpoint names for types that are not compiler generated since - // compiler generated types are mangled by default. This logic can be changed once - // https://github.com/dotnet/roslyn/issues/55651 is addressed. For now, this will - // not set the endpoint name metadata for: - // - Local functions - // - Inline lambdas - // - Static functions - if (!TypeHelper.IsCompilerGenerated(action.Method.Name)) + // Methods defined in a top-level program are generated as statics so the delegate + // target will be null. Inline lambdas are compiler generated properties so they can + // be filtered that way. + if (action.Target == null || !TypeHelper.IsCompilerGenerated(action.Method.Name)) { - builder.Metadata.Add(new EndpointNameMetadata(action.Method.Name)); + if (GeneratedNameParser.TryParseLocalFunctionName(action.Method.Name, out var endpointName)) + { + builder.Metadata.Add(new EndpointNameMetadata(endpointName)); + builder.Metadata.Add(new RouteNameMetadata(endpointName)); + } + else + { + builder.Metadata.Add(new EndpointNameMetadata(action.Method.Name)); + builder.Metadata.Add(new RouteNameMetadata(action.Method.Name)); + } } // Add delegate attributes as metadata diff --git a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs index 24b9b3df6c88..4c29a1dce365 100644 --- a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Linq; using Microsoft.AspNetCore.Routing; namespace Microsoft.AspNetCore.Builder diff --git a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj index b992da41da2c..dabaeffa8273 100644 --- a/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj +++ b/src/Http/Routing/src/Microsoft.AspNetCore.Routing.csproj @@ -24,7 +24,10 @@ Microsoft.AspNetCore.Routing.RouteCollection - + + + + diff --git a/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs index ec8480c072cf..6bb049c116aa 100644 --- a/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/MinimalActionEndpointRouteBuilderExtensionsTest.cs @@ -361,8 +361,8 @@ public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder() [Fact] // This test scenario simulates methods defined in a top-level program - // which are compiler generated. This can be re-examined once - // https://github.com/dotnet/roslyn/issues/55651 is addressed. + // which are compiler generated. We currently do some manually parsing leveraging + // code in Roslyn to support this scenario. More info at https://github.com/dotnet/roslyn/issues/55651. public void MapMethod_DoesNotEndpointNameForInnerMethod() { var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); @@ -374,7 +374,8 @@ public void MapMethod_DoesNotEndpointNameForInnerMethod() var endpoint = Assert.Single(dataSource.Endpoints); var endpointName = endpoint.Metadata.GetMetadata(); - Assert.Null(endpointName); + Assert.NotNull(endpointName); + Assert.Equal("InnerGetString", endpointName?.EndpointName); } [Fact] @@ -392,6 +393,21 @@ public void MapMethod_SetsEndpointNameForMethodGroup() Assert.Equal("GetString", endpointName?.EndpointName); } + [Fact] + public void WithNameOverridesDefaultEndpointName() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvdier())); + _ = builder.MapDelete("/", GetString).WithName("SomeCustomName"); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var endpointName = endpoint.Metadata.GetMetadata(); + Assert.NotNull(endpointName); + Assert.Equal("SomeCustomName", endpointName?.EndpointName); + } + private string GetString() => "TestString"; [Fact] diff --git a/src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj b/src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj index a9748b7bc86b..a1170d9393e4 100644 --- a/src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj +++ b/src/Mvc/Mvc.ApiExplorer/src/Microsoft.AspNetCore.Mvc.ApiExplorer.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Shared/RoslynUtils/GeneratedNameConstants.cs b/src/Shared/RoslynUtils/GeneratedNameConstants.cs new file mode 100644 index 000000000000..b771fb784464 --- /dev/null +++ b/src/Shared/RoslynUtils/GeneratedNameConstants.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// These sources are copied from https://github.com/dotnet/roslyn/blob/7d7bf0cc73e335390d73c9de6d7afd1e49605c9d/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNameConstants.cs +// and exist to address the issues with extracting original method names for +// generated local functions. See https://github.com/dotnet/roslyn/issues/55651 +// for more info. +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + internal static class GeneratedNameConstants + { + internal const char DotReplacementInTypeNames = '-'; + internal const string SynthesizedLocalNamePrefix = "CS$"; + internal const string SuffixSeparator = "__"; + internal const char LocalFunctionNameTerminator = '|'; + } +} \ No newline at end of file diff --git a/src/Shared/RoslynUtils/GeneratedNameKind.cs b/src/Shared/RoslynUtils/GeneratedNameKind.cs new file mode 100644 index 000000000000..262ebf5264d8 --- /dev/null +++ b/src/Shared/RoslynUtils/GeneratedNameKind.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +// These sources are copied from https://github.com/dotnet/roslyn/blob/7d7bf0cc73e335390d73c9de6d7afd1e49605c9d/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNameKind.cs +// and exist to address the issues with extracting original method names for +// generated local functions. See https://github.com/dotnet/roslyn/issues/55651 +// for more info. +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + internal enum GeneratedNameKind + { + None = 0, + + // Used by EE: + ThisProxyField = '4', + HoistedLocalField = '5', + DisplayClassLocalOrField = '8', + LambdaMethod = 'b', + LambdaDisplayClass = 'c', + StateMachineType = 'd', + LocalFunction = 'g', // note collision with Deprecated_InitializerLocal, however this one is only used for method names + + // Used by EnC: + AwaiterField = 'u', + HoistedSynthesizedLocalField = 's', + + // Currently not parsed: + StateMachineStateField = '1', + IteratorCurrentBackingField = '2', + StateMachineParameterProxyField = '3', + ReusableHoistedLocalField = '7', + LambdaCacheField = '9', + FixedBufferField = 'e', + AnonymousType = 'f', + TransparentIdentifier = 'h', + AnonymousTypeField = 'i', + AnonymousTypeTypeParameter = 'j', + AutoPropertyBackingField = 'k', + IteratorCurrentThreadIdField = 'l', + IteratorFinallyMethod = 'm', + BaseMethodWrapper = 'n', + AsyncBuilderField = 't', + DynamicCallSiteContainerType = 'o', + DynamicCallSiteField = 'p', + AsyncIteratorPromiseOfValueOrEndBackingField = 'v', + DisposeModeField = 'w', + CombinedTokensField = 'x', // last + + // Deprecated - emitted by Dev12, but not by Roslyn. + // Don't reuse the values because the debugger might encounter them when consuming old binaries. + [Obsolete] + Deprecated_OuterscopeLocals = '6', + [Obsolete] + Deprecated_IteratorInstance = 'a', + [Obsolete] + Deprecated_InitializerLocal = 'g', + [Obsolete] + Deprecated_DynamicDelegate = 'q', + [Obsolete] + Deprecated_ComrefCallLocal = 'r', + } + + internal static class GeneratedNameKindExtensions + { + internal static bool IsTypeName(this GeneratedNameKind kind) + => kind is GeneratedNameKind.LambdaDisplayClass or GeneratedNameKind.StateMachineType or GeneratedNameKind.DynamicCallSiteContainerType; + } +} \ No newline at end of file diff --git a/src/Shared/RoslynUtils/GeneratedNameParser.cs b/src/Shared/RoslynUtils/GeneratedNameParser.cs new file mode 100644 index 000000000000..ba6086484861 --- /dev/null +++ b/src/Shared/RoslynUtils/GeneratedNameParser.cs @@ -0,0 +1,170 @@ +// 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.CodeAnalysis; +using System.Globalization; + +// These sources are copied from https://github.com/dotnet/roslyn/blob/7d7bf0cc73e335390d73c9de6d7afd1e49605c9d/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNameParser.cs +// and exist to address the issues with extracting original method names for +// generated local functions. See https://github.com/dotnet/roslyn/issues/55651 +// for more info. +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + internal static class GeneratedNameParser + { + // Parse the generated name. Returns true for names of the form + // [CS$]<[middle]>c[__[suffix]] where [CS$] is included for certain + // generated names, where [middle] and [__[suffix]] are optional, + // and where c is a single character in [1-9a-z] + // (csharp\LanguageAnalysis\LIB\SpecialName.cpp). + internal static bool TryParseGeneratedName( + string name, + out GeneratedNameKind kind, + out int openBracketOffset, + out int closeBracketOffset) + { + openBracketOffset = -1; + if (name.StartsWith("CS$<", StringComparison.Ordinal)) + { + openBracketOffset = 3; + } + else if (name.StartsWith("<", StringComparison.Ordinal)) + { + openBracketOffset = 0; + } + + if (openBracketOffset >= 0) + { + closeBracketOffset = IndexOfBalancedParenthesis(name, openBracketOffset, '>'); + if (closeBracketOffset >= 0 && closeBracketOffset + 1 < name.Length) + { + int c = name[closeBracketOffset + 1]; + if (c is >= '1' and <= '9' or >= 'a' and <= 'z') // Note '0' is not special. + { + kind = (GeneratedNameKind)c; + return true; + } + } + } + + kind = GeneratedNameKind.None; + openBracketOffset = -1; + closeBracketOffset = -1; + return false; + } + + private static int IndexOfBalancedParenthesis(string str, int openingOffset, char closing) + { + char opening = str[openingOffset]; + + int depth = 1; + for (int i = openingOffset + 1; i < str.Length; i++) + { + var c = str[i]; + if (c == opening) + { + depth++; + } + else if (c == closing) + { + depth--; + if (depth == 0) + { + return i; + } + } + } + + return -1; + } + + internal static bool TryParseSourceMethodNameFromGeneratedName(string generatedName, GeneratedNameKind requiredKind, [NotNullWhen(true)] out string? methodName) + { + if (!TryParseGeneratedName(generatedName, out var kind, out int openBracketOffset, out int closeBracketOffset)) + { + methodName = null; + return false; + } + + if (requiredKind != 0 && kind != requiredKind) + { + methodName = null; + return false; + } + + methodName = generatedName.Substring(openBracketOffset + 1, closeBracketOffset - openBracketOffset - 1); + + if (kind.IsTypeName()) + { + methodName = methodName.Replace(GeneratedNameConstants.DotReplacementInTypeNames, '.'); + } + + return true; + } + + /// + /// Parses generated local function name out of a generated method name. + /// + internal static bool TryParseLocalFunctionName(string generatedName, [NotNullWhen(true)] out string? localFunctionName) + { + localFunctionName = null; + + // '<' containing-method-name '>' 'g' '__' local-function-name '|' method-ordinal '_' lambda-ordinal + if (!TryParseGeneratedName(generatedName, out var kind, out _, out int closeBracketOffset) || kind != GeneratedNameKind.LocalFunction) + { + return false; + } + + int localFunctionNameStart = closeBracketOffset + 2 + GeneratedNameConstants.SuffixSeparator.Length; + if (localFunctionNameStart >= generatedName.Length) + { + return false; + } + + int localFunctionNameEnd = generatedName.IndexOf(GeneratedNameConstants.LocalFunctionNameTerminator, localFunctionNameStart); + if (localFunctionNameEnd < 0) + { + return false; + } + + localFunctionName = generatedName.Substring(localFunctionNameStart, localFunctionNameEnd - localFunctionNameStart); + return true; + } + + // Extracts the slot index from a name of a field that stores hoisted variables or awaiters. + // Such a name ends with "__{slot index + 1}". + // Returned slot index is >= 0. + internal static bool TryParseSlotIndex(string fieldName, out int slotIndex) + { + int lastUnder = fieldName.LastIndexOf('_'); + if (lastUnder - 1 < 0 || lastUnder == fieldName.Length || fieldName[lastUnder - 1] != '_') + { + slotIndex = -1; + return false; + } + + if (int.TryParse(fieldName.AsSpan(lastUnder + 1), NumberStyles.None, CultureInfo.InvariantCulture, out slotIndex) && slotIndex >= 1) + { + slotIndex--; + return true; + } + + slotIndex = -1; + return false; + } + + internal static bool TryParseAnonymousTypeParameterName(string typeParameterName, [NotNullWhen(true)] out string? propertyName) + { + if (typeParameterName.StartsWith("<", StringComparison.Ordinal) && + typeParameterName.EndsWith(">j__TPar", StringComparison.Ordinal)) + { + propertyName = typeParameterName.Substring(1, typeParameterName.Length - 9); + return true; + } + + propertyName = null; + return false; + } + } +} \ No newline at end of file diff --git a/src/Shared/TypeHelper.cs b/src/Shared/RoslynUtils/TypeHelper.cs similarity index 100% rename from src/Shared/TypeHelper.cs rename to src/Shared/RoslynUtils/TypeHelper.cs