From 9a3da4be4e3f2748cbabb85e3bf25035245209ca Mon Sep 17 00:00:00 2001 From: lbmaian Date: Sun, 19 Jul 2020 01:54:30 -0700 Subject: [PATCH] AccessTools.*FieldRefAccess improvements, including struct support Specifically, handle edge cases and have consistent error handling in AccessTools.*FieldRefAccess. Add AccessTools.StructFieldRef/StructFieldRefAccess. --- Harmony/Tools/AccessTools.cs | 519 +++++++++++++++++++---- HarmonyTests/Tools/TestFieldRefAccess.cs | 513 +++++++--------------- 2 files changed, 598 insertions(+), 434 deletions(-) diff --git a/Harmony/Tools/AccessTools.cs b/Harmony/Tools/AccessTools.cs index f8dd8726..a85a5d17 100644 --- a/Harmony/Tools/AccessTools.cs +++ b/Harmony/Tools/AccessTools.cs @@ -782,30 +782,69 @@ namespace HarmonyLib }).ToArray(); } - /// A read/writable reference to an instance field - /// The class the field is defined in or "object" if type cannot be accessed at compile time - /// The type of the field - /// The runtime instance to access the field (leave empty for static fields) - /// An readable/assignable object representing the field + /// A readable/assignable reference delegate to an instance field of a class or static field (NOT an instance field of a struct) + /// + /// An arbitrary type if the field is static; otherwise the class that defines the field, or a parent class (including ), + /// implemented interface, or derived class of this type + /// + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// The runtime instance to access the field (ignored and can be omitted for static fields) + /// A readable/assignable reference to the field + /// Null instance passed to a non-static field ref delegate + /// + /// Instance of invalid type passed to a non-static field ref delegate + /// (this can happen if is a parent class or interface of the field's declaring type) + /// + /// + /// + /// This delegate cannot be used for instance fields of structs, since a struct instance passed to the delegate would be passed by + /// value and thus would be a copy that only exists within the delegate's invocation. This is fine for a readonly reference, + /// but makes assigning to the reference an exercise in futility. Use instead. + /// + /// + /// Note that is not required to be the field's declaring type. It can be a parent class (including ), + /// implemented interface, or a derived class of the field's declaring type ("instanceOfT is FieldDeclaringType" must be possible). + /// Specifically, must be assignable from OR to the field's declaring type. + /// Technically, this allows Nullable, although Nullable is only relevant for structs, and since only static fields of structs + /// are allowed for this delegate, and the instance passed to such a delegate is ignored, this hardly matters. + /// + /// + /// Similarly, is not required to be the field's field type, unless that type is a value type. + /// It can be a parent class (including object) or implemented interface of the field's field type. It cannot be a derived class. + /// This variance is not allowed for value types, since that would require boxing/unboxing, which is not allowed for ref values. + /// Specifically, for reference types, must be assignable from + /// the field's field type; and for value types, must be exactly the field's field type. + /// + /// + /// This delegate supports static fields, even those defined in structs, for legacy reasons. + /// For such static fields, is effectively ignored. + /// This is also the reason that this delegate lacks a generic class constraint (it was added to certain FieldRefAccess methods, + /// but such a constraint cannot be added to this delegate without breaking binary compatibility). + /// Consider using (and StaticFieldRefAccess methods that return it) instead for static fields. + /// + /// /// - public delegate ref F FieldRef(T obj = default); + public delegate ref F FieldRef(T instance = default); - /// Creates an instance field reference - /// The class the field is defined in - /// The type of the field + /// Creates a field reference delegate for an instance field of a class + /// The class that defines the instance field, or derived class of this type + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// /// The name of the field - /// A read and writable field reference delegate + /// A readable/assignable delegate /// - public static FieldRef FieldRefAccess(string fieldName) + public static FieldRef FieldRefAccess(string fieldName) where T : class { - const BindingFlags bf = BindingFlags.NonPublic | - BindingFlags.Instance | - BindingFlags.DeclaredOnly; - + if (fieldName is null) + throw new ArgumentNullException(nameof(fieldName)); try { - var fi = typeof(T).GetField(fieldName, bf); - return FieldRefAccess(fi); + return FieldRefAccessInternal(GetInstanceField(typeof(T), fieldName), needCastclass: false); } catch (Exception ex) { @@ -813,62 +852,363 @@ namespace HarmonyLib } } - /// Creates an instance field reference for a specific instance - /// The class the field is defined in - /// The type of the field + /// Creates an instance field reference for a specific instance of a class + /// The class that defines the instance field, or derived class of this type + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// /// The instance /// The name of the field - /// An readable/assignable object representing the field + /// A readable/assignable reference to the field /// - public static ref F FieldRefAccess(T instance, string fieldName) + public static ref F FieldRefAccess(T instance, string fieldName) where T : class { - return ref FieldRefAccess(fieldName)(instance); + if (instance is null) + throw new ArgumentNullException(nameof(instance)); + if (fieldName is null) + throw new ArgumentNullException(nameof(fieldName)); + try + { + return ref FieldRefAccessInternal(GetInstanceField(typeof(T), fieldName), needCastclass: false)(instance); + } + catch (Exception ex) + { + throw new ArgumentException($"FieldRefAccess<{typeof(T)}, {typeof(F)}> for {instance}, {fieldName} caused an exception", ex); + } } - /// Creates an instance field reference delegate for a private type - /// The type of the field - /// The class/type + /// Creates a field reference delegate for an instance field of a class or static field (NOT an instance field of a struct) + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// + /// The type that defines the field, or derived class of this type; must not be a struct type unless the field is static + /// /// The name of the field - /// A read and writable delegate + /// + /// A readable/assignable delegate with T=object + /// (for static fields, the instance delegate parameter is ignored) + /// + /// + /// This method supports static fields, even those defined in structs, for legacy reasons. + /// Consider using (and other overloads) instead for static fields. + /// /// public static FieldRef FieldRefAccess(Type type, string fieldName) { - return FieldRefAccess(Field(type, fieldName)); + if (type is null) + throw new ArgumentNullException(nameof(type)); + if (fieldName is null) + throw new ArgumentNullException(nameof(fieldName)); + try + { + var fieldInfo = Field(type, fieldName); + if (fieldInfo is null) + throw new MissingFieldException(type.Name, fieldName); + // Backwards compatibility: This supports static fields, even those defined in structs. For static fields, T is effectively ignored. + if (fieldInfo.IsStatic is false && fieldInfo.DeclaringType is Type declaringType) + { + // When fieldInfo is passed to FieldRefAccess methods, the T generic class constraint is insufficient to ensure that + // the field is not a struct instance field, since T could be object, ValueType, or an interface that the struct implements. + if (declaringType.IsValueType) + throw new ArgumentException("Either FieldDeclaringType must be a class or field must be static"); + } + // Field's declaring type cannot be object, since object has no fields, so always need a castclass for T=object. + return FieldRefAccessInternal(fieldInfo, needCastclass: true); + } + catch (Exception ex) + { + throw new ArgumentException($"FieldRefAccess<{typeof(F)}> for {type}, {fieldName} caused an exception", ex); + } } - /// Creates an instance field reference delegate for a fieldinfo - /// The class the field is defined in or "object" if type cannot be accessed at compile time - /// The type of the field - /// The field of the field - /// A read and writable delegate + /// Creates a field reference delegate for an instance field of a class or static field (NOT an instance field of a struct) + /// + /// An arbitrary type if the field is static; otherwise the class that defines the field, or a parent class (including ), + /// implemented interface, or derived class of this type ("instanceOfT is FieldDeclaringType" must be possible) + /// + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// The field + /// A readable/assignable delegate + /// + /// + /// This method supports static fields, even those defined in structs, for legacy reasons. + /// For such static fields, is effectively ignored. + /// Consider using (and other overloads) instead for static fields. + /// + /// /// - public static FieldRef FieldRefAccess(FieldInfo fieldInfo) + public static FieldRef FieldRefAccess(FieldInfo fieldInfo) where T : class { - if (fieldInfo == null) + if (fieldInfo is null) throw new ArgumentNullException(nameof(fieldInfo)); - if (!typeof(F).IsAssignableFrom(fieldInfo.FieldType)) - throw new ArgumentException("FieldInfo type does not match FieldRefAccess return type."); - if (typeof(T) != typeof(object)) - if (fieldInfo.DeclaringType == null || !fieldInfo.DeclaringType.IsAssignableFrom(typeof(T))) - throw new MissingFieldException(typeof(T).Name, fieldInfo.Name); + try + { + var needCastclass = false; + // Backwards compatibility: FieldRefAccess(Type type, string fieldName) used to delegate to this method, + // and thus this method must support the same cases - namely, static fields. For static fields, T is effectively ignored. + if (fieldInfo.IsStatic is false && fieldInfo.DeclaringType is Type declaringType) + { + // When fieldInfo is passed to FieldRefAccess methods, the T generic class constraint is insufficient to ensure that + // the field is not a struct instance field, since T could be object, ValueType, or an interface that the struct implements. + if (declaringType.IsValueType) + throw new ArgumentException("Either FieldDeclaringType must be a class or field must be static"); + needCastclass = FieldRefNeedsClasscast(typeof(T), declaringType); + } + return FieldRefAccessInternal(fieldInfo, needCastclass); + } + catch (Exception ex) + { + throw new ArgumentException($"FieldRefAccess<{typeof(T)}, {typeof(F)}> for {fieldInfo} caused an exception", ex); + } + } - var s_name = $"__refget_{typeof(T).Name}_fi_{fieldInfo.Name}"; + /// Creates a field reference for an instance field of a class + /// + /// The type that defines the field; or a parent class (including ), implemented interface, or derived class of this type + /// ("instanceOfT is FieldDeclaringType" must be possible) + /// + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// The instance + /// The field + /// A readable/assignable reference to the field + /// + public static ref F FieldRefAccess(T instance, FieldInfo fieldInfo) where T : class + { + if (instance is null) + throw new ArgumentNullException(nameof(instance)); + if (fieldInfo is null) + throw new ArgumentNullException(nameof(fieldInfo)); + try + { + if (fieldInfo.IsStatic) + throw new ArgumentException("Field must not be static"); + var needCastclass = false; + if (fieldInfo.DeclaringType is Type declaringType) + { + // When fieldInfo is passed to FieldRefAccess methods, the T generic class constraint is insufficient to ensure that + // the field is not a struct instance field, since T could be object, ValueType, or an interface that the struct implements. + if (declaringType.IsValueType) + throw new ArgumentException("FieldDeclaringType must be a class"); + needCastclass = FieldRefNeedsClasscast(typeof(T), declaringType); + } + return ref FieldRefAccessInternal(fieldInfo, needCastclass)(instance); + } + catch (Exception ex) + { + throw new ArgumentException($"FieldRefAccess<{typeof(T)}, {typeof(F)}> for {instance}, {fieldInfo} caused an exception", ex); + } + } - var dm = new DynamicMethodDefinition(s_name, typeof(F).MakeByRefType(), new[] { typeof(T) }); + static bool FieldRefNeedsClasscast(Type delegateInstanceType, Type declaringType) + { + var needCastclass = false; + if (delegateInstanceType != declaringType) + { + needCastclass = delegateInstanceType.IsAssignableFrom(declaringType); + if (needCastclass is false && declaringType.IsAssignableFrom(delegateInstanceType) is false) + throw new ArgumentException("FieldDeclaringType must be assignable from or to T (FieldRefAccess instance type) - " + + "\"instanceOfT is FieldDeclaringType\" must be possible"); + } + return needCastclass; + } + + static FieldRef FieldRefAccessInternal(FieldInfo fieldInfo, bool needCastclass) where T : class + { + ValidateFieldType(fieldInfo); + var delegateInstanceType = typeof(T); + var declaringType = fieldInfo.DeclaringType; + + var dm = new DynamicMethodDefinition($"__refget_{delegateInstanceType.Name}_fi_{fieldInfo.Name}", + typeof(F).MakeByRefType(), new[] { delegateInstanceType }); + + var il = dm.GetILGenerator(); + // Backwards compatibility: This supports static fields, even those defined in structs. + if (fieldInfo.IsStatic) + { + // ldarg.0 + ldflda actually works for static fields, but the potential castclass (and InvalidCastException) below must be avoided + // so might as well use the singular ldsflda for static fields. + il.Emit(OpCodes.Ldsflda, fieldInfo); + } + else + { + il.Emit(OpCodes.Ldarg_0); + // The castclass is needed when T is a parent class or interface of declaring type (e.g. if T is object), + // since there's no guarantee the instance passed to the delegate is actually of the declaring type. + // In such a situation, the castclass will throw an InvalidCastException and thus prevent undefined behavior. + if (needCastclass) + il.Emit(OpCodes.Castclass, declaringType); + il.Emit(OpCodes.Ldflda, fieldInfo); + } + il.Emit(OpCodes.Ret); + + return (FieldRef)dm.Generate().CreateDelegate(typeof(FieldRef)); + } + + /// A readable/assignable reference delegate to an instance field of a struct + /// The struct that defines the instance field + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// A reference to the runtime instance to access the field + /// A readable/assignable reference to the field + /// + public delegate ref F StructFieldRef(ref T instance) where T : struct; + + /// Creates a field reference delegate for an instance field of a struct + /// The struct that defines the instance field + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// The name of the field + /// A readable/assignable delegate + /// + public static StructFieldRef StructFieldRefAccess(string fieldName) where T : struct + { + if (fieldName is null) + throw new ArgumentNullException(nameof(fieldName)); + try + { + return StructFieldRefAccessInternal(GetInstanceField(typeof(T), fieldName)); + } + catch (Exception ex) + { + throw new ArgumentException($"StructFieldRefAccess<{typeof(T)}, {typeof(F)}> for {fieldName} caused an exception", ex); + } + } + + /// Creates an instance field reference for a specific instance of a struct + /// The struct that defines the instance field + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// The instance + /// The name of the field + /// A readable/assignable reference to the field + /// + public static ref F StructFieldRefAccess(ref T instance, string fieldName) where T : struct + { + if (fieldName is null) + throw new ArgumentNullException(nameof(fieldName)); + try + { + return ref StructFieldRefAccessInternal(GetInstanceField(typeof(T), fieldName))(ref instance); + } + catch (Exception ex) + { + throw new ArgumentException($"StructFieldRefAccess<{typeof(T)}, {typeof(F)}> for {instance}, {fieldName} caused an exception", ex); + } + } + + /// Creates a field reference delegate for an instance field of a struct + /// The struct that defines the instance field + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// The field + /// A readable/assignable delegate + public static StructFieldRef StructFieldRefAccess(FieldInfo fieldInfo) where T : struct + { + if (fieldInfo is null) + throw new ArgumentNullException(nameof(fieldInfo)); + try + { + ValidateStructField(fieldInfo); + return StructFieldRefAccessInternal(fieldInfo); + } + catch (Exception ex) + { + throw new ArgumentException($"StructFieldRefAccess<{typeof(T)}, {typeof(F)}> for {fieldInfo} caused an exception", ex); + } + } + + /// Creates a field reference for an instance field of a struct + /// The struct that defines the instance field + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// The instance + /// The field + /// A readable/assignable reference to the field + /// + public static ref F StructFieldRefAccess(ref T instance, FieldInfo fieldInfo) where T : struct + { + if (fieldInfo is null) + throw new ArgumentNullException(nameof(fieldInfo)); + try + { + ValidateStructField(fieldInfo); + return ref StructFieldRefAccessInternal(fieldInfo)(ref instance); + } + catch (Exception ex) + { + throw new ArgumentException($"StructFieldRefAccess<{typeof(T)}, {typeof(F)}> for {instance}, {fieldInfo} caused an exception", ex); + } + } + + static void ValidateStructField(FieldInfo fieldInfo) where T : struct + { + if (fieldInfo.IsStatic) + throw new ArgumentException("Field must not be static"); + if (fieldInfo.DeclaringType != typeof(T)) + throw new ArgumentException("FieldDeclaringType must be T (StructFieldRefAccess instance type)"); + } + + static StructFieldRef StructFieldRefAccessInternal(FieldInfo fieldInfo) where T : struct + { + ValidateFieldType(fieldInfo); + + var dm = new DynamicMethodDefinition($"__refget_{typeof(T).Name}_struct_fi_{fieldInfo.Name}", + typeof(F).MakeByRefType(), new[] { typeof(T).MakeByRefType() }); var il = dm.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldflda, fieldInfo); il.Emit(OpCodes.Ret); - return (FieldRef)dm.Generate().CreateDelegate(typeof(FieldRef)); + return (StructFieldRef)dm.Generate().CreateDelegate(typeof(StructFieldRef)); } + static FieldInfo GetInstanceField(Type type, string fieldName) + { + var fieldInfo = Field(type, fieldName); + if (fieldInfo is null) + throw new MissingFieldException(type.Name, fieldName); + if (fieldInfo.IsStatic) + throw new ArgumentException("Field must not be static"); + return fieldInfo; + } + + /// A readable/assignable reference delegate to a static field + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// A readable/assignable reference to the field + /// + public delegate ref F FieldRef(); + /// Creates a static field reference - /// The class the field is defined in or "object" if type cannot be accessed at compile time - /// The type of the field + /// The type (can be class or struct) the field is defined in + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// /// The name of the field - /// An readable/assignable object representing the static field + /// A readable/assignable reference to the field /// public static ref F StaticFieldRefAccess(string fieldName) { @@ -876,67 +1216,86 @@ namespace HarmonyLib } /// Creates a static field reference - /// The type of the field - /// The class/type + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// + /// The type (can be class or struct) the field is defined in /// The name of the field - /// An readable/assignable object representing the static field + /// A readable/assignable reference to the field /// public static ref F StaticFieldRefAccess(Type type, string fieldName) { - const BindingFlags bf = BindingFlags.NonPublic | - BindingFlags.Static | - BindingFlags.DeclaredOnly; try { - var fi = type.GetField(fieldName, bf); - return ref StaticFieldRefAccess(fi)(); + var fieldInfo = Field(type, fieldName); + if (fieldInfo is null) + throw new MissingFieldException(type.Name, fieldName); + return ref StaticFieldRefAccessInternal(fieldInfo)(); } catch (Exception ex) { throw new ArgumentException($"StaticFieldRefAccess<{typeof(F)}> for {type}, {fieldName} caused an exception", ex); - throw; } } /// Creates a static field reference - /// The class the field is defined in or "object" if type cannot be accessed at compile time - /// The type of the field + /// An arbitrary type (by convention, the type the field is defined in) + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// /// The field - /// An readable/assignable object representing the static field + /// A readable/assignable reference to the field + /// + /// The type parameter is only used in exception messaging and to distinguish between this method overload + /// and the overload (which returns a rather than a reference). + /// /// public static ref F StaticFieldRefAccess(FieldInfo fieldInfo) { + if (fieldInfo is null) + throw new ArgumentNullException(nameof(fieldInfo)); try { - return ref StaticFieldRefAccess(fieldInfo)(); + return ref StaticFieldRefAccessInternal(fieldInfo)(); } catch (Exception ex) { throw new ArgumentException($"StaticFieldRefAccess<{typeof(T)}, {typeof(F)}> for {fieldInfo} caused an exception", ex); - throw; } } - /// A read/writable reference delegate to a static field - /// The type of the field - /// An readable/assignable object representing the static field - /// - public delegate ref F FieldRef(); - /// Creates a static field reference delegate - /// The type of the field + /// + /// The type of the field; or if the field's type is a reference type (a class or interface, NOT a struct or other value type), + /// a type that is assignable from the field's type + /// /// The field - /// A read and writable delegate + /// A readable/assignable delegate /// public static FieldRef StaticFieldRefAccess(FieldInfo fieldInfo) { - if (fieldInfo == null) + if (fieldInfo is null) throw new ArgumentNullException(nameof(fieldInfo)); - var type = fieldInfo.DeclaringType; + try + { + return StaticFieldRefAccessInternal(fieldInfo); + } + catch (Exception ex) + { + throw new ArgumentException($"StaticFieldRefAccess<{typeof(F)}> for {fieldInfo} caused an exception", ex); + } + } - var s_name = $"__refget_{type?.Name ?? "null"}_static_fi_{fieldInfo.Name}"; + static FieldRef StaticFieldRefAccessInternal(FieldInfo fieldInfo) + { + if (fieldInfo.IsStatic is false) + throw new ArgumentException("Field must be static"); + ValidateFieldType(fieldInfo); - var dm = new DynamicMethodDefinition(s_name, typeof(F).MakeByRefType(), new Type[0]); + var dm = new DynamicMethodDefinition($"__refget_{fieldInfo.DeclaringType?.Name ?? "null"}_static_fi_{fieldInfo.Name}", + typeof(F).MakeByRefType(), new Type[0]); var il = dm.GetILGenerator(); il.Emit(OpCodes.Ldsflda, fieldInfo); @@ -945,6 +1304,22 @@ namespace HarmonyLib return (FieldRef)dm.Generate().CreateDelegate(typeof(FieldRef)); } + static void ValidateFieldType(FieldInfo fieldInfo) + { + var fieldType = fieldInfo.FieldType; + if (fieldType.IsValueType) + { + // Boxing/unboxing is not allowed for ref values of value types. + if (typeof(F) != fieldType) + throw new ArgumentException("FieldRefAccess return type must be the same as FieldType for value types"); + } + else + { + if (typeof(F).IsAssignableFrom(fieldType) is false) + throw new ArgumentException("FieldRefAccess return type must be assignable from FieldType for reference types"); + } + } + /// Creates a delegate to a given method /// The delegate Type /// The method to create a delegate from. diff --git a/HarmonyTests/Tools/TestFieldRefAccess.cs b/HarmonyTests/Tools/TestFieldRefAccess.cs index 0a9273d5..46cdb7e2 100644 --- a/HarmonyTests/Tools/TestFieldRefAccess.cs +++ b/HarmonyTests/Tools/TestFieldRefAccess.cs @@ -57,31 +57,30 @@ namespace HarmonyLibTests } } - // TODO: AccessTools.StructFieldRefAccess - //static IATestCase ATestCase(AccessTools.StructFieldRef fieldRef) where T : struct - //{ - // return new StructFieldRefTestCase(fieldRef); - //} + static IATestCase ATestCase(AccessTools.StructFieldRef fieldRef) where T : struct + { + return new StructFieldRefTestCase(fieldRef); + } - //class StructFieldRefTestCase : IATestCase where T : struct - //{ - // readonly AccessTools.StructFieldRef fieldRef; + class StructFieldRefTestCase : IATestCase where T : struct + { + readonly AccessTools.StructFieldRef fieldRef; - // public StructFieldRefTestCase(AccessTools.StructFieldRef fieldRef) - // { - // this.fieldRef = fieldRef; - // } + public StructFieldRefTestCase(AccessTools.StructFieldRef fieldRef) + { + this.fieldRef = fieldRef; + } - // public F Get(ref T instance) - // { - // return fieldRef(ref instance); - // } + public F Get(ref T instance) + { + return fieldRef(ref instance); + } - // public void Set(ref T instance, F value) - // { - // fieldRef(ref instance) = value; - // } - //} + public void Set(ref T instance, F value) + { + fieldRef(ref instance) = value; + } + } // AccessTools.StaticFieldRefAccess static IATestCase ATestCase(AccessTools.FieldRef fieldRef) @@ -240,14 +239,6 @@ namespace HarmonyLibTests var value = testCase.Get(ref instance); // The ?.ToString() is a trick to ensure that value is fully evaluated from the ref value. _ = value?.ToString(); - // If the constraint is just Throws.Exception (rather than Throws.InstanceOf(field)(instance)"] = ATestCase(instance => ref AccessTools.FieldRefAccess(field)(instance)), ["FieldRefAccess(field)()"] = ATestCase(instance => ref AccessTools.FieldRefAccess(field)()), - //["FieldRefAccess(instance, field)"] = ATestCase(instance => ref AccessTools.FieldRefAccess(instance, field)), // TODO: implement this overload + ["FieldRefAccess(instance, field)"] = ATestCase(instance => ref AccessTools.FieldRefAccess(instance, field)), }; } - // TODO: Once generic class constraint is added to most FieldRefAccess methods, remove the calls that are no longer compilable. static Dictionary> AvailableTestCases_FieldRefAccess_Struct_ByName( string fieldName) where T : struct { return new Dictionary> { - ["FieldRefAccess(fieldName)(instance)"] = ATestCase(instance => ref AccessTools.FieldRefAccess(fieldName)(instance)), - ["FieldRefAccess(instance, fieldName)"] = ATestCase(instance => ref AccessTools.FieldRefAccess(instance, fieldName)), ["FieldRefAccess(typeof(T), fieldName)(instance)"] = ATestCase(instance => ref AccessTools.FieldRefAccess(typeof(T), fieldName)(instance)), ["FieldRefAccess(typeof(T), fieldName)()"] = ATestCase(instance => ref AccessTools.FieldRefAccess(typeof(T), fieldName)()), }; } - // TODO: Once generic class constraint is added to most FieldRefAccess methods, remove the calls that are no longer compilable. - static Dictionary> AvailableTestCases_FieldRefAccess_Struct_ByFieldInfo( - FieldInfo field) where T : struct - { - return new Dictionary> - { - ["FieldRefAccess(field)(instance)"] = ATestCase(instance => ref AccessTools.FieldRefAccess(field)(instance)), - ["FieldRefAccess(field)()"] = ATestCase(instance => ref AccessTools.FieldRefAccess(field)()), - }; - } - - // TODO: StructFieldRefAccess static Dictionary> AvailableTestCases_StructFieldRefAccess(FieldInfo field, string fieldName) where T : struct { return new Dictionary> { - //["StructFieldRefAccess(fieldName)(ref instance)"] = ATestCase((ref T instance) => ref AccessTools.StructFieldRefAccess(fieldName)(ref instance)), - //["StructFieldRefAccess(ref instance, fieldName)"] = ATestCase((ref T instance) => ref AccessTools.StructFieldRefAccess(ref instance, fieldName)), - //["StructFieldRefAccess(field)(ref instance)"] = ATestCase((ref T instance) => ref AccessTools.StructFieldRefAccess(field)(ref instance)), - //["StructFieldRefAccess(ref instance, field)"] = ATestCase((ref T instance) => ref AccessTools.StructFieldRefAccess(ref instance, field)), + ["StructFieldRefAccess(fieldName)(ref instance)"] = ATestCase((ref T instance) => ref AccessTools.StructFieldRefAccess(fieldName)(ref instance)), + ["StructFieldRefAccess(ref instance, fieldName)"] = ATestCase((ref T instance) => ref AccessTools.StructFieldRefAccess(ref instance, fieldName)), + ["StructFieldRefAccess(field)(ref instance)"] = ATestCase((ref T instance) => ref AccessTools.StructFieldRefAccess(field)(ref instance)), + ["StructFieldRefAccess(ref instance, field)"] = ATestCase((ref T instance) => ref AccessTools.StructFieldRefAccess(ref instance, field)), }; } @@ -383,7 +359,6 @@ namespace HarmonyLibTests var availableTestCases = Merge( AvailableTestCases_StructFieldRefAccess(field, field.Name), AvailableTestCases_FieldRefAccess_Struct_ByName(field.Name), - AvailableTestCases_FieldRefAccess_Struct_ByFieldInfo(field), AvailableTestCases_StaticFieldRefAccess_ByName(field.Name), AvailableTestCases_StaticFieldRefAccess_ByFieldInfo(field)); new ATestSuite(typeof(T), field, testValue, expectedCaseToConstraint, availableTestCases).Run(); @@ -404,36 +379,12 @@ namespace HarmonyLibTests return newExpectedCaseToConstraint; } - // TODO: This shouldn't exist - public fields should be treated equivalently as private fields. - static Dictionary PublicField(Dictionary expectedCaseToConstraint) - { - return expectedCaseToConstraint.Merge(ReusableConstraints(new Dictionary - { - ["FieldRefAccess(fieldName)(instance)"] = Throws.InstanceOf(), - ["FieldRefAccess(instance, fieldName)"] = Throws.InstanceOf(), - ["StaticFieldRefAccess(fieldName)"] = Throws.InstanceOf(), - ["StaticFieldRefAccess(typeof(T), fieldName)"] = Throws.InstanceOf(), - }).Where(pair => expectedCaseToConstraint.ContainsKey(pair.Key))); - } - - // TODO: This shouldn't exist - FieldRefAccess's T=object special-casing should be generalized to handle any type assignable from field's declaring type. - // Only FieldMissingOnTypeT should be used when T is an interface type. - static Dictionary InterfaceT(Dictionary expectedCaseToConstraint) - { - return expectedCaseToConstraint.Merge(ReusableConstraints(new Dictionary - { - ["FieldRefAccess(field)(instance)"] = Throws.Exception, - ["FieldRefAccess(field)()"] = Throws.Exception, - }).Where(pair => expectedCaseToConstraint.ContainsKey(pair.Key))); - } - static Dictionary FieldMissingOnTypeT(Dictionary expectedCaseToConstraint) { return expectedCaseToConstraint.Merge(ReusableConstraints(new Dictionary { - // TODO: StructFieldRefAccess - //["StructFieldRefAccess(fieldName)(ref instance)"] = Throws.InstanceOf(), - //["StructFieldRefAccess(ref instance, fieldName)"] = Throws.InstanceOf(), + ["StructFieldRefAccess(fieldName)(ref instance)"] = Throws.InstanceOf(), + ["StructFieldRefAccess(ref instance, fieldName)"] = Throws.InstanceOf(), ["FieldRefAccess(fieldName)(instance)"] = Throws.InstanceOf(), ["FieldRefAccess(instance, fieldName)"] = Throws.InstanceOf(), ["FieldRefAccess(typeof(T), fieldName)(instance)"] = Throws.InstanceOf(), @@ -447,14 +398,21 @@ namespace HarmonyLibTests { // Given that type T must be assignable from instance type, and that instance type is incompatible with field's declaring type, // assume that the field cannot be found on type T. - return FieldMissingOnTypeT(expectedCaseToConstraint).Merge(ReusableConstraints(new Dictionary + var newExpectedCaseToConstraint = FieldMissingOnTypeT(expectedCaseToConstraint).Merge(ReusableConstraints(new Dictionary { - // TODO: StructFieldRefAccess - //["StructFieldRefAccess(field)(ref instance)"] = Throws.InstanceOf(), - //["StructFieldRefAccess(ref instance, field)"] = Throws.InstanceOf(), - ["FieldRefAccess(field)(instance)"] = SkipTest("incompatible instance type can cause crash"), // TODO: should be InvalidCastException if not already another Throws constraint - //["FieldRefAccess(instance, field)"] = Throws.InstanceOf(), // TODO: implement this overload + ["StructFieldRefAccess(field)(ref instance)"] = Throws.InstanceOf(), + ["StructFieldRefAccess(ref instance, field)"] = Throws.InstanceOf(), + ["FieldRefAccess(instance, field)"] = Throws.InstanceOf(), }).Where(pair => expectedCaseToConstraint.ContainsKey(pair.Key))); + // Only override Throws.Nothing constraint with InvalidCastException for these test cases, + // since other Throws constraints should have precedence over InvalidCastException: + // - ArgumentException is only thrown from FieldRefAccess + // - NullReferenceException is only thrown when invoking FieldRefAccess-returned delegate with null instance + // - InvalidCastException is only thrown when invoking FieldRefAccess-returned delegate with an instance of incompatible type + return newExpectedCaseToConstraint.Merge(ReusableConstraints(new Dictionary + { + ["FieldRefAccess(field)(instance)"] = Throws.TypeOf(), + }).Where(pair => expectedCaseToConstraint.TryGetValue(pair.Key, out var constraint) && constraint.Resolve() is ThrowsNothingConstraint)); } static Dictionary IncompatibleTypeT(Dictionary expectedCaseToConstraint) @@ -464,45 +422,8 @@ namespace HarmonyLibTests // Also assume that the field cannot be found on type T (already assumed in IncompatibleInstanceType). return IncompatibleInstanceType(expectedCaseToConstraint).Merge(ReusableConstraints(new Dictionary { - ["FieldRefAccess(field)(instance)"] = Throws.Exception, // TODO: should be ArgumentException - ["FieldRefAccess(field)()"] = Throws.Exception, // TODO: should be ArgumentException - }).Where(pair => expectedCaseToConstraint.ContainsKey(pair.Key))); - } - - // TODO: This shouldn't exist - FieldRefAccess should ignore T for static fields. - static Dictionary StaticIncompatibleTypeT(Dictionary expectedCaseToConstraint) - { - return expectedCaseToConstraint.Merge(ReusableConstraints(new Dictionary - { - ["FieldRefAccess(field)(instance)"] = Throws.Exception, - ["FieldRefAccess(field)()"] = Throws.Exception, - }).Where(pair => expectedCaseToConstraint.ContainsKey(pair.Key))); - } - - // TODO: This shouldn't exist - FieldRefAccess should be using AccessTools.Field for field search. - // For static and non-protected / public / internal-and-same-assembly instance fields declared in parent classes. - static Dictionary FieldNotInheritedBySubClass(Dictionary expectedCaseToConstraint) - { - return expectedCaseToConstraint.Merge(ReusableConstraints(new Dictionary - { - // Following search for only declared fields (excludes all fields from parents). - ["FieldRefAccess(fieldName)(instance)"] = Throws.InstanceOf(), - ["FieldRefAccess(instance, fieldName)"] = Throws.InstanceOf(), - ["StaticFieldRefAccess(fieldName)"] = Throws.InstanceOf(), - ["StaticFieldRefAccess(typeof(T), fieldName)"] = Throws.InstanceOf(), - }).Where(pair => expectedCaseToConstraint.ContainsKey(pair.Key))); - } - - // TODO: This shouldn't exist - FieldRefAccess should be using AccessTools.Field for field search. - // For public / protected / internal-and-same-assembly instance fields declared in parent classes. - static Dictionary FieldInheritedBySubClass(Dictionary expectedCaseToConstraint) - { - return expectedCaseToConstraint.Merge(ReusableConstraints(new Dictionary - { - // Following search for only declared fields (excludes all fields from parents). - // This doesn't include StaticFieldRefAccess since FieldNotInheritedBySubClass should always be used instead for such fields. - ["FieldRefAccess(fieldName)(instance)"] = Throws.InstanceOf(), - ["FieldRefAccess(instance, fieldName)"] = Throws.InstanceOf(), + ["FieldRefAccess(field)(instance)"] = Throws.InstanceOf(), + ["FieldRefAccess(field)()"] = Throws.InstanceOf(), }).Where(pair => expectedCaseToConstraint.ContainsKey(pair.Key))); } @@ -517,11 +438,7 @@ namespace HarmonyLibTests continue; var expectedExceptionType = TestTools.ThrowsConstraintExceptionType(expectedConstraint); if (expectedExceptionType is null || expectedExceptionType == typeof(NullReferenceException)) - { - // TODO: StaticFieldRefAccess should throw ArgumentException just like FieldRefAccess. - newExpectedCaseToConstraint[testCaseName] = new ReusableConstraint(Throws.InstanceOf( - testCaseName.StartsWith("StaticFieldRefAccess") ? typeof(IncompatibleFieldTypeException) : typeof(ArgumentException))); - } + newExpectedCaseToConstraint[testCaseName] = new ReusableConstraint(Throws.InstanceOf()); } return newExpectedCaseToConstraint; } @@ -535,11 +452,11 @@ namespace HarmonyLibTests ["FieldRefAccess(typeof(T), fieldName)()"] = Throws.TypeOf(), ["FieldRefAccess(field)(instance)"] = Throws.Nothing, ["FieldRefAccess(field)()"] = Throws.TypeOf(), - //["FieldRefAccess(instance, field)"] = Throws.Nothing, // TODO: implement this overload + ["FieldRefAccess(instance, field)"] = Throws.Nothing, ["StaticFieldRefAccess(fieldName)"] = Throws.InstanceOf(), ["StaticFieldRefAccess(typeof(T), fieldName)"] = Throws.InstanceOf(), - ["StaticFieldRefAccess(field)()"] = Throws.Exception, // TODO: should be ArgumentException - ["StaticFieldRefAccess(field)"] = Throws.Exception, // TODO: should be ArgumentException + ["StaticFieldRefAccess(field)()"] = Throws.InstanceOf(), + ["StaticFieldRefAccess(field)"] = Throws.InstanceOf(), }); static readonly Dictionary expectedCaseToConstraint_ClassStatic = @@ -551,7 +468,7 @@ namespace HarmonyLibTests ["FieldRefAccess(typeof(T), fieldName)()"] = Throws.Nothing, ["FieldRefAccess(field)(instance)"] = Throws.Nothing, // T is ignored ["FieldRefAccess(field)()"] = Throws.Nothing, // T is ignored - //["FieldRefAccess(instance, field)"] = Throws.InstanceOf(), // TODO: implement this overload + ["FieldRefAccess(instance, field)"] = Throws.InstanceOf(), ["StaticFieldRefAccess(fieldName)"] = Throws.Nothing, ["StaticFieldRefAccess(typeof(T), fieldName)"] = Throws.Nothing, ["StaticFieldRefAccess(field)()"] = Throws.Nothing, @@ -561,41 +478,27 @@ namespace HarmonyLibTests static readonly Dictionary expectedCaseToConstraint_StructInstance = ReusableConstraints(new Dictionary { - // TODO: StructFieldRefAccess - //["StructFieldRefAccess(fieldName)(ref instance)"] = Throws.Nothing, - //["StructFieldRefAccess(ref instance, fieldName)"] = Throws.Nothing, - //["StructFieldRefAccess(field)(ref instance)"] = Throws.Nothing, - //["StructFieldRefAccess(ref instance, field)"] = Throws.Nothing, - ["FieldRefAccess(fieldName)(instance)"] = SkipTest("struct instance can cause crash"), // TODO: will be non-compilable due to class constraint - ["FieldRefAccess(instance, fieldName)"] = SkipTest("struct instance can cause crash"), // TODO: will be non-compilable due to class constraint - ["FieldRefAccess(typeof(T), fieldName)(instance)"] = SkipTest("struct instance can cause crash"), // TODO: should be ArgumentException - ["FieldRefAccess(typeof(T), fieldName)()"] = Throws.TypeOf(), // TODO: should be ArgumentException - ["FieldRefAccess(field)(instance)"] = SkipTest("struct instance can cause crash"), // TODO: will be non-compilable due to class constraint - ["FieldRefAccess(field)()"] = SkipTest("struct instance can cause crash"), // TODO: will be non-compilable due to class constraint + ["StructFieldRefAccess(fieldName)(ref instance)"] = Throws.Nothing, + ["StructFieldRefAccess(ref instance, fieldName)"] = Throws.Nothing, + ["StructFieldRefAccess(field)(ref instance)"] = Throws.Nothing, + ["StructFieldRefAccess(ref instance, field)"] = Throws.Nothing, + ["FieldRefAccess(typeof(T), fieldName)(instance)"] = Throws.InstanceOf(), + ["FieldRefAccess(typeof(T), fieldName)()"] = Throws.InstanceOf(), ["StaticFieldRefAccess(fieldName)"] = Throws.InstanceOf(), ["StaticFieldRefAccess(typeof(T), fieldName)"] = Throws.InstanceOf(), - ["StaticFieldRefAccess(field)()"] = SkipTest("struct instance can cause crash"), // TODO: should be ArgumentException - ["StaticFieldRefAccess(field)"] = SkipTest("struct instance can cause crash"), // TODO: should be ArgumentException + ["StaticFieldRefAccess(field)()"] = Throws.InstanceOf(), + ["StaticFieldRefAccess(field)"] = Throws.InstanceOf(), }); - // TODO: This shouldn't need to exist. - static IResolveConstraint MonoThrowsException => AccessTools.IsMonoRuntime ? - (IResolveConstraint)Throws.Exception : Throws.Nothing; - static readonly Dictionary expectedCaseToConstraint_StructStatic = ReusableConstraints(new Dictionary { - // TODO: StructFieldRefAccess - //["StructFieldRefAccess(fieldName)(ref instance)"] = Throws.InstanceOf(), - //["StructFieldRefAccess(ref instance, fieldName)"] = Throws.InstanceOf(), - //["StructFieldRefAccess(field)(ref instance)"] = Throws.InstanceOf(), - //["StructFieldRefAccess(ref instance, field)"] = Throws.InstanceOf(), - ["FieldRefAccess(fieldName)(instance)"] = Throws.InstanceOf(), // TODO: will be non-compilable due to class constraint - ["FieldRefAccess(instance, fieldName)"] = Throws.InstanceOf(), // TODO: will be non-compilable due to class constraint + ["StructFieldRefAccess(fieldName)(ref instance)"] = Throws.InstanceOf(), + ["StructFieldRefAccess(ref instance, fieldName)"] = Throws.InstanceOf(), + ["StructFieldRefAccess(field)(ref instance)"] = Throws.InstanceOf(), + ["StructFieldRefAccess(ref instance, field)"] = Throws.InstanceOf(), ["FieldRefAccess(typeof(T), fieldName)(instance)"] = Throws.Nothing, ["FieldRefAccess(typeof(T), fieldName)()"] = Throws.Nothing, - ["FieldRefAccess(field)(instance)"] = MonoThrowsException, // TODO: will be non-compilable due to class constraint - ["FieldRefAccess(field)()"] = MonoThrowsException, // TODO: will be non-compilable due to class constraint ["StaticFieldRefAccess(fieldName)"] = Throws.Nothing, ["StaticFieldRefAccess(typeof(T), fieldName)"] = Throws.Nothing, ["StaticFieldRefAccess(field)()"] = Throws.Nothing, @@ -607,17 +510,15 @@ namespace HarmonyLibTests IncompatibleTypeT(expectedCaseToConstraint_StructInstance); static readonly Dictionary expectedCaseToConstraint_ClassStatic_StructT = - StaticIncompatibleTypeT(FieldMissingOnTypeT(expectedCaseToConstraint_StructStatic)); + FieldMissingOnTypeT(expectedCaseToConstraint_StructStatic); // AccessToolsStruct is compatible with object/ValueType/IAccessToolsType reference types (classes/interfaces), so NOT using IncompatibleTypeT here. static readonly Dictionary expectedCaseToConstraint_StructInstance_ClassT = FieldMissingOnTypeT(expectedCaseToConstraint_ClassInstance).Merge(ReusableConstraints(new Dictionary { - ["FieldRefAccess(field)(instance)"] = SkipTest("struct instance can cause crash"), // TODO: should be ArgumentException - ["FieldRefAccess(field)()"] = Throws.TypeOf(), // TODO: should be ArgumentException - //["FieldRefAccess(instance, field)"] = Throws.InstanceOf(), // TODO: implement this overload - ["StaticFieldRefAccess(field)()"] = SkipTest("struct instance can cause crash"), // TODO: should be ArgumentException - ["StaticFieldRefAccess(field)"] = SkipTest("struct instance can cause crash"), // TODO: should be ArgumentException + ["FieldRefAccess(field)(instance)"] = Throws.InstanceOf(), + ["FieldRefAccess(field)()"] = Throws.InstanceOf(), + ["FieldRefAccess(instance, field)"] = Throws.InstanceOf(), })); static readonly Dictionary expectedCaseToConstraint_StructStatic_ClassT = @@ -633,11 +534,11 @@ namespace HarmonyLibTests TestSuite_Class( field, "field1test", expectedCaseToConstraint); TestSuite_Class( - field, "field1test", FieldInheritedBySubClass(expectedCaseToConstraint)); + field, "field1test", expectedCaseToConstraint); TestSuite_Class( field, "field1test", expectedCaseToConstraint); TestSuite_Class( - field, "field1test", InterfaceT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, "field1test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( field, "field1test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( @@ -652,10 +553,8 @@ namespace HarmonyLibTests field, "field1test", expectedCaseToConstraint); TestSuite_Class( field, new[] { "should always throw" }, IncompatibleFieldType(expectedCaseToConstraint)); - // TODO: Following tests will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error. - // Fix StaticFieldRefAccess to consistently throw ArgumentException when field type is incompatible with F. - //TestSuite_Class( - // field, 1337, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 1337, IncompatibleFieldType(expectedCaseToConstraint)); }); } @@ -665,15 +564,15 @@ namespace HarmonyLibTests Assert.Multiple(() => { var field = AccessTools.Field(typeof(AccessToolsClass), "field2"); - var expectedCaseToConstraint = PublicField(expectedCaseToConstraint_ClassInstance); + var expectedCaseToConstraint = expectedCaseToConstraint_ClassInstance; TestSuite_Class( field, 314f, expectedCaseToConstraint); TestSuite_Class( - field, 314f, FieldInheritedBySubClass(expectedCaseToConstraint)); + field, 314f, expectedCaseToConstraint); TestSuite_Class( field, 314f, expectedCaseToConstraint); TestSuite_Class( - field, 314f, InterfaceT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, 314f, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( field, 314f, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( @@ -682,18 +581,16 @@ namespace HarmonyLibTests field, 314f, IncompatibleTypeT(expectedCaseToConstraint)); TestSuite_Struct( field, 314f, expectedCaseToConstraint_ClassInstance_StructT); - // TODO: Following tests will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error. - // Fix *FieldRefAccess to consistently throw ArgumentException when field type is a value type and F is a different type. - //TestSuite_Class( - // field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 314f, IncompatibleFieldType(expectedCaseToConstraint)); }); } @@ -703,34 +600,32 @@ namespace HarmonyLibTests Assert.Multiple(() => { var field = AccessTools.Field(typeof(AccessToolsClass), "field3"); - var expectedCaseToConstraint = PublicField(expectedCaseToConstraint_ClassStatic); + var expectedCaseToConstraint = expectedCaseToConstraint_ClassStatic; // Note: As this is as static field, instance type is ignored, so IncompatibleInstanceType is never needed. TestSuite_Class( field, 314L, expectedCaseToConstraint); TestSuite_Class( - field, 314L, FieldNotInheritedBySubClass(expectedCaseToConstraint)); + field, 314L, expectedCaseToConstraint); TestSuite_Class( - field, 314L, InterfaceT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, 314L, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( field, 314L, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( field, 314L, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( - field, 314L, StaticIncompatibleTypeT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, 314L, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Struct( - field, 314L, PublicField(expectedCaseToConstraint_ClassStatic_StructT)); - // TODO: Following tests will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error. - // Fix *FieldRefAccess to consistently throw ArgumentException when field type is a value type and F is a different type. - //TestSuite_Class( - // field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); + field, 314L, expectedCaseToConstraint_ClassStatic_StructT); + TestSuite_Class( + field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 314L, IncompatibleFieldType(expectedCaseToConstraint)); }); } @@ -745,15 +640,15 @@ namespace HarmonyLibTests TestSuite_Class( field, "field4test", expectedCaseToConstraint); TestSuite_Class( - field, "field4test", FieldNotInheritedBySubClass(expectedCaseToConstraint)); + field, "field4test", expectedCaseToConstraint); TestSuite_Class( - field, "field4test", InterfaceT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, "field4test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( field, "field4test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( field, "field4test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( - field, "field4test", StaticIncompatibleTypeT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, "field4test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Struct( field, "field4test", expectedCaseToConstraint_ClassStatic_StructT); TestSuite_Class( @@ -762,10 +657,8 @@ namespace HarmonyLibTests field, "field4test", expectedCaseToConstraint); TestSuite_Class>( field, new[] { "should always throw" }, IncompatibleFieldType(expectedCaseToConstraint)); - // TODO: Following tests will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error. - // Fix StaticFieldRefAccess to consistently throw ArgumentException when field type is incompatible with F. - //TestSuite_Class( - // field, 1337, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 1337, IncompatibleFieldType(expectedCaseToConstraint)); }); } @@ -776,14 +669,14 @@ namespace HarmonyLibTests { var field = AccessTools.Field(typeof(AccessToolsSubClass), "subclassField1"); var expectedCaseToConstraint = expectedCaseToConstraint_ClassInstance; - TestSuite_Class( // TODO: should be IncompatibleInstanceType once T=object special-casing is generalized - field, "subclassField1test", IncompatibleTypeT(expectedCaseToConstraint)); + TestSuite_Class( + field, "subclassField1test", IncompatibleInstanceType(expectedCaseToConstraint)); TestSuite_Class( field, "subclassField1test", expectedCaseToConstraint); - TestSuite_Class( // TODO: should be FieldMissingOnTypeT once T=object special-casing is generalized - field, "subclassField1test", IncompatibleTypeT(expectedCaseToConstraint)); + TestSuite_Class( + field, "subclassField1test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( - field, "subclassField1test", InterfaceT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, "subclassField1test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( field, "subclassField1test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( @@ -812,11 +705,11 @@ namespace HarmonyLibTests var expectedCaseToConstraint = expectedCaseToConstraint_ClassStatic; // Note: As this is as static field, instance type is ignored, so IncompatibleInstanceType is never needed. TestSuite_Class( - field, 123, StaticIncompatibleTypeT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, 123, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( field, 123, expectedCaseToConstraint); TestSuite_Class( - field, 123, InterfaceT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, 123, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( field, 123, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( @@ -824,21 +717,19 @@ namespace HarmonyLibTests TestSuite_Class( field, 123, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Class( - field, 123, StaticIncompatibleTypeT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, 123, FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Struct( field, 123, expectedCaseToConstraint_ClassStatic_StructT); - // TODO: Following tests will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error. - // Fix *FieldRefAccess to consistently throw ArgumentException when field type is a value type and F is a different type. - //TestSuite_Class( - // field, 123, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 123, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 123, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 123, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Class( - // field, 123, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 123, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 123, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 123, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 123, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Class( + field, 123, IncompatibleFieldType(expectedCaseToConstraint)); }); } @@ -848,12 +739,12 @@ namespace HarmonyLibTests Assert.Multiple(() => { var field = AccessTools.Field(typeof(AccessToolsStruct), "structField1"); - var expectedCaseToConstraint = PublicField(expectedCaseToConstraint_StructInstance); + var expectedCaseToConstraint = expectedCaseToConstraint_StructInstance; var expectedCaseToConstraintClassT = expectedCaseToConstraint_StructInstance_ClassT; TestSuite_Struct( field, "structField1test", expectedCaseToConstraint); TestSuite_Class( - field, "structField1test", InterfaceT(expectedCaseToConstraintClassT)); + field, "structField1test", expectedCaseToConstraintClassT); TestSuite_Class( field, "structField1test", expectedCaseToConstraintClassT); TestSuite_Class( @@ -882,7 +773,7 @@ namespace HarmonyLibTests TestSuite_Struct( field, 1234, expectedCaseToConstraint); TestSuite_Class( - field, 1234, InterfaceT(expectedCaseToConstraintClassT)); + field, 1234, expectedCaseToConstraintClassT); TestSuite_Class( field, 1234, expectedCaseToConstraintClassT); TestSuite_Class( @@ -891,18 +782,16 @@ namespace HarmonyLibTests field, 1234, IncompatibleTypeT(expectedCaseToConstraintClassT)); TestSuite_Struct( field, 1234, IncompatibleTypeT(expectedCaseToConstraint)); - // TODO: Following tests will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error. - // Fix *FieldRefAccess to consistently throw ArgumentException when field type is a value type and F is a different type. - //TestSuite_Struct( - // field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Struct( - // field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Struct( - // field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Struct( - // field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Struct( - // field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 1234, IncompatibleFieldType(expectedCaseToConstraint)); }); } @@ -918,27 +807,25 @@ namespace HarmonyLibTests TestSuite_Struct( field, 4321, expectedCaseToConstraint); TestSuite_Class( - field, 4321, InterfaceT(expectedCaseToConstraintClassT)); + field, 4321, expectedCaseToConstraintClassT); TestSuite_Class( field, 4321, expectedCaseToConstraintClassT); TestSuite_Class( field, 4321, expectedCaseToConstraintClassT); TestSuite_Class( - field, 4321, StaticIncompatibleTypeT(FieldMissingOnTypeT(expectedCaseToConstraintClassT))); + field, 4321, FieldMissingOnTypeT(expectedCaseToConstraintClassT)); TestSuite_Struct( - field, 4321, StaticIncompatibleTypeT(FieldMissingOnTypeT(expectedCaseToConstraint))); - // TODO: Following tests will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error. - // Fix StaticFieldRefAccess to consistently throw ArgumentException when field type is incompatible with F. - //TestSuite_Struct( - // field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Struct( - // field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Struct( - // field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Struct( - // field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); - //TestSuite_Struct( - // field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); + field, 4321, FieldMissingOnTypeT(expectedCaseToConstraint)); + TestSuite_Struct( + field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 4321, IncompatibleFieldType(expectedCaseToConstraint)); }); } @@ -948,126 +835,28 @@ namespace HarmonyLibTests Assert.Multiple(() => { var field = AccessTools.Field(typeof(AccessToolsStruct), "structField4"); - var expectedCaseToConstraint = PublicField(expectedCaseToConstraint_StructStatic); - var expectedCaseToConstraintClassT = PublicField(expectedCaseToConstraint_StructStatic_ClassT); + var expectedCaseToConstraint = expectedCaseToConstraint_StructStatic; + var expectedCaseToConstraintClassT = expectedCaseToConstraint_StructStatic_ClassT; // Note: As this is as static field, instance type is ignored, so IncompatibleInstanceType is never needed. TestSuite_Struct( field, "structField4test", expectedCaseToConstraint); TestSuite_Class( - field, "structField4test", InterfaceT(expectedCaseToConstraintClassT)); + field, "structField4test", expectedCaseToConstraintClassT); TestSuite_Class( field, "structField4test", expectedCaseToConstraintClassT); TestSuite_Class( field, "structField4test", expectedCaseToConstraintClassT); TestSuite_Class( - field, "structField4test", StaticIncompatibleTypeT(FieldMissingOnTypeT(expectedCaseToConstraintClassT))); + field, "structField4test", FieldMissingOnTypeT(expectedCaseToConstraintClassT)); TestSuite_Struct( - field, "structField4test", StaticIncompatibleTypeT(FieldMissingOnTypeT(expectedCaseToConstraint))); + field, "structField4test", FieldMissingOnTypeT(expectedCaseToConstraint)); TestSuite_Struct( field, "structField4test", expectedCaseToConstraint); TestSuite_Struct( field, "structField4test", expectedCaseToConstraint); - // TODO: Following tests will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error. - // Fix StaticFieldRefAccess to consistently throw ArgumentException when field type is incompatible with F. - //TestSuite_Struct( - // field, 1337, IncompatibleFieldType(expectedCaseToConstraint)); + TestSuite_Struct( + field, 1337, IncompatibleFieldType(expectedCaseToConstraint)); }); } - - // TODO: Fix FieldRefAccess to consistently throw ArgumentException for struct instance fields, - // removing the need for these separate explicit tests. - static void TestCase_StructInstance_CanCrash(string fieldName, F testValue, string testCaseName) where T : struct - { - var field = AccessTools.Field(typeof(T), fieldName); - // Superset of problematic test cases - var availableTestCases = Merge( - AvailableTestCases_FieldRefAccess_Struct_ByName(fieldName), - AvailableTestCases_FieldRefAccess_Struct_ByFieldInfo(field), - AvailableTestCases_StaticFieldRefAccess_ByFieldInfo(field)); - TestCase_CanCrash(testValue, testCaseName, field, availableTestCases); - } - - // TODO: Fix FieldRefAccess to consistently throw ArgumentException when field type is a value type and F is a different type, - // removing the need for these separate explicit tests. - static void TestCase_ClassInstance_ValueTypeField_DifferentF_CanCrash(string fieldName, F testValue, - string testCaseName) where T : class - { - var field = AccessTools.Field(typeof(T), fieldName); - // Superset of problematic test cases - var availableTestCases = AvailableTestCases_FieldRefAccess_Class_ByFieldInfo(field); - TestCase_CanCrash(testValue, testCaseName, field, availableTestCases); - } - - // TODO: Fix StaticFieldRefAccess to consistently throw ArgumentException when field type is incompatible with F, - // (specifically: if field type is reference type, any value type F; if field type is value type, any different F) - // removing the need for these separate explicit tests. - static void TestCase_Static_IncompatibleF_CanCrash(string fieldName, F testValue, - string testCaseName) - { - var field = AccessTools.Field(typeof(T), fieldName); - // Superset of problematic test cases - var availableTestCases = AvailableTestCases_StaticFieldRefAccess_ByFieldInfo(field); - TestCase_CanCrash(testValue, testCaseName, field, availableTestCases); - } - - static void TestCase_CanCrash(F testValue, string testCaseName, FieldInfo field, - Dictionary> availableTestCases) - { - var instance = CloneInstancePrototype(typeof(T)); - try - { - var origValue = field.GetValue(instance); - var testCase = availableTestCases[testCaseName]; - var value = testCase.Get(ref instance); - Assert.AreNotEqual(testValue, value, "expected !Equals(testValue, value) (before set)"); - testCase.Set(ref instance, testValue); - var currentValue = field.GetValue(instance); - Assert.AreNotEqual(testValue, currentValue, "expected !Equals(testValue, field.GetValue(instance)) (after set)"); - Console.Error.WriteLine($"Test failed as expected: origValue={origValue}, testValue={testValue}, currentValue={currentValue}"); - } - catch (Exception ex) when (ex is InvalidProgramException || ex is NullReferenceException || ex is AccessViolationException) - { - // If an assertion failure or fatal crash hasn't happened yet, any of the above exceptions could be thrown, - // depending on the environment. - Console.Error.WriteLine("Test is known to sometimes throw:\n" + ex); - } - } - - [Test, Explicit("These tests will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error")] - [TestCase(typeof(AccessToolsStruct), typeof(string), "structField1", "structField1testcrash", "FieldRefAccess(field)(instance)")] - [TestCase(typeof(AccessToolsStruct), typeof(string), "structField1", "structField1testcrash", "FieldRefAccess(typeof(T), fieldName)(instance)")] - [TestCase(typeof(AccessToolsStruct), typeof(string), "structField1", "structField1testcrash", "StaticFieldRefAccess(field)()")] - [TestCase(typeof(AccessToolsStruct), typeof(int), "structField2", 1234, "FieldRefAccess(field)(instance)")] - [TestCase(typeof(AccessToolsStruct), typeof(int), "structField2", 1234, "FieldRefAccess(typeof(T), fieldName)(instance)")] - [TestCase(typeof(AccessToolsStruct), typeof(int), "structField2", 1234, "StaticFieldRefAccess(field)()")] - public void Test_StructInstance_CanCrash(Type typeT, Type typeF, string fieldName, object testValue, string testCaseName) - { - TestTools.AssertIgnoreIfVSTest(); // uncomment this to actually run the test in Visual Studio - var method = AccessTools.Method(typeof(TestFieldRefAccess), nameof(TestCase_StructInstance_CanCrash)); - _ = method.MakeGenericMethod(typeT, typeF).Invoke(this, new object[] { fieldName, testValue, testCaseName }); - } - - [Test, Explicit("This test will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error")] - [TestCase(typeof(AccessToolsClass), typeof(object), "field2", 123f, "FieldRefAccess(field)(instance)")] // field2 is non-static float - [TestCase(typeof(AccessToolsClass), typeof(float?), "field2", 123f, "FieldRefAccess(field)(instance)")] - [TestCase(typeof(AccessToolsClass), typeof(IComparable), "field2", 123f, "FieldRefAccess(field)(instance)")] - public void Test_ClassInstance_ValueTypeField_DifferentF_CanCrash(Type typeT, Type typeF, string fieldName, object testValue, string testCaseName) - { - TestTools.AssertIgnoreIfVSTest(); // uncomment this to actually run the test in Visual Studio - var method = AccessTools.Method(typeof(TestFieldRefAccess), nameof(TestCase_ClassInstance_ValueTypeField_DifferentF_CanCrash)); - _ = method.MakeGenericMethod(typeT, typeF).Invoke(this, new object[] { fieldName, testValue, testCaseName }); - } - - [Test, Explicit("This test will either fail to get/set correctly or crash the runtime due to invalid IL code causing some fatal error")] - [TestCase(typeof(AccessToolsClass), typeof(int), "field4", 321, "StaticFieldRefAccess(field)()")] // field4 is static string - [TestCase(typeof(AccessToolsStruct), typeof(object), "structField3", 456, "StaticFieldRefAccess(field)()")] // structField3 is static int - [TestCase(typeof(AccessToolsStruct), typeof(int?), "structField3", 456, "StaticFieldRefAccess(field)()")] - [TestCase(typeof(AccessToolsStruct), typeof(IComparable), "structField3", 456, "StaticFieldRefAccess(field)()")] - public void Test_Static_IncompatibleF_CanCrash(Type typeT, Type typeF, string fieldName, object testValue, string testCaseName) - { - TestTools.AssertIgnoreIfVSTest(); // uncomment this to actually run the test in Visual Studio - var method = AccessTools.Method(typeof(TestFieldRefAccess), nameof(TestCase_Static_IncompatibleF_CanCrash)); - _ = method.MakeGenericMethod(typeT, typeF).Invoke(this, new object[] { fieldName, testValue, testCaseName }); - } } }