.NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1230 lines
42 KiB

// 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, §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, §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++;
break;
}
depth--;
}
else if (c == ',' && depth == 0)
{
count++;
}
pos++;
}
return count;
}
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
}
}