Skip to content

Commit 617f9ee

Browse files
teo-tsirpanisadamsitnikjkotas
authored
Add TypeName APIs to simplify metadata lookup. (#111598)
* Add `TypeName.Namespace` and tests. * Add `TypeName.Unescape`. * Update reference assembly. * Remove file with duplicate `Unescape` method. * Reduce allocations when calling `Namespace` across a type name hierachy. * Fix nested types with namespaces. * Add tests for `Unescape`. * Update `GetNamespace` to fail if a nested type has a namespace. * Remove support for nested types and escaped dots in namespaces. * Remove support for escaped dots in `GetName`, and optimize it if the type is not nested. * Simplify `Name` to avoid linear search of the full name in nested types. * [mono] Do not treat nested type names as full names. * Update algorithm to find namespace delimiter. * Redirect all `Type.GetType` overloads in Mono through `TypeNameResolver`. * Update tests to account for Mono using the managed type parser in `Type.GetType`. * Disallow getting the namespace of non-simple nested type names. * Ignore ambiguous match exceptions in non-extensible `Type.GetType` overloads. This matches the behavior of CoreCLR and Native AOT. * Use alternative strategy to provide a `(message, typeName)` overload in Mono, while still avoiding changes to the Mono runtime code. --------- Co-authored-by: Adam Sitnik <[email protected]> Co-authored-by: Jan Kotas <[email protected]>
1 parent fe9e119 commit 617f9ee

File tree

15 files changed

+321
-111
lines changed

15 files changed

+321
-111
lines changed

src/coreclr/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.CoreCLR.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,15 +231,15 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName,
231231
}
232232
return null;
233233
}
234-
return GetTypeFromDefaultAssemblies(TypeNameHelpers.Unescape(escapedTypeName), nestedTypeNames, parsedName);
234+
return GetTypeFromDefaultAssemblies(TypeName.Unescape(escapedTypeName), nestedTypeNames, parsedName);
235235
}
236236

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

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

245245
if (type is null)
@@ -282,7 +282,7 @@ internal static RuntimeType GetTypeReferencedByCustomAttribute(string typeName,
282282
if (_throwOnError)
283283
{
284284
throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType,
285-
nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : TypeNameHelpers.Unescape(escapedTypeName)),
285+
nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : TypeName.Unescape(escapedTypeName)),
286286
typeName: parsedName.FullName);
287287
}
288288
return null;

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.NativeAot.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ internal partial struct TypeNameResolver
158158
{
159159
if (assembly is RuntimeAssemblyInfo runtimeAssembly)
160160
{
161-
type = runtimeAssembly.GetTypeCore(TypeNameHelpers.Unescape(escapedTypeName), throwOnError: _throwOnError, ignoreCase: _ignoreCase);
161+
type = runtimeAssembly.GetTypeCore(TypeName.Unescape(escapedTypeName), throwOnError: _throwOnError, ignoreCase: _ignoreCase);
162162
}
163163
else
164164
{
@@ -173,7 +173,7 @@ internal partial struct TypeNameResolver
173173
}
174174
else
175175
{
176-
string? unescapedTypeName = TypeNameHelpers.Unescape(escapedTypeName);
176+
string? unescapedTypeName = TypeName.Unescape(escapedTypeName);
177177

178178
RuntimeAssemblyInfo? defaultAssembly = null;
179179
if (_defaultAssemblyName != null)
@@ -235,7 +235,7 @@ internal partial struct TypeNameResolver
235235
if (_throwOnError)
236236
{
237237
throw new TypeLoadException(SR.Format(SR.TypeLoad_ResolveNestedType,
238-
nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : TypeNameHelpers.Unescape(escapedTypeName)),
238+
nestedTypeNames[i], (i > 0) ? nestedTypeNames[i - 1] : TypeName.Unescape(escapedTypeName)),
239239
typeName: parsedName.FullName);
240240
}
241241
return null;

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,9 +1489,6 @@
14891489
<Compile Include="$(LibrariesProjectRoot)System.Reflection.Metadata\src\System\Reflection\Metadata\TypeName.cs">
14901490
<Link>Common\System\Reflection\Metadata\TypeName.cs</Link>
14911491
</Compile>
1492-
<Compile Include="$(CommonPath)System\Reflection\Metadata\TypeNameHelpers.cs">
1493-
<Link>Common\System\Reflection\Metadata\TypeNameHelpers.cs</Link>
1494-
</Compile>
14951492
<Compile Include="$(LibrariesProjectRoot)System.Reflection.Metadata\src\System\Reflection\Metadata\TypeNameParser.cs">
14961493
<Link>Common\System\Reflection\Metadata\TypeNameParser.cs</Link>
14971494
</Compile>

src/libraries/System.Private.CoreLib/src/System/Reflection/TypeNameResolver.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ internal partial struct TypeNameResolver
8080
current = typeName;
8181
while (current.IsNested)
8282
{
83-
nestedTypeNames[--nestingDepth] = TypeNameHelpers.Unescape(current.Name);
83+
nestedTypeNames[--nestingDepth] = TypeName.Unescape(current.Name);
8484
current = current.DeclaringType!;
8585
}
8686

src/libraries/System.Reflection.Metadata/ref/System.Reflection.Metadata.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2439,6 +2439,7 @@ internal TypeName() { }
24392439
public bool IsSZArray { get { throw null; } }
24402440
public bool IsVariableBoundArrayType { get { throw null; } }
24412441
public string Name { get { throw null; } }
2442+
public string Namespace { get { throw null; } }
24422443
public int GetArrayRank() { throw null; }
24432444
public System.Reflection.Metadata.TypeName GetElementType() { throw null; }
24442445
public System.Collections.Immutable.ImmutableArray<System.Reflection.Metadata.TypeName> GetGenericArguments() { throw null; }
@@ -2451,6 +2452,7 @@ internal TypeName() { }
24512452
public System.Reflection.Metadata.TypeName MakeSZArrayTypeName() { throw null; }
24522453
public static System.Reflection.Metadata.TypeName Parse(System.ReadOnlySpan<char> typeName, System.Reflection.Metadata.TypeNameParseOptions? options = null) { throw null; }
24532454
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; }
2455+
public static string Unescape(string name) { throw null; }
24542456
public System.Reflection.Metadata.TypeName WithAssemblyName(System.Reflection.Metadata.AssemblyNameInfo? assemblyName) { throw null; }
24552457
}
24562458
public sealed partial class TypeNameParseOptions

src/libraries/System.Reflection.Metadata/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,9 @@
429429
<data name="InvalidOperation_NoElement" xml:space="preserve">
430430
<value>This operation is only valid on arrays, pointers and references.</value>
431431
</data>
432+
<data name="InvalidOperation_NestedTypeNamespace" xml:space="preserve">
433+
<value>Cannot retrieve the namespace of a nested type.</value>
434+
</data>
432435
<data name="InvalidAssemblyName" xml:space="preserve">
433436
<value>The given assembly name was invalid.</value>
434437
</data>

src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeName.cs

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
@@ -39,7 +39,7 @@ sealed class TypeName
3939
#else
4040
private readonly ImmutableArray<TypeName> _genericArguments;
4141
#endif
42-
private string? _name, _fullName, _assemblyQualifiedName;
42+
private string? _name, _namespace, _fullName, _assemblyQualifiedName;
4343

4444
internal TypeName(string? fullName,
4545
AssemblyNameInfo? assemblyName,
@@ -217,6 +217,7 @@ public string FullName
217217
/// This is because determining whether a type truly is a generic type requires loading the type
218218
/// and performing a runtime check.</para>
219219
/// </remarks>
220+
[MemberNotNullWhen(false, nameof(_elementOrGenericType))]
220221
public bool IsSimple => _elementOrGenericType is null;
221222

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

234236
/// <summary>
@@ -262,28 +264,89 @@ public string Name
262264
{
263265
if (IsConstructedGenericType)
264266
{
265-
_name = TypeNameParserHelpers.GetName(GetGenericTypeDefinition().FullName.AsSpan()).ToString();
267+
_name = GetGenericTypeDefinition().Name;
266268
}
267269
else if (IsPointer || IsByRef || IsArray)
268270
{
269271
ValueStringBuilder builder = new(stackalloc char[64]);
270272
builder.Append(GetElementType().Name);
271273
_name = TypeNameParserHelpers.GetRankOrModifierStringRepresentation(_rankOrModifier, ref builder);
272274
}
273-
else if (_nestedNameLength > 0 && _fullName is not null)
274-
{
275-
_name = TypeNameParserHelpers.GetName(_fullName.AsSpan(0, _nestedNameLength)).ToString();
276-
}
277275
else
278276
{
279-
_name = TypeNameParserHelpers.GetName(FullName.AsSpan()).ToString();
277+
// _fullName can be null only in constructed generic or modified types, which we handled above.
278+
Debug.Assert(_fullName is not null);
279+
ReadOnlySpan<char> name = _fullName.AsSpan();
280+
if (_nestedNameLength > 0)
281+
{
282+
name = name.Slice(0, _nestedNameLength);
283+
}
284+
if (IsNested)
285+
{
286+
// If the type is nested, we know the length of the declaring type's full name.
287+
// Get the characters after that plus one for the '+' separator.
288+
name = name.Slice(_declaringType._nestedNameLength + 1);
289+
}
290+
else if (TypeNameParserHelpers.IndexOfNamespaceDelimiter(name) is int idx && idx >= 0)
291+
{
292+
// If the type is not nested, find the namespace delimiter in the full name and return the substring after it.
293+
name = name.Slice(idx + 1);
294+
}
295+
_name = name.ToString();
280296
}
281297
}
282298

283299
return _name;
284300
}
285301
}
286302

303+
/// <summary>
304+
/// The namespace of this type; e.g., "System".
305+
/// </summary>
306+
/// <exception cref="InvalidOperationException">This instance is a nested type.</exception>
307+
public string Namespace
308+
{
309+
get
310+
{
311+
if (_namespace is null)
312+
{
313+
TypeName rootTypeName = this;
314+
while (!rootTypeName.IsSimple)
315+
{
316+
rootTypeName = rootTypeName._elementOrGenericType;
317+
}
318+
319+
if (rootTypeName.IsNested)
320+
{
321+
TypeNameParserHelpers.ThrowInvalidOperation_NestedTypeNamespace();
322+
}
323+
324+
// By setting the namespace field at the root type name, we avoid recomputing it for all derived names.
325+
if (rootTypeName._namespace is null)
326+
{
327+
// At this point the type does not have a modifier applied to it, so it should have its full name set.
328+
Debug.Assert(rootTypeName._fullName is not null);
329+
ReadOnlySpan<char> rootFullName = rootTypeName._fullName.AsSpan();
330+
if (rootTypeName._nestedNameLength > 0)
331+
{
332+
rootFullName = rootFullName.Slice(0, rootTypeName._nestedNameLength);
333+
}
334+
if (TypeNameParserHelpers.IndexOfNamespaceDelimiter(rootFullName) is int idx && idx >= 0)
335+
{
336+
rootTypeName._namespace = rootFullName.Slice(0, idx).ToString();
337+
}
338+
else
339+
{
340+
rootTypeName._namespace = string.Empty;
341+
}
342+
}
343+
_namespace = rootTypeName._namespace;
344+
}
345+
346+
return _namespace;
347+
}
348+
}
349+
287350
/// <summary>
288351
/// Represents the total number of <see cref="TypeName"/> instances that are used to describe
289352
/// this instance, including any generic arguments or underlying types.
@@ -401,6 +464,25 @@ public static bool TryParse(ReadOnlySpan<char> typeName, [NotNullWhen(true)] out
401464
return result is not null;
402465
}
403466

467+
/// <summary>
468+
/// Converts any escaped characters in the input type name or namespace.
469+
/// </summary>
470+
/// <param name="name">The input string containing the name to convert.</param>
471+
/// <returns>A string of characters with any escaped characters converted to their unescaped form.</returns>
472+
/// <remarks>
473+
/// <para>The unescaped string can be used for looking up the type name or namespace in metadata.</para>
474+
/// <para>This method removes escape characters even if they precede a character that does not require escaping.</para>
475+
/// </remarks>
476+
public static string Unescape(string name)
477+
{
478+
if (name is null)
479+
{
480+
TypeNameParserHelpers.ThrowArgumentNullException(nameof(name));
481+
}
482+
483+
return TypeNameParserHelpers.Unescape(name);
484+
}
485+
404486
/// <summary>
405487
/// Gets the number of dimensions in an array.
406488
/// </summary>

src/libraries/System.Reflection.Metadata/src/System/Reflection/Metadata/TypeNameParserHelpers.cs

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

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

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

111-
int offset = fullName.LastIndexOfAny('.', '+');
111+
if (index > 0 && fullName[index - 1] == '.')
112+
{
113+
index--;
114+
}
115+
116+
return index;
117+
}
112118

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

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

120-
static int GetUnescapedOffset(ReadOnlySpan<char> fullName, int startIndex)
130+
static string UnescapeToBuilder(string name, int indexOfEscapeCharacter)
121131
{
122-
int offset = startIndex;
123-
for (; offset >= 0; offset--)
132+
// This code path is executed very rarely (IL Emit or pure IL with chars not allowed in C# or F#).
133+
var sb = new ValueStringBuilder(stackalloc char[64]);
134+
sb.EnsureCapacity(name.Length);
135+
sb.Append(name.AsSpan(0, indexOfEscapeCharacter));
136+
137+
for (int i = indexOfEscapeCharacter; i < name.Length;)
124138
{
125-
if (fullName[offset] is '.' or '+')
139+
char c = name[i++];
140+
141+
if (c != EscapeCharacter || i == name.Length)
126142
{
127-
if (offset == 0 || fullName[offset - 1] != EscapeCharacter)
128-
{
129-
break;
130-
}
131-
offset--; // skip the escaping character
143+
sb.Append(c);
144+
}
145+
else if (name[i] == EscapeCharacter) // escaped escape character ;)
146+
{
147+
sb.Append(c);
148+
// Consume the escaped escape character, it's important for edge cases
149+
// like escaped escape character followed by another escaped char (example: "\\\\\\+")
150+
i++;
132151
}
133152
}
134-
return offset;
153+
154+
return sb.ToString();
135155
}
136156
}
137157

@@ -350,6 +370,12 @@ internal static bool TryStripFirstCharAndTrailingSpaces(ref ReadOnlySpan<char> s
350370
return false;
351371
}
352372

373+
[DoesNotReturn]
374+
internal static void ThrowArgumentNullException(string paramName)
375+
{
376+
throw new ArgumentNullException(paramName);
377+
}
378+
353379
[DoesNotReturn]
354380
internal static void ThrowArgumentException_InvalidTypeName(int errorIndex)
355381
{
@@ -411,6 +437,17 @@ internal static void ThrowInvalidOperation_HasToBeArrayClass()
411437
#endif
412438
}
413439

440+
[DoesNotReturn]
441+
internal static void ThrowInvalidOperation_NestedTypeNamespace()
442+
{
443+
#if SYSTEM_REFLECTION_METADATA
444+
throw new InvalidOperationException(SR.InvalidOperation_NestedTypeNamespace);
445+
#else
446+
Debug.Fail("Expected to be unreachable");
447+
throw new InvalidOperationException();
448+
#endif
449+
}
450+
414451
internal static bool IsMaxDepthExceeded(TypeNameParseOptions options, int depth)
415452
#if SYSTEM_PRIVATE_CORELIB
416453
=> false; // CoreLib does not enforce any limits

src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNameParserHelpersTests.cs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Generic;
@@ -38,16 +38,14 @@ public void GetFullTypeNameLengthReturnsExpectedValue(string input, int expected
3838
}
3939

4040
[Theory]
41-
[InlineData("JustTypeName", "JustTypeName")]
42-
[InlineData("Namespace.TypeName", "TypeName")]
43-
[InlineData("Namespace1.Namespace2.TypeName", "TypeName")]
44-
[InlineData("Namespace.NotNamespace\\.TypeName", "NotNamespace\\.TypeName")]
45-
[InlineData("Namespace1.Namespace2.Containing+Nested", "Nested")]
46-
[InlineData("Namespace1.Namespace2.Not\\+Nested", "Not\\+Nested")]
47-
[InlineData("NotNamespace1\\.NotNamespace2\\.TypeName", "NotNamespace1\\.NotNamespace2\\.TypeName")]
48-
[InlineData("NotNamespace1\\.NotNamespace2\\.Not\\+Nested", "NotNamespace1\\.NotNamespace2\\.Not\\+Nested")]
49-
public void GetNameReturnsJustName(string fullName, string expected)
50-
=> Assert.Equal(expected, TypeNameParserHelpers.GetName(fullName.AsSpan()).ToString());
41+
[InlineData("JustTypeName", -1)]
42+
[InlineData("Namespace.TypeName", 9)]
43+
[InlineData("Namespace1.Namespace2.TypeName", 21)]
44+
[InlineData("Namespace..Name", 9)]
45+
[InlineData("Namespace...Name", 10)]
46+
[InlineData("Namespace..Name.", 15)]
47+
public void IndexOfNamespaceDelimiter(string fullName, int expected)
48+
=> Assert.Equal(expected, TypeNameParserHelpers.IndexOfNamespaceDelimiter(fullName.AsSpan()));
5149

5250
[Theory]
5351
[InlineData("simple", "simple")]

0 commit comments

Comments
 (0)