Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TypeName APIs to simplify metadata lookup. #111598

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a573f96
Add `TypeName.Namespace` and tests.
teo-tsirpanis Jan 20, 2025
dfe213f
Add `TypeName.Unescape`.
teo-tsirpanis Jan 20, 2025
2037805
Fix infinite loops.
teo-tsirpanis Jan 20, 2025
d0c0e02
Simplify loop.
teo-tsirpanis Jan 20, 2025
3567d78
Update reference assembly.
teo-tsirpanis Jan 20, 2025
afc4cb1
Address PR feedback around `Unescape`.
teo-tsirpanis Jan 20, 2025
dde6eda
Remove file with duplicate `Unescape` method.
teo-tsirpanis Jan 20, 2025
1da808a
Fix tests.
teo-tsirpanis Jan 20, 2025
ae14c6a
Reduce allocations when calling `Namespace` across a type name hierachy.
teo-tsirpanis Jan 20, 2025
0b03353
Fix nested types with namespaces.
teo-tsirpanis Jan 20, 2025
41d2f7c
Add tests for `Unescape`.
teo-tsirpanis Jan 20, 2025
1e3f969
Fix compile errors.
teo-tsirpanis Jan 20, 2025
269d0cc
Restore `TypeNameHelpers` and use it in all places except CoreLib.
teo-tsirpanis Jan 20, 2025
9548bee
Merge branch 'main' into typename-namespace-unescape
teo-tsirpanis Jan 25, 2025
94441e7
Do not omit escape character at the end when unescaping.
teo-tsirpanis Jan 25, 2025
c98beec
Return the namespace of the innermost nested type that has one.
teo-tsirpanis Jan 26, 2025
3763dbe
Update `TypeName.Name` to return the whole name of nested types.
teo-tsirpanis Jan 26, 2025
00c0ab7
Remove unnecessary `ValueStringBuilder.Dispose`.
teo-tsirpanis Jan 26, 2025
999d4e4
Update `GetNamespace` to fail if a nested type has a namespace.
teo-tsirpanis Jan 26, 2025
2885d4c
Remove support for nested types and escaped dots in namespaces.
teo-tsirpanis Jan 26, 2025
c84c41e
Remove support for escaped dots in `GetName`, and optimize it if the …
teo-tsirpanis Jan 26, 2025
fa7b459
Update tests.
teo-tsirpanis Jan 27, 2025
a6100e7
Simplify `Name` to avoid linear search of the full name in nested types.
teo-tsirpanis Jan 27, 2025
ae3b7e2
[mono] Do not treat nested type names as full names.
teo-tsirpanis Jan 27, 2025
d618093
Fix compile errors.
teo-tsirpanis Jan 27, 2025
82e156f
Update algorithm to find namespace delimiter.
teo-tsirpanis Feb 3, 2025
0d220f2
Revert nullable annotations changes.
teo-tsirpanis Feb 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,15 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName,
}
return null;
}
return GetTypeFromDefaultAssemblies(TypeNameHelpers.Unescape(escapedTypeName), nestedTypeNames, parsedName);
return GetTypeFromDefaultAssemblies(TypeName.Unescape(escapedTypeName), nestedTypeNames, parsedName);
}

if (assembly is RuntimeAssembly runtimeAssembly)
{
// Compat: Non-extensible parser allows ambiguous matches with ignore case lookup
bool useReflectionForNestedTypes = _extensibleParser && _ignoreCase;

type = runtimeAssembly.GetTypeCore(TypeNameHelpers.Unescape(escapedTypeName), useReflectionForNestedTypes ? default : nestedTypeNames,
type = runtimeAssembly.GetTypeCore(TypeName.Unescape(escapedTypeName), useReflectionForNestedTypes ? default : nestedTypeNames,
throwOnFileNotFound: _throwOnError, ignoreCase: _ignoreCase);

if (type is null)
Expand Down Expand Up @@ -282,7 +282,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName,
if (_throwOnError)
{
throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType,
nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : TypeNameHelpers.Unescape(escapedTypeName)),
nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : TypeName.Unescape(escapedTypeName)),
typeName: parsedName.FullName);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ internal partial struct TypeNameResolver
{
if (assembly is RuntimeAssemblyInfo runtimeAssembly)
{
type = runtimeAssembly.GetTypeCore(TypeNameHelpers.Unescape(escapedTypeName), throwOnError: _throwOnError, ignoreCase: _ignoreCase);
type = runtimeAssembly.GetTypeCore(TypeName.Unescape(escapedTypeName), throwOnError: _throwOnError, ignoreCase: _ignoreCase);
}
else
{
Expand All @@ -173,7 +173,7 @@ internal partial struct TypeNameResolver
}
else
{
string? unescapedTypeName = TypeNameHelpers.Unescape(escapedTypeName);
string? unescapedTypeName = TypeName.Unescape(escapedTypeName);

RuntimeAssemblyInfo? defaultAssembly = null;
if (_defaultAssemblyName != null)
Expand Down Expand Up @@ -235,7 +235,7 @@ internal partial struct TypeNameResolver
if (_throwOnError)
{
throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType,
nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : TypeNameHelpers.Unescape(escapedTypeName)),
nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : TypeName.Unescape(escapedTypeName)),
typeName: parsedName.FullName);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1489,9 +1489,6 @@
<Compile Include="$(LibrariesProjectRoot)System.Reflection.Metadata\src\System\Reflection\Metadata\TypeName.cs">
<Link>Common\System\Reflection\Metadata\TypeName.cs</Link>
</Compile>
<Compile Include="$(CommonPath)System\Reflection\Metadata\TypeNameHelpers.cs">
<Link>Common\System\Reflection\Metadata\TypeNameHelpers.cs</Link>
</Compile>
<Compile Include="$(LibrariesProjectRoot)System.Reflection.Metadata\src\System\Reflection\Metadata\TypeNameParser.cs">
<Link>Common\System\Reflection\Metadata\TypeNameParser.cs</Link>
</Compile>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ internal partial struct TypeNameResolver
current = typeName;
while (current.IsNested)
{
nestedTypeNames[--nestingDepth] = TypeNameHelpers.Unescape(current.Name);
nestedTypeNames[--nestingDepth] = TypeName.Unescape(current.Name);
current = current.DeclaringType!;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2437,6 +2437,7 @@ internal TypeName() { }
public bool IsSZArray { get { throw null; } }
public bool IsVariableBoundArrayType { get { throw null; } }
public string Name { get { throw null; } }
public string Namespace { get { throw null; } }
public int GetArrayRank() { throw null; }
public System.Reflection.Metadata.TypeName GetElementType() { throw null; }
public System.Collections.Immutable.ImmutableArray<System.Reflection.Metadata.TypeName> GetGenericArguments() { throw null; }
Expand All @@ -2449,6 +2450,7 @@ internal TypeName() { }
public System.Reflection.Metadata.TypeName MakeSZArrayTypeName() { throw null; }
public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan<char> typeName, System.Reflection.Metadata.TypeNameParseOptions? options = null) { throw null; }
public static bool TryParse(System.ReadOnlySpan<char> typeName, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Reflection.Metadata.TypeName? result, System.Reflection.Metadata.TypeNameParseOptions? options = null) { throw null; }
public static string Unescape(string name) { throw null; }
public System.Reflection.Metadata.TypeName WithAssemblyName(System.Reflection.Metadata.AssemblyNameInfo? assemblyName) { throw null; }
}
public sealed partial class TypeNameParseOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@
<data name="InvalidOperation_NoElement" xml:space="preserve">
<value>This operation is only valid on arrays, pointers and references.</value>
</data>
<data name="InvalidOperation_NestedTypeNamespace" xml:space="preserve">
<value>Cannot retrieve the namespace of a nested type.</value>
</data>
<data name="InvalidAssemblyName" xml:space="preserve">
<value>The given assembly name was invalid.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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;
Expand Down Expand Up @@ -39,7 +39,7 @@ sealed class TypeName
#else
private readonly ImmutableArray<TypeName> _genericArguments;
#endif
private string? _name, _fullName, _assemblyQualifiedName;
private string? _name, _namespace, _fullName, _assemblyQualifiedName;

internal TypeName(string? fullName,
AssemblyNameInfo? assemblyName,
Expand Down Expand Up @@ -217,6 +217,7 @@ public string FullName
/// This is because determining whether a type truly is a generic type requires loading the type
/// and performing a runtime check.</para>
/// </remarks>
[MemberNotNullWhen(false, nameof(_elementOrGenericType))]
public bool IsSimple => _elementOrGenericType is null;
teo-tsirpanis marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
Expand All @@ -229,6 +230,7 @@ public string FullName
/// Returns true if this is a nested type (e.g., "Namespace.Declaring+Nested").
/// For nested types <seealso cref="DeclaringType"/> returns their declaring type.
/// </summary>
[MemberNotNullWhen(true, nameof(_declaringType))]
public bool IsNested => _declaringType is not null;

/// <summary>
Expand Down Expand Up @@ -262,28 +264,89 @@ public string Name
{
if (IsConstructedGenericType)
{
_name = TypeNameParserHelpers.GetName(GetGenericTypeDefinition().FullName.AsSpan()).ToString();
_name = GetGenericTypeDefinition().Name;
}
teo-tsirpanis marked this conversation as resolved.
Show resolved Hide resolved
else if (IsPointer || IsByRef || IsArray)
{
ValueStringBuilder builder = new(stackalloc char[64]);
builder.Append(GetElementType().Name);
_name = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, ref builder);
}
else if (_nestedNameLength > 0 && _fullName is not null)
{
_name = TypeNameParserHelpers.GetName(_fullName.AsSpan(0, _nestedNameLength)).ToString();
}
else
{
_name = TypeNameParserHelpers.GetName(FullName.AsSpan()).ToString();
// _fullName can be null only in constructed generic or modified types, which we handled above.
Debug.Assert(_fullName is not null);
ReadOnlySpan<char> name = _fullName.AsSpan();
if (_nestedNameLength > 0)
{
name = name.Slice(0, _nestedNameLength);
}
if (IsNested)
{
// If the type is nested, we know the length of the declaring type's full name.
// Get the characters after that plus one for the '+' separator.
name = name.Slice(_declaringType._nestedNameLength + 1);
}
else if (TypeNameParserHelpers.IndexOfNamespaceDelimiter(name) is int idx && idx >= 0)
{
// If the type is not nested, find the namespace delimiter in the full name and and return the substring after it.
name = name.Slice(idx + 1);
}
_name = name.ToString();
}
}

return _name;
}
}

/// <summary>
/// The namespace of this type; e.g., "System".
/// </summary>
/// <exception cref="InvalidOperationException">This instance is a nested type.</exception>
public string Namespace
{
get
{
if (IsNested)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check be done on rootTypeName?

It does not make sense for "MyNamespace.A+B[]" to return MyNamespace when "MyNamespace.A+B" fails with InvalidOperationException.

TypeNameParserHelpers.ThrowInvalidOperation_NestedTypeNamespace();
}

if (_namespace is null)
{
TypeName rootTypeName = this;
while (!rootTypeName.IsSimple)
{
rootTypeName = rootTypeName._elementOrGenericType;
}

// By setting the namespace field at the root type name, we avoid recomputing it for all derived names.
if (rootTypeName._namespace is null)
{
// At this point the type does not have a modifier applied to it, so it should have its full name set.
Debug.Assert(rootTypeName._fullName is not null);
ReadOnlySpan<char> rootFullName = rootTypeName._fullName.AsSpan();
if (rootTypeName._nestedNameLength > 0)
{
rootFullName = rootFullName.Slice(0, rootTypeName._nestedNameLength);
}
if (TypeNameParserHelpers.IndexOfNamespaceDelimiter(rootFullName) is int idx && idx >= 0)
{
rootTypeName._namespace = rootFullName.Slice(0, idx).ToString();
}
else
{
rootTypeName._namespace = string.Empty;
}
}
_namespace = rootTypeName._namespace;
}

return _namespace;
}
}

/// <summary>
/// Represents the total number of <see cref="TypeName"/> instances that are used to describe
/// this instance, including any generic arguments or underlying types.
Expand Down Expand Up @@ -401,6 +464,25 @@ public static bool TryParse(ReadOnlySpan<char> typeName, [NotNullWhen(true)] out
return result is not null;
}

/// <summary>
/// Converts any escaped characters in the input type name or namespace.
/// </summary>
/// <param name="name">The input string containing the name to convert.</param>
/// <returns>A string of characters with any escaped characters converted to their unescaped form.</returns>
/// <remarks>
/// <para>The unescaped string can be used for looking up the type name or namespace in metadata.</para>
/// <para>This method removes escape characters even if they precede a character that does not require escaping.</para>
/// </remarks>
jkotas marked this conversation as resolved.
Show resolved Hide resolved
public static string Unescape(string name)
{
if (name is null)
{
TypeNameParserHelpers.ThrowArgumentNullException(nameof(name));
}
jkotas marked this conversation as resolved.
Show resolved Hide resolved

return TypeNameParserHelpers.Unescape(name);
}

/// <summary>
/// Gets the number of dimensions in an array.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
Expand Down Expand Up @@ -102,36 +102,56 @@ static int GetUnescapedOffset(ReadOnlySpan<char> input, int startOffset)
static bool NeedsEscaping(char c) => c is '[' or ']' or '&' or '*' or ',' or '+' or EscapeCharacter;
}

internal static ReadOnlySpan<char> GetName(ReadOnlySpan<char> fullName)
internal static int IndexOfNamespaceDelimiter(ReadOnlySpan<char> fullName)
{
// The two-value form of MemoryExtensions.LastIndexOfAny does not suffer
// from the behavior mentioned in the comment at the top of GetFullTypeNameLength.
// It always takes O(m * i) worst-case time and is safe to use here.
// Matches algorithm from ns::FindSep in src\coreclr\utilcode\namespaceutil.cpp
// This could result in the type name beginning with a '.' character.
int index = fullName.LastIndexOf('.');

int offset = fullName.LastIndexOfAny('.', '+');
if (index > 0 && fullName[index - 1] == '.')
{
index--;
}

return index;
}

if (offset > 0 && fullName[offset - 1] == EscapeCharacter) // this should be very rare (IL Emit & pure IL)
internal static string Unescape(string input)
{
int indexOfEscapeCharacter = input.IndexOf(EscapeCharacter);
if (indexOfEscapeCharacter < 0)
{
offset = GetUnescapedOffset(fullName, startIndex: offset);
// Nothing to escape, just return the original value.
return input;
}

return offset < 0 ? fullName : fullName.Slice(offset + 1);
return UnescapeToBuilder(input, indexOfEscapeCharacter);

static int GetUnescapedOffset(ReadOnlySpan<char> fullName, int startIndex)
static string UnescapeToBuilder(string name, int indexOfEscapeCharacter)
{
int offset = startIndex;
for (; offset >= 0; offset--)
// this code path is executed very rarely (IL Emit or pure IL with chars not allowed in C# or F#)
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.EnsureCapacity(name.Length);
sb.Append(name.AsSpan(0, indexOfEscapeCharacter));

for (int i = indexOfEscapeCharacter; i < name.Length;)
{
if (fullName[offset] is '.' or '+')
char c = name[i++];

if (c != EscapeCharacter || i == name.Length)
{
if (offset == 0 || fullName[offset - 1] != EscapeCharacter)
{
break;
}
offset--; // skip the escaping character
sb.Append(c);
}
else if (name[i] == EscapeCharacter) // escaped escape character ;)
{
sb.Append(c);
// Consume the escaped escape character, it's important for edge cases
// like escaped escape character followed by another escaped char (example: "\\\\\\+")
i++;
}
}
return offset;

return sb.ToString();
}
}

Expand Down Expand Up @@ -350,6 +370,12 @@ internal static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan<char> s
return false;
}

[DoesNotReturn]
internal static void ThrowArgumentNullException(string paramName)
{
throw new ArgumentNullException(paramName);
}

[DoesNotReturn]
internal static void ThrowArgumentException_InvalidTypeName(int errorIndex)
{
Expand Down Expand Up @@ -411,6 +437,17 @@ internal static void ThrowInvalidOperation_HasToBeArrayClass()
#endif
}

[DoesNotReturn]
internal static void ThrowInvalidOperation_NestedTypeNamespace()
{
#if SYSTEM_REFLECTION_METADATA
throw new InvalidOperationException(SR.InvalidOperation_NestedTypeNamespace);
#else
Debug.Fail("Expected to be unreachable");
throw new InvalidOperationException();
#endif
}

internal static bool IsMaxDepthExceeded(TypeNameParseOptions options, int depth)
#if SYSTEM_PRIVATE_CORELIB
=> false; // CoreLib does not enforce any limits
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// 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;
Expand Down Expand Up @@ -38,16 +38,14 @@ public void GetFullTypeNameLengthReturnsExpectedValue(string input, int expected
}

[Theory]
[InlineData("JustTypeName", "JustTypeName")]
[InlineData("Namespace.TypeName", "TypeName")]
[InlineData("Namespace1.Namespace2.TypeName", "TypeName")]
[InlineData("Namespace.NotNamespace\\.TypeName", "NotNamespace\\.TypeName")]
[InlineData("Namespace1.Namespace2.Containing+Nested", "Nested")]
[InlineData("Namespace1.Namespace2.Not\\+Nested", "Not\\+Nested")]
[InlineData("NotNamespace1\\.NotNamespace2\\.TypeName", "NotNamespace1\\.NotNamespace2\\.TypeName")]
[InlineData("NotNamespace1\\.NotNamespace2\\.Not\\+Nested", "NotNamespace1\\.NotNamespace2\\.Not\\+Nested")]
public void GetNameReturnsJustName(string fullName, string expected)
=> Assert.Equal(expected, TypeNameParserHelpers.GetName(fullName.AsSpan()).ToString());
[InlineData("JustTypeName", -1)]
[InlineData("Namespace.TypeName", 9)]
[InlineData("Namespace1.Namespace2.TypeName", 21)]
[InlineData("Namespace..Name", 9)]
[InlineData("Namespace...Name", 10)]
[InlineData("Namespace..Name.", 15)]
public void IndexOfNamespaceDelimiter(string fullName, int expected)
=> Assert.Equal(expected, TypeNameParserHelpers.IndexOfNamespaceDelimiter(fullName.AsSpan()));

[Theory]
[InlineData("simple", "simple")]
Expand Down
Loading
Loading