using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEngine; using UnityObject = UnityEngine.Object; namespace Unity.VisualScripting { [SerializationVersion("A")] public sealed class Member : ISerializationCallbackReceiver { public enum Source { Unknown, Field, Property, Method, Constructor } [Obsolete(Serialization.ConstructorWarning)] public Member() { } public Member(Type targetType, string name, Type[] parameterTypes = null) { Ensure.That(nameof(targetType)).IsNotNull(targetType); Ensure.That(nameof(name)).IsNotNull(name); if (parameterTypes != null) { for (int i = 0; i < parameterTypes.Length; i++) { if (parameterTypes[i] == null) { throw new ArgumentNullException(nameof(parameterTypes) + $"[{i}]"); } } } this.targetType = targetType; this.name = name; this.parameterTypes = parameterTypes; } public Member(Type targetType, FieldInfo fieldInfo) { Ensure.That(nameof(targetType)).IsNotNull(targetType); Ensure.That(nameof(fieldInfo)).IsNotNull(fieldInfo); source = Source.Field; this.fieldInfo = fieldInfo; this.targetType = targetType; name = fieldInfo.Name; parameterTypes = null; isReflected = true; } public Member(Type targetType, PropertyInfo propertyInfo) { Ensure.That(nameof(targetType)).IsNotNull(targetType); Ensure.That(nameof(propertyInfo)).IsNotNull(propertyInfo); source = Source.Property; this.propertyInfo = propertyInfo; this.targetType = targetType; name = propertyInfo.Name; parameterTypes = null; isReflected = true; } public Member(Type targetType, MethodInfo methodInfo) { Ensure.That(nameof(targetType)).IsNotNull(targetType); Ensure.That(nameof(methodInfo)).IsNotNull(methodInfo); source = Source.Method; this.methodInfo = methodInfo; this.targetType = targetType; name = methodInfo.Name; isExtension = methodInfo.IsExtension(); isInvokedAsExtension = methodInfo.IsInvokedAsExtension(targetType); parameterTypes = methodInfo.GetInvocationParameters(_isInvokedAsExtension).Select(pi => pi.ParameterType).ToArray(); isReflected = true; } public Member(Type targetType, ConstructorInfo constructorInfo) { Ensure.That(nameof(targetType)).IsNotNull(targetType); Ensure.That(nameof(constructorInfo)).IsNotNull(constructorInfo); source = Source.Constructor; this.constructorInfo = constructorInfo; this.targetType = targetType; name = constructorInfo.Name; parameterTypes = constructorInfo.GetParameters().Select(pi => pi.ParameterType).ToArray(); isReflected = true; } [SerializeAs(nameof(name))] private string _name; [SerializeAs(nameof(parameterTypes))] private Type[] _parameterTypes; [SerializeAs(nameof(targetType))] private Type _targetType; [SerializeAs(nameof(targetTypeName))] private string _targetTypeName; [DoNotSerialize] private Source _source; [DoNotSerialize] private FieldInfo _fieldInfo; [DoNotSerialize] private PropertyInfo _propertyInfo; [DoNotSerialize] private MethodInfo _methodInfo; [DoNotSerialize] private ConstructorInfo _constructorInfo; [DoNotSerialize] private bool _isExtension; [DoNotSerialize] private bool _isInvokedAsExtension; [DoNotSerialize] private IOptimizedAccessor fieldAccessor; [DoNotSerialize] private IOptimizedAccessor propertyAccessor; [DoNotSerialize] private IOptimizedInvoker methodInvoker; [DoNotSerialize] public Type targetType { get { return _targetType; } private set { if (value == targetType) { return; } isReflected = false; _targetType = value; if (value == null) { _targetTypeName = null; } else { _targetTypeName = RuntimeCodebase.SerializeType(value); } } } [DoNotSerialize] public string targetTypeName => _targetTypeName; [DoNotSerialize] public string name { get { return _name; } private set { if (value != name) { isReflected = false; } _name = value; } } [DoNotSerialize] public bool isReflected { get; private set; } [DoNotSerialize] public Source source { get { EnsureReflected(); return _source; } private set { _source = value; } } [DoNotSerialize] public FieldInfo fieldInfo { get { EnsureReflected(); return _fieldInfo; } private set { _fieldInfo = value; } } [DoNotSerialize] public PropertyInfo propertyInfo { get { EnsureReflected(); return _propertyInfo; } private set { _propertyInfo = value; } } [DoNotSerialize] public MethodInfo methodInfo { get { EnsureReflected(); return _methodInfo; } private set { _methodInfo = value; } } [DoNotSerialize] public ConstructorInfo constructorInfo { get { EnsureReflected(); return _constructorInfo; } private set { _constructorInfo = value; } } [DoNotSerialize] public bool isExtension { get { EnsureReflected(); return _isExtension; } private set { _isExtension = value; } } [DoNotSerialize] public bool isInvokedAsExtension { get { EnsureReflected(); return _isInvokedAsExtension; } private set { _isInvokedAsExtension = value; } } [DoNotSerialize] public Type[] parameterTypes { get { return _parameterTypes; } private set { _parameterTypes = value; isReflected = false; } } public MethodBase methodBase { get { switch (source) { case Source.Method: return methodInfo; case Source.Constructor: return constructorInfo; default: return null; } } } private MemberInfo _info { get { switch (source) { case Source.Field: return _fieldInfo; case Source.Property: return _propertyInfo; case Source.Method: return _methodInfo; case Source.Constructor: return _constructorInfo; default: throw new UnexpectedEnumValueException(source); } } } public MemberInfo info { get { switch (source) { case Source.Field: return fieldInfo; case Source.Property: return propertyInfo; case Source.Method: return methodInfo; case Source.Constructor: return constructorInfo; default: throw new UnexpectedEnumValueException(source); } } } public Type type { get { switch (source) { case Source.Field: return fieldInfo.FieldType; case Source.Property: return propertyInfo.PropertyType; case Source.Method: return methodInfo.ReturnType; case Source.Constructor: return constructorInfo.DeclaringType; default: throw new UnexpectedEnumValueException(source); } } } public bool isCoroutine { get { if (!isGettable) { return false; } return type == typeof(IEnumerator); } } public bool isYieldInstruction { get { if (!isGettable) { return false; } return typeof(YieldInstruction).IsAssignableFrom(type); } } public bool isGettable => IsGettable(true); public bool isPubliclyGettable => IsGettable(false); public bool isSettable => IsSettable(true); public bool isPubliclySettable => IsSettable(false); public bool isInvocable => IsInvocable(true); public bool isPubliclyInvocable => IsInvocable(false); public bool isAccessor { get { switch (source) { case Source.Field: return true; case Source.Property: return true; case Source.Method: return false; case Source.Constructor: return false; default: throw new UnexpectedEnumValueException(source); } } } public bool isField => source == Source.Field; public bool isProperty => source == Source.Property; public bool isMethod => source == Source.Method; public bool isConstructor => source == Source.Constructor; public bool requiresTarget { get { switch (source) { case Source.Field: return !fieldInfo.IsStatic; case Source.Property: return !(propertyInfo.GetGetMethod(true) ?? propertyInfo.GetSetMethod(true)).IsStatic; case Source.Method: return !methodInfo.IsStatic || isInvokedAsExtension; case Source.Constructor: return false; default: throw new UnexpectedEnumValueException(source); } } } public bool isOperator => isMethod && methodInfo.IsOperator(); public bool isConversion => isMethod && methodInfo.IsUserDefinedConversion(); public int order => info.MetadataToken; public Type declaringType => info.ExtendedDeclaringType(isInvokedAsExtension); public bool isInherited => targetType != declaringType; public Type pseudoDeclaringType { get { // For Unity objects, we'll consider parent types to be only root types, // to allow common objects like BoxCollider to show Collider members as self-defined. // We'll also consider them as absolute roots, and therefore none of their members // should display as inherited. var declaringType = this.declaringType; if (typeof(UnityObject).IsAssignableFrom(targetType)) { if (targetType == typeof(GameObject) || targetType == typeof(Component) || targetType == typeof(ScriptableObject)) { return targetType; } else { if (declaringType != typeof(UnityObject) && declaringType != typeof(GameObject) && declaringType != typeof(Component) && declaringType != typeof(MonoBehaviour) && declaringType != typeof(ScriptableObject) && declaringType != typeof(object)) { return targetType; } } } return declaringType; } } public bool isPseudoInherited => targetType != pseudoDeclaringType || (isMethod && methodInfo.IsGenericExtension()); public bool isIndexer => isProperty && propertyInfo.GetIndexParameters().Length > 0; public bool isPredictable => isField || info.HasAttribute(); public bool allowsNull => isSettable && ((type.IsReferenceType() && info.HasAttribute()) || Nullable.GetUnderlyingType(type) != null); void ISerializationCallbackReceiver.OnBeforeSerialize() { } void ISerializationCallbackReceiver.OnAfterDeserialize() { // Attempt to preserve and restore the target type even if // it wasn't available during an assembly reload. if (targetType != null) { _targetTypeName = RuntimeCodebase.SerializeType(targetType); } else if (_targetTypeName != null) { try { targetType = RuntimeCodebase.DeserializeType(_targetTypeName); } catch { } } } public bool IsGettable(bool nonPublic) { switch (source) { case Source.Field: return nonPublic || fieldInfo.IsPublic; case Source.Property: return propertyInfo.CanRead && (nonPublic || propertyInfo.GetGetMethod(false) != null); case Source.Method: return methodInfo.ReturnType != typeof(void) && (nonPublic || methodInfo.IsPublic); case Source.Constructor: return nonPublic || constructorInfo.IsPublic; default: throw new UnexpectedEnumValueException(source); } } public bool IsSettable(bool nonPublic) { switch (source) { case Source.Field: return !(fieldInfo.IsLiteral || fieldInfo.IsInitOnly) && (nonPublic || fieldInfo.IsPublic); case Source.Property: return propertyInfo.CanWrite && (nonPublic || propertyInfo.GetSetMethod(false) != null); case Source.Method: return false; case Source.Constructor: return false; default: throw new UnexpectedEnumValueException(source); } } public bool IsInvocable(bool nonPublic) { switch (source) { case Source.Field: return false; case Source.Property: return false; case Source.Method: return nonPublic || methodInfo.IsPublic; case Source.Constructor: return nonPublic || constructorInfo.IsPublic; default: throw new UnexpectedEnumValueException(source); } } private void EnsureExplicitParameterTypes() { if (parameterTypes == null) { throw new InvalidOperationException("Missing parameter types."); } } public void Reflect() { // Cannot happen from the constructor, but will occur // if the type doesn't exist and fails to be deserialized if (targetType == null) { if (targetTypeName != null) { throw new MissingMemberException(targetTypeName, name); } else { throw new MissingMemberException("Target type not found."); } } _source = Source.Unknown; _fieldInfo = null; _propertyInfo = null; _methodInfo = null; _constructorInfo = null; fieldAccessor = null; propertyAccessor = null; methodInvoker = null; MemberInfo[] candidates; try { candidates = targetType.GetExtendedMember(name, SupportedMemberTypes, SupportedBindingFlags); } catch (NotSupportedException e) { throw new InvalidOperationException($"An error occured when trying to reflect the member '{name}' of the type '{targetType.FullName}' in a '{GetType().Name}' unit. Supported member types: {SupportedMemberTypes}, supported binding flags: {SupportedBindingFlags}", e); } if (candidates.Length == 0) // Not found, check if it might have been renamed { var renamedMembers = RuntimeCodebase.RenamedMembers(targetType); string newName; if (renamedMembers.TryGetValue(name, out newName)) { name = newName; try { candidates = targetType.GetExtendedMember(name, SupportedMemberTypes, SupportedBindingFlags); } catch (NotSupportedException e) { throw new InvalidOperationException($"An error occured when trying to reflect the renamed member '{name}' of the type '{targetType.FullName}' in a '{GetType().Name}' unit. Supported member types: {SupportedMemberTypes}, supported binding flags: {SupportedBindingFlags}", e); } } } if (candidates.Length == 0) // Nope, not even, abort { throw new MissingMemberException($"No matching member found: '{targetType.Name}.{name}'"); } MemberTypes? memberType = null; foreach (var candidate in candidates) { if (memberType == null) { memberType = candidate.MemberType; } else if (candidate.MemberType != memberType && !candidate.IsExtensionMethod()) { // This theoretically shouldn't happen according to the .NET specification, I believe Debug.LogWarning($"Multiple members with the same name are of a different type: '{targetType.Name}.{name}'"); break; } } switch (memberType) { case MemberTypes.Field: ReflectField(candidates); break; case MemberTypes.Property: ReflectProperty(candidates); break; case MemberTypes.Method: ReflectMethod(candidates); break; case MemberTypes.Constructor: ReflectConstructor(candidates); break; default: throw new UnexpectedEnumValueException(memberType.Value); } isReflected = true; } private void ReflectField(IEnumerable candidates) { _source = Source.Field; _fieldInfo = candidates.OfType().Disambiguate(targetType); if (_fieldInfo == null) { throw new MissingMemberException($"No matching field found: '{targetType.Name}.{name}'"); } } private void ReflectProperty(IEnumerable candidates) { _source = Source.Property; _propertyInfo = candidates.OfType().Disambiguate(targetType); if (_propertyInfo == null) { throw new MissingMemberException($"No matching property found: '{targetType.Name}.{name}'"); } } private void ReflectConstructor(IEnumerable candidates) { _source = Source.Constructor; EnsureExplicitParameterTypes(); // Exclude static constructors (type initializers) because calling them // is always a violation of types expecting it to be called only once. // http://stackoverflow.com/a/2524938 _constructorInfo = candidates.OfType().Where(c => !c.IsStatic).Disambiguate(targetType, parameterTypes); if (_constructorInfo == null) { throw new MissingMemberException($"No matching constructor found: '{targetType.Name} ({parameterTypes.Select(t => t.Name).ToCommaSeparatedString()})'"); } } private void ReflectMethod(IEnumerable candidates) { _source = Source.Method; EnsureExplicitParameterTypes(); _methodInfo = candidates.OfType().Disambiguate(targetType, parameterTypes); if (_methodInfo == null) { throw new MissingMemberException($"No matching method found: '{targetType.Name}.{name} ({parameterTypes.Select(t => t.Name).ToCommaSeparatedString()})'\nCandidates:\n{candidates.ToLineSeparatedString()}"); } _isExtension = _methodInfo.IsExtension(); _isInvokedAsExtension = _methodInfo.IsInvokedAsExtension(targetType); } public void Prewarm() { if (fieldAccessor == null) { fieldAccessor = fieldInfo?.Prewarm(); } if (propertyAccessor == null) { propertyAccessor = propertyInfo?.Prewarm(); } if (methodInvoker == null) { methodInvoker = methodInfo?.Prewarm(); } } public void EnsureReflected() { if (!isReflected) { Reflect(); } } public void EnsureReady(object target) { EnsureReflected(); if (target == null && requiresTarget) { throw new InvalidOperationException($"Missing target object for '{targetType}.{name}'."); } else if (target != null && !requiresTarget) { throw new InvalidOperationException($"Superfluous target object for '{targetType}.{name}'."); } } public object Get(object target) { EnsureReady(target); switch (source) { case Source.Field: if (fieldAccessor == null) { fieldAccessor = fieldInfo.Prewarm(); } return fieldAccessor.GetValue(target); case Source.Property: if (propertyAccessor == null) { propertyAccessor = propertyInfo.Prewarm(); } return propertyAccessor.GetValue(target); case Source.Method: throw new NotSupportedException("Member is a method. Consider using 'Invoke' instead."); case Source.Constructor: throw new NotSupportedException("Member is a constructor. Consider using 'Invoke' instead."); default: throw new UnexpectedEnumValueException(source); } } public T Get(object target) { return (T)Get(target); } public object Set(object target, object value) { EnsureReady(target); // When setting, we return the assigned value, not the updated field or property. // This is consistent with C# language behaviour: https://msdn.microsoft.com/en-us/library/sbkb459w.aspx // "The assignment operator (=) [...] returns the value as its result" // See confirmation here: https://dotnetfiddle.net/n4RZcW switch (source) { case Source.Field: if (fieldAccessor == null) { fieldAccessor = fieldInfo.Prewarm(); } fieldAccessor.SetValue(target, value); return value; case Source.Property: if (propertyAccessor == null) { propertyAccessor = propertyInfo.Prewarm(); } propertyAccessor.SetValue(target, value); return value; case Source.Method: throw new NotSupportedException("Member is a method."); case Source.Constructor: throw new NotSupportedException("Member is a constructor."); default: throw new UnexpectedEnumValueException(source); } } private void EnsureInvocable(object target) { EnsureReady(target); if (source == Source.Field || source == Source.Property) { throw new NotSupportedException("Member is a field or property."); } else if (source == Source.Method) { if (methodInfo.ContainsGenericParameters) { throw new NotSupportedException($"Trying to invoke an open-constructed generic method: '{methodInfo}'."); } if (methodInvoker == null) { methodInvoker = methodInfo.Prewarm(); } } else if (source == Source.Constructor) { if (constructorInfo.ContainsGenericParameters) { throw new NotSupportedException($"Trying to invoke an open-constructed generic constructor: '{constructorInfo}'."); } } else { throw new UnexpectedEnumValueException(source); } } public IEnumerable GetParameterInfos() { EnsureReflected(); return methodBase.GetInvocationParameters(isInvokedAsExtension); } public object Invoke(object target) { EnsureInvocable(target); if (source == Source.Method) { if (isInvokedAsExtension) { return methodInvoker.Invoke(null, target); } else { return methodInvoker.Invoke(target); } } else // if (source == Source.Constructor) { return constructorInfo.Invoke(EmptyObjects); } } public object Invoke(object target, object arg0) { EnsureInvocable(target); if (source == Source.Method) { if (isInvokedAsExtension) { return methodInvoker.Invoke(null, target, arg0); } else { return methodInvoker.Invoke(target, arg0); } } else // if (source == Source.Constructor) { return constructorInfo.Invoke(new[] { arg0 }); } } public object Invoke(object target, object arg0, object arg1) { EnsureInvocable(target); if (source == Source.Method) { if (isInvokedAsExtension) { return methodInvoker.Invoke(null, target, arg0, arg1); } else { return methodInvoker.Invoke(target, arg0, arg1); } } else // if (source == Source.Constructor) { return constructorInfo.Invoke(new[] { arg0, arg1 }); } } public object Invoke(object target, object arg0, object arg1, object arg2) { EnsureInvocable(target); if (source == Source.Method) { if (isInvokedAsExtension) { return methodInvoker.Invoke(null, target, arg0, arg1, arg2); } else { return methodInvoker.Invoke(target, arg0, arg1, arg2); } } else // if (source == Source.Constructor) { return constructorInfo.Invoke(new[] { arg0, arg1, arg2 }); } } public object Invoke(object target, object arg0, object arg1, object arg2, object arg3) { EnsureInvocable(target); if (source == Source.Method) { if (isInvokedAsExtension) { return methodInvoker.Invoke(null, target, arg0, arg1, arg2, arg3); } else { return methodInvoker.Invoke(target, arg0, arg1, arg2, arg3); } } else // if (source == Source.Constructor) { return constructorInfo.Invoke(new[] { arg0, arg1, arg2, arg3 }); } } public object Invoke(object target, object arg0, object arg1, object arg2, object arg3, object arg4) { EnsureInvocable(target); if (source == Source.Method) { if (isInvokedAsExtension) { return methodInvoker.Invoke(null, target, arg0, arg1, arg2, arg3, arg4); } else { return methodInvoker.Invoke(target, arg0, arg1, arg2, arg3, arg4); } } else // if (source == Source.Constructor) { return constructorInfo.Invoke(new[] { arg0, arg1, arg2, arg3, arg4 }); } } public object Invoke(object target, params object[] arguments) { EnsureInvocable(target); if (source == Source.Method) { if (isInvokedAsExtension) { var argumentsWithThis = new object[arguments.Length + 1]; argumentsWithThis[0] = target; Array.Copy(arguments, 0, argumentsWithThis, 1, arguments.Length); return methodInvoker.Invoke(null, argumentsWithThis); } else { return methodInvoker.Invoke(target, arguments); } } else // if (source == Source.Constructor) { return constructorInfo.Invoke(arguments); } } public T Invoke(object target) { return (T)Invoke(target); } public T Invoke(object target, object arg0) { return (T)Invoke(target, arg0); } public T Invoke(object target, object arg0, object arg1) { return (T)Invoke(target, arg0, arg1); } public T Invoke(object target, object arg0, object arg1, object arg2) { return (T)Invoke(target, arg0, arg1, arg2); } public T Invoke(object target, object arg0, object arg1, object arg2, object arg3) { return (T)Invoke(target, arg0, arg1, arg2, arg3); } public T Invoke(object target, object arg0, object arg1, object arg2, object arg3, object arg4) { return (T)Invoke(target, arg0, arg1, arg2, arg3, arg4); } public T Invoke(object target, params object[] arguments) { return (T)Invoke(target, arguments); } public override bool Equals(object obj) { var other = obj as Member; var equals = other != null && targetType == other.targetType && name == other.name; if (!equals) { return false; } var selfHasParameters = parameterTypes != null; var otherHasParameters = other.parameterTypes != null; if (selfHasParameters != otherHasParameters) { return false; } if (selfHasParameters /* && otherHasParameters */) { var selfCount = parameterTypes.Length; var otherCount = other.parameterTypes.Length; if (selfCount != otherCount) { return false; } for (var i = 0; i < selfCount; i++) { if (parameterTypes[i] != other.parameterTypes[i]) { return false; } } } return true; } public override int GetHashCode() { unchecked { var hash = 17; hash = hash * 23 + (targetType?.GetHashCode() ?? 0); hash = hash * 23 + (name?.GetHashCode() ?? 0); if (parameterTypes != null) { foreach (var parameterType in parameterTypes) { hash = hash * 23 + parameterType.GetHashCode(); } } else { hash = hash * 23 + 0; } return hash; } } public static bool operator ==(Member a, Member b) { if (ReferenceEquals(a, b)) { return true; } if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) { return false; } return a.Equals(b); } public static bool operator !=(Member a, Member b) { return !(a == b); } public string ToUniqueString() { var s = targetType.FullName + "." + this.name; if (parameterTypes != null) { s += "("; foreach (var parameterType in parameterTypes) { s += parameterType.FullName; } s += ")"; } return s; } public override string ToString() { return $"{targetType.CSharpName()}.{name}"; } public Member ToDeclarer() { return new Member(declaringType, name, parameterTypes); } public Member ToPseudoDeclarer() { return new Member(pseudoDeclaringType, name, parameterTypes); } public const MemberTypes SupportedMemberTypes = MemberTypes.Property | MemberTypes.Field | MemberTypes.Method | MemberTypes.Constructor; public const BindingFlags SupportedBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.FlattenHierarchy; private static readonly object[] EmptyObjects = new object[0]; } }