File: Linker\SerializationMarker.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 Mono.Cecil;
 
namespace Mono.Linker
{
	// This recursively marks members of types for serialization. This is not supposed to be complete; it is just
	// here as a heuristic to enable some serialization scenarios.
	//
	// Xamarin-android had some heuristics for serialization which behaved as follows:
	//
	// Discover members in the "link" assemblies with certain attributes:
	//   for XMLSerializer: Xml*Attribute, except XmlIgnoreAttribute
	//   for DataContractSerializer: DataContractAttribute or DataMemberAttribute
	// These members are considered "roots" for serialization.
	//
	// For each "root":
	//    in an SDK assembly, set TypePreserve.All for types, or conditionally preserve methods or property methods
	//      event methods were not preserved.
	//    in a non-SDK assembly, mark types, fields, methods, property methods, and event methods
	//    recursively scan types of properties and fields (including generic arguments)
	// For each recursive type:
	//   conditionally preserve the default ctor
	//
	// We want to match the above behavior in a more correct way, even if this means not marking some members
	// which used to be marked. We also would like to avoid serializer-specific logic in the recursive marking.
	//
	// Instead of conditionally preserving things, we will just mark them, and we will do so consistently for every
	// type discovered as part of the type graph reachable from the discovered roots. We also do not distinguish between
	// SDK and non-SDK assemblies.
	//
	// The behavior is as follows:
	//
	// Discover attributed "roots" by looking for the same attributes, but only on marked types.
	//
	// For each "root":
	//   recursively scan types of public properties and fields, and the base type (including generic arguments)
	// For each recursive type:
	//   mark the type and its public instance {fields, properties, and parameterless constructors}
 
	[Flags]
	public enum SerializerKind
	{
		None = 0,
		XmlSerializer = 1,
		DataContractSerializer = 2,
	}
 
	public class SerializationMarker
	{
		readonly LinkContext _context;
 
		SerializerKind ActiveSerializers { get; set; }
 
		Dictionary<SerializerKind, HashSet<ICustomAttributeProvider>>? _trackedRoots;
		Dictionary<SerializerKind, HashSet<ICustomAttributeProvider>> TrackedRoots {
			get {
				_trackedRoots ??= new Dictionary<SerializerKind, HashSet<ICustomAttributeProvider>> ();
 
				return _trackedRoots;
			}
		}
 
		HashSet<TypeDefinition>? _recursiveTypes;
		HashSet<TypeDefinition> RecursiveTypes {
			get {
				_recursiveTypes ??= new HashSet<TypeDefinition> ();
 
				return _recursiveTypes;
			}
		}
 
		public SerializationMarker (LinkContext context)
		{
			_context = context;
		}
 
		public bool IsActive (SerializerKind serializerKind) => ActiveSerializers.HasFlag (serializerKind);
 
		static DependencyKind ToDependencyKind (SerializerKind serializerKind) => serializerKind switch {
			SerializerKind.DataContractSerializer => DependencyKind.DataContractSerialized,
			SerializerKind.XmlSerializer => DependencyKind.XmlSerialized,
			_ => throw new ArgumentException (nameof (SerializerKind))
		};
 
		public void TrackForSerialization (ICustomAttributeProvider provider, SerializerKind serializerKind)
		{
			if (ActiveSerializers.HasFlag (serializerKind)) {
				MarkRecursiveMembers (provider, serializerKind);
				return;
			}
 
			if (!TrackedRoots.TryGetValue (serializerKind, out var roots)) {
				roots = new HashSet<ICustomAttributeProvider> ();
				TrackedRoots.Add (serializerKind, roots);
			}
 
			roots.Add (provider);
		}
 
		public void Activate (SerializerKind serializerKind)
		{
			if (!Enum.IsDefined<SerializerKind> (serializerKind) || serializerKind == SerializerKind.None)
				throw new ArgumentException ($"Unexpected serializer kind {nameof (serializerKind)}");
 
			if (ActiveSerializers.HasFlag (serializerKind))
				return;
 
			ActiveSerializers |= serializerKind;
 
			if (!TrackedRoots.TryGetValue (serializerKind, out var roots))
				return;
 
			foreach (var provider in roots)
				MarkRecursiveMembers (provider, serializerKind);
 
			TrackedRoots.Remove (serializerKind);
		}
 
		public void MarkRecursiveMembers (ICustomAttributeProvider provider, SerializerKind serializerKind)
		{
			TypeDefinition type;
			var reason = new DependencyInfo (ToDependencyKind (serializerKind), provider);
			var origin = new MessageOrigin (provider);
 
			// Mark field and property types up-front in case the root field/property is
			// not discovered recursively from the declaring type (for example, it may be private).
			// Also mark the root members because the recursive logic doesn't mark all member types.
			switch (provider) {
			case TypeDefinition td:
				type = td;
				break;
			case FieldDefinition field:
				type = field.DeclaringType;
				MarkRecursiveMembersInternal (field.FieldType, reason);
				_context.Annotations.Mark (field, reason, origin);
				break;
			case PropertyDefinition property:
				type = property.DeclaringType;
				MarkRecursiveMembersInternal (property.PropertyType, reason);
				if (property.GetMethod != null)
					_context.Annotations.Mark (property.GetMethod, reason, origin);
				if (property.SetMethod != null)
					_context.Annotations.Mark (property.SetMethod, reason, origin);
				break;
			case MethodDefinition method:
				type = method.DeclaringType;
				_context.Annotations.Mark (method, reason, origin);
				break;
			case EventDefinition @event:
				type = @event.DeclaringType;
				if (@event.AddMethod != null)
					_context.Annotations.Mark (@event.AddMethod, reason, origin);
				if (@event.InvokeMethod != null)
					_context.Annotations.Mark (@event.InvokeMethod, reason, origin);
				if (@event.RemoveMethod != null)
					_context.Annotations.Mark (@event.RemoveMethod, reason, origin);
				break;
			default:
				throw new ArgumentException ($"{nameof (provider)} has invalid provider type {provider.GetType ()}");
			}
 
			MarkRecursiveMembersInternal (type, reason);
		}
 
		void MarkRecursiveMembersInternal (TypeReference typeRef, in DependencyInfo reason)
		{
			if (typeRef == null)
				return;
 
			DependencyInfo typeReason = reason;
			while (typeRef is GenericInstanceType git) {
				if (git.HasGenericArguments) {
					foreach (var argType in git.GenericArguments)
						MarkRecursiveMembersInternal (argType, new DependencyInfo (DependencyKind.GenericArgumentType, typeRef));
				}
				_context.Tracer.AddDirectDependency (typeRef, typeReason, marked: false);
				typeReason = new DependencyInfo (DependencyKind.ElementType, typeRef);
				typeRef = git.ElementType;
			}
			// This doesn't handle other TypeSpecs. We are only matching what xamarin-android used to do.
			// Arrays will still work because Resolve returns the array element type.
 
			TypeDefinition? type = _context.TryResolve (typeRef);
			if (type == null)
				return;
 
			_context.Annotations.Mark (type, typeReason, new MessageOrigin (reason.Source as ICustomAttributeProvider));
 
			if (!RecursiveTypes.Add (type))
				return;
 
			// Unlike xamarin-android, don't preserve all members.
 
			// Unlike xamarin-android, we preserve base type members recursively.
			MarkRecursiveMembersInternal (type.BaseType, new DependencyInfo (DependencyKind.SerializedRecursiveType, type));
 
			if (type.HasFields) {
				foreach (var field in type.Fields) {
					// Unlike xamarin-android, don't preserve non-public or static fields.
					if (!field.IsPublic || field.IsStatic)
						continue;
 
					MarkRecursiveMembersInternal (field.FieldType, new DependencyInfo (DependencyKind.SerializedRecursiveType, type));
					_context.Annotations.Mark (field, new DependencyInfo (DependencyKind.SerializedMember, type), new MessageOrigin (type));
				}
			}
 
			if (type.HasProperties) {
				foreach (var property in type.Properties) {
					// Unlike xamarin-android, don't preserve non-public or static properties.
					var get = property.GetMethod;
					var set = property.SetMethod;
					if ((get == null || !get.IsPublic || get.IsStatic) &&
						(set == null || !set.IsPublic || set.IsStatic))
						continue;
 
					MarkRecursiveMembersInternal (property.PropertyType, new DependencyInfo (DependencyKind.SerializedRecursiveType, type));
					if (get != null)
						_context.Annotations.Mark (get, new DependencyInfo (DependencyKind.SerializedMember, type), new MessageOrigin (type));
					if (set != null)
						_context.Annotations.Mark (set, new DependencyInfo (DependencyKind.SerializedMember, type), new MessageOrigin (type));
					// The property will be marked as a consequence of marking the getter/setter.
				}
			}
 
			if (type.HasMethods) {
				foreach (var method in type.Methods) {
					// Unlike xamarin-android, don't preserve non-public, static, or parameterless constructors.
					if (!method.IsPublic || !method.IsDefaultConstructor ())
						continue;
 
					_context.Annotations.Mark (method, new DependencyInfo (DependencyKind.SerializedMember, type), new MessageOrigin (type));
				}
			}
		}
	}
}