File: Linker.Dataflow\HandleCallAction.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.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using ILLink.Shared.TypeSystemProxy;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Linker;
using Mono.Linker.Dataflow;
using Mono.Linker.Steps;
using MultiValue = ILLink.Shared.DataFlow.ValueSet<ILLink.Shared.DataFlow.SingleValue>;
 
namespace ILLink.Shared.TrimAnalysis
{
	internal partial struct HandleCallAction
	{
#pragma warning disable CA1822 // Mark members as static - the other partial implementations might need to be instance methods
 
		readonly LinkContext _context;
		readonly Instruction _operation;
		readonly MarkStep _markStep;
		readonly ReflectionMarker _reflectionMarker;
		readonly MethodDefinition _callingMethodDefinition;
 
		public HandleCallAction (
			LinkContext context,
			Instruction operation,
			MarkStep markStep,
			ReflectionMarker reflectionMarker,
			in DiagnosticContext diagnosticContext,
			MethodDefinition callingMethodDefinition)
		{
			_context = context;
			_operation = operation;
			_isNewObj = operation.OpCode == OpCodes.Newobj;
			_markStep = markStep;
			_reflectionMarker = reflectionMarker;
			_diagnosticContext = diagnosticContext;
			_callingMethodDefinition = callingMethodDefinition;
			_annotations = context.Annotations.FlowAnnotations;
			_requireDynamicallyAccessedMembersAction = new (context, reflectionMarker, diagnosticContext);
		}
 
		private partial bool TryHandleIntrinsic (
			MethodProxy calledMethod,
			MultiValue instanceValue,
			IReadOnlyList<MultiValue> argumentValues,
			IntrinsicId intrinsicId,
			out MultiValue? methodReturnValue)
		{
			MultiValue? maybeMethodReturnValue = methodReturnValue = null;
 
			switch (intrinsicId) {
			case IntrinsicId.None: {
					if (ReflectionMethodBodyScanner.IsPInvokeDangerous (calledMethod.Definition, _context, out bool comDangerousMethod)) {
						Debug.Assert (comDangerousMethod); // Currently COM dangerous is the only one we detect
						_diagnosticContext.AddDiagnostic (DiagnosticId.CorrectnessOfCOMCannotBeGuaranteed, calledMethod.GetDisplayName ());
					}
					if (_context.Annotations.DoesMethodRequireUnreferencedCode (calledMethod.Definition, out RequiresUnreferencedCodeAttribute? requiresUnreferencedCode))
						MarkStep.ReportRequiresUnreferencedCode (calledMethod.GetDisplayName (), requiresUnreferencedCode, _diagnosticContext);
 
					return TryHandleSharedIntrinsic (calledMethod, instanceValue, argumentValues, intrinsicId, out methodReturnValue);
				}
 
			case IntrinsicId.TypeDelegator_Ctor: {
					// This is an identity function for analysis purposes
					if (_operation.OpCode == OpCodes.Newobj)
						AddReturnValue (argumentValues[0]);
				}
				break;
 
			case IntrinsicId.Array_Empty: {
					AddReturnValue (ArrayValue.Create (0, ((GenericInstanceMethod) calledMethod.Method).GenericArguments[0]));
				}
				break;
 
			case IntrinsicId.Array_CreateInstance:
			case IntrinsicId.Enum_GetValues:
			case IntrinsicId.Marshal_SizeOf:
			case IntrinsicId.Marshal_OffsetOf:
			case IntrinsicId.Marshal_PtrToStructure:
			case IntrinsicId.Marshal_DestroyStructure:
			case IntrinsicId.Marshal_GetDelegateForFunctionPointer:
			case IntrinsicId.Assembly_get_Location:
			case IntrinsicId.Assembly_GetFile:
			case IntrinsicId.Assembly_GetFiles:
			case IntrinsicId.AssemblyName_get_CodeBase:
			case IntrinsicId.AssemblyName_get_EscapedCodeBase:
			case IntrinsicId.RuntimeReflectionExtensions_GetMethodInfo:
			case IntrinsicId.Delegate_get_Method:
				// These intrinsics are not interesting for trimmer (they are interesting for AOT and that's why they are recognized)
				break;
 
			//
			// System.Object
			//
			// GetType()
			//
			case IntrinsicId.Object_GetType: {
					if (instanceValue.IsEmpty ()) {
						AddReturnValue (MultiValueLattice.Top);
						break;
					}
 
					foreach (var valueNode in instanceValue.AsEnumerable ()) {
						// Note that valueNode can be statically typed in IL as some generic argument type.
						// For example:
						//   void Method<T>(T instance) { instance.GetType().... }
						// It could be that T is annotated with for example PublicMethods:
						//   void Method<[DAM(PublicMethods)] T>(T instance) { instance.GetType().GetMethod("Test"); }
						// In this case it's in theory possible to handle it, by treating the T basically as a base class
						// for the actual type of "instance". But the analysis for this would be pretty complicated (as the marking
						// has to happen on the callsite, which doesn't know that GetType() will be used...).
						// For now we're intentionally ignoring this case - it will produce a warning.
						// The counter example is:
						//   Method<Base>(new Derived);
						// In this case to get correct results, trimmer would have to mark all public methods on Derived. Which
						// currently it won't do.
 
						TypeReference? staticType = (valueNode as IValueWithStaticType)?.StaticType?.Type;
						if (staticType?.IsByReference == true)
							staticType = ((ByReferenceType) staticType).ElementType;
						TypeDefinition? staticTypeDef = staticType?.ResolveToTypeDefinition (_context);
						if (staticType is null || staticTypeDef is null) {
							DynamicallyAccessedMemberTypes annotation = default;
							if (staticType is GenericParameter genericParam && genericParam.HasConstraints) {
								foreach (var constraint in genericParam.Constraints) {
									if (constraint.ConstraintType.IsTypeOf ("System", "Enum"))
										annotation = DynamicallyAccessedMemberTypes.PublicFields;
								}
							}
 
							if (annotation != default) {
								AddReturnValue (_context.Annotations.FlowAnnotations.GetMethodReturnValue (calledMethod, _isNewObj, annotation));
							} else {
								// We don't know anything about the type GetType was called on. Track this as a usual result of a method call without any annotations
								AddReturnValue (_context.Annotations.FlowAnnotations.GetMethodReturnValue (calledMethod, _isNewObj));
							}
						} else if (staticTypeDef.IsSealed || staticTypeDef.IsTypeOf ("System", "Delegate") || staticTypeDef.IsTypeOf ("System", "Array")) {
							// We can treat this one the same as if it was a typeof() expression
 
							// We can allow Object.GetType to be modeled as System.Delegate because we keep all methods
							// on delegates anyway so reflection on something this approximation would miss is actually safe.
 
							// We can also treat all arrays as "sealed" since it's not legal to derive from Array type (even though it is not sealed itself)
 
							// We ignore the fact that the type can be annotated (see below for handling of annotated types)
							// This means the annotations (if any) won't be applied - instead we rely on the exact knowledge
							// of the type. So for example even if the type is annotated with PublicMethods
							// but the code calls GetProperties on it - it will work - mark properties, don't mark methods
							// since we ignored the fact that it's annotated.
							// This can be seen a little bit as a violation of the annotation, but we already have similar cases
							// where a parameter is annotated and if something in the method sets a specific known type to it
							// we will also make it just work, even if the annotation doesn't match the usage.
							AddReturnValue (new SystemTypeValue (new (staticType, _context)));
						} else if (staticTypeDef.IsTypeOf ("System", "Enum")) {
							AddReturnValue (_context.Annotations.FlowAnnotations.GetMethodReturnValue (calledMethod, _isNewObj, DynamicallyAccessedMemberTypes.PublicFields));
						} else {
							// Make sure the type is marked (this will mark it as used via reflection, which is sort of true)
							// This should already be true for most cases (method params, fields, ...), but just in case
							_reflectionMarker.MarkType (_diagnosticContext.Origin, staticType);
 
							var annotation = _markStep.DynamicallyAccessedMembersTypeHierarchy
								.ApplyDynamicallyAccessedMembersToTypeHierarchy (staticTypeDef);
 
							// Return a value which is "unknown type" with annotation. For now we'll use the return value node
							// for the method, which means we're loosing the information about which staticType this
							// started with. For now we don't need it, but we can add it later on.
							AddReturnValue (_context.Annotations.FlowAnnotations.GetMethodReturnValue (calledMethod, _isNewObj, annotation));
						}
					}
				}
				break;
 
			// Note about Activator.CreateInstance<T>
			// There are 2 interesting cases:
			//  - The generic argument for T is either specific type or annotated - in that case generic instantiation will handle this
			//    since from .NET 6+ the T is annotated with PublicParameterlessConstructor annotation, so the trimming tools would apply this as for any other method.
			//  - The generic argument for T is unannotated type - the generic instantiantion handling has a special case for handling PublicParameterlessConstructor requirement
			//    in such that if the generic argument type has the "new" constraint it will not warn (as it is effectively the same thing semantically).
			//    For all other cases, the trimming tools would have already produced a warning.
 
			default:
				return false;
			}
 
			methodReturnValue = maybeMethodReturnValue;
			return true;
 
			void AddReturnValue (MultiValue value)
			{
				maybeMethodReturnValue = (maybeMethodReturnValue is null) ? value : MultiValueLattice.Meet ((MultiValue) maybeMethodReturnValue, value);
			}
		}
 
		private partial bool MethodIsTypeConstructor (MethodProxy method)
		{
			if (!method.Definition.IsConstructor)
				return false;
			TypeDefinition? type = method.Definition.DeclaringType;
			while (type is not null) {
				if (type.IsTypeOf (WellKnownType.System_Type))
					return true;
				type = _context.Resolve (type.BaseType);
			}
			return false;
		}
 
		private partial IEnumerable<SystemReflectionMethodBaseValue> GetMethodsOnTypeHierarchy (TypeProxy type, string name, BindingFlags? bindingFlags)
		{
			foreach (var method in type.Type.GetMethodsOnTypeHierarchy (_context, m => m.Name == name, bindingFlags)) {
				if (MethodProxy.TryCreate (method, _context, out MethodProxy? methodProxy))
					yield return new SystemReflectionMethodBaseValue (methodProxy.Value);
			}
		}
 
		private partial IEnumerable<SystemTypeValue> GetNestedTypesOnType (TypeProxy type, string name, BindingFlags? bindingFlags)
		{
			foreach (var nestedType in type.Type.GetNestedTypesOnType (_context, t => t.Name == name, bindingFlags))
				yield return new SystemTypeValue (new TypeProxy (nestedType, _context));
		}
 
		private partial bool TryGetBaseType (TypeProxy type, out TypeProxy? baseType)
		{
			if (type.Type.ResolveToTypeDefinition (_context)?.BaseType is TypeReference baseTypeRef && _context.TryResolve (baseTypeRef) is TypeDefinition baseTypeDefinition) {
				baseType = new TypeProxy (baseTypeDefinition, _context);
				return true;
			}
 
			baseType = null;
			return false;
		}
 
		private partial bool TryResolveTypeNameForCreateInstanceAndMark (in MethodProxy calledMethod, string assemblyName, string typeName, out TypeProxy resolvedType)
		{
			var resolvedAssembly = _context.TryResolve (assemblyName);
			if (resolvedAssembly == null) {
				_diagnosticContext.AddDiagnostic (DiagnosticId.UnresolvedAssemblyInCreateInstance,
					assemblyName,
					calledMethod.GetDisplayName ());
				resolvedType = default;
				return false;
			}
 
			if (!_reflectionMarker.TryResolveTypeNameAndMark (resolvedAssembly, typeName, _diagnosticContext, out TypeReference? foundType)) {
				// It's not wrong to have a reference to non-existing type - the code may well expect to get an exception in this case
				// Note that we did find the assembly, so it's not a ILLink config problem, it's either intentional, or wrong versions of assemblies
				// but ILLink can't know that. In case a user tries to create an array using System.Activator we should simply ignore it, the user
				// might expect an exception to be thrown.
				resolvedType = default;
				return false;
			}
 
			resolvedType = new TypeProxy (foundType, _context);
			return true;
		}
 
		private partial void MarkStaticConstructor (TypeProxy type)
			=> _reflectionMarker.MarkStaticConstructor (_diagnosticContext.Origin, type.Type);
 
		private partial void MarkEventsOnTypeHierarchy (TypeProxy type, string name, BindingFlags? bindingFlags)
			=> _reflectionMarker.MarkEventsOnTypeHierarchy (_diagnosticContext.Origin, type.Type, e => e.Name == name, bindingFlags);
 
		private partial void MarkFieldsOnTypeHierarchy (TypeProxy type, string name, BindingFlags? bindingFlags)
			=> _reflectionMarker.MarkFieldsOnTypeHierarchy (_diagnosticContext.Origin, type.Type, f => f.Name == name, bindingFlags);
 
		private partial void MarkPropertiesOnTypeHierarchy (TypeProxy type, string name, BindingFlags? bindingFlags)
			=> _reflectionMarker.MarkPropertiesOnTypeHierarchy (_diagnosticContext.Origin, type.Type, p => p.Name == name, bindingFlags);
 
		private partial void MarkPublicParameterlessConstructorOnType (TypeProxy type)
			=> _reflectionMarker.MarkConstructorsOnType (_diagnosticContext.Origin, type.Type, m => m.IsPublic && !m.HasMetadataParameters ());
 
		private partial void MarkConstructorsOnType (TypeProxy type, BindingFlags? bindingFlags, int? parameterCount)
			=> _reflectionMarker.MarkConstructorsOnType (_diagnosticContext.Origin, type.Type, (parameterCount == null) ? null : m => m.GetMetadataParametersCount () == parameterCount, bindingFlags);
 
		private partial void MarkMethod (MethodProxy method)
			=> _reflectionMarker.MarkMethod (_diagnosticContext.Origin, method.Method);
 
		private partial void MarkType (TypeProxy type)
			=> _reflectionMarker.MarkType (_diagnosticContext.Origin, type.Type);
 
		private partial bool MarkAssociatedProperty (MethodProxy method)
		{
			if (method.Definition.TryGetProperty (out PropertyDefinition? propertyDefinition)) {
				_reflectionMarker.MarkProperty (_diagnosticContext.Origin, propertyDefinition);
				return true;
			}
 
			return false;
		}
 
		private partial string GetContainingSymbolDisplayName () => _callingMethodDefinition.GetDisplayName ();
	}
}