// 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 { /// /// Provides ID strings for entities. (C# 4.0 spec, §A.3.1) /// ID strings are used to identify members in XML documentation files. /// public static class IdStringProvider { #region GetIdString /// /// 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. /// public static string GetIdString(this MetadataFile module, EntityHandle handle) { return GetIdString(module, handle, cppCliDialect: false); } /// /// 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. /// public static IEnumerable 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; } /// /// True when the C#/Roslyn-form ID of 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. /// 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(); } /// /// Gets the ID string (C# 4.0 spec, §A.3.1) for the specified entity. /// /// /// The ID string is computed from the entity's metadata; for specialized members /// it describes the underlying member definition. /// 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); } /// /// Appends the fully qualified name of a type definition, handling nested types. /// The metadata name already contains the `n arity suffix for generic types. /// 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 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'; /// /// Signature type provider that produces ID string fragments. With /// set, produces the MSVC C++/CLI form /// (custom modifiers rendered, arity markers kept on generic instantiations) /// instead of the C#/Roslyn form. /// readonly struct IdStringSignatureTypeProvider(bool cppCliDialect) : ISignatureTypeProvider { 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 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 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 /// /// Finds the entity in the given type resolve context. /// /// ID string of the entity. /// Type resolve context /// Returns the entity, or null if it is not found. /// The syntax of the ID string is invalid 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); } /// /// 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. /// 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; } /// /// Finds the entity with the given ID string in the provided modules. /// /// ID string of the entity (e.g., "T:System.String", "M:System.String.Contains(System.String)"). /// The list of modules to search, in priority order. /// /// A tuple of (MetadataFile, EntityHandle) for the found entity. /// Returns default if the entity is not found. /// /// The syntax of the ID string is invalid. /// /// /// 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. /// /// /// 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 . /// /// public static (MetadataFile Module, EntityHandle Handle) FindEntity(string idString, IReadOnlyList 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); } } /// /// 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). /// static (MetadataFile, EntityHandle) FindTypeDefinition(string typeName, IReadOnlyList 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; } /// /// 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. /// /// /// 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. /// 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 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; } /// /// 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. /// 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('}', '>'); } /// /// 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. /// 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 /// /// 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. /// struct TypeNamePart { public string Name; public int TypeParameterCount; } /// /// 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}] /// static List ParseTypeNameParts(string typeName) { var parts = new List(); 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; } /// /// 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). /// 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); } /// /// 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). /// 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; } /// /// 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. /// static EntityHandle ResolveTypeInModule(List 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; } /// /// Finds a nested type by name and type parameter count within a type definition. /// Returns a nil handle if not found. /// 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 } }