File: Linker\DocumentationSignatureGenerator.PartVisitor.cs
Web Access
Project: src\src\tools\illink\src\linker\Mono.Linker.csproj (illink)
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Mono.Cecil;
 
namespace Mono.Linker
{
	internal sealed partial class DocumentationSignatureGenerator
	{
		/// <summary>
		///  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
		/// </summary>
		internal sealed class PartVisitor
		{
			internal static readonly PartVisitor Instance = new PartVisitor ();
 
			private PartVisitor ()
			{
			}
 
			public void VisitArrayType (ArrayType arrayType, StringBuilder builder, ITryResolveMetadata resolver)
			{
				VisitTypeReference (arrayType.ElementType, builder, resolver);
 
				// 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, ITryResolveMetadata resolver)
			{
				VisitTypeReference (field.DeclaringType, builder, resolver);
				builder.Append ('.').Append (field.Name);
			}
 
			private void VisitParameters (IEnumerable<ParameterDefinition> parameters, bool isVararg, StringBuilder builder, ITryResolveMetadata resolver)
			{
				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, resolver);
					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, ITryResolveMetadata resolver)
			{
				VisitTypeReference (method.DeclaringType, builder, resolver);
				builder.Append ('.').Append (GetEscapedMetadataName (method));
 
				if (method.HasGenericParameters)
					builder.Append ("``").Append (method.GenericParameters.Count);
 
				if (method.HasMetadataParameters () || (method.CallingConvention == MethodCallingConvention.VarArg))
#pragma warning disable RS0030 // MethodReference.Parameters is banned. This generates documentation signatures, so it's okay to use it here
					VisitParameters (method.Parameters, method.CallingConvention == MethodCallingConvention.VarArg, builder, resolver);
#pragma warning restore RS0030
 
				if (method.Name == "op_Implicit" || method.Name == "op_Explicit") {
					builder.Append ('~');
					VisitTypeReference (method.ReturnType, builder, resolver);
				}
			}
 
			public void VisitProperty (PropertyDefinition property, StringBuilder builder, ITryResolveMetadata resolver)
			{
				VisitTypeReference (property.DeclaringType, builder, resolver);
				builder.Append ('.').Append (GetEscapedMetadataName (property));
 
				if (property.Parameters.Count > 0)
					VisitParameters (property.Parameters, false, builder, resolver);
			}
 
			public void VisitEvent (EventDefinition evt, StringBuilder builder, ITryResolveMetadata resolver)
			{
				VisitTypeReference (evt.DeclaringType, builder, resolver);
				builder.Append ('.').Append (GetEscapedMetadataName (evt));
			}
 
			public static 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<T>.B<U>.M<V>(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, ITryResolveMetadata resolver)
			{
				switch (typeReference) {
				case ByReferenceType byReferenceType:
					VisitByReferenceType (byReferenceType, builder, resolver);
					return;
				case PointerType pointerType:
					VisitPointerType (pointerType, builder, resolver);
					return;
				case ArrayType arrayType:
					VisitArrayType (arrayType, builder, resolver);
					return;
				case GenericParameter genericParameter:
					VisitGenericParameter (genericParameter, builder);
					return;
				}
 
				if (typeReference.IsNested) {
					Debug.Assert (typeReference is not SentinelType && typeReference is not PinnedType);
					// GetInflatedDeclaringType may return null for generic parameters, byrefs, and pointers, but these
					// are separately handled above.
					VisitTypeReference (typeReference.GetInflatedDeclaringType (resolver)!, builder, resolver);
					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 is not GenericInstanceType genericInstance)
					return;
 
				// 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, resolver);
					needsComma = true;
				}
				builder.Append ('}');
			}
 
			public void VisitPointerType (PointerType pointerType, StringBuilder builder, ITryResolveMetadata resolver)
			{
				VisitTypeReference (pointerType.ElementType, builder, resolver);
				builder.Append ('*');
			}
 
			public void VisitByReferenceType (ByReferenceType byReferenceType, StringBuilder builder, ITryResolveMetadata resolver)
			{
				VisitTypeReference (byReferenceType.ElementType, builder, resolver);
				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 ('>', '}');
			}
		}
	}
}