using System;
using System.Reflection;
using StardewModdingAPI.Framework.Utilities;
namespace StardewModdingAPI.Framework.Reflection
{
/// Provides helper methods for accessing inaccessible code.
/// This implementation searches up the type hierarchy, and caches the reflected fields and methods with a sliding expiry (to optimize performance without unnecessary memory usage).
internal class Reflector
{
/*********
** Fields
*********/
/// The cached fields and methods found via reflection.
private readonly IntervalMemoryCache Cache = new();
/*********
** Public methods
*********/
/****
** Fields
****/
/// Get a instance field.
/// The field type.
/// The object which has the field.
/// The field name.
/// Whether to throw an exception if the field isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the field wrapper, or null if is false and the field doesn't exist.
/// The target field doesn't exist, and is true.
public IReflectedField GetField(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a instance field from a null object.");
// get field from hierarchy
IReflectedField? field = this.GetFieldFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && field == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance field.");
return field!;
}
/// Get a static field.
/// The field type.
/// The type which has the field.
/// The field name.
/// Whether to throw an exception if the field isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the field wrapper, or null if is false and the field doesn't exist.
/// The target field doesn't exist, and is true.
public IReflectedField GetField(Type type, string name, bool required = true)
{
// get field from hierarchy
IReflectedField? field = this.GetFieldFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public);
if (required && field == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static field.");
return field!;
}
/****
** Properties
****/
/// Get a instance property.
/// The property type.
/// The object which has the property.
/// The property name.
/// Whether to throw an exception if the property isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the property wrapper, or null if is false and the property doesn't exist.
/// The target property doesn't exist, and is true.
public IReflectedProperty GetProperty(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a instance property from a null object.");
// get property from hierarchy
IReflectedProperty? property = this.GetPropertyFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && property == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance property.");
return property!;
}
/// Get a static property.
/// The property type.
/// The type which has the property.
/// The property name.
/// Whether to throw an exception if the property isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the property wrapper, or null if is false and the property doesn't exist.
/// The target property doesn't exist, and is true.
public IReflectedProperty GetProperty(Type type, string name, bool required = true)
{
// get field from hierarchy
IReflectedProperty? property = this.GetPropertyFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (required && property == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static property.");
return property!;
}
/****
** Methods
****/
/// Get a instance method.
/// The object which has the method.
/// The method name.
/// Whether to throw an exception if the method isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the method wrapper, or null if is false and the method doesn't exist.
/// The target method doesn't exist, and is true.
public IReflectedMethod GetMethod(object obj, string name, bool required = true)
{
// validate
if (obj == null)
throw new ArgumentNullException(nameof(obj), "Can't get a instance method from a null object.");
// get method from hierarchy
IReflectedMethod? method = this.GetMethodFromHierarchy(obj.GetType(), obj, name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (required && method == null)
throw new InvalidOperationException($"The {obj.GetType().FullName} object doesn't have a '{name}' instance method.");
return method!;
}
/// Get a static method.
/// The type which has the method.
/// The method name.
/// Whether to throw an exception if the method isn't found. Due to limitations with nullable reference types, setting this to false will still mark the value non-nullable.
/// Returns the method wrapper, or null if is false and the method doesn't exist.
/// The target method doesn't exist, and is true.
public IReflectedMethod GetMethod(Type type, string name, bool required = true)
{
// get method from hierarchy
IReflectedMethod? method = this.GetMethodFromHierarchy(type, null, name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (required && method == null)
throw new InvalidOperationException($"The {type.FullName} object doesn't have a '{name}' static method.");
return method!;
}
/****
** Management
****/
/// Start a new cache interval, clearing stale reflection lookups.
public void NewCacheInterval()
{
this.Cache.StartNewInterval();
}
/*********
** Private methods
*********/
/// Get a field from the type hierarchy.
/// The expected field type.
/// The type which has the field.
/// The object which has the field, or null for a static field.
/// The field name.
/// The reflection binding which flags which indicates what type of field to find.
private IReflectedField? GetFieldFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
FieldInfo? field = this.GetCached(
'f', type, name, isStatic,
fetch: () =>
{
for (Type? curType = type; curType != null; curType = curType.BaseType)
{
FieldInfo? fieldInfo = curType.GetField(name, bindingFlags);
if (fieldInfo != null)
{
type = curType;
return fieldInfo;
}
}
return null;
}
);
return field != null
? new ReflectedField(type, obj, field, isStatic)
: null;
}
/// Get a property from the type hierarchy.
/// The expected property type.
/// The type which has the property.
/// The object which has the property, or null for a static property.
/// The property name.
/// The reflection binding which flags which indicates what type of property to find.
private IReflectedProperty? GetPropertyFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
PropertyInfo? property = this.GetCached(
'p', type, name, isStatic,
fetch: () =>
{
for (Type? curType = type; curType != null; curType = curType.BaseType)
{
PropertyInfo? propertyInfo = curType.GetProperty(name, bindingFlags);
if (propertyInfo != null)
{
type = curType;
return propertyInfo;
}
}
return null;
}
);
return property != null
? new ReflectedProperty(type, obj, property, isStatic)
: null;
}
/// Get a method from the type hierarchy.
/// The type which has the method.
/// The object which has the method, or null for a static method.
/// The method name.
/// The reflection binding which flags which indicates what type of method to find.
private IReflectedMethod? GetMethodFromHierarchy(Type type, object? obj, string name, BindingFlags bindingFlags)
{
bool isStatic = bindingFlags.HasFlag(BindingFlags.Static);
MethodInfo? method = this.GetCached(
'm', type, name, isStatic,
fetch: () =>
{
for (Type? curType = type; curType != null; curType = curType.BaseType)
{
MethodInfo? methodInfo = curType.GetMethod(name, bindingFlags);
if (methodInfo != null)
{
type = curType;
return methodInfo;
}
}
return null;
}
);
return method != null
? new ReflectedMethod(type, obj, method, isStatic: isStatic)
: null;
}
/// Get a method or field through the cache.
/// The expected type.
/// A letter representing the member type (like 'm' for method).
/// The type whose members are being reflected.
/// The member name.
/// Whether the member is static.
/// Fetches a new value to cache.
private TMemberInfo? GetCached(char memberType, Type type, string memberName, bool isStatic, Func fetch)
where TMemberInfo : MemberInfo
{
string key = $"{memberType}{(isStatic ? 's' : 'i')}{type.FullName}:{memberName}";
return (TMemberInfo?)this.Cache.GetOrSet(key, fetch);
}
}
}