Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
37 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
e1f044c
Redirect all `Type.GetType` overloads in Mono through `TypeNameResolv…
teo-tsirpanis Feb 4, 2025
a3bf79d
Update tests to account for Mono using the managed type parser in `Ty…
teo-tsirpanis Feb 4, 2025
c023336
Disallow getting the namespace of non-simple nested type names.
teo-tsirpanis Feb 5, 2025
cb2bd2d
Ignore ambiguous match exceptions in non-extensible `Type.GetType` ov…
teo-tsirpanis Feb 6, 2025
02c2efe
Set `TypeLoadException.TypeName` in Mono as well.
teo-tsirpanis Feb 6, 2025
ed19a97
Update src/libraries/System.Reflection.Metadata/tests/Metadata/TypeNa…
teo-tsirpanis Feb 6, 2025
0325ce1
Merge branch 'main' into typename-namespace-unescape
teo-tsirpanis Feb 6, 2025
b924a6f
Use alternative strategy to provide a `(message, typeName)` overload …
teo-tsirpanis Feb 8, 2025
c55b487
Address PR feedback.
teo-tsirpanis Feb 9, 2025
7378b4a
Update src/libraries/System.Reflection.Metadata/src/System/Reflection…
jkotas Feb 10, 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 @@ -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 @@ -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;

/// <summary>
Expand Down Expand Up @@ -284,6 +285,35 @@ public string Name
}
}

/// <summary>
/// The namespace of this type; e.g., "System".
/// </summary>
public string Namespace
{
get
{
if (_namespace is null)
{
TypeName rootTypeName = this;
while (!rootTypeName.IsSimple)
{
rootTypeName = rootTypeName._elementOrGenericType;
}

// At this point the type does not have a modifier applied to it, so it should have its full name initialized.
Debug.Assert(rootTypeName._fullName is not null);
ReadOnlySpan<char> rootFullName = rootTypeName._fullName.AsSpan();
if (rootTypeName._nestedNameLength > 0)
{
rootFullName = rootFullName.Slice(0, rootTypeName._nestedNameLength);
}
_namespace = TypeNameParserHelpers.GetNamespace(rootFullName).ToString();
}

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 +431,22 @@ 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>The unescaped string can be used for looking up the type name or namespace in metadata.</remarks>
public static string Unescape(string name)
{
if (name is null)
{
TypeNameParserHelpers.ThrowArgumentNullException(nameof(name));
}

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
Expand Up @@ -16,8 +16,10 @@ internal static class TypeNameParserHelpers
internal const int ByRef = -3;
private const char EscapeCharacter = '\\';
#if NET8_0_OR_GREATER
// Keep this in sync with GetFullTypeNameLength/NeedsEscaping
private static readonly SearchValues<char> s_endOfFullTypeNameDelimitersSearchValues = SearchValues.Create("[]&*,+\\");
private static bool NeedsEscaping(char c) => s_endOfFullTypeNameDelimitersSearchValues.Contains(c);
#else
private static bool NeedsEscaping(char c) => c is '[' or ']' or '&' or '*' or ',' or '+' or EscapeCharacter;
#endif

internal static string GetGenericTypeFullName(ReadOnlySpan<char> fullTypeName, ReadOnlySpan<TypeName> genericArgs)
Expand Down Expand Up @@ -97,9 +99,35 @@ static int GetUnescapedOffset(ReadOnlySpan<char> input, int startOffset)
}
return offset;
}
}

internal static ReadOnlySpan<char> GetNamespace(ReadOnlySpan<char> fullName)
{
int offset = fullName.LastIndexOf('.');

// Keep this in sync with s_endOfFullTypeNameDelimitersSearchValues
static bool NeedsEscaping(char c) => c is '[' or ']' or '&' or '*' or ',' or '+' or EscapeCharacter;
if (offset > 0 && fullName[offset - 1] == EscapeCharacter) // this should be very rare (IL Emit & pure IL)
{
offset = GetUnescapedOffset(fullName, startIndex: offset);
}

return offset < 0 ? [] : fullName.Slice(0, offset);

static int GetUnescapedOffset(ReadOnlySpan<char> fullName, int startIndex)
{
int offset = startIndex;
for (; offset >= 0; offset--)
{
if (fullName[offset] == '.')
{
if (offset == 0 || fullName[offset - 1] != EscapeCharacter)
{
break;
}
offset--; // skip the escaping character
}
}
return offset;
}
}

internal static ReadOnlySpan<char> GetName(ReadOnlySpan<char> fullName)
Expand Down Expand Up @@ -135,6 +163,50 @@ static int GetUnescapedOffset(ReadOnlySpan<char> fullName, int startIndex)
}
}

internal static string Unescape(string input)
{
int indexOfEscapeChar = input.IndexOf(EscapeCharacter);
if (indexOfEscapeChar < 0)
{
// Nothing to escape, just return the original value.
return input;
}

ValueStringBuilder builder = new(stackalloc char[128]);
builder.EnsureCapacity(input.Length);

UnescapeToBuilder(input.AsSpan(), ref builder);

string result = builder.ToString();
builder.Dispose();
return result;

static void UnescapeToBuilder(ReadOnlySpan<char> input, ref ValueStringBuilder builder)
{
while (!input.IsEmpty)
{
int indexOfEscapeChar = input.IndexOf(EscapeCharacter);
if (indexOfEscapeChar < 0)
{
builder.Append(input);
break;
}
builder.Append(input.Slice(0, indexOfEscapeChar));
int indexOfNextChar = indexOfEscapeChar + 1;
if (indexOfNextChar < input.Length && input[indexOfNextChar] is char c && NeedsEscaping(c))
{
builder.Append(c);
indexOfNextChar++;
}
else
{
builder.Append(EscapeCharacter);
}
input = input.Slice(indexOfNextChar);
}
}
}

// this method handles escaping of the ] just to let the AssemblyNameParser fail for the right input
internal static ReadOnlySpan<char> GetAssemblyNameCandidate(ReadOnlySpan<char> input)
{
Expand Down Expand Up @@ -350,6 +422,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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ public void GetFullTypeNameLengthReturnsExpectedValue(string input, int expected
Assert.Equal(expectedIsNested, isNested);
}

[Theory]
[InlineData("JustTypeName", "")]
[InlineData("Namespace.TypeName", "Namespace")]
[InlineData("Namespace1.Namespace2.TypeName", "Namespace1.Namespace2")]
[InlineData("Namespace.NotNamespace\\.TypeName", "Namespace")]
[InlineData("Namespace1.Namespace2.Containing+Nested", "Namespace1.Namespace2")]
[InlineData("Namespace1.Namespace2.Not\\+Nested", "Namespace1.Namespace2")]
[InlineData("NotNamespace1\\.NotNamespace2\\.TypeName", "")]
[InlineData("NotNamespace1\\.NotNamespace2\\.Not\\+Nested", "")]
public void GetNamespaceReturnsJustNamespace(string fullName, string expected)
=> Assert.Equal(expected, TypeNameParserHelpers.GetNamespace(fullName.AsSpan()).ToString());

[Theory]
[InlineData("JustTypeName", "JustTypeName")]
[InlineData("Namespace.TypeName", "TypeName")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ namespace System.Reflection.Metadata.Tests
public class TypeNameTests
{
[Theory]
[InlineData(" System.Int32", "System.Int32", "Int32")]
[InlineData(" MyNamespace.MyType+NestedType", "MyNamespace.MyType+NestedType", "NestedType")]
public void SpacesAtTheBeginningAreOK(string input, string expectedFullName, string expectedName)
[InlineData(" System.Int32", "System.Int32", "System", "Int32")]
[InlineData(" MyNamespace.MyType+NestedType", "MyNamespace.MyType+NestedType", "MyNamespace", "NestedType")]
public void SpacesAtTheBeginningAreOK(string input, string expectedFullName, string expectedNamespace, string expectedName)
{
TypeName parsed = TypeName.Parse(input.AsSpan());

Assert.Equal(expectedName, parsed.Name);
Assert.Equal(expectedNamespace, parsed.Namespace);
Assert.Equal(expectedFullName, parsed.FullName);
Assert.Equal(expectedFullName, parsed.AssemblyQualifiedName);
}
Expand All @@ -32,6 +33,7 @@ public void LeadingDotIsNotConsumedForFullTypeNamesWithoutNamespace()
TypeName parsed = TypeName.Parse(".NoNamespace".AsSpan());

Assert.Equal("NoNamespace", parsed.Name);
Assert.Empty(parsed.Namespace);
Assert.Equal(".NoNamespace", parsed.FullName);
Assert.Equal(".NoNamespace", parsed.AssemblyQualifiedName);
}
Expand Down Expand Up @@ -698,6 +700,7 @@ private static void VerifyNestedNames(TypeName parsed, TypeName made, AssemblyNa
while (true)
{
Assert.Equal(parsed.Name, made.Name);
Assert.Equal(parsed.Namespace, made.Namespace);
Assert.Equal(parsed.FullName, made.FullName);
Assert.Equal(assemblyName, made.AssemblyName);
Assert.NotEqual(parsed.AssemblyQualifiedName, made.AssemblyQualifiedName);
Expand Down Expand Up @@ -792,6 +795,7 @@ public void ParsedNamesMatchSystemTypeNames(Type type)
Type genericType = type.GetGenericTypeDefinition();
TypeName genericTypeName = parsed.GetGenericTypeDefinition();
Assert.Equal(genericType.Name, genericTypeName.Name);
Assert.Equal(genericType.Namespace, genericTypeName.Namespace);
Assert.Equal(genericType.FullName, genericTypeName.FullName);
Assert.Equal(genericType.AssemblyQualifiedName, genericTypeName.AssemblyQualifiedName);
}
Expand Down Expand Up @@ -965,6 +969,7 @@ private static void EnsureBasicMatch(TypeName typeName, Type type)
{
Assert.Equal(type.AssemblyQualifiedName, typeName.AssemblyQualifiedName);
Assert.Equal(type.FullName, typeName.FullName);
Assert.Equal(type.Namespace, typeName.Namespace);
Assert.Equal(type.Name, typeName.Name);

#if NET
Expand Down
Loading