using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Reflection
{
public abstract partial class ReflectionManager : IReflectionManager
{
[Dependency] private ILogManager _logMan = default!;
///
/// Enumerable over prefixes that are added to the type provided to
/// if the type can't be found in any assemblies.
///
///
/// First prefix should probably be "".
///
protected abstract IEnumerable TypePrefixes { get; }
private readonly List assemblies = new();
public event EventHandler? OnAssemblyAdded;
[ViewVariables] public IReadOnlyList Assemblies => assemblies;
private readonly ConcurrentDictionary<(Type baseType, string typeName), Type?> _yamlTypeTagCache = new();
private readonly Dictionary _looseTypeCache = new();
private readonly ConcurrentDictionary _enumCache = new();
private readonly ConcurrentDictionary _reverseEnumCache = new();
private ImmutableArray _getAllTypesCache = ImmutableArray.Empty;
private ImmutableDictionary> _inheritanceCache =
ImmutableDictionary>.Empty;
private ImmutableDictionary> _attributeCache =
ImmutableDictionary>.Empty;
private ImmutableDictionary> _allEnumCache =
ImmutableDictionary>.Empty;
private ISawmill _sawmill = default!;
public void Initialize()
{
_sawmill = _logMan.GetSawmill("Reflection");
EnsureGetAllTypesCache();
}
///
public IEnumerable GetAllChildren(bool inclusive = false)
{
return GetAllChildren(typeof(T), inclusive);
}
///
public IEnumerable GetAllChildren(Type baseType, bool inclusive = false)
{
EnsureGetAllTypesCache();
if (inclusive)
yield return baseType;
if (!_inheritanceCache.TryGetValue(baseType, out var inheritors))
yield break;
foreach (var inheritor in inheritors)
{
if (!inheritor.IsAbstract)
yield return inheritor;
}
}
internal void EnsureGetAllTypesCache()
{
if (_getAllTypesCache.Length != 0)
return;
var totalLength = 0;
var typeSets = new List();
foreach (var assembly in assemblies)
{
var types = assembly.GetTypes();
typeSets.Add(types);
totalLength += types.Length;
}
var typesCache = ImmutableArray.CreateBuilder(totalLength);
var inheritanceCache = ImmutableDictionary.CreateBuilder List, HashSet Set)>();
var attributeCache = ImmutableDictionary.CreateBuilder>();
var enumCache = ImmutableDictionary.CreateBuilder>();
foreach (var typeSet in typeSets)
{
foreach (var type in typeSet)
{
var attribute = (ReflectAttribute?)Attribute.GetCustomAttribute(type, typeof(ReflectAttribute));
if (!(attribute?.Discoverable ?? ReflectAttribute.DEFAULT_DISCOVERABLE))
continue;
typesCache.Add(type);
var baseType = type.BaseType;
foreach (var @interface in type.GetInterfaces())
{
if (!inheritanceCache.TryGetValue(@interface, out var interfaces))
{
interfaces = ([], []);
inheritanceCache[@interface] = interfaces;
}
if (interfaces.Set.Add(type))
interfaces.List.Add(type);
}
while (baseType != null)
{
if (!inheritanceCache.TryGetValue(baseType, out var subTypes))
{
subTypes = ([], []);
inheritanceCache[baseType] = subTypes;
}
if (subTypes.Set.Add(type))
subTypes.List.Add(type);
foreach (var @interface in baseType.GetInterfaces())
{
if (!inheritanceCache.TryGetValue(@interface, out var interfaces))
{
interfaces = ([], []);
inheritanceCache[@interface] = interfaces;
}
if (interfaces.Set.Add(type))
interfaces.List.Add(type);
}
baseType = baseType.BaseType;
}
foreach (var typeAttribute in type.CustomAttributes)
{
if (!attributeCache.TryGetValue(typeAttribute.AttributeType, out var attributes))
{
attributes = [];
attributeCache[typeAttribute.AttributeType] = attributes;
}
attributes.Add(type);
}
if (type.IsEnum)
{
var fullName = type.FullName!;
var types = enumCache.GetOrNew(fullName);
types.Add(type);
types = enumCache.GetOrNew(type.Name);
types.Add(type);
var declaringType = type.DeclaringType;
var lastIndexOf = fullName.LastIndexOf('.');
while (declaringType != null && lastIndexOf != -1)
{
types = enumCache.GetOrNew(fullName[(lastIndexOf + 1)..]);
types.Add(type);
declaringType = declaringType.DeclaringType;
lastIndexOf = fullName.LastIndexOf('.', lastIndexOf - 1, lastIndexOf - 1);
}
}
}
}
var toAdd = new HashSet();
foreach (var (attributeType, types) in attributeCache)
{
if (attributeType.GetCustomAttribute() is not { Inherited: true })
{
continue;
}
toAdd.Clear();
foreach (var type in types)
{
if (inheritanceCache.TryGetValue(type, out var inheritors))
toAdd.UnionWith(inheritors.Set);
}
types.UnionWith(toAdd);
}
_getAllTypesCache = typesCache.ToImmutable();
_inheritanceCache = inheritanceCache
.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.List.ToImmutableArray());
_attributeCache = attributeCache
.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableHashSet());
_allEnumCache = enumCache.ToImmutableDictionary(kvp => kvp.Key, kvp => kvp.Value.ToImmutableArray());
}
public void LoadAssemblies(params Assembly[] args) => LoadAssemblies(args.AsEnumerable());
public void LoadAssemblies(IEnumerable assemblies)
{
var assembliesArray = assemblies.Distinct().ToArray();
if (this.assemblies.Intersect(assembliesArray).Any())
throw new InvalidOperationException("Attempted to load the same assembly multiple times!");
this.assemblies.AddRange(assembliesArray);
_getAllTypesCache = ImmutableArray.Empty;
_inheritanceCache = ImmutableDictionary>.Empty;
_allEnumCache = ImmutableDictionary>.Empty;
OnAssemblyAdded?.Invoke(this, new ReflectionUpdateEventArgs(this));
}
///
public Type? GetType(string name)
{
// The priority in which types are retrieved is based on the TypePrefixes list.
// This is an implementation detail. If you need it: make a better API.
foreach (string prefix in TypePrefixes)
{
string appendedName = prefix + name;
foreach (var assembly in Assemblies)
{
var theType = assembly.GetType(appendedName);
if (theType != null)
{
return theType;
}
}
}
return null;
}
///
public Type LooseGetType(string name)
{
if (TryLooseGetType(name, out var ret))
{
return ret;
}
throw new ArgumentException($"Unable to find type: {name}.");
}
public bool TryLooseGetType(string name, [NotNullWhen(true)] out Type? type)
{
lock (_looseTypeCache)
{
if (_looseTypeCache.TryGetValue(name, out type))
return true;
// Check standard types first.
switch (name)
{
case "Byte":
type = typeof(byte);
_looseTypeCache[name] = type;
return true;
case "Bool":
type = typeof(bool);
_looseTypeCache[name] = type;
return true;
case "Double":
type = typeof(double);
_looseTypeCache[name] = type;
return true;
case "SByte":
type = typeof(sbyte);
_looseTypeCache[name] = type;
return true;
case "Single":
type = typeof(float);
_looseTypeCache[name] = type;
return true;
case "String":
type = typeof(string);
_looseTypeCache[name] = type;
return true;
}
foreach (var assembly in assemblies)
{
foreach (var tryType in assembly.DefinedTypes)
{
if (tryType.FullName!.EndsWith(name))
{
type = tryType;
_looseTypeCache[name] = type;
return true;
}
}
}
type = default;
return false;
}
}
///
public IEnumerable FindTypesWithAttribute() where T : Attribute
{
return FindTypesWithAttribute(typeof(T));
}
///
public IEnumerable FindTypesWithAttribute(Type attributeType)
{
EnsureGetAllTypesCache();
return _attributeCache.GetValueOrDefault(attributeType) ?? Enumerable.Empty();
}
public IEnumerable FindAllTypes()
{
EnsureGetAllTypesCache();
return _getAllTypesCache;
}
///
public string GetEnumReference(Enum @enum)
{
return _reverseEnumCache.GetOrAdd(@enum,
_ =>
{
// if there is more than one enum with the same basic name, the reference may need to be the fully qualified name.
// but if possible we want to avoid that and use a shorter string.
string reference;
var fullName = @enum.GetType().FullName!;
var dotIndex = fullName.LastIndexOf('.');
if (dotIndex > 0 && dotIndex != fullName.Length)
{
var name = fullName.Substring(dotIndex + 1);
reference = $"enum.{name}.{@enum}";
if (_enumCache.TryAdd(reference, @enum))
return reference;
}
// If that failed, just use the full name.
reference = $"enum.{fullName}.{@enum}";
_enumCache.TryAdd(reference, @enum);
return reference;
});
}
///
public bool TryParseEnumReference(
string reference,
[NotNullWhen(true)] out Enum? @enum,
bool shouldThrow = true)
{
if (!reference.StartsWith("enum."))
{
@enum = default;
return false;
}
@enum = _enumCache.GetOrAdd(reference,
r =>
{
var cropped = r.AsSpan(5);
// Doesn't exist, add it.
var dotIndex = cropped.LastIndexOf('.');
var typeName = cropped[..dotIndex];
var firstDot = typeName.IndexOf('.');
if (firstDot != -1)
typeName = typeName[(firstDot + 1)..];
var value = cropped[(dotIndex + 1)..];
if (!_allEnumCache.TryGetValue(typeName.ToString(), out var enums))
return null;
foreach (var @enum in enums)
{
if (!TypeNameMatchesEnumReference(@enum.FullName!, typeName))
continue;
var e = (Enum)Enum.Parse(@enum, value);
if (!_reverseEnumCache.TryAdd(e, r) &&
r != _reverseEnumCache[e])
{
_sawmill.Warning(
$"Conflicting enum references encountered. Enum: {e}. Existing: {_reverseEnumCache[e]}. New: {r}");
}
return e;
}
return null;
});
if (@enum == null && shouldThrow)
throw new ArgumentException($"Could not resolve enum reference: {reference}.");
return @enum != null;
}
private static bool TypeNameMatchesEnumReference(ReadOnlySpan fullName, ReadOnlySpan typeName)
{
if (fullName.SequenceEqual(typeName))
return true;
if (fullName.Length <= typeName.Length)
return false;
var prefixIndex = fullName.Length - typeName.Length - 1;
var separator = fullName[prefixIndex];
return separator is '.' or '+'
&& fullName[(prefixIndex + 1)..].SequenceEqual(typeName);
}
public Type? YamlTypeTagLookup(Type baseType, string typeName)
{
return _yamlTypeTagCache.GetOrAdd((baseType, typeName),
_ =>
{
Type? found = null;
foreach (var derivedType in GetAllChildren(baseType))
{
if (!derivedType.IsPublic)
{
continue;
}
if (derivedType.Name == typeName)
{
found = derivedType;
break;
}
}
// Fallback
if (found == null)
{
TryLooseGetType(typeName, out found);
// If we may have gotten the type but it's still abstract then don't return it.
if (found == null || found.IsAbstract || !found.IsAssignableTo(baseType))
found = null;
}
return found;
});
}
public bool IsAttributeDefined(Type type, Type attribute)
{
return _attributeCache.TryGetValue(attribute, out var attributes) &&
attributes.Contains(type);
}
public ImmutableHashSet FindTypesWithAttributeSet()
{
EnsureGetAllTypesCache();
return _attributeCache.GetValueOrDefault(typeof(T)) ?? ImmutableHashSet.Empty;
}
}
}