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; } } }