diff --git a/src/linker/Linker/DocumentationSignatureGenerator.PartVisitor.cs b/src/linker/Linker/DocumentationSignatureGenerator.PartVisitor.cs
new file mode 100644
index 000000000000..4fa6202efffb
--- /dev/null
+++ b/src/linker/Linker/DocumentationSignatureGenerator.PartVisitor.cs
@@ -0,0 +1,213 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text;
+using Mono.Cecil;
+
+namespace Mono.Linker
+{
+
+ public sealed partial class DocumentationSignatureGenerator
+ {
+ ///
+ /// A visitor that generates the part of the documentation comment after the initial type
+ /// and colon.
+ /// Adapted from Roslyn's DocumentattionCommentIDVisitor.PartVisitor:
+ /// https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/DocumentationComments/DocumentationCommentIDVisitor.PartVisitor.cs
+ ///
+ internal sealed class PartVisitor
+ {
+ internal static readonly PartVisitor Instance = new PartVisitor ();
+
+ private PartVisitor ()
+ {
+ }
+
+ public void VisitArrayType (ArrayType arrayType, StringBuilder builder)
+ {
+ VisitTypeReference (arrayType.ElementType, builder);
+
+ // Rank-one arrays are displayed different than rectangular arrays
+ if (arrayType.IsVector) {
+ builder.Append ("[]");
+ } else {
+ // C# arrays only support zero lower bounds
+ if (arrayType.Dimensions[0].LowerBound != 0)
+ throw new NotImplementedException ();
+ builder.Append ("[0:");
+ for (int i = 1; i < arrayType.Rank; i++) {
+ if (arrayType.Dimensions[0].LowerBound != 0)
+ throw new NotImplementedException ();
+ builder.Append (",0:");
+ }
+
+ builder.Append (']');
+ }
+ }
+
+ public void VisitField (FieldDefinition field, StringBuilder builder)
+ {
+ VisitTypeReference (field.DeclaringType, builder);
+ builder.Append ('.').Append (field.Name);
+ }
+
+ private void VisitParameters (IEnumerable parameters, bool isVararg, StringBuilder builder)
+ {
+ builder.Append ('(');
+ bool needsComma = false;
+
+ foreach (var parameter in parameters) {
+ if (needsComma)
+ builder.Append (',');
+
+ // byrefs are tracked on the parameter type, not the parameter,
+ // so we don't have VisitParameter that Roslyn uses.
+ VisitTypeReference (parameter.ParameterType, builder);
+ needsComma = true;
+ }
+
+ // note: the C# doc comment generator outputs an extra comma for varargs
+ // methods that also have fixed parameters
+ if (isVararg && needsComma)
+ builder.Append (',');
+
+ builder.Append (')');
+ }
+
+ public void VisitMethodDefinition (MethodDefinition method, StringBuilder builder)
+ {
+ VisitTypeReference (method.DeclaringType, builder);
+ builder.Append ('.').Append (GetEscapedMetadataName (method));
+
+ if (method.HasGenericParameters)
+ builder.Append ("``").Append (method.GenericParameters.Count);
+
+ if (method.HasParameters || (method.CallingConvention == MethodCallingConvention.VarArg))
+ VisitParameters (method.Parameters, method.CallingConvention == MethodCallingConvention.VarArg, builder);
+
+ if (method.Name == "op_Implicit" || method.Name == "op_Explicit") {
+ builder.Append ('~');
+ VisitTypeReference (method.ReturnType, builder);
+ }
+ }
+
+ public void VisitProperty (PropertyDefinition property, StringBuilder builder)
+ {
+ VisitTypeReference (property.DeclaringType, builder);
+ builder.Append ('.').Append (GetEscapedMetadataName (property));
+
+ if (property.Parameters.Count > 0)
+ VisitParameters (property.Parameters, false, builder);
+ }
+
+ public void VisitEvent (EventDefinition evt, StringBuilder builder)
+ {
+ VisitTypeReference (evt.DeclaringType, builder);
+ builder.Append ('.').Append (GetEscapedMetadataName (evt));
+ }
+
+ public void VisitGenericParameter (GenericParameter genericParameter, StringBuilder builder)
+ {
+ Debug.Assert ((genericParameter.DeclaringMethod == null) != (genericParameter.DeclaringType == null));
+ // Is this a type parameter on a type?
+ if (genericParameter.DeclaringMethod != null) {
+ builder.Append ("``");
+ } else {
+ Debug.Assert (genericParameter.DeclaringType != null);
+
+ // If the containing type is nested within other types.
+ // e.g. A.B.M(T t, U u, V v) should be M(`0, `1, ``0).
+ // Roslyn needs to add generic arities of parents, but the innermost type redeclares
+ // all generic parameters so we don't need to add them.
+ builder.Append ('`');
+ }
+
+ builder.Append (genericParameter.Position);
+ }
+
+ public void VisitTypeReference (TypeReference typeReference, StringBuilder builder)
+ {
+ switch (typeReference) {
+ case ByReferenceType byReferenceType:
+ VisitByReferenceType (byReferenceType, builder);
+ return;
+ case PointerType pointerType:
+ VisitPointerType (pointerType, builder);
+ return;
+ case ArrayType arrayType:
+ VisitArrayType (arrayType, builder);
+ return;
+ case GenericParameter genericParameter:
+ VisitGenericParameter (genericParameter, builder);
+ return;
+ }
+
+ if (typeReference.IsNested) {
+ VisitTypeReference (typeReference.GetInflatedDeclaringType (), builder);
+ builder.Append ('.');
+ }
+
+ if (!String.IsNullOrEmpty (typeReference.Namespace))
+ builder.Append (typeReference.Namespace).Append ('.');
+
+ // This includes '`n' for mangled generic types
+ builder.Append (typeReference.Name);
+
+ // For uninstantiated generic types (we already built the mangled name)
+ // or non-generic types, we are done.
+ if (typeReference.HasGenericParameters || !typeReference.IsGenericInstance)
+ return;
+
+ var genericInstance = typeReference as GenericInstanceType;
+
+ // Compute arity counting only the newly-introduced generic parameters
+ var declaringType = genericInstance.DeclaringType;
+ var declaringArity = 0;
+ if (declaringType != null && declaringType.HasGenericParameters)
+ declaringArity = declaringType.GenericParameters.Count;
+ var totalArity = genericInstance.GenericArguments.Count;
+ var arity = totalArity - declaringArity;
+
+ // Un-mangle the generic type name
+ var suffixLength = arity.ToString ().Length + 1;
+ builder.Remove (builder.Length - suffixLength, suffixLength);
+
+ // Append type arguments excluding arguments for re-declared parent generic parameters
+ builder.Append ('{');
+ bool needsComma = false;
+ for (int i = totalArity - arity; i < totalArity; ++i) {
+ if (needsComma)
+ builder.Append (',');
+ var typeArgument = genericInstance.GenericArguments[i];
+ VisitTypeReference (typeArgument, builder);
+ needsComma = true;
+ }
+ builder.Append ('}');
+ }
+
+ public void VisitPointerType (PointerType pointerType, StringBuilder builder)
+ {
+ VisitTypeReference (pointerType.ElementType, builder);
+ builder.Append ('*');
+ }
+
+ public void VisitByReferenceType (ByReferenceType byReferenceType, StringBuilder builder)
+ {
+ VisitTypeReference (byReferenceType.ElementType, builder);
+ builder.Append ('@');
+ }
+
+ private static string GetEscapedMetadataName (IMemberDefinition member)
+ {
+ var name = member.Name.Replace ('.', '#');
+ // Not sure if the following replacements are necessary, but
+ // they are included to match Roslyn.
+ return name.Replace ('<', '{').Replace ('>', '}');
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/linker/Linker/DocumentationSignatureGenerator.cs b/src/linker/Linker/DocumentationSignatureGenerator.cs
new file mode 100644
index 000000000000..3289b189d7cf
--- /dev/null
+++ b/src/linker/Linker/DocumentationSignatureGenerator.cs
@@ -0,0 +1,55 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Text;
+using Mono.Cecil;
+
+namespace Mono.Linker
+{
+ ///
+ /// Generates a signature for a member, in the format used for C# Documentation Comments:
+ /// https://github.com/dotnet/csharplang/blob/master/spec/documentation-comments.md#id-string-format
+ /// Adapted from Roslyn's DocumentationCommentIDVisitor:
+ /// https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/DocumentationComments/DocumentationCommentIDVisitor.cs
+ ///
+ public sealed partial class DocumentationSignatureGenerator
+ {
+ public static readonly DocumentationSignatureGenerator Instance = new DocumentationSignatureGenerator ();
+
+ private DocumentationSignatureGenerator ()
+ {
+ }
+
+ public void VisitMethod (MethodDefinition method, StringBuilder builder)
+ {
+ builder.Append ("M:");
+ PartVisitor.Instance.VisitMethodDefinition (method, builder);
+ }
+
+ public void VisitField (FieldDefinition field, StringBuilder builder)
+ {
+ builder.Append ("F:");
+ PartVisitor.Instance.VisitField (field, builder);
+ }
+
+ public void VisitEvent (EventDefinition evt, StringBuilder builder)
+ {
+ builder.Append ("E:");
+ PartVisitor.Instance.VisitEvent (evt, builder);
+ }
+
+ public void VisitProperty (PropertyDefinition property, StringBuilder builder)
+ {
+ builder.Append ("P:");
+ PartVisitor.Instance.VisitProperty (property, builder);
+ }
+
+ public void VisitTypeDefinition (TypeDefinition type, StringBuilder builder)
+ {
+ builder.Append ("T:");
+ PartVisitor.Instance.VisitTypeReference (type, builder);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/linker/Linker/DocumentationSignatureParser.cs b/src/linker/Linker/DocumentationSignatureParser.cs
new file mode 100644
index 000000000000..a9a0c191059f
--- /dev/null
+++ b/src/linker/Linker/DocumentationSignatureParser.cs
@@ -0,0 +1,699 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using Mono.Cecil;
+using Mono.Collections.Generic;
+
+namespace Mono.Linker
+{
+ ///
+ /// Parses a signature for a member, in the format used for C# Documentation Comments:
+ /// https://github.com/dotnet/csharplang/blob/master/spec/documentation-comments.md#id-string-format
+ /// Adapted from Roslyn's DocumentationCommentId:
+ /// https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/DocumentationCommentId.cs
+ ///
+ ///
+ /// Roslyn's API works with ISymbol, which represents a symbol exposed by the compiler.
+ /// a Symbol has information about the source language, name, metadata name,
+ /// containing scopes, visibility/accessibility, attributes, etc.
+ /// This API instead works with the Cecil OM. It can be used to refer to IL definitions
+ /// where the signature of a member can contain references to instantiated generics.
+ ///
+ public static class DocumentationSignatureParser
+ {
+ [Flags]
+ public enum MemberType
+ {
+ Method = 0x0001,
+ Field = 0x0002,
+ Type = 0x0004,
+ Property = 0x0008,
+ Event = 0x0010,
+ All = Method | Field | Type | Property | Event
+ }
+
+ public static IEnumerable GetMembersForDocumentationSignature (string id, ModuleDefinition module)
+ {
+ if (id == null)
+ throw new ArgumentNullException (nameof (id));
+
+ if (module == null)
+ throw new ArgumentNullException (nameof (module));
+
+ var results = new List ();
+ ParseDocumentationSignature (id, module, results);
+ return results;
+ }
+
+ static string GetSignaturePart (TypeReference type)
+ {
+ var builder = new StringBuilder ();
+ DocumentationSignatureGenerator.PartVisitor.Instance.VisitTypeReference (type, builder);
+ return builder.ToString ();
+ }
+
+ static bool ParseDocumentationSignature (string id, ModuleDefinition module, List results)
+ {
+ if (id == null)
+ return false;
+
+ if (id.Length < 2)
+ return false;
+
+ int index = 0;
+ results.Clear ();
+ ParseSignature (id, ref index, module, results);
+ return results.Count > 0;
+ }
+
+ static void ParseSignature (string id, ref int index, ModuleDefinition module, List results)
+ {
+ Debug.Assert (results.Count == 0);
+ var memberTypeChar = PeekNextChar (id, index);
+ MemberType memberType;
+
+ switch (memberTypeChar) {
+ case 'E':
+ memberType = MemberType.Event;
+ break;
+ case 'F':
+ memberType = MemberType.Field;
+ break;
+ case 'M':
+ memberType = MemberType.Method;
+ break;
+ case 'N':
+ // We do not support namespaces, which do not exist in IL.
+ return;
+ case 'P':
+ memberType = MemberType.Property;
+ break;
+ case 'T':
+ memberType = MemberType.Type;
+ break;
+ default:
+ // Documentation comment id must start with E, F, M, P, or T
+ return;
+ }
+
+ index++;
+ // Note: this allows leaving out the ':'.
+ if (PeekNextChar (id, index) == ':')
+ index++;
+
+ ParseSignaturePart (id, ref index, module, memberType, results);
+ }
+
+ // Parses and resolves a fully-qualified (namespace and nested types but no assembly) member signature,
+ // without the member type prefix. The results include all members matching the specified member types.
+ public static void ParseSignaturePart (string id, ref int index, ModuleDefinition module, MemberType memberTypes, List results)
+ {
+ // Roslyn resolves types by searching namespaces top-down.
+ // We don't have namespace info. Instead try treating each part of a
+ // dotted name as a type first, then as a namespace if it fails
+ // to resolve to a type.
+ TypeDefinition? containingType = null;
+ var nameBuilder = new StringBuilder ();
+
+ string name;
+ int arity;
+
+ // process dotted names
+ while (true) {
+ (name, arity) = ParseTypeOrNamespaceName (id, ref index, nameBuilder);
+ // if we are at the end of the dotted name and still haven't resolved it to
+ // a type, there are no results.
+ if (String.IsNullOrEmpty (name))
+ return;
+
+ // no more dots, so don't loop any more
+ if (PeekNextChar (id, index) != '.')
+ break;
+
+ // must be a namespace or type since name continues after dot
+ index++;
+
+ // try to resolve it as a type
+ var typeOrNamespaceName = nameBuilder.ToString ();
+ GetMatchingTypes (module, declaringType: containingType, name: typeOrNamespaceName, results: results);
+ Debug.Assert (results.Count <= 1);
+ if (results.Any ()) {
+ // the name resolved to a type
+ var result = results.Single ();
+ Debug.Assert (result is TypeDefinition);
+ // result becomes the new container
+ containingType = result as TypeDefinition;
+ nameBuilder.Clear ();
+ results.Clear ();
+ continue;
+ }
+
+ // it didn't resolve as a type.
+
+ // only types have arity.
+ if (arity > 0)
+ return;
+
+ // treat it as a namespace and continue building up the type name
+ nameBuilder.Append ('.');
+ }
+
+ var memberName = nameBuilder.ToString ();
+ GetMatchingMembers (id, ref index, module, containingType, memberName, arity, memberTypes, results);
+ }
+
+ // Gets all members of the specified member kinds of the containing type, with
+ // mathing name, arity, and signature at the current index (for methods and properties).
+ // This will also resolve types from the given module if no containing type is given.
+ public static void GetMatchingMembers (string id, ref int index, ModuleDefinition module, TypeDefinition? containingType, string memberName, int arity, MemberType memberTypes, List results)
+ {
+ if (memberTypes.HasFlag (MemberType.Type))
+ GetMatchingTypes (module, containingType, memberName, results);
+
+ if (containingType == null)
+ return;
+
+ int startIndex = index;
+ int endIndex = index;
+
+ if (memberTypes.HasFlag (MemberType.Method)) {
+ GetMatchingMethods (id, ref index, containingType, memberName, arity, results);
+ endIndex = index;
+ index = startIndex;
+ }
+
+ if (memberTypes.HasFlag (MemberType.Property)) {
+ GetMatchingProperties (id, ref index, containingType, memberName, results);
+ endIndex = index;
+ index = startIndex;
+ }
+
+ index = endIndex;
+
+ if (memberTypes.HasFlag (MemberType.Event))
+ GetMatchingEvents (containingType, memberName, results);
+
+ if (memberTypes.HasFlag (MemberType.Field))
+ GetMatchingFields (containingType, memberName, results);
+ }
+
+ // Parses a part of a dotted declaration name, including generic definitions.
+ // Returns the name (either a namespace or the unmangled name of a C# type) and an arity
+ // which may be non-zero for generic types.
+ public static (string name, int arity) ParseTypeOrNamespaceName (string id, ref int index, StringBuilder nameBuilder)
+ {
+ var name = ParseName (id, ref index);
+ // don't parse ` after an empty name
+ if (string.IsNullOrEmpty (name))
+ return (name, 0);
+
+ nameBuilder.Append (name);
+ var arity = 0;
+
+ // has type parameters?
+ if (PeekNextChar (id, index) == '`') {
+ index++;
+
+ bool genericType = true;
+
+ // method type parameters?
+ // note: this allows `` for type parameters
+ if (PeekNextChar (id, index) == '`') {
+ index++;
+ genericType = false;
+ }
+
+ arity = ReadNextInteger (id, ref index);
+
+ if (genericType) {
+ // We need to mangle generic type names but not generic method names.
+ nameBuilder.Append ('`');
+ nameBuilder.Append (arity);
+ }
+ }
+
+ return (name, arity);
+ }
+
+ // Roslyn resolves types in a signature to their declaration by searching through namespaces.
+ // To avoid looking for types by name in all referenced assemblies, we just represent types
+ // that are part of a signature by their doc comment strings, and we check for matching
+ // strings when looking for matching member signatures.
+ static string? ParseTypeSymbol (string id, ref int index, IGenericParameterProvider? typeParameterContext)
+ {
+ var results = new List ();
+ ParseTypeSymbol (id, ref index, typeParameterContext, results);
+ if (results.Count == 1)
+ return results[0];
+
+ Debug.Assert (results.Count == 0);
+ return null;
+ }
+
+ static void ParseTypeSymbol (string id, ref int index, IGenericParameterProvider? typeParameterContext, List results)
+ {
+ // Note: Roslyn has a special case that deviates from the language spec, which
+ // allows context expressions embedded in a type reference => :
+ // We do not support this special format.
+
+ Debug.Assert (results.Count == 0);
+
+ if (PeekNextChar (id, index) == '`')
+ ParseTypeParameterSymbol (id, ref index, typeParameterContext, results);
+ else
+ ParseNamedTypeSymbol (id, ref index, typeParameterContext, results);
+
+ // apply any array or pointer constructions to results
+ var startIndex = index;
+ var endIndex = index;
+
+ for (int i = 0; i < results.Count; i++) {
+ index = startIndex;
+ var typeReference = results[i];
+
+ while (true) {
+ if (PeekNextChar (id, index) == '[') {
+ var boundsStartIndex = index;
+ var bounds = ParseArrayBounds (id, ref index);
+ var boundsEndIndex = index;
+ Debug.Assert (bounds > 0);
+ // Instead of constructing a representation of the array bounds, we
+ // use the original input to represent the bounds, and later match it
+ // against the generated strings for types in signatures.
+ // This ensures that we will only resolve members with supported array bounds.
+ typeReference += id.Substring (boundsStartIndex, boundsEndIndex - boundsStartIndex);
+ continue;
+ }
+
+ if (PeekNextChar (id, index) == '*') {
+ index++;
+ typeReference += '*';
+ continue;
+ }
+
+ break;
+ }
+
+ if (PeekNextChar (id, index) == '@') {
+ index++;
+ typeReference += '@';
+ }
+
+ results[i] = typeReference;
+ endIndex = index;
+ }
+
+ index = endIndex;
+ }
+
+ static void ParseTypeParameterSymbol (string id, ref int index, IGenericParameterProvider? typeParameterContext, List results)
+ {
+ // skip the first `
+ Debug.Assert (PeekNextChar (id, index) == '`');
+ index++;
+
+ Debug.Assert (
+ typeParameterContext == null ||
+ (typeParameterContext is MethodDefinition && typeParameterContext.GenericParameterType == GenericParameterType.Method) ||
+ (typeParameterContext is TypeDefinition && typeParameterContext.GenericParameterType == GenericParameterType.Type)
+ );
+
+ if (PeekNextChar (id, index) == '`') {
+ // `` means this is a method type parameter
+ index++;
+ var methodTypeParameterIndex = ReadNextInteger (id, ref index);
+
+ if (typeParameterContext is MethodDefinition methodContext) {
+ var count = methodContext.HasGenericParameters ? methodContext.GenericParameters.Count : 0;
+ if (count > 0 && methodTypeParameterIndex < count) {
+ results.Add ("``" + methodTypeParameterIndex);
+ }
+ }
+ } else {
+ // regular type parameter
+ var typeParameterIndex = ReadNextInteger (id, ref index);
+
+ var typeContext = typeParameterContext is MethodDefinition methodContext
+ ? methodContext.DeclaringType
+ : typeParameterContext as TypeDefinition;
+
+ if (typeParameterIndex >= 0 ||
+ typeParameterIndex < typeContext?.GenericParameters.Count) {
+ // No need to look at declaring types like Roslyn, because type parameters are redeclared.
+ results.Add ("`" + typeParameterIndex);
+ }
+ }
+ }
+
+ static void ParseNamedTypeSymbol (string id, ref int index, IGenericParameterProvider? typeParameterContext, List results)
+ {
+ Debug.Assert (results.Count == 0);
+ var nameBuilder = new StringBuilder ();
+ // loop for dotted names
+ while (true) {
+ var name = ParseName (id, ref index);
+ if (String.IsNullOrEmpty (name))
+ return;
+
+ nameBuilder.Append (name);
+
+ List? typeArguments = null;
+ int arity = 0;
+
+ // type arguments
+ if (PeekNextChar (id, index) == '{') {
+ typeArguments = new List ();
+ if (!ParseTypeArguments (id, ref index, typeParameterContext, typeArguments)) {
+ continue;
+ }
+
+ arity = typeArguments.Count;
+ }
+
+ if (arity != 0) {
+ Debug.Assert (typeArguments != null && typeArguments.Count != 0);
+ nameBuilder.Append ('{');
+ bool needsComma = false;
+ foreach (var typeArg in typeArguments) {
+ if (needsComma) {
+ nameBuilder.Append (',');
+ }
+ nameBuilder.Append (typeArg);
+ needsComma = true;
+ }
+ nameBuilder.Append ('}');
+ }
+
+ if (PeekNextChar (id, index) != '.')
+ break;
+
+ index++;
+ nameBuilder.Append ('.');
+ }
+
+ results.Add (nameBuilder.ToString ());
+ }
+
+ static int ParseArrayBounds (string id, ref int index)
+ {
+ index++; // skip '['
+
+ int bounds = 0;
+
+ while (true) {
+ // note: the actual bounds are ignored.
+ // C# only supports arrays with lower bound zero.
+ // size is not known.
+
+ if (char.IsDigit (PeekNextChar (id, index)))
+ ReadNextInteger (id, ref index);
+
+ if (PeekNextChar (id, index) == ':') {
+ index++;
+
+ // note: the spec says that omitting both the lower bounds and the size
+ // should omit the ':' as well, but this allows for it in the input.
+ if (char.IsDigit (PeekNextChar (id, index)))
+ ReadNextInteger (id, ref index);
+ }
+
+ bounds++;
+
+ if (PeekNextChar (id, index) == ',') {
+ index++;
+ continue;
+ }
+
+ break;
+ }
+
+ // note: this allows leaving out the closing ']'
+ if (PeekNextChar (id, index) == ']')
+ index++;
+
+ return bounds;
+ }
+
+ static bool ParseTypeArguments (string id, ref int index, IGenericParameterProvider? typeParameterContext, List typeArguments)
+ {
+ index++; // skip over {
+
+ while (true) {
+ var type = ParseTypeSymbol (id, ref index, typeParameterContext);
+
+ if (type == null) {
+ // if a type argument cannot be identified, argument list is no good
+ return false;
+ }
+
+ // add first one
+ typeArguments.Add (type);
+
+ if (PeekNextChar (id, index) == ',') {
+ index++;
+ continue;
+ }
+
+ break;
+ }
+
+ // note: this doesn't require closing }
+ if (PeekNextChar (id, index) == '}') {
+ index++;
+ }
+
+ return true;
+ }
+
+ static void GetMatchingTypes (ModuleDefinition module, TypeDefinition? declaringType, string name, List results)
+ {
+ Debug.Assert (module != null);
+
+ if (declaringType == null) {
+ var type = module.GetType (name);
+ if (type != null) {
+ results.Add (type);
+ }
+ return;
+ }
+
+ if (!declaringType.HasNestedTypes)
+ return;
+
+ foreach (var nestedType in declaringType.NestedTypes) {
+ Debug.Assert (String.IsNullOrEmpty (nestedType.Namespace));
+ if (nestedType.Name != name)
+ continue;
+ results.Add (nestedType);
+ return;
+ }
+ }
+
+ static void GetMatchingMethods (string id, ref int index, TypeDefinition? type, string memberName, int arity, List results)
+ {
+ if (type == null)
+ return;
+
+ var parameters = new List ();
+ var startIndex = index;
+ var endIndex = index;
+
+ foreach (var method in type.Methods) {
+ index = startIndex;
+ if (method.Name != memberName)
+ continue;
+
+ var methodArity = method.HasGenericParameters ? method.GenericParameters.Count : 0;
+ if (methodArity != arity)
+ continue;
+
+ parameters.Clear ();
+ if (PeekNextChar (id, index) == '(') {
+ // if the parameters cannot be identified (some error), then the symbol cannot match, try next method symbol
+ if (!ParseParameterList (id, ref index, method, parameters))
+ continue;
+ }
+
+ // note: this allows extra characters at the end
+
+ if (!AllParametersMatch (method.Parameters, parameters))
+ continue;
+
+ if (PeekNextChar (id, index) == '~') {
+ index++;
+ string? returnType = ParseTypeSymbol (id, ref index, method);
+ if (returnType == null)
+ continue;
+
+ // if return type is specified, then it must match
+ if (GetSignaturePart (method.ReturnType) == returnType) {
+ results.Add (method);
+ endIndex = index;
+ }
+ } else {
+ // no return type specified, then any matches
+ results.Add (method);
+ endIndex = index;
+ }
+ }
+ index = endIndex;
+ }
+
+ static void GetMatchingProperties (string id, ref int index, TypeDefinition? type, string memberName, List results)
+ {
+ if (type == null)
+ return;
+
+ int startIndex = index;
+ int endIndex = index;
+
+ List? parameters = null;
+ // Unlike Roslyn, we don't need to decode property names because we are working
+ // directly with IL.
+ foreach (var property in type.Properties) {
+ index = startIndex;
+ if (property.Name != memberName)
+ continue;
+ if (PeekNextChar (id, index) == '(') {
+ if (parameters == null) {
+ parameters = new List ();
+ } else {
+ parameters.Clear ();
+ }
+
+ if (ParseParameterList (id, ref index, property.DeclaringType, parameters)
+ && AllParametersMatch (property.Parameters, parameters)) {
+ results.Add (property);
+ endIndex = index;
+ }
+ } else if (property.Parameters.Count == 0) {
+ results.Add (property);
+ endIndex = index;
+ }
+ }
+
+ index = endIndex;
+ }
+
+ static void GetMatchingFields (TypeDefinition? type, string memberName, List results)
+ {
+ if (type == null)
+ return;
+ foreach (var field in type.Fields) {
+ if (field.Name != memberName)
+ continue;
+ results.Add (field);
+ }
+ }
+
+ static void GetMatchingEvents (TypeDefinition? type, string memberName, List results)
+ {
+ if (type == null)
+ return;
+ foreach (var evt in type.Events) {
+ if (evt.Name != memberName)
+ continue;
+ results.Add (evt);
+ }
+ }
+
+ static bool AllParametersMatch (Collection methodParameters, List expectedParameters)
+ {
+ if (methodParameters.Count != expectedParameters.Count)
+ return false;
+
+ for (int i = 0; i < expectedParameters.Count; i++) {
+ if (GetSignaturePart (methodParameters[i].ParameterType) != expectedParameters[i])
+ return false;
+ }
+
+ return true;
+ }
+
+ static bool ParseParameterList (string id, ref int index, IGenericParameterProvider typeParameterContext, List parameters)
+ {
+ System.Diagnostics.Debug.Assert (typeParameterContext != null);
+
+ index++; // skip over '('
+
+ if (PeekNextChar (id, index) == ')') {
+ // note: this will match parameterless methods, or methods with only varargs parameters
+ index++;
+ return true;
+ }
+
+ string? parameter = ParseTypeSymbol (id, ref index, typeParameterContext);
+ if (parameter == null)
+ return false;
+
+ parameters.Add (parameter);
+
+ while (PeekNextChar (id, index) == ',') {
+ index++;
+
+ parameter = ParseTypeSymbol (id, ref index, typeParameterContext);
+ if (parameter == null)
+ return false;
+
+ parameters.Add (parameter);
+ }
+
+ // note: this doesn't require the trailing ')'
+ if (PeekNextChar (id, index) == ')') {
+ index++;
+ }
+
+ return true;
+ }
+
+ static char PeekNextChar (string id, int index)
+ {
+ return index >= id.Length ? '\0' : id[index];
+ }
+
+ static readonly char[] s_nameDelimiters = { ':', '.', '(', ')', '{', '}', '[', ']', ',', '\'', '@', '*', '`', '~' };
+
+ static string ParseName (string id, ref int index)
+ {
+ string name;
+
+ int delimiterOffset = id.IndexOfAny (s_nameDelimiters, index);
+ if (delimiterOffset >= 0) {
+ name = id.Substring (index, delimiterOffset - index);
+ index = delimiterOffset;
+ } else {
+ name = id.Substring (index);
+ index = id.Length;
+ }
+
+ return DecodeName (name);
+ }
+
+ // undoes dot encodings within names...
+ static string DecodeName (string name)
+ {
+ return name.Replace ('#', '.');
+ }
+
+ static int ReadNextInteger (string id, ref int index)
+ {
+ int n = 0;
+
+ // note: this can overflow
+ while (index < id.Length && char.IsDigit (id[index])) {
+ n = n * 10 + (id[index] - '0');
+ index++;
+ }
+
+ return n;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/linker/Linker/TypeReferenceExtensions.cs b/src/linker/Linker/TypeReferenceExtensions.cs
index 6910de04c0f0..8498d788969d 100644
--- a/src/linker/Linker/TypeReferenceExtensions.cs
+++ b/src/linker/Linker/TypeReferenceExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using System.Text;
using Mono.Cecil;
using System.Collections.Generic;
using System.Linq;
@@ -36,6 +37,42 @@ public static TypeReference GetInflatedBaseType (this TypeReference type)
return type.Resolve ()?.BaseType;
}
+ public static TypeReference GetInflatedDeclaringType (this TypeReference type)
+ {
+ if (type == null)
+ return null;
+
+ if (type.IsGenericParameter || type.IsByReference || type.IsPointer)
+ return null;
+
+ if (type is SentinelType sentinelType)
+ return sentinelType.ElementType.GetInflatedDeclaringType ();
+
+ if (type is PinnedType pinnedType)
+ return pinnedType.ElementType.GetInflatedDeclaringType ();
+
+ if (type is RequiredModifierType requiredModifierType)
+ return requiredModifierType.ElementType.GetInflatedDeclaringType ();
+
+ if (type is GenericInstanceType genericInstance) {
+ var declaringType = genericInstance.DeclaringType;
+
+ if (declaringType.HasGenericParameters) {
+ var result = new GenericInstanceType (declaringType);
+ for (var i = 0; i < declaringType.GenericParameters.Count; ++i)
+ result.GenericArguments.Add (genericInstance.GenericArguments[i]);
+
+ return result;
+ }
+
+ return declaringType;
+ }
+
+ var resolved = type.Resolve ();
+ System.Diagnostics.Debug.Assert (resolved == type);
+ return resolved?.DeclaringType;
+ }
+
public static IEnumerable GetInflatedInterfaces (this TypeReference typeRef)
{
var typeDef = typeRef.Resolve ();
diff --git a/test/Mono.Linker.Tests.Cases.Expectations/Assertions/BaseMemberAssertionAttribute.cs b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/BaseMemberAssertionAttribute.cs
new file mode 100644
index 000000000000..563ebfb8f611
--- /dev/null
+++ b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/BaseMemberAssertionAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Mono.Linker.Tests.Cases.Expectations.Assertions
+{
+ /// A base class for attributes that make assertions about a particular member.
+ // The test infrastructure is expected to check the assertion on the member to which
+ // the attribute is applied.
+ [AttributeUsage (AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Delegate, AllowMultiple = true)]
+ public abstract class BaseMemberAssertionAttribute : Attribute
+ {
+ }
+}
\ No newline at end of file
diff --git a/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectExactlyResolvedDocumentationSignatureAttribute.cs b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectExactlyResolvedDocumentationSignatureAttribute.cs
new file mode 100644
index 000000000000..19d582509dc6
--- /dev/null
+++ b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectExactlyResolvedDocumentationSignatureAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Mono.Linker.Tests.Cases.Expectations.Assertions
+{
+ /// Asserts that the given documentation signature string resolves to the
+ // member with this attribute, and only that member.
+ public class ExpectExactlyResolvedDocumentationSignatureAttribute : BaseMemberAssertionAttribute
+ {
+ public ExpectExactlyResolvedDocumentationSignatureAttribute (string input)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectGeneratedDocumentationSignatureAttribute.cs b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectGeneratedDocumentationSignatureAttribute.cs
new file mode 100644
index 000000000000..2a4402199557
--- /dev/null
+++ b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectGeneratedDocumentationSignatureAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Mono.Linker.Tests.Cases.Expectations.Assertions
+{
+ /// Asserts that the member to which this attribute is applied has the given
+ /// documentation signature.
+ public class ExpectGeneratedDocumentationSignatureAttribute : BaseMemberAssertionAttribute
+ {
+ public ExpectGeneratedDocumentationSignatureAttribute (string expected)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectResolvedDocumentationSignatureAttribute.cs b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectResolvedDocumentationSignatureAttribute.cs
new file mode 100644
index 000000000000..da9bfde4c5bc
--- /dev/null
+++ b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectResolvedDocumentationSignatureAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Mono.Linker.Tests.Cases.Expectations.Assertions
+{
+ /// Asserts that the given documentation signature string resolves to the
+ // member with this attribute.
+ public class ExpectResolvedDocumentationSignatureAttribute : BaseMemberAssertionAttribute
+ {
+ public ExpectResolvedDocumentationSignatureAttribute (string input)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectUnresolvedDocumentationSignatureAttribute.cs b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectUnresolvedDocumentationSignatureAttribute.cs
new file mode 100644
index 000000000000..9c71567ac5b3
--- /dev/null
+++ b/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectUnresolvedDocumentationSignatureAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Mono.Linker.Tests.Cases.Expectations.Assertions
+{
+ /// Asserts that the given documentation signature string does not resolve
+ /// to the member with this attribute.
+ public class ExpectUnresolvedDocumentationSignatureAttribute : BaseMemberAssertionAttribute
+ {
+ public ExpectUnresolvedDocumentationSignatureAttribute (string expected)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj b/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj
index 8255852b387f..be4a9a3e7283 100644
--- a/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj
+++ b/test/Mono.Linker.Tests/Mono.Linker.Tests.csproj
@@ -3,6 +3,7 @@
latest
+ true
diff --git a/test/Mono.Linker.Tests/Tests/DocumentationSignatureParserTests.cs b/test/Mono.Linker.Tests/Tests/DocumentationSignatureParserTests.cs
new file mode 100644
index 000000000000..2745cd63831b
--- /dev/null
+++ b/test/Mono.Linker.Tests/Tests/DocumentationSignatureParserTests.cs
@@ -0,0 +1,610 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using NUnit.Framework;
+using Mono.Cecil;
+using Mono.Linker.Tests.Cases.Expectations.Assertions;
+using Mono.Linker.Tests.Extensions;
+
+namespace Mono.Linker.Tests
+{
+ [TestFixture]
+ public class DocumentationSignatureParserTests
+ {
+ [TestCaseSource (nameof (GetMemberAssertionsAsArray), new object[] { typeof (DocumentationSignatureParserTests) })]
+ public void TestSignatureParsing (IMemberDefinition member, CustomAttribute customAttribute)
+ {
+ var attributeString = (string) customAttribute.ConstructorArguments[0].Value;
+ switch (customAttribute.AttributeType.Name) {
+ case nameof (ExpectExactlyResolvedDocumentationSignatureAttribute):
+ CheckUniqueParsedString (member, attributeString);
+ break;
+ case nameof (ExpectGeneratedDocumentationSignatureAttribute):
+ CheckGeneratedString (member, attributeString);
+ break;
+ case nameof (ExpectResolvedDocumentationSignatureAttribute):
+ CheckParsedString (member, attributeString);
+ break;
+ case nameof (ExpectUnresolvedDocumentationSignatureAttribute):
+ CheckUnresolvedDocumentationSignature (member, attributeString);
+ break;
+ default:
+ throw new NotImplementedException ();
+ }
+ }
+
+ public static void CheckUniqueParsedString (IMemberDefinition member, string input)
+ {
+ var module = (member as TypeDefinition)?.Module ?? member.DeclaringType?.Module;
+ Assert.NotNull (module);
+ var parseResults = DocumentationSignatureParser.GetMembersForDocumentationSignature (input, module);
+ Assert.AreEqual (1, parseResults.Count ());
+ Assert.AreEqual (member, parseResults.First ());
+ }
+
+ public static void CheckGeneratedString (IMemberDefinition member, string expected)
+ {
+ var generator = DocumentationSignatureGenerator.Instance;
+ var builder = new StringBuilder ();
+ switch (member) {
+ case TypeDefinition type:
+ generator.VisitTypeDefinition (type, builder);
+ break;
+ case MethodDefinition method:
+ generator.VisitMethod (method, builder);
+ break;
+ case FieldDefinition field:
+ generator.VisitField (field, builder);
+ break;
+ case PropertyDefinition property:
+ generator.VisitProperty (property, builder);
+ break;
+ case EventDefinition evt:
+ generator.VisitEvent (evt, builder);
+ break;
+ default:
+ throw new NotImplementedException ();
+ }
+ Assert.AreEqual (expected, builder.ToString ());
+ }
+
+ public static void CheckParsedString (IMemberDefinition member, string input)
+ {
+ var module = (member as TypeDefinition)?.Module ?? member.DeclaringType?.Module;
+ Assert.NotNull (module);
+ var parseResults = DocumentationSignatureParser.GetMembersForDocumentationSignature (input, module);
+ CollectionAssert.Contains (parseResults, member);
+ }
+
+ public static void CheckUnresolvedDocumentationSignature (IMemberDefinition member, string input)
+ {
+ var module = (member as TypeDefinition)?.Module ?? member.DeclaringType?.Module;
+ Assert.NotNull (module);
+ var parseResults = DocumentationSignatureParser.GetMembersForDocumentationSignature (input, module);
+ CollectionAssert.DoesNotContain (parseResults, member);
+ }
+
+ static IEnumerable<(IMemberDefinition member, CustomAttribute ca)> GetMemberAssertions (Type type)
+ {
+ var resolver = new DefaultAssemblyResolver ();
+ resolver.AddSearchDirectory (Path.GetDirectoryName (type.Assembly.Location));
+ var assembly = resolver.Resolve (new AssemblyNameReference (type.Assembly.GetName ().Name, null));
+ var t = assembly.MainModule.GetType (type.Namespace + "." + type.Name);
+ if (t == null)
+ throw new InvalidOperationException ($"type {type} not found in {assembly}");
+ var results = new List<(IMemberDefinition, CustomAttribute)> ();
+ CollectMemberAssertions (t, results);
+ return results;
+ }
+
+ private static bool IsMemberAssertion (TypeReference attributeType)
+ {
+ if (attributeType == null)
+ return false;
+
+ if (attributeType.Namespace != "Mono.Linker.Tests.Cases.Expectations.Assertions")
+ return false;
+
+ return attributeType.Resolve ().DerivesFrom (nameof (BaseMemberAssertionAttribute));
+ }
+
+ private static void CollectMemberAssertions (TypeDefinition type, List<(IMemberDefinition, CustomAttribute)> results)
+ {
+ if (type.HasCustomAttributes) {
+ foreach (var ca in type.CustomAttributes) {
+ if (!IsMemberAssertion (ca.AttributeType))
+ continue;
+ results.Add ((type, ca));
+ }
+ }
+
+ foreach (var m in type.Methods) {
+ if (!m.HasCustomAttributes)
+ continue;
+
+ foreach (var ca in m.CustomAttributes) {
+ if (!IsMemberAssertion (ca.AttributeType))
+ continue;
+ results.Add ((m, ca));
+ }
+ }
+
+ foreach (var f in type.Fields) {
+ if (!f.HasCustomAttributes)
+ continue;
+
+ foreach (var ca in f.CustomAttributes) {
+ if (!IsMemberAssertion (ca.AttributeType))
+ continue;
+ results.Add ((f, ca));
+ }
+ }
+
+ foreach (var p in type.Properties) {
+ if (!p.HasCustomAttributes)
+ continue;
+
+ foreach (var ca in p.CustomAttributes) {
+ if (!IsMemberAssertion (ca.AttributeType))
+ continue;
+ results.Add ((p, ca));
+ }
+ }
+
+ foreach (var e in type.Events) {
+ if (!e.HasCustomAttributes)
+ continue;
+
+ foreach (var ca in e.CustomAttributes) {
+ if (!IsMemberAssertion (ca.AttributeType))
+ continue;
+ results.Add ((e, ca));
+ }
+ }
+
+ if (!type.HasNestedTypes)
+ return;
+
+ foreach (var nested in type.NestedTypes) {
+ CollectMemberAssertions (nested, results);
+ }
+ }
+
+ static IEnumerable