Files
Siegfried Pammer bba00519d0 Throw on unbalanced braces in ID string type names
A bound generic argument list without its closing brace fell off the
end of the string and silently produced an arity, making malformed ID
strings appear to parse and resolve to nothing instead of failing with
the documented ReflectionNameParseException. Also replaces the non-ASCII
punctuation in comments added by this branch with ASCII equivalents,
per the repository convention. Both raised by review on #3926.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-30 08:35:07 +02:00

1233 lines
42 KiB
C#

// Copyright (c) 2010-2018 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection.Metadata;
using System.Text;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.TypeSystem.Implementation;
namespace ICSharpCode.Decompiler.Documentation
{
/// <summary>
/// Provides ID strings for entities. (C# 4.0 spec, §A.3.1)
/// ID strings are used to identify members in XML documentation files.
/// </summary>
public static class IdStringProvider
{
#region GetIdString
/// <summary>
/// Gets the ID string (C# 4.0 spec, section A.3.1) for the specified entity,
/// in the form the C# compiler writes into xml documentation files.
/// </summary>
public static string GetIdString(this MetadataFile module, EntityHandle handle)
{
return GetIdString(module, handle, cppCliDialect: false);
}
/// <summary>
/// Gets the ID string candidates for the entity, most specific first: the MSVC
/// C++/CLI (ECMA-372-style) form when it differs from the C#/Roslyn form, then the
/// C#/Roslyn form. Documentation lookup should try the candidates in order, so that
/// xml doc files written by either compiler can be matched. The dialects differ in
/// signatures only: Roslyn ignores custom modifiers and strips the arity marker of
/// instantiated generic types, while MSVC renders modifiers ('!' or '|' followed by
/// the modifier type), keeps arity markers, and refers to a default indexed
/// property as 'default'. The C++/CLI form comes first because wherever it differs
/// it contains character sequences Roslyn never writes, so it can only match
/// MSVC-generated keys; the stripped Roslyn form of one member can collide with the
/// key of a different member in an MSVC-generated file (e.g. overloads differing
/// only in a custom modifier). When such a colliding sibling overload exists, the
/// Roslyn form is omitted entirely: assemblies containing such overloads cannot
/// come from the C# compiler, so their xml files use the C++/CLI dialect, where
/// that key documents the sibling.
/// </summary>
public static IEnumerable<string> GetIdStringCandidates(this MetadataFile module, EntityHandle handle)
{
string primary = GetIdString(module, handle, cppCliDialect: false);
string cppCli = GetIdString(module, handle, cppCliDialect: true);
if (cppCli == primary)
{
yield return primary;
yield break;
}
yield return cppCli;
if (!RoslynFormBelongsToSibling(module, handle, primary))
yield return primary;
}
/// <summary>
/// True when the C#/Roslyn-form ID of <paramref name="handle"/> equals the
/// C++/CLI-form ID of a same-named sibling member of the same type (only members
/// with a signature portion can diverge, so only methods and properties are
/// checked). Keys embed the declaring type and member name, so no other member
/// can own the string.
/// </summary>
static bool RoslynFormBelongsToSibling(MetadataFile module, EntityHandle handle, string roslynForm)
{
var metadata = module.Metadata;
switch (handle.Kind)
{
case HandleKind.MethodDefinition:
{
var methodHandle = (MethodDefinitionHandle)handle;
var methodDef = metadata.GetMethodDefinition(methodHandle);
string name = metadata.GetString(methodDef.Name);
foreach (var sibling in metadata.GetTypeDefinition(methodDef.GetDeclaringType()).GetMethods())
{
if (sibling == methodHandle
|| !metadata.StringComparer.Equals(metadata.GetMethodDefinition(sibling).Name, name))
{
continue;
}
if (GetIdString(module, sibling, cppCliDialect: true) == roslynForm)
return true;
}
return false;
}
case HandleKind.PropertyDefinition:
{
var propertyHandle = (PropertyDefinitionHandle)handle;
string name = metadata.GetString(metadata.GetPropertyDefinition(propertyHandle).Name);
var declaringType = FindDeclaringTypeOfProperty(metadata, propertyHandle);
foreach (var sibling in metadata.GetTypeDefinition(declaringType).GetProperties())
{
if (sibling == propertyHandle
|| !metadata.StringComparer.Equals(metadata.GetPropertyDefinition(sibling).Name, name))
{
continue;
}
if (GetIdString(module, sibling, cppCliDialect: true) == roslynForm)
return true;
}
return false;
}
default:
return false;
}
}
static string GetIdString(MetadataFile module, EntityHandle handle, bool cppCliDialect)
{
if (handle.IsNil)
throw new ArgumentException("The handle must not be nil.", nameof(handle));
var metadata = module.Metadata;
var b = new StringBuilder();
switch (handle.Kind)
{
case HandleKind.TypeDefinition:
b.Append("T:");
AppendTypeDefinitionName(b, metadata, (TypeDefinitionHandle)handle);
break;
case HandleKind.FieldDefinition:
b.Append("F:");
AppendFieldIdString(b, metadata, (FieldDefinitionHandle)handle);
break;
case HandleKind.MethodDefinition:
b.Append("M:");
AppendMethodIdString(b, metadata, (MethodDefinitionHandle)handle, cppCliDialect);
break;
case HandleKind.PropertyDefinition:
b.Append("P:");
AppendPropertyIdString(b, metadata, (PropertyDefinitionHandle)handle, cppCliDialect);
break;
case HandleKind.EventDefinition:
b.Append("E:");
AppendEventIdString(b, metadata, (EventDefinitionHandle)handle);
break;
default:
throw new ArgumentException($"Unsupported handle kind: {handle.Kind}", nameof(handle));
}
return b.ToString();
}
/// <summary>
/// Gets the ID string (C# 4.0 spec, section A.3.1) for the specified entity.
/// </summary>
/// <remarks>
/// The ID string is computed from the entity's metadata; for specialized members
/// it describes the underlying member definition.
/// </remarks>
public static string GetIdString(this IEntity entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
var module = entity.ParentModule?.MetadataFile;
if (module == null || entity.MetadataToken.IsNil)
throw new NotSupportedException("Cannot compute an ID string for an entity that is not backed by metadata.");
return GetIdString(module, entity.MetadataToken);
}
/// <summary>
/// Appends the fully qualified name of a type definition, handling nested types.
/// The metadata name already contains the `n arity suffix for generic types.
/// </summary>
static void AppendTypeDefinitionName(StringBuilder b, MetadataReader metadata, TypeDefinitionHandle handle)
{
var typeDef = metadata.GetTypeDefinition(handle);
var declaringType = typeDef.GetDeclaringType();
if (declaringType.IsNil)
{
var ns = metadata.GetString(typeDef.Namespace);
if (!string.IsNullOrEmpty(ns))
{
b.Append(ns);
b.Append('.');
}
b.Append(metadata.GetString(typeDef.Name));
}
else
{
AppendTypeDefinitionName(b, metadata, declaringType);
b.Append('.');
b.Append(metadata.GetString(typeDef.Name));
}
}
static void AppendTypeReferenceName(StringBuilder b, MetadataReader metadata, TypeReference typeRef)
{
if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference)
{
var outerRef = metadata.GetTypeReference((TypeReferenceHandle)typeRef.ResolutionScope);
AppendTypeReferenceName(b, metadata, outerRef);
b.Append('.');
b.Append(metadata.GetString(typeRef.Name));
}
else
{
var ns = metadata.GetString(typeRef.Namespace);
if (!string.IsNullOrEmpty(ns))
{
b.Append(ns);
b.Append('.');
}
b.Append(metadata.GetString(typeRef.Name));
}
}
static void AppendFieldIdString(StringBuilder b, MetadataReader metadata, FieldDefinitionHandle handle)
{
var fieldDef = metadata.GetFieldDefinition(handle);
var declaringType = fieldDef.GetDeclaringType();
AppendTypeDefinitionName(b, metadata, declaringType);
b.Append('.');
b.Append(metadata.GetString(fieldDef.Name));
}
static void AppendMethodIdString(StringBuilder b, MetadataReader metadata, MethodDefinitionHandle handle, bool cppCliDialect)
{
var methodDef = metadata.GetMethodDefinition(handle);
var declaringType = methodDef.GetDeclaringType();
AppendTypeDefinitionName(b, metadata, declaringType);
b.Append('.');
var methodName = metadata.GetString(methodDef.Name);
b.Append(methodName.Replace('.', '#').Replace('<', '{').Replace('>', '}'));
// Method type parameter count
var genericParams = methodDef.GetGenericParameters();
if (genericParams.Count > 0)
{
b.Append("``");
b.Append(genericParams.Count);
}
// Parameters
var signature = methodDef.DecodeSignature(
new IdStringSignatureTypeProvider(cppCliDialect),
new MetadataGenericContext(handle, metadata));
AppendParameterList(b, signature.ParameterTypes);
// Return type for conversion operators
if (methodName is "op_Implicit" or "op_Explicit" or "op_CheckedExplicit")
{
b.Append('~');
b.Append(signature.ReturnType);
}
}
static void AppendParameterList(StringBuilder b, ImmutableArray<string> parameters)
{
if (parameters.Length > 0)
{
b.Append('(');
for (int i = 0; i < parameters.Length; i++)
{
if (i > 0)
b.Append(',');
b.Append(parameters[i]);
}
b.Append(')');
}
}
static void AppendPropertyIdString(StringBuilder b, MetadataReader metadata, PropertyDefinitionHandle handle, bool cppCliDialect)
{
var propertyDef = metadata.GetPropertyDefinition(handle);
var declaringType = FindDeclaringTypeOfProperty(metadata, handle);
AppendTypeDefinitionName(b, metadata, declaringType);
b.Append('.');
var signature = propertyDef.DecodeSignature(
new IdStringSignatureTypeProvider(cppCliDialect),
new MetadataGenericContext(declaringType, metadata));
string name = metadata.GetString(propertyDef.Name);
// The MSVC xml doc generator refers to a type's default indexed property by the
// C++/CLI keyword 'default' instead of the property's metadata name.
if (cppCliDialect && signature.ParameterTypes.Length > 0
&& name == GetDefaultMemberName(metadata, declaringType))
{
b.Append("default");
}
else
{
b.Append(name.Replace('.', '#').Replace('<', '{').Replace('>', '}'));
}
// Indexers have parameters
AppendParameterList(b, signature.ParameterTypes);
}
static string GetDefaultMemberName(MetadataReader metadata, TypeDefinitionHandle declaringType)
{
foreach (var h in metadata.GetTypeDefinition(declaringType).GetCustomAttributes())
{
var customAttribute = metadata.GetCustomAttribute(h);
if (!customAttribute.IsKnownAttribute(metadata, KnownAttribute.DefaultMember))
continue;
try
{
var value = customAttribute.DecodeValue(Metadata.MetadataExtensions.MinimalAttributeTypeProvider);
if (value.FixedArguments.Length == 1 && value.FixedArguments[0].Value is string name)
return name;
}
catch (BadImageFormatException)
{
}
catch (Metadata.EnumUnderlyingTypeResolveException)
{
}
}
return null;
}
static TypeDefinitionHandle FindDeclaringTypeOfProperty(MetadataReader metadata, PropertyDefinitionHandle propertyHandle)
{
var accessors = metadata.GetPropertyDefinition(propertyHandle).GetAccessors();
var accessor = !accessors.Getter.IsNil ? accessors.Getter
: !accessors.Setter.IsNil ? accessors.Setter
: accessors.Others.Length > 0 ? accessors.Others[0]
: default;
if (!accessor.IsNil)
return metadata.GetMethodDefinition(accessor).GetDeclaringType();
// Accessor-less properties are invalid metadata; fall back to scanning all types.
foreach (var typeHandle in metadata.TypeDefinitions)
{
var typeDef = metadata.GetTypeDefinition(typeHandle);
foreach (var ph in typeDef.GetProperties())
{
if (ph == propertyHandle)
return typeHandle;
}
}
return default;
}
static void AppendEventIdString(StringBuilder b, MetadataReader metadata, EventDefinitionHandle handle)
{
var eventDef = metadata.GetEventDefinition(handle);
var declaringType = FindDeclaringTypeOfEvent(metadata, handle);
AppendTypeDefinitionName(b, metadata, declaringType);
b.Append('.');
b.Append(metadata.GetString(eventDef.Name).Replace('.', '#').Replace('<', '{').Replace('>', '}'));
}
static TypeDefinitionHandle FindDeclaringTypeOfEvent(MetadataReader metadata, EventDefinitionHandle eventHandle)
{
var accessors = metadata.GetEventDefinition(eventHandle).GetAccessors();
var accessor = !accessors.Adder.IsNil ? accessors.Adder
: !accessors.Remover.IsNil ? accessors.Remover
: !accessors.Raiser.IsNil ? accessors.Raiser
: accessors.Others.Length > 0 ? accessors.Others[0]
: default;
if (!accessor.IsNil)
return metadata.GetMethodDefinition(accessor).GetDeclaringType();
// Accessor-less events are invalid metadata; fall back to scanning all types.
foreach (var typeHandle in metadata.TypeDefinitions)
{
var typeDef = metadata.GetTypeDefinition(typeHandle);
foreach (var eh in typeDef.GetEvents())
{
if (eh == eventHandle)
return typeHandle;
}
}
return default;
}
static bool IsAsciiDigit(char c) => c >= '0' && c <= '9';
/// <summary>
/// Signature type provider that produces ID string fragments. With
/// <paramref name="cppCliDialect"/> set, produces the MSVC C++/CLI form
/// (custom modifiers rendered, arity markers kept on generic instantiations)
/// instead of the C#/Roslyn form.
/// </summary>
readonly struct IdStringSignatureTypeProvider(bool cppCliDialect) : ISignatureTypeProvider<string, MetadataGenericContext>
{
readonly bool cppCliDialect = cppCliDialect;
public string GetPrimitiveType(PrimitiveTypeCode typeCode)
{
return typeCode switch {
PrimitiveTypeCode.Void => "System.Void",
PrimitiveTypeCode.Boolean => "System.Boolean",
PrimitiveTypeCode.Char => "System.Char",
PrimitiveTypeCode.SByte => "System.SByte",
PrimitiveTypeCode.Byte => "System.Byte",
PrimitiveTypeCode.Int16 => "System.Int16",
PrimitiveTypeCode.UInt16 => "System.UInt16",
PrimitiveTypeCode.Int32 => "System.Int32",
PrimitiveTypeCode.UInt32 => "System.UInt32",
PrimitiveTypeCode.Int64 => "System.Int64",
PrimitiveTypeCode.UInt64 => "System.UInt64",
PrimitiveTypeCode.Single => "System.Single",
PrimitiveTypeCode.Double => "System.Double",
PrimitiveTypeCode.String => "System.String",
PrimitiveTypeCode.Object => "System.Object",
PrimitiveTypeCode.IntPtr => "System.IntPtr",
PrimitiveTypeCode.UIntPtr => "System.UIntPtr",
PrimitiveTypeCode.TypedReference => "System.TypedReference",
_ => throw new ArgumentOutOfRangeException(nameof(typeCode))
};
}
public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind)
{
var sb = new StringBuilder();
AppendTypeDefinitionName(sb, reader, handle);
return sb.ToString();
}
public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind)
{
var sb = new StringBuilder();
var typeRef = reader.GetTypeReference(handle);
AppendTypeReferenceName(sb, reader, typeRef);
return sb.ToString();
}
public string GetTypeFromSpecification(MetadataReader reader, MetadataGenericContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind)
{
var typeSpec = reader.GetTypeSpecification(handle);
return typeSpec.DecodeSignature(this, genericContext);
}
public string GetGenericTypeParameter(MetadataGenericContext genericContext, int index)
{
return "`" + index;
}
public string GetGenericMethodParameter(MetadataGenericContext genericContext, int index)
{
return "``" + index;
}
public string GetGenericInstantiation(string genericType, ImmutableArray<string> typeArguments)
{
// The generic arguments must be distributed to their nesting level:
// "Ns.Outer`1.Inner`2" + [A, B, C] => "Ns.Outer{A}.Inner{B,C}".
// The uninstantiated name carries a `k arity marker at every generic
// nesting level and the arguments are ordered outermost-first, so each
// marker consumes the next k arguments.
var sb = new StringBuilder(genericType.Length + typeArguments.Length * 16);
int nextArgument = 0;
for (int i = 0; i < genericType.Length; i++)
{
char c = genericType[i];
if (c != '`' || i + 1 >= genericType.Length || !IsAsciiDigit(genericType[i + 1]))
{
sb.Append(c);
continue;
}
int markerEnd = i + 1;
int arity = 0;
while (markerEnd < genericType.Length && IsAsciiDigit(genericType[markerEnd]))
{
arity = arity * 10 + (genericType[markerEnd] - '0');
markerEnd++;
}
if (arity > typeArguments.Length - nextArgument)
{
// The name does not follow the `k arity convention; keep the
// marker verbatim rather than inventing an argument split.
sb.Append(genericType, i, markerEnd - i);
}
else
{
// MSVC keeps the arity marker in front of the argument list
// (List`1{System.Int32}); Roslyn strips it (List{System.Int32}).
if (cppCliDialect)
sb.Append(genericType, i, markerEnd - i);
sb.Append('{');
for (int k = 0; k < arity; k++)
{
if (k > 0)
sb.Append(',');
sb.Append(typeArguments[nextArgument++]);
}
sb.Append('}');
}
i = markerEnd - 1;
}
if (nextArgument < typeArguments.Length)
{
// Arity markers did not account for all arguments; append the
// remainder so no argument is silently dropped.
sb.Append('{');
for (int k = nextArgument; k < typeArguments.Length; k++)
{
if (k > nextArgument)
sb.Append(',');
sb.Append(typeArguments[k]);
}
sb.Append('}');
}
return sb.ToString();
}
public string GetArrayType(string elementType, ArrayShape shape)
{
// C# 4.0 spec, section A.3.1: each dimension is rendered as
// "lowerbound:size", omitting either part when it is unspecified.
// Compilers emit neither bound for single-dimensional arrays and
// a zero lower bound with unspecified size ("0:") otherwise.
var sb = new StringBuilder(elementType);
sb.Append('[');
if (shape.Rank > 1 || shape.LowerBounds.Length > 0 || shape.Sizes.Length > 0)
{
for (int i = 0; i < shape.Rank; i++)
{
if (i > 0)
sb.Append(',');
if (i < shape.LowerBounds.Length)
{
sb.Append(shape.LowerBounds[i]);
sb.Append(':');
}
if (i < shape.Sizes.Length)
sb.Append(shape.Sizes[i]);
}
}
sb.Append(']');
return sb.ToString();
}
public string GetSZArrayType(string elementType)
{
return elementType + "[]";
}
public string GetPointerType(string elementType)
{
return elementType + "*";
}
public string GetByReferenceType(string elementType)
{
return elementType + "@";
}
public string GetFunctionPointerType(MethodSignature<string> signature)
{
//var sb = new StringBuilder("method ");
//sb.Append(signature.ReturnType);
//sb.Append(" *(");
//for (int i = 0; i < signature.ParameterTypes.Length; i++)
//{
// if (i > 0)
// sb.Append(',');
// sb.Append(signature.ParameterTypes[i]);
//}
//sb.Append(')');
//return sb.ToString();
// The C# spec does not define a syntax for function pointer types in ID strings
// Roslyn just returns an empty string, so we'll do the same to avoid confusion.
return "";
}
public string GetModifiedType(string modifier, string unmodifiedType, bool isRequired)
{
// Roslyn ignores custom modifiers entirely (e.g. a virtual method's 'in'
// parameter carries modreq(InAttribute) but is documented as T@).
if (!cppCliDialect)
return unmodifiedType;
// The MSVC xml doc generator renders a modifier after the modified type,
// e.g. System.Int32!System.Runtime.CompilerServices.IsConst for a C++/CLI
// 'const int' parameter. Its documented mapping is '!' for modopt and '|'
// for modreq, but observed output uses '|' only for modreq(IsVolatile);
// modreq(IsByValue) on conversion operator operands is rendered with '!'.
char prefix = isRequired && modifier == "System.Runtime.CompilerServices.IsVolatile"
? '|' : '!';
return unmodifiedType + prefix + modifier;
}
public string GetPinnedType(string elementType)
{
// '^' following the modified type, per the MSVC xml doc format. Pinned
// types cannot occur in member signatures, only in local variable
// signatures, so no compiler ever generates this in an ID string.
return elementType + "^";
}
}
#endregion
#region GetTypeName
public static string GetTypeName(IType type)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
StringBuilder b = new StringBuilder();
AppendTypeName(b, type, false);
return b.ToString();
}
static void AppendTypeName(StringBuilder b, IType type, bool explicitInterfaceImpl)
{
switch (type.Kind)
{
case TypeKind.Dynamic:
b.Append(explicitInterfaceImpl ? "System#Object" : "System.Object");
break;
case TypeKind.TypeParameter:
ITypeParameter tp = (ITypeParameter)type;
if (explicitInterfaceImpl)
{
b.Append(tp.Name);
}
else
{
b.Append('`');
if (tp.OwnerType == SymbolKind.Method)
b.Append('`');
b.Append(tp.Index);
}
break;
case TypeKind.Array:
ArrayType array = (ArrayType)type;
AppendTypeName(b, array.ElementType, explicitInterfaceImpl);
b.Append('[');
if (array.Dimensions > 1)
{
for (int i = 0; i < array.Dimensions; i++)
{
if (i > 0)
b.Append(explicitInterfaceImpl ? '@' : ',');
if (!explicitInterfaceImpl)
b.Append("0:");
}
}
b.Append(']');
break;
case TypeKind.Pointer:
AppendTypeName(b, ((PointerType)type).ElementType, explicitInterfaceImpl);
b.Append('*');
break;
case TypeKind.ByReference:
AppendTypeName(b, ((ByReferenceType)type).ElementType, explicitInterfaceImpl);
b.Append('@');
break;
default:
IType declType = type.DeclaringType;
if (declType != null)
{
AppendTypeName(b, declType, explicitInterfaceImpl);
b.Append(explicitInterfaceImpl ? '#' : '.');
b.Append(type.Name);
AppendTypeParameters(b, type, declType.TypeParameterCount, explicitInterfaceImpl);
}
else
{
if (explicitInterfaceImpl)
b.Append(type.FullName.Replace('.', '#'));
else
b.Append(type.FullName);
AppendTypeParameters(b, type, 0, explicitInterfaceImpl);
}
break;
}
}
static void AppendTypeParameters(StringBuilder b, IType type, int outerTypeParameterCount, bool explicitInterfaceImpl)
{
int tpc = type.TypeParameterCount - outerTypeParameterCount;
if (tpc > 0)
{
ParameterizedType pt = type as ParameterizedType;
if (pt != null)
{
b.Append('{');
var ta = pt.TypeArguments;
for (int i = outerTypeParameterCount; i < ta.Count; i++)
{
if (i > outerTypeParameterCount)
b.Append(explicitInterfaceImpl ? '@' : ',');
AppendTypeName(b, ta[i], explicitInterfaceImpl);
}
b.Append('}');
}
else
{
b.Append('`');
b.Append(tpc);
}
}
}
#endregion
#region FindEntity
/// <summary>
/// Finds the entity in the given type resolve context.
/// </summary>
/// <param name="idString">ID string of the entity.</param>
/// <param name="context">Type resolve context</param>
/// <returns>Returns the entity, or null if it is not found.</returns>
/// <exception cref="ReflectionNameParseException">The syntax of the ID string is invalid</exception>
public static IEntity FindEntity(string idString, ITypeResolveContext context)
{
if (idString == null)
throw new ArgumentNullException(nameof(idString));
if (context == null)
throw new ArgumentNullException(nameof(context));
if (idString.Length < 2 || idString[1] != ':')
throw new ReflectionNameParseException(0, "Missing type tag");
if (idString[0] == 'T')
return FindTypeDefinition(idString.Substring(2), context);
int dotPos = FindMemberNameDot(idString);
if (dotPos <= 2)
throw new ReflectionNameParseException(0, "Could not find '.' separating type name from member name");
var declaringType = FindTypeDefinition(idString.Substring(2, dotPos - 2), context);
if (declaringType?.ParentModule is not MetadataModule metadataModule)
return null;
var typeDef = metadataModule.MetadataFile.Metadata.GetTypeDefinition(
(TypeDefinitionHandle)declaringType.MetadataToken);
var handle = FindMemberInType(metadataModule.MetadataFile, typeDef, idString[0], idString);
return handle.IsNil ? null : metadataModule.ResolveEntity(handle);
}
/// <summary>
/// Resolves a type name from an ID string (without the "T:" prefix) in the given
/// type resolve context, trying all namespace/type-name boundary splits.
/// </summary>
static ITypeDefinition FindTypeDefinition(string typeName, ITypeResolveContext context)
{
var parts = ParseTypeNameParts(typeName);
string[] dotParts = parts[0].Name.Split('.');
for (int i = dotParts.Length - 1; i >= 0; i--)
{
string ns = string.Join(".", dotParts, 0, i);
string name = dotParts[i];
int topLevelTpc = (i == dotParts.Length - 1) ? parts[0].TypeParameterCount : 0;
var typeDef = context.Compilation.FindType(new TopLevelTypeName(ns, name, topLevelTpc)).GetDefinition();
for (int j = i + 1; j < dotParts.Length && typeDef != null; j++)
{
int tpc = (j == dotParts.Length - 1 && parts.Count == 1) ? parts[0].TypeParameterCount : 0;
typeDef = FindNestedType(typeDef, dotParts[j], tpc);
}
for (int j = 1; j < parts.Count && typeDef != null; j++)
{
typeDef = FindNestedType(typeDef, parts[j].Name, parts[j].TypeParameterCount);
}
if (typeDef != null)
return typeDef;
}
return null;
}
static ITypeDefinition FindNestedType(ITypeDefinition declaringType, string name, int additionalTypeParameterCount)
{
foreach (var nested in declaringType.NestedTypes)
{
var def = nested.GetDefinition();
if (def != null && def.Name == name
&& def.TypeParameterCount - declaringType.TypeParameterCount == additionalTypeParameterCount)
{
return def;
}
}
return null;
}
/// <summary>
/// Finds the entity with the given ID string in the provided modules.
/// </summary>
/// <param name="idString">ID string of the entity (e.g., "T:System.String", "M:System.String.Contains(System.String)").</param>
/// <param name="modules">The list of modules to search, in priority order.</param>
/// <returns>
/// A tuple of (MetadataFile, EntityHandle) for the found entity.
/// Returns default if the entity is not found.
/// </returns>
/// <exception cref="ReflectionNameParseException">The syntax of the ID string is invalid.</exception>
/// <remarks>
/// <para>
/// The ID string format cannot represent all names valid in metadata: GetIdString
/// emits raw metadata names, but a name that itself contains ID string special
/// characters (e.g. a dot in a type name) is ambiguous when parsed back, because
/// namespace/type-name splits are only tried at dots. Function pointer parameter
/// types render as empty (matching Roslyn), so overloads differing only by a
/// function pointer type share an ID and resolve to the first candidate.
/// </para>
/// <para>
/// A type only present as a type forwarder is returned as its ExportedType handle;
/// members of such a type are not followed into the target assembly unless that
/// assembly is itself part of <paramref name="modules"/>.
/// </para>
/// </remarks>
public static (MetadataFile Module, EntityHandle Handle) FindEntity(string idString, IReadOnlyList<MetadataFile> modules)
{
if (idString == null)
throw new ArgumentNullException(nameof(idString));
if (modules == null)
throw new ArgumentNullException(nameof(modules));
if (idString.Length < 2 || idString[1] != ':')
throw new ReflectionNameParseException(0, "Missing type tag");
char typeChar = idString[0];
if (typeChar == 'T')
{
return FindTypeDefinition(idString.Substring(2), modules);
}
else
{
return FindMember(typeChar, idString, modules);
}
}
/// <summary>
/// Resolves a type name from an ID string to a TypeDefinitionHandle or ExportedTypeHandle.
/// Tries all possible namespace/type-name boundary splits (mirrors the algorithm from
/// GetPotentiallyNestedClassTypeReference.ResolveInPEFile).
/// </summary>
static (MetadataFile, EntityHandle) FindTypeDefinition(string typeName, IReadOnlyList<MetadataFile> modules)
{
var parts = ParseTypeNameParts(typeName);
foreach (var module in modules)
{
if (module == null)
continue;
var result = ResolveTypeInModule(parts, module);
if (!result.IsNil)
return (module, result);
}
return default;
}
/// <summary>
/// Finds a member (field, method, property, event) by its ID string.
/// First resolves the declaring type, then enumerates candidate members
/// and compares their computed ID strings.
/// </summary>
/// <summary>
/// Finds the '.' separating the declaring type name from the member name: the last
/// '.' before '(' or '~' or end-of-string. Returns a negative value if there is none.
/// </summary>
static int FindMemberNameDot(string idString)
{
int parenPos = idString.IndexOf('(');
if (parenPos < 0)
parenPos = idString.LastIndexOf('~');
if (parenPos < 0)
parenPos = idString.Length;
return idString.LastIndexOf('.', parenPos - 1);
}
static (MetadataFile, EntityHandle) FindMember(char typeChar, string idString, IReadOnlyList<MetadataFile> modules)
{
int dotPos = FindMemberNameDot(idString);
if (dotPos < 0)
throw new ReflectionNameParseException(0, "Could not find '.' separating type name from member name");
// The type name portion is from index 2 (after "X:") to dotPos.
string typeName = idString.Substring(2, dotPos - 2);
var typeParts = ParseTypeNameParts(typeName);
foreach (var module in modules)
{
if (module == null)
continue;
var typeHandle = ResolveTypeInModule(typeParts, module);
if (typeHandle.IsNil || typeHandle.Kind != HandleKind.TypeDefinition)
continue;
var typeDef = module.Metadata.GetTypeDefinition((TypeDefinitionHandle)typeHandle);
EntityHandle memberHandle = FindMemberInType(module, typeDef, typeChar, idString);
if (!memberHandle.IsNil)
return (module, memberHandle);
}
return default;
}
/// <summary>
/// Extracts the member-name portion of a member ID string and undoes the name
/// mangling, approximating the metadata name to narrow candidates with. The result
/// can disagree with the actual metadata name (a 'default' indexer key, or names
/// that genuinely contain mangled characters), so a miss with the filter must fall
/// back to an unfiltered scan.
/// </summary>
static string GetApproximateMemberName(string idString)
{
int dotPos = FindMemberNameDot(idString);
if (dotPos < 0)
return null;
int end = idString.Length;
int parenPos = idString.IndexOf('(', dotPos);
if (parenPos >= 0)
end = parenPos;
else if ((parenPos = idString.LastIndexOf('~')) > dotPos)
end = parenPos;
string name = idString.Substring(dotPos + 1, end - dotPos - 1);
int arityMarker = name.IndexOf("``", StringComparison.Ordinal);
if (arityMarker >= 0)
name = name.Substring(0, arityMarker);
return name.Replace('#', '.').Replace('{', '<').Replace('}', '>');
}
/// <summary>
/// Searches for a member within a resolved type definition by computing the ID
/// string of each member and comparing, so that IDs in either dialect (e.g. crefs
/// from MSVC-generated xml files) resolve. Candidates are first narrowed by the
/// member name; the dialects are matched in two passes over the whole member list,
/// most specific first: the stripped C#/Roslyn form of one member can equal the
/// C++/CLI-dialect key of a different member (overloads differing only in a custom
/// modifier), so mixing dialects within a single pass could resolve to the wrong
/// member.
/// </summary>
static EntityHandle FindMemberInType(MetadataFile module, TypeDefinition typeDef, char typeChar, string idString)
{
string nameFilter = GetApproximateMemberName(idString);
var handle = FindMemberInType(module, typeDef, typeChar, idString, nameFilter);
if (handle.IsNil && nameFilter != null)
handle = FindMemberInType(module, typeDef, typeChar, idString, nameFilter: null);
return handle;
}
static EntityHandle FindMemberInType(MetadataFile module, TypeDefinition typeDef, char typeChar, string idString, string nameFilter)
{
var metadata = module.Metadata;
bool NameMatches(StringHandle name)
=> nameFilter == null || metadata.StringComparer.Equals(name, nameFilter);
for (int pass = 0; pass < 2; pass++)
{
bool cppCliDialect = pass == 0;
switch (typeChar)
{
case 'F':
foreach (var handle in typeDef.GetFields())
{
if (NameMatches(metadata.GetFieldDefinition(handle).Name)
&& GetIdString(module, handle, cppCliDialect) == idString)
{
return handle;
}
}
break;
case 'M':
foreach (var handle in typeDef.GetMethods())
{
if (NameMatches(metadata.GetMethodDefinition(handle).Name)
&& GetIdString(module, handle, cppCliDialect) == idString)
{
return handle;
}
}
break;
case 'P':
foreach (var handle in typeDef.GetProperties())
{
if (NameMatches(metadata.GetPropertyDefinition(handle).Name)
&& GetIdString(module, handle, cppCliDialect) == idString)
{
return handle;
}
}
break;
case 'E':
foreach (var handle in typeDef.GetEvents())
{
if (NameMatches(metadata.GetEventDefinition(handle).Name)
&& GetIdString(module, handle, cppCliDialect) == idString)
{
return handle;
}
}
break;
}
}
return default;
}
#endregion
#region Type Name Parsing and Resolution
/// <summary>
/// Represents a parsed segment of a potentially nested type name in an ID string.
/// The first part's Name may contain dots (namespace + top-level type name);
/// subsequent parts are nested type names without dots.
/// </summary>
struct TypeNamePart
{
public string Name;
public int TypeParameterCount;
}
/// <summary>
/// Parses a type name (without the "T:" prefix) into its constituent parts,
/// handling nested types separated by '.', and generic arity via `n or {args}.
///
/// The first part's Name contains the full dotted name (namespace + top-level type),
/// because we don't know where the namespace ends. Resolution will try all splits.
///
/// Examples:
/// "System.Collections.Generic.Dictionary`2.KeyCollection"
/// -> [{Name="System.Collections.Generic.Dictionary", TPC=2}, {Name="KeyCollection", TPC=0}]
///
/// "Outer.Inner{System.Int32}"
/// -> [{Name="Outer", TPC=0}, {Name="Inner", TPC=1}]
/// </summary>
static List<TypeNamePart> ParseTypeNameParts(string typeName)
{
var parts = new List<TypeNamePart>();
int pos = 0;
string firstName = ReadTypeNameSegment(typeName, ref pos, allowDots: true);
int firstTpc = ReadTypeParameterCountFromIdString(typeName, ref pos);
parts.Add(new TypeNamePart { Name = firstName, TypeParameterCount = firstTpc });
while (pos < typeName.Length && typeName[pos] == '.')
{
pos++;
string nestedName = ReadTypeNameSegment(typeName, ref pos, allowDots: false);
int nestedTpc = ReadTypeParameterCountFromIdString(typeName, ref pos);
parts.Add(new TypeNamePart { Name = nestedName, TypeParameterCount = nestedTpc });
}
return parts;
}
/// <summary>
/// Reads a type name segment (no special characters). If allowDots is true,
/// dots are included in the segment (for the top-level name which includes namespace).
/// </summary>
static string ReadTypeNameSegment(string typeName, ref int pos, bool allowDots)
{
int start = pos;
while (pos < typeName.Length)
{
char c = typeName[pos];
if (IsIDStringSpecialCharacter(c))
break;
if (!allowDots && c == '.')
break;
pos++;
}
if (pos == start)
throw new ReflectionNameParseException(pos, "Expected type name");
return typeName.Substring(start, pos - start);
}
/// <summary>
/// Reads a type parameter count from the current position in an ID string.
/// Handles both `n (unbound) and {T1,T2,...} (bound) syntax.
/// For bound syntax, counts the arguments without fully parsing them
/// (we only need the arity for type definition lookup).
/// </summary>
static int ReadTypeParameterCountFromIdString(string typeName, ref int pos)
{
if (pos >= typeName.Length)
return 0;
if (typeName[pos] == '`')
{
pos++;
return ReflectionHelper.ReadTypeParameterCount(typeName, ref pos);
}
else if (typeName[pos] == '{')
{
int count = 1;
int depth = 0;
pos++; // skip '{'
while (pos < typeName.Length)
{
char c = typeName[pos];
if (c == '{')
{
depth++;
}
else if (c == '}')
{
if (depth == 0)
{
pos++;
return count;
}
depth--;
}
else if (c == ',' && depth == 0)
{
count++;
}
pos++;
}
throw new ReflectionNameParseException(pos, "Expected '}'");
}
return 0;
}
/// <summary>
/// Attempts to resolve a parsed type name within a single module.
/// The first part's Name is a dotted name like "A.B.C", and we try all possible
/// splits between namespace and top-level type name, from right to left.
/// For each candidate top-level type, we walk the nested types.
/// Also checks type forwarders.
/// </summary>
static EntityHandle ResolveTypeInModule(List<TypeNamePart> parts, MetadataFile module)
{
var metadata = module.Metadata;
string topLevelDottedName = parts[0].Name;
string[] dotParts = topLevelDottedName.Split('.');
for (int i = dotParts.Length - 1; i >= 0; i--)
{
string ns = string.Join(".", dotParts, 0, i);
string name = dotParts[i];
int topLevelTpc = (i == dotParts.Length - 1) ? parts[0].TypeParameterCount : 0;
var topLevelName = new TopLevelTypeName(ns, name, topLevelTpc);
var typeHandle = module.GetTypeDefinition(topLevelName);
// Walk remaining dotParts as nested types, then explicit nested parts
for (int j = i + 1; j < dotParts.Length && !typeHandle.IsNil; j++)
{
int tpc = (j == dotParts.Length - 1 && parts.Count == 1) ? parts[0].TypeParameterCount : 0;
typeHandle = FindNestedType(metadata, typeHandle, dotParts[j], tpc);
}
// Walk explicit nested parts (from '.' after `n or {args})
for (int j = 1; j < parts.Count && !typeHandle.IsNil; j++)
{
typeHandle = FindNestedType(metadata, typeHandle, parts[j].Name, parts[j].TypeParameterCount);
}
if (!typeHandle.IsNil)
return typeHandle;
// Try as type forwarder with the same structure
FullTypeName fullTypeName = topLevelName;
for (int j = i + 1; j < dotParts.Length; j++)
{
int tpc = (j == dotParts.Length - 1 && parts.Count == 1) ? parts[0].TypeParameterCount : 0;
fullTypeName = fullTypeName.NestedType(dotParts[j], tpc);
}
for (int j = 1; j < parts.Count; j++)
{
fullTypeName = fullTypeName.NestedType(parts[j].Name, parts[j].TypeParameterCount);
}
var exportedType = module.GetTypeForwarder(fullTypeName);
if (!exportedType.IsNil)
return exportedType;
}
return default;
}
/// <summary>
/// Finds a nested type by name and type parameter count within a type definition.
/// Returns a nil handle if not found.
/// </summary>
static TypeDefinitionHandle FindNestedType(MetadataReader metadata, TypeDefinitionHandle declaringTypeHandle, string name, int typeParameterCount)
{
var typeDef = metadata.GetTypeDefinition(declaringTypeHandle);
string lookupName = typeParameterCount > 0 ? name + "`" + typeParameterCount : name;
foreach (var nestedHandle in typeDef.GetNestedTypes())
{
var nestedDef = metadata.GetTypeDefinition(nestedHandle);
if (metadata.StringComparer.Equals(nestedDef.Name, lookupName))
return nestedHandle;
}
return default;
}
static bool IsIDStringSpecialCharacter(char c)
{
switch (c)
{
case ':':
case '{':
case '}':
case '[':
case ']':
case '(':
case ')':
case '`':
case '*':
case '@':
case ',':
return true;
default:
return false;
}
}
#endregion
}
}