File: Linker.Steps\LinkAttributesParser.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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.XPath;
using ILLink.Shared;
using ILLink.Shared.TrimAnalysis;
using ILLink.Shared.TypeSystemProxy;
using Mono.Cecil;
 
namespace Mono.Linker.Steps
{
	public class LinkAttributesParser : ProcessLinkerXmlBase
	{
		AttributeInfo? _attributeInfo;
 
		public LinkAttributesParser (LinkContext context, Stream documentStream, string xmlDocumentLocation)
			: base (context, documentStream, xmlDocumentLocation)
		{
		}
 
		public LinkAttributesParser (LinkContext context, Stream documentStream, EmbeddedResource resource, AssemblyDefinition resourceAssembly, string xmlDocumentLocation = "<unspecified>")
			: base (context, documentStream, resource, resourceAssembly, xmlDocumentLocation)
		{
		}
 
		public void Parse (AttributeInfo xmlInfo)
		{
			_attributeInfo = xmlInfo;
			bool stripLinkAttributes = _context.IsOptimizationEnabled (CodeOptimizations.RemoveLinkAttributes, _resource?.Assembly);
			ProcessXml (stripLinkAttributes, _context.IgnoreLinkAttributes);
		}
 
		static bool IsRemoveAttributeInstances (string attributeName) => attributeName == "RemoveAttributeInstances" || attributeName == "RemoveAttributeInstancesAttribute";
 
		(CustomAttribute[]? customAttributes, MessageOrigin[]? origins) ProcessAttributes (XPathNavigator nav, ICustomAttributeProvider provider)
		{
			ArrayBuilder<CustomAttribute> customAttributesBuilder = default;
			ArrayBuilder<MessageOrigin> originsBuilder = default;
			foreach (XPathNavigator attributeNav in nav.SelectChildren ("attribute", string.Empty)) {
				if (!ShouldProcessElement (attributeNav))
					continue;
 
				TypeDefinition? attributeType;
				string internalAttribute = GetAttribute (attributeNav, "internal");
				if (!string.IsNullOrEmpty (internalAttribute)) {
					if (!IsRemoveAttributeInstances (internalAttribute)) {
						LogWarning (attributeNav, DiagnosticId.UnrecognizedInternalAttribute, internalAttribute);
						continue;
					}
					if (provider is not TypeDefinition) {
						LogWarning (attributeNav, DiagnosticId.XmlRemoveAttributeInstancesCanOnlyBeUsedOnType, nameof (RemoveAttributeInstancesAttribute));
						continue;
					}
 
					attributeType = GenerateRemoveAttributeInstancesAttribute ();
					if (attributeType == null)
						continue;
				} else {
					string attributeFullName = GetFullName (attributeNav);
					if (string.IsNullOrEmpty (attributeFullName)) {
						LogWarning (attributeNav, DiagnosticId.XmlElementDoesNotContainRequiredAttributeFullname);
						continue;
					}
 
					if (!GetAttributeType (attributeNav, attributeFullName, out attributeType))
						continue;
				}
 
				CustomAttribute? customAttribute = CreateCustomAttribute (attributeNav, attributeType, provider);
				if (customAttribute != null) {
					_context.LogMessage ($"Assigning external custom attribute '{FormatCustomAttribute (customAttribute)}' instance to '{provider}'.");
					customAttributesBuilder.Add (customAttribute);
					originsBuilder.Add (GetMessageOriginForPosition (attributeNav));
				}
			}
 
			return (customAttributesBuilder.ToArray (), originsBuilder.ToArray ());
 
			static string FormatCustomAttribute (CustomAttribute ca)
			{
				StringBuilder sb = new StringBuilder ();
				sb.Append (ca.Constructor.GetDisplayName ());
				sb.Append (" { args: ");
				for (int i = 0; i < ca.ConstructorArguments.Count; ++i) {
					if (i > 0)
						sb.Append (", ");
 
					var caa = ca.ConstructorArguments[i];
					sb.Append ($"{caa.Type.GetDisplayName ()} {caa.Value}");
				}
				sb.Append (" }");
 
				return sb.ToString ();
			}
		}
		TypeDefinition? GenerateRemoveAttributeInstancesAttribute ()
		{
			TypeDefinition? td = null;
 
			if (_context.MarkedKnownMembers.RemoveAttributeInstancesAttributeDefinition is TypeDefinition knownTypeDef) {
				return knownTypeDef;
			}
 
			var voidType = BCL.FindPredefinedType (WellKnownType.System_Void, _context);
			if (voidType == null)
				return null;
 
			var attributeType = BCL.FindPredefinedType (WellKnownType.System_Attribute, _context);
			if (attributeType == null)
				return null;
 
			var objectType = BCL.FindPredefinedType (WellKnownType.System_Object, _context);
			if (objectType == null)
				return null;
			var objectArrayType = new ArrayType (objectType);
			if (objectArrayType == null)
				return null;
 
			//
			// Generates metadata information for internal type
			//
			// public sealed class RemoveAttributeInstancesAttribute : Attribute
			// {
			//  public RemoveAttributeInstancesAttribute () {}
			//  public RemoveAttributeInstancesAttribute (object values) {} // For legacy uses
			//  public RemoveAttributeInstancesAttribute (params object[] values) {}
			// }
			//
			const MethodAttributes ctorAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.Final;
 
			td = new TypeDefinition ("", nameof (RemoveAttributeInstancesAttribute), TypeAttributes.Public);
			td.BaseType = attributeType;
 
			var ctor = new MethodDefinition (".ctor", ctorAttributes, voidType);
			td.Methods.Add (ctor);
			var ctor1 = new MethodDefinition (".ctor", ctorAttributes, voidType);
			var param = new ParameterDefinition (objectType);
			td.Methods.Add (ctor1);
 
			var ctorN = new MethodDefinition (".ctor", ctorAttributes, voidType);
			var paramN = new ParameterDefinition (objectArrayType);
#pragma warning disable RS0030 // MethodReference.Parameters is banned. It's necessary to build the method definition here, though.
			ctorN.Parameters.Add (paramN);
#pragma warning restore RS0030
			td.Methods.Add (ctorN);
 
			return _context.MarkedKnownMembers.RemoveAttributeInstancesAttributeDefinition = td;
		}
 
		CustomAttribute? CreateCustomAttribute (XPathNavigator nav, TypeDefinition attributeType, ICustomAttributeProvider provider)
		{
			CustomAttributeArgument[] arguments = ReadCustomAttributeArguments (nav, provider);
 
			MethodDefinition? constructor = FindBestMatchingConstructor (attributeType, arguments);
			if (constructor == null) {
				LogWarning (nav, DiagnosticId.XmlCouldNotFindMatchingConstructorForCustomAttribute, attributeType.GetDisplayName ());
				return null;
			}
 
			CustomAttribute customAttribute = new CustomAttribute (constructor);
			foreach (var argument in arguments)
				customAttribute.ConstructorArguments.Add (argument);
 
			ReadCustomAttributeProperties (nav, attributeType, customAttribute);
 
			return customAttribute;
		}
 
		MethodDefinition? FindBestMatchingConstructor (TypeDefinition attributeType, CustomAttributeArgument[] args)
		{
			var methods = attributeType.Methods;
			for (int i = 0; i < attributeType.Methods.Count; ++i) {
				var method = methods[i];
				if (!method.IsInstanceConstructor ())
					continue;
 
				if (args.Length != method.GetMetadataParametersCount ())
					continue;
 
				bool match = true;
				foreach (var p in method.GetMetadataParameters ()) {
					//
					// No candidates betterness, only exact matches are supported
					//
					var parameterType = _context.TryResolve (p.ParameterType);
					if (parameterType == null || parameterType != _context.TryResolve (args[p.MetadataIndex].Type))
						match = false;
				}
 
				if (match)
					return method;
			}
 
			return null;
		}
 
		void ReadCustomAttributeProperties (XPathNavigator nav, TypeDefinition attributeType, CustomAttribute customAttribute)
		{
			foreach (XPathNavigator propertyNav in nav.SelectChildren ("property", string.Empty)) {
				string propertyName = GetName (propertyNav);
				if (string.IsNullOrEmpty (propertyName)) {
					LogWarning (propertyNav, DiagnosticId.XmlPropertyDoesNotContainAttributeName);
					continue;
				}
 
				PropertyDefinition? property = attributeType.Properties.Where (prop => prop.Name == propertyName).FirstOrDefault ();
				if (property == null) {
					LogWarning (propertyNav, DiagnosticId.XmlCouldNotFindProperty, propertyName);
					continue;
				}
 
				var caa = ReadCustomAttributeArgument (propertyNav, property);
				if (caa is null)
					continue;
 
				customAttribute.Properties.Add (new CustomAttributeNamedArgument (property.Name, caa.Value));
			}
		}
 
		CustomAttributeArgument[] ReadCustomAttributeArguments (XPathNavigator nav, ICustomAttributeProvider provider)
		{
			ArrayBuilder<CustomAttributeArgument> args = default;
 
			foreach (XPathNavigator argumentNav in nav.SelectChildren ("argument", string.Empty)) {
				CustomAttributeArgument? caa = ReadCustomAttributeArgument (argumentNav, provider);
				if (caa is not null)
					args.Add (caa.Value);
			}
 
			return args.ToArray () ?? Array.Empty<CustomAttributeArgument> ();
		}
 
		CustomAttributeArgument? ReadCustomAttributeArgument (XPathNavigator nav, ICustomAttributeProvider provider)
		{
			TypeReference? typeref = ResolveArgumentType (nav, provider);
			if (typeref is null)
				return null;
 
			string svalue = nav.Value;
 
			//
			// Builds CustomAttributeArgument in the same way as it would be
			// represented in the metadata if encoded there. This simplifies
			// any custom attributes handling in ILLink by using same attributes
			// value extraction or mathing logic.
			//
			switch (typeref.MetadataType) {
			case MetadataType.Object:
				var argumentIterator = nav.SelectChildren ("argument", string.Empty);
				if (argumentIterator?.MoveNext () != true) {
					_context.LogError (null, DiagnosticId.CustomAttributeArgumentForTypeRequiresNestedNode, "System.Object", "argument");
					return null;
				}
 
				var typedef = _context.TryResolve (typeref);
				if (typedef == null)
					return null;
 
				var boxedValue = ReadCustomAttributeArgument (argumentIterator.Current!, typedef);
				if (boxedValue is null)
					return null;
 
				return new CustomAttributeArgument (typeref, boxedValue);
 
			case MetadataType.Char:
			case MetadataType.Byte:
			case MetadataType.SByte:
			case MetadataType.Int16:
			case MetadataType.UInt16:
			case MetadataType.Int32:
			case MetadataType.UInt32:
			case MetadataType.UInt64:
			case MetadataType.Int64:
			case MetadataType.String:
				return new CustomAttributeArgument (typeref, ConvertStringValue (svalue, typeref));
 
			case MetadataType.ValueType:
				var enumType = _context.Resolve (typeref);
				if (enumType?.IsEnum != true)
					goto default;
 
				var enumField = enumType.Fields.Where (f => f.IsStatic && f.Name == svalue).FirstOrDefault ();
				object evalue = enumField?.Constant ?? svalue;
 
				typeref = enumType.GetEnumUnderlyingType ();
				return new CustomAttributeArgument (enumType, ConvertStringValue (evalue, typeref));
 
			case MetadataType.Class:
				if (!typeref.IsTypeOf (WellKnownType.System_Type))
					goto default;
 
				var diagnosticContext = new DiagnosticContext (new MessageOrigin (provider), diagnosticsEnabled: true, _context);
				if (!_context.TypeNameResolver.TryResolveTypeName (svalue, diagnosticContext, out TypeReference? type, out _, needsAssemblyName: false)) {
					_context.LogError (GetMessageOriginForPosition (nav), DiagnosticId.CouldNotResolveCustomAttributeTypeValue, svalue);
					return null;
				}
 
				return new CustomAttributeArgument (typeref, type);
			case MetadataType.Array:
				if (typeref is ArrayType arrayTypeRef) {
					var elementType = arrayTypeRef.ElementType;
					var arrayArgumentIterator = nav.SelectChildren ("argument", string.Empty);
					ArrayBuilder<CustomAttributeArgument> elements = default;
					foreach (XPathNavigator elementNav in arrayArgumentIterator) {
						if (ReadCustomAttributeArgument (elementNav, provider) is CustomAttributeArgument arg) {
							// To match Cecil, elements of a list that are subclasses of the list type must be boxed in the base type
							// e.g. object[] { 73 } translates to Cecil.CAA { Type: object[] : Value: CAA{ Type: object, Value: CAA{ Type: int, Value: 73} } }
							if (arg.Type == elementType) {
								elements.Add (arg);
							}
							// This check allows the xml to be less verbose by allowing subtypes to not be boxed in the Array's element type
							// e.g. here string doesn't need to be boxed in an "object" argument
							// <argument type="System.Object[]">
							//   <argument type="System.String">hello</argument>
							// </argument>
							//
							else if (arg.Type.IsSubclassOf (elementType.Namespace, elementType.Name, _context)) {
								elements.Add (new CustomAttributeArgument (elementType, arg));
							} else {
								_context.LogError (GetMessageOriginForPosition (nav), DiagnosticId.UnexpectedAttributeArgumentType, typeref.GetDisplayName ());
							}
						} else {
							return null;
						}
					}
					return new CustomAttributeArgument (arrayTypeRef, elements.ToArray ());
				}
				goto default;
			default:
				// No support for null, consider adding - dotnet/linker/issues/1957
				_context.LogError (GetMessageOriginForPosition (nav), DiagnosticId.UnexpectedAttributeArgumentType, typeref.GetDisplayName ());
				return null;
			}
 
			TypeReference? ResolveArgumentType (XPathNavigator nav, ICustomAttributeProvider provider)
			{
				string typeName = GetAttribute (nav, "type");
				if (string.IsNullOrEmpty (typeName))
					typeName = "System.String";
 
				var diagnosticContext = new DiagnosticContext (new MessageOrigin (provider), diagnosticsEnabled: true, _context);
				if (!_context.TypeNameResolver.TryResolveTypeName (typeName, diagnosticContext, out TypeReference? typeref, out _, needsAssemblyName: false)) {
					_context.LogError (GetMessageOriginForPosition (nav), DiagnosticId.TypeUsedWithAttributeValueCouldNotBeFound, typeName, nav.Value);
					return null;
				}
 
				return typeref;
			}
		}
 
		object? ConvertStringValue (object value, TypeReference targetType)
		{
			TypeCode typeCode;
			switch (targetType.MetadataType) {
			case MetadataType.String:
				typeCode = TypeCode.String;
				break;
			case MetadataType.Char:
				typeCode = TypeCode.Char;
				break;
			case MetadataType.Byte:
				typeCode = TypeCode.Byte;
				break;
			case MetadataType.SByte:
				typeCode = TypeCode.SByte;
				break;
			case MetadataType.Int16:
				typeCode = TypeCode.Int16;
				break;
			case MetadataType.UInt16:
				typeCode = TypeCode.UInt16;
				break;
			case MetadataType.Int32:
				typeCode = TypeCode.Int32;
				break;
			case MetadataType.UInt32:
				typeCode = TypeCode.UInt32;
				break;
			case MetadataType.UInt64:
				typeCode = TypeCode.UInt64;
				break;
			case MetadataType.Int64:
				typeCode = TypeCode.Int64;
				break;
			case MetadataType.Boolean:
				typeCode = TypeCode.Boolean;
				break;
			case MetadataType.Single:
				typeCode = TypeCode.Single;
				break;
			case MetadataType.Double:
				typeCode = TypeCode.Double;
				break;
			default:
				throw new NotSupportedException (targetType.ToString ());
			}
 
			try {
				return Convert.ChangeType (value, typeCode);
			} catch {
				_context.LogError (null, DiagnosticId.CannotConverValueToType, value.ToString () ?? "", targetType.GetDisplayName ());
				return null;
			}
		}
 
		bool GetAttributeType (XPathNavigator nav, string attributeFullName, [NotNullWhen (true)] out TypeDefinition? attributeType)
		{
			string assemblyName = GetAttribute (nav, "assembly");
			if (string.IsNullOrEmpty (assemblyName)) {
				attributeType = _context.GetType (attributeFullName);
			} else {
				AssemblyDefinition? assembly;
				try {
					assembly = _context.TryResolve (AssemblyNameReference.Parse (assemblyName));
					if (assembly == null) {
						LogWarning (nav, DiagnosticId.XmlCouldNotResolveAssemblyForAttribute, assemblyName, attributeFullName);
 
						attributeType = default;
						return false;
					}
				} catch (Exception) {
					LogWarning (nav, DiagnosticId.XmlCouldNotResolveAssemblyForAttribute, assemblyName, attributeFullName);
					attributeType = default;
					return false;
				}
 
				attributeType = _context.TryResolve (assembly, attributeFullName);
			}
 
			if (attributeType == null) {
				LogWarning (nav, DiagnosticId.XmlAttributeTypeCouldNotBeFound, attributeFullName);
				return false;
			}
 
			return true;
		}
 
		protected override AllowedAssemblies AllowedAssemblySelector {
			get {
				if (_resource?.Assembly == null)
					return AllowedAssemblies.AllAssemblies;
 
				// Corelib XML may contain assembly wildcard to support compiler-injected attribute types
				if (_resource?.Assembly.Name.Name == PlatformAssemblies.CoreLib)
					return AllowedAssemblies.AllAssemblies;
 
				return AllowedAssemblies.ContainingAssembly;
			}
		}
 
		protected override void ProcessAssembly (AssemblyDefinition assembly, XPathNavigator nav, bool warnOnUnresolvedTypes)
		{
			PopulateAttributeInfo (assembly, nav);
			ProcessTypes (assembly, nav, warnOnUnresolvedTypes);
		}
 
		protected override void ProcessType (TypeDefinition type, XPathNavigator nav)
		{
			Debug.Assert (ShouldProcessElement (nav));
 
			PopulateAttributeInfo (type, nav);
			ProcessTypeChildren (type, nav);
 
			if (!type.HasNestedTypes)
				return;
 
			foreach (XPathNavigator nestedTypeNav in nav.SelectChildren ("type", string.Empty)) {
				foreach (TypeDefinition nested in type.NestedTypes) {
					if (nested.Name == GetAttribute (nestedTypeNav, "name") && ShouldProcessElement (nestedTypeNav))
						ProcessType (nested, nestedTypeNav);
				}
			}
		}
 
		protected override void ProcessField (TypeDefinition type, FieldDefinition field, XPathNavigator nav)
		{
			PopulateAttributeInfo (field, nav);
		}
 
		protected override void ProcessMethod (TypeDefinition type, MethodDefinition method, XPathNavigator nav, object? customData)
		{
			PopulateAttributeInfo (method, nav);
			ProcessReturnParameters (method, nav);
			ProcessParameters (method, nav);
		}
 
		void ProcessParameters (MethodDefinition method, XPathNavigator nav)
		{
			Debug.Assert (_attributeInfo != null);
			foreach (XPathNavigator parameterNav in nav.SelectChildren ("parameter", string.Empty)) {
				var (attributes, origins) = ProcessAttributes (parameterNav, method);
				if (attributes != null && origins != null) {
					string paramName = GetAttribute (parameterNav, "name");
#pragma warning disable RS0030 // MethodReference.Parameters is banned. It's easiest to leave existing code as is
					foreach (ParameterDefinition parameter in method.Parameters) {
						if (paramName == parameter.Name) {
							if (parameter.HasCustomAttributes || _attributeInfo.CustomAttributes.ContainsKey (parameter))
								LogWarning (parameterNav, DiagnosticId.XmlMoreThanOneValueForParameterOfMethod, paramName, method.GetDisplayName ());
							_attributeInfo.AddCustomAttributes (parameter, attributes, origins);
							break;
						}
					}
#pragma warning restore RS0030
				}
			}
		}
 
		void ProcessReturnParameters (MethodDefinition method, XPathNavigator nav)
		{
			Debug.Assert (_attributeInfo != null);
			bool firstAppearance = true;
			foreach (XPathNavigator returnNav in nav.SelectChildren ("return", string.Empty)) {
				if (firstAppearance) {
					firstAppearance = false;
					var (attributes, origins) = ProcessAttributes (returnNav, method);
					if (attributes != null && origins != null) {
						_attributeInfo.AddCustomAttributes (method.MethodReturnType, attributes, origins);
					}
				} else {
					LogWarning (returnNav, DiagnosticId.XmlMoreThanOneReturnElementForMethod, method.GetDisplayName ());
				}
			}
		}
 
		protected override MethodDefinition? GetMethod (TypeDefinition type, string signature)
		{
			if (type.HasMethods)
				foreach (MethodDefinition method in type.Methods)
					if (signature.Replace (" ", "") == GetMethodSignature (method) || signature.Replace (" ", "") == GetMethodSignature (method, true))
						return method;
 
			return null;
		}
 
#pragma warning disable RS0030 // MethdReference.Parameters is banned. It's easiest to leave existing code as is.
		static string GetMethodSignature (MethodDefinition method, bool includeReturnType = false)
		{
			StringBuilder sb = new StringBuilder ();
			if (includeReturnType) {
				sb.Append (method.ReturnType.FullName);
			}
			sb.Append (method.Name);
			if (method.HasGenericParameters) {
				sb.Append ('<');
				for (int i = 0; i < method.GenericParameters.Count; i++) {
					if (i > 0)
						sb.Append (',');
 
					sb.Append (method.GenericParameters[i].Name);
				}
				sb.Append ('>');
			}
			sb.Append ('(');
			if (method.HasMetadataParameters ()) {
				for (int i = 0; i < method.Parameters.Count; i++) {
					if (i > 0)
						sb.Append (',');
 
					sb.Append (method.Parameters[i].ParameterType.FullName);
				}
			}
			sb.Append (')');
			return sb.ToString ();
		}
#pragma warning restore RS0030
 
		protected override void ProcessProperty (TypeDefinition type, PropertyDefinition property, XPathNavigator nav, object? customData, bool fromSignature)
		{
			PopulateAttributeInfo (property, nav);
		}
 
		protected override void ProcessEvent (TypeDefinition type, EventDefinition @event, XPathNavigator nav, object? customData)
		{
			PopulateAttributeInfo (@event, nav);
		}
 
		void PopulateAttributeInfo (ICustomAttributeProvider provider, XPathNavigator nav)
		{
			Debug.Assert (_attributeInfo != null);
			var (attributes, origins) = ProcessAttributes (nav, provider);
			if (attributes != null && origins != null)
				_attributeInfo.AddCustomAttributes (provider, attributes, origins);
		}
	}
}