diff --git a/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs new file mode 100644 index 00000000..e73240b2 --- /dev/null +++ b/ReflectorNet.Tests/src/Utils/AssemblyUtilsTests.cs @@ -0,0 +1,502 @@ +/* + * ReflectorNet + * Author: Ivan Murzak (https://github.com/IvanMurzak) + * Copyright (c) 2025 Ivan Murzak + * Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for full license information. + */ + +using com.IvanMurzak.ReflectorNet.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace com.IvanMurzak.ReflectorNet.Tests.Utils +{ + public class AssemblyUtilsTests + { + #region GetAssemblyTypes Tests + + [Fact] + public void GetAssemblyTypes_CurrentAssembly_ReturnsTypes() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.NotNull(types); + Assert.NotEmpty(types); + Assert.Contains(typeof(AssemblyUtilsTests), types); + } + + [Fact] + public void GetAssemblyTypes_MscorlibAssembly_ReturnsTypes() + { + // Arrange + var assembly = typeof(object).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.NotNull(types); + Assert.NotEmpty(types); + Assert.Contains(typeof(object), types); + Assert.Contains(typeof(string), types); + } + + [Fact] + public void GetAssemblyTypes_SameAssembly_ReturnsDifferentInstances() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + + // Act + var types1 = AssemblyUtils.GetAssemblyTypes(assembly); + var types2 = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert - should return different array instances (no caching) + Assert.NotSame(types1, types2); + Assert.Equal(types1.Length, types2.Length); + } + + [Fact] + public void GetAssemblyTypes_MultipleAssemblies_ReturnsDifferentArrays() + { + // Arrange + var testAssembly = typeof(AssemblyUtilsTests).Assembly; + var coreAssembly = typeof(object).Assembly; + + // Act + var testTypes = AssemblyUtils.GetAssemblyTypes(testAssembly); + var coreTypes = AssemblyUtils.GetAssemblyTypes(coreAssembly); + + // Assert + Assert.NotSame(testTypes, coreTypes); + } + + [Fact] + public void GetAssemblyTypes_ReturnsNonNullTypes() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.All(types, t => Assert.NotNull(t)); + } + + [Fact] + public void GetAssemblyTypes_ReflectorNetAssembly_ContainsExpectedTypes() + { + // Arrange + var assembly = typeof(AssemblyUtils).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.Contains(types, t => t.Name == "AssemblyUtils"); + Assert.Contains(types, t => t.Name == "TypeUtils"); + Assert.Contains(types, t => t.Name == "Reflector"); + } + + #endregion + + #region AllAssemblies Tests + + [Fact] + public void AllAssemblies_ReturnsAssemblies() + { + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.NotEmpty(allAssemblies); + } + + [Fact] + public void AllAssemblies_ContainsCurrentTestAssembly() + { + // Arrange + var testAssembly = typeof(AssemblyUtilsTests).Assembly; + + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.Contains(testAssembly, allAssemblies); + } + + [Fact] + public void AllAssemblies_ContainsReflectorNetAssembly() + { + // Arrange + var reflectorAssembly = typeof(AssemblyUtils).Assembly; + + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.Contains(reflectorAssembly, allAssemblies); + } + + [Fact] + public void AllAssemblies_ContainsCoreLibAssembly() + { + // Arrange + var coreLibAssembly = typeof(object).Assembly; + + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.Contains(coreLibAssembly, allAssemblies); + } + + [Fact] + public void AllAssemblies_AllAssembliesAreNotNull() + { + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert + Assert.All(allAssemblies, a => Assert.NotNull(a)); + } + + [Fact] + public void AllAssemblies_MultipleEnumerations_Succeed() + { + // Act - enumerate multiple times + var count1 = AssemblyUtils.AllAssemblies.Count(); + var count2 = AssemblyUtils.AllAssemblies.Count(); + + // Assert - both enumerations should return assemblies + // Note: counts may differ slightly if new assemblies are loaded between enumerations + Assert.True(count1 > 0, "First enumeration should return assemblies"); + Assert.True(count2 > 0, "Second enumeration should return assemblies"); + // The counts should be reasonably close (within 50 assemblies) + var difference = Math.Abs(count1 - count2); + Assert.True(difference <= 50, + $"Counts differ too much: {count1} vs {count2} (difference: {difference})"); + } + + [Fact] + public void AllAssemblies_CanIterateWithForeach() + { + // Act + var count = 0; + foreach (var assembly in AssemblyUtils.AllAssemblies) + { + Assert.NotNull(assembly); + count++; + } + + // Assert + Assert.True(count > 0, "Should iterate at least one assembly"); + } + + [Fact] + public void AllAssemblies_ConcurrentEnumeration_Succeeds() + { + // Arrange + var counts = new int[5]; + + // Act - enumerate from multiple threads simultaneously + Parallel.For(0, 5, i => + { + counts[i] = AssemblyUtils.AllAssemblies.Count(); + }); + + // Assert - all counts should be positive + Assert.All(counts, c => Assert.True(c > 0)); + } + + [Fact] + public void AllAssemblies_ContainsMultipleAssemblies() + { + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + + // Assert - should have multiple assemblies loaded + Assert.True(allAssemblies.Count > 5, $"Expected more than 5 assemblies, got {allAssemblies.Count}"); + } + + [Fact] + public void AllAssemblies_NoDuplicates() + { + // Act + var allAssemblies = AssemblyUtils.AllAssemblies.ToList(); + var distinctCount = allAssemblies.Distinct().Count(); + + // Assert + Assert.Equal(allAssemblies.Count, distinctCount); + } + + #endregion + + #region AllTypes Tests + + [Fact] + public void AllTypes_ReturnsTypes() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.NotEmpty(allTypes); + } + + [Fact] + public void AllTypes_ContainsCommonTypes() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(typeof(object), allTypes); + Assert.Contains(typeof(string), allTypes); + Assert.Contains(typeof(int), allTypes); + } + + [Fact] + public void AllTypes_ContainsTestTypes() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(typeof(AssemblyUtilsTests), allTypes); + } + + [Fact] + public void AllTypes_ContainsAssemblyUtilsType() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(typeof(AssemblyUtils), allTypes); + } + + [Fact] + public void AllTypes_AllTypesAreNotNull() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.All(allTypes, t => Assert.NotNull(t)); + } + + [Fact] + public void AllTypes_MultipleEnumerations_Succeed() + { + // Act - enumerate multiple times + var count1 = AssemblyUtils.AllTypes.Count(); + var count2 = AssemblyUtils.AllTypes.Count(); + + // Assert - both enumerations should return types + // Note: counts may differ slightly if new assemblies are loaded between enumerations + Assert.True(count1 > 0, "First enumeration should return types"); + Assert.True(count2 > 0, "Second enumeration should return types"); + // The counts should be reasonably close (within 10% or 500 types) + var difference = Math.Abs(count1 - count2); + var maxAllowedDifference = Math.Max(500, count1 / 10); + Assert.True(difference <= maxAllowedDifference, + $"Counts differ too much: {count1} vs {count2} (difference: {difference})"); + } + + [Fact] + public void AllTypes_CanIterateWithForeach() + { + // Act + var count = 0; + foreach (var type in AssemblyUtils.AllTypes) + { + Assert.NotNull(type); + count++; + if (count > 100) break; // Don't iterate all to keep test fast + } + + // Assert + Assert.True(count > 100); + } + + [Fact] + public void AllTypes_ContainsTypesFromMultipleAssemblies() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Get assemblies that contributed types + var assemblies = allTypes.Select(t => t.Assembly).Distinct().ToList(); + + // Assert - should have types from multiple assemblies + Assert.True(assemblies.Count > 1, "Should contain types from multiple assemblies"); + } + + #endregion + + #region Thread Safety Tests + + [Fact] + public void GetAssemblyTypes_ConcurrentAccess_ReturnsValidTypes() + { + // Arrange + var assembly = typeof(AssemblyUtilsTests).Assembly; + var results = new Type[10][]; + using (var barrier = new Barrier(10)) + { + // Act - access from multiple threads simultaneously + Parallel.For(0, 10, i => + { + barrier.SignalAndWait(); // Ensure all threads start at the same time + results[i] = AssemblyUtils.GetAssemblyTypes(assembly); + }); + } + + // Assert - all should be valid and different instances + for (int i = 0; i < results.Length; i++) + { + Assert.NotNull(results[i]); + Assert.NotEmpty(results[i]); + if (i > 0) + { + Assert.NotSame(results[0], results[i]); + } + } + } + + [Fact] + public void GetAssemblyTypes_ConcurrentAccessDifferentAssemblies_Succeeds() + { + // Arrange + var assemblies = AppDomain.CurrentDomain.GetAssemblies().Take(10).ToArray(); + var results = new Dictionary(); + var lockObj = new object(); + + // Act - access different assemblies from multiple threads + Parallel.ForEach(assemblies, assembly => + { + var types = AssemblyUtils.GetAssemblyTypes(assembly); + lock (lockObj) + { + results[assembly] = types; + } + }); + + // Assert + Assert.Equal(assemblies.Length, results.Count); + foreach (var kvp in results) + { + Assert.NotNull(kvp.Value); + } + } + + [Fact] + public void AllTypes_ConcurrentEnumeration_Succeeds() + { + // Arrange + var counts = new int[5]; + + // Act - enumerate from multiple threads simultaneously + Parallel.For(0, 5, i => + { + counts[i] = AssemblyUtils.AllTypes.Count(); + }); + for (int i = 1; i < counts.Length; i++) + { + var diff = Math.Abs(counts[0] - counts[i]); + Assert.Equal(counts[0], counts[i]); + } + } + + #endregion + + #region Edge Cases + + [Fact] + public void AllTypes_ContainsGenericTypeDefinitions() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert - should contain generic type definitions like List<> + Assert.Contains(allTypes, t => t.IsGenericTypeDefinition); + } + + [Fact] + public void AllTypes_ContainsNestedTypes() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert - should contain nested types + Assert.Contains(allTypes, t => t.IsNested); + } + + [Fact] + public void AllTypes_ContainsInterfaces() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(allTypes, t => t.IsInterface); + } + + [Fact] + public void AllTypes_ContainsAbstractClasses() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(allTypes, t => t.IsAbstract && t.IsClass); + } + + [Fact] + public void AllTypes_ContainsEnums() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(allTypes, t => t.IsEnum); + } + + [Fact] + public void AllTypes_ContainsStructs() + { + // Act + var allTypes = AssemblyUtils.AllTypes.ToList(); + + // Assert + Assert.Contains(allTypes, t => t.IsValueType && !t.IsEnum && !t.IsPrimitive); + } + + [Fact] + public void GetAssemblyTypes_SystemAssembly_ContainsExceptionTypes() + { + // Arrange + var assembly = typeof(Exception).Assembly; + + // Act + var types = AssemblyUtils.GetAssemblyTypes(assembly); + + // Assert + Assert.Contains(types, t => typeof(Exception).IsAssignableFrom(t)); + } + + #endregion + + } +} diff --git a/ReflectorNet/ReflectorNet.csproj b/ReflectorNet/ReflectorNet.csproj index 19e22289..b3a67747 100644 --- a/ReflectorNet/ReflectorNet.csproj +++ b/ReflectorNet/ReflectorNet.csproj @@ -9,7 +9,7 @@ com.IvanMurzak.ReflectorNet - 3.3.1 + 3.4.0 Ivan Murzak Copyright © Ivan Murzak 2025 ReflectorNet is an advanced .NET reflection toolkit designed for AI-driven scenarios. Effortlessly search for C# methods using natural language queries, invoke any method by supplying arguments as JSON, and receive results as JSON. The library also provides a powerful API to inspect, modify, and manage in-memory object instances dynamically via JSON data. Ideal for automation, testing, and AI integration workflows. diff --git a/ReflectorNet/src/Reflector/Reflector.FindMethod.cs b/ReflectorNet/src/Reflector/Reflector.FindMethod.cs index 27653a1f..2ac403a1 100644 --- a/ReflectorNet/src/Reflector/Reflector.FindMethod.cs +++ b/ReflectorNet/src/Reflector/Reflector.FindMethod.cs @@ -9,7 +9,7 @@ namespace com.IvanMurzak.ReflectorNet { public partial class Reflector { - public static IEnumerable AllMethods => TypeUtils.AllTypes + public static IEnumerable AllMethods => AssemblyUtils.AllTypes .SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) .Where(method => method.DeclaringType != null && !method.DeclaringType.IsAbstract); @@ -152,7 +152,7 @@ public IEnumerable FindMethod( // Prepare Namespace filter.Namespace = filter.Namespace?.Trim()?.Replace(StringUtils.Null, string.Empty); - var typesEnumerable = TypeUtils.AllTypes + var typesEnumerable = AssemblyUtils.AllTypes .Where(type => type.IsVisible) .Where(type => !type.IsInterface) .Where(type => !type.IsAbstract || type.IsSealed) diff --git a/ReflectorNet/src/Utils/AssemblyUtils.cs b/ReflectorNet/src/Utils/AssemblyUtils.cs new file mode 100644 index 00000000..18c3b643 --- /dev/null +++ b/ReflectorNet/src/Utils/AssemblyUtils.cs @@ -0,0 +1,97 @@ +/* + * ReflectorNet + * Author: Ivan Murzak (https://github.com/IvanMurzak) + * Copyright (c) 2025 Ivan Murzak + * Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for full license information. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace com.IvanMurzak.ReflectorNet.Utils +{ + public static class AssemblyUtils + { + /// + /// Gets all assemblies loaded in the current application domain with exception protection. + /// + public static IEnumerable AllAssemblies + { + get + { + Assembly[] assemblies; + try + { + assemblies = AppDomain.CurrentDomain.GetAssemblies(); + } + catch (AppDomainUnloadedException) + { + yield break; + } + + for (int i = 0; i < assemblies.Length; i++) + { + yield return assemblies[i]; + } + } + } + + /// + /// Gets all types from all loaded assemblies with exception protection. + /// + public static IEnumerable AllTypes + { + get + { + foreach (var assembly in AllAssemblies) + { + var types = GetAssemblyTypes(assembly); + for (int i = 0; i < types.Length; i++) + { + yield return types[i]; + } + } + } + } + + /// + /// Gets all types from an assembly. + /// + /// + /// This method handles exceptions gracefully to ensure robust type enumeration: + /// + /// : Returns only the types that loaded successfully. + /// This commonly occurs when an assembly references types from dependencies that are not loaded. + /// Other exceptions (e.g., , ): + /// Returns an empty array. These can occur with dynamic assemblies, native assemblies, or corrupted modules. + /// + /// + /// The assembly to get types from. + /// Array of types from the assembly. May be empty if the assembly cannot be inspected. + public static Type[] GetAssemblyTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + // Some types failed to load (e.g., missing dependencies). + // Return the types that did load successfully. + return ex.Types + ?.Where(t => t != null) + .Select(x => x!) + .ToArray() ?? Array.Empty(); + } + catch + { + // Other exceptions (FileNotFoundException, BadImageFormatException, etc.) + // can occur with dynamic assemblies, native assemblies, or corrupted modules. + // Return empty array to allow enumeration to continue with other assemblies. + return Array.Empty(); + } + } + } +} diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs index dbb0adc4..e041e5dd 100644 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ b/ReflectorNet/src/Utils/TypeUtils.cs @@ -17,12 +17,43 @@ namespace com.IvanMurzak.ReflectorNet.Utils { public static partial class TypeUtils { - public static IEnumerable AllTypes => AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(assembly => assembly.GetTypes()); + /// + /// Gets all types from all loaded assemblies. + /// + /// + /// This property delegates to and provides + /// exception-safe enumeration of types across all loaded assemblies. + /// + public static IEnumerable AllTypes => AssemblyUtils.AllTypes; // Cache for resolved type names to avoid repeated AllTypes enumeration (thread-safe) - private static readonly ConcurrentDictionary _typeCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _typeCache = new(); + + /// + /// Clears the type name resolution cache. + /// + public static void ClearTypeCache() + { + _typeCache.Clear(); + } + /// + /// Resolves a from its string representation. + /// + /// + /// This method attempts to resolve types using multiple strategies in order: + /// + /// Built-in + /// Array type resolution (e.g., "Namespace.Type[]", "int[,]") + /// C#-style generic types (e.g., "List<int>", "Dictionary<string, int>") + /// CLR-style generic types (e.g., "System.Collections.Generic.List`1[[System.Int32]]") + /// Search across all loaded assemblies by FullName, AssemblyQualifiedName, or TypeId + /// + /// Results are cached for performance. Use to clear the cache. + /// + /// The type name to resolve. Can be a simple name, full name, assembly-qualified name, + /// or C#-style generic syntax. + /// The resolved , or null if the type cannot be found. public static Type? GetType(string? typeName) { if (string.IsNullOrWhiteSpace(typeName)) @@ -74,7 +105,7 @@ public static partial class TypeUtils } // If Type.GetType() fails, try to find the type in all loaded assemblies - type = AllTypes.FirstOrDefault(t => + type = AssemblyUtils.AllTypes.FirstOrDefault(t => typeName == t.FullName || typeName == t.AssemblyQualifiedName || typeName == t.GetTypeId()); @@ -94,7 +125,7 @@ public static partial class TypeUtils if (type != null) return type; - return AllTypes.FirstOrDefault(t => + return AssemblyUtils.AllTypes.FirstOrDefault(t => name == t.AssemblyQualifiedName || name == t.FullName || name == t.Name); @@ -469,6 +500,12 @@ private static int FindMatchingCloseBracket(string typeName, int openIndex) return args.Count > 0 ? args.ToArray() : null; } + /// + /// Determines whether the specified type is a dictionary type. + /// + /// The type to check. + /// true if the type is , + /// , or implements . public static bool IsDictionary(Type type) { if (type.IsGenericType && @@ -482,6 +519,11 @@ public static bool IsDictionary(Type type) .Any(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))); } + /// + /// Gets the key and value types from a dictionary type. + /// + /// The dictionary type to inspect. + /// An array containing [TKey, TValue] types, or null if the type is not a dictionary. public static Type[]? GetDictionaryGenericArguments(Type type) { if (type.IsGenericType && @@ -497,6 +539,12 @@ public static bool IsDictionary(Type type) return dictionaryInterface?.GetGenericArguments(); } + /// + /// Gets the description from a on a type. + /// + /// The type to get the description from. + /// The description string, or null if no description is found. + /// Falls back to the base type's description if the type itself has none. public static string? GetDescription(Type type) { return type @@ -506,6 +554,13 @@ public static bool IsDictionary(Type type) ? GetDescription(type.BaseType!) : null); } + + /// + /// Gets the description from a on a parameter. + /// + /// The parameter to get the description from. + /// The description string, or null if no description is found. + /// Falls back to the parameter type's description if the parameter itself has none. public static string? GetDescription(ParameterInfo? parameterInfo) { return parameterInfo @@ -515,6 +570,13 @@ public static bool IsDictionary(Type type) ? GetDescription(parameterInfo.ParameterType) : null); } + + /// + /// Gets the description from a on a member. + /// + /// The member to get the description from. + /// The description string, or null if no description is found. + /// For fields and properties, falls back to the member type's description. public static string? GetDescription(MemberInfo? memberInfo) { if (memberInfo == null) @@ -534,6 +596,13 @@ public static bool IsDictionary(Type type) _ => null }; } + + /// + /// Gets the description from a on a field. + /// + /// The field to get the description from. + /// The description string, or null if no description is found. + /// Falls back to the field type's description if the field itself has none. public static string? GetFieldDescription(FieldInfo? fieldInfo) { if (fieldInfo == null) @@ -546,6 +615,13 @@ public static bool IsDictionary(Type type) ? GetDescription(fieldInfo.FieldType) : null); } + + /// + /// Gets the description from a on a property. + /// + /// The property to get the description from. + /// The description string, or null if no description is found. + /// Falls back to the property type's description if the property itself has none. public static string? GetPropertyDescription(PropertyInfo? propertyInfo) { if (propertyInfo == null) @@ -558,12 +634,32 @@ public static bool IsDictionary(Type type) ? GetDescription(propertyInfo.PropertyType) : null); } + + /// + /// Gets the description from a on a property by name. + /// + /// The type containing the property. + /// The name of the property. + /// The description string, or null if the property is not found or has no description. public static string? GetPropertyDescription(Type type, string propertyName) { var propertyInfo = type.GetProperty(propertyName); return propertyInfo != null ? GetPropertyDescription(propertyInfo) : null; } #if NETSTANDARD2_1_OR_GREATER || NET8_0_OR_GREATER + /// + /// Gets the description from a using JSON schema exporter context. + /// + /// + /// This method handles JSON naming policy transformations by attempting to match: + /// + /// Exact name match + /// PascalCase conversion from camelCase + /// Case-insensitive match + /// + /// + /// The JSON schema exporter context containing property information. + /// The description string, or null if no description is found. public static string? GetPropertyDescription(System.Text.Json.Schema.JsonSchemaExporterContext context) { if (context.PropertyInfo == null || context.PropertyInfo.DeclaringType == null) @@ -610,6 +706,13 @@ private static string ToPascalCase(string camelCase) return char.ToUpperInvariant(camelCase[0]) + camelCase.Substring(1); } + + /// + /// Gets the description from a on a field by name. + /// + /// The type containing the field. + /// The name of the field. + /// The description string, or null if the field is not found or has no description. public static string? GetFieldDescription(Type type, string fieldName) { var fieldInfo = type.GetField(fieldName); @@ -636,6 +739,21 @@ public static bool IsAssignableTo(object? obj, Type targetType) return targetType.IsAssignableFrom(obj.GetType()); } + /// + /// Determines whether a type can be cast to another type. + /// + /// + /// This method checks for: + /// + /// Direct assignability (including inheritance) + /// Primitive type conversions + /// String to object conversion + /// + /// Nullable types are unwrapped before comparison. + /// + /// The source type to cast from. + /// The target type to cast to. + /// true if the cast is possible; otherwise, false. public static bool IsCastable(Type? type, Type to) { if (type == null || to == null) @@ -662,6 +780,13 @@ public static bool IsCastable(Type? type, Type to) return false; } + /// + /// Calculates the inheritance distance between two types. + /// + /// The base type (ancestor). + /// The target type (descendant). + /// The number of inheritance levels between the types, or -1 if + /// does not inherit from . public static int GetInheritanceDistance(Type baseType, Type targetType) { if (!baseType.IsAssignableFrom(targetType)) @@ -676,6 +801,17 @@ public static int GetInheritanceDistance(Type baseType, Type targetType) } return current == baseType ? distance : -1; } + + /// + /// Determines whether a type is considered a primitive type for serialization purposes. + /// + /// + /// This includes CLR primitives, enums, and common value types: + /// , , , + /// , , and . + /// + /// The type to check. + /// true if the type is a primitive or primitive-like type. public static bool IsPrimitive(Type type) { return type.IsPrimitive || @@ -687,6 +823,13 @@ public static bool IsPrimitive(Type type) type == typeof(TimeSpan) || type == typeof(Guid); } + + /// + /// Recursively enumerates all generic type arguments from a type and its base types. + /// + /// The type to extract generic arguments from. + /// Optional set to track visited types and prevent infinite recursion. + /// An enumerable of all generic type arguments found in the type hierarchy. public static IEnumerable GetGenericTypes(Type type, HashSet? visited = null) { visited ??= new HashSet(); @@ -722,6 +865,12 @@ public static IEnumerable GetGenericTypes(Type type, HashSet? visited foreach (var baseGenericType in GetGenericTypes(type.BaseType, visited)) yield return baseGenericType; } + + /// + /// Determines whether a type implements . + /// + /// The type to check. + /// true if the type is an array or implements . public static bool IsIEnumerable(Type type) { if (type.IsArray) @@ -733,6 +882,12 @@ public static bool IsIEnumerable(Type type) return type.GetInterfaces() .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); } + + /// + /// Gets the element type of an enumerable or array type. + /// + /// The enumerable type to inspect. + /// The element type (T in ), or null if the type is not enumerable. public static Type? GetEnumerableItemType(Type type) { if (type.IsArray) @@ -768,6 +923,14 @@ public static bool IsIEnumerable(Type type) return null; } + + /// + /// Resolves a type with priority given to the object's runtime type. + /// + /// The object whose runtime type to use (if not null). + /// The fallback type to use if the object is null. + /// Set to an error message if the type cannot be resolved; otherwise, null. + /// The resolved type, or null if resolution fails. public static Type? GetTypeWithObjectPriority(object? obj, Type? fallbackType, out string? error) { var type = obj?.GetType() ?? fallbackType; @@ -780,6 +943,14 @@ public static bool IsIEnumerable(Type type) error = null; return type; } + + /// + /// Resolves a type with priority given to the . + /// + /// The serialized member containing the type name. + /// The fallback type to use if the type name cannot be resolved. + /// Set to an error message if the type cannot be resolved; otherwise, null. + /// The resolved type, or null if resolution fails. public static Type? GetTypeWithNamePriority(SerializedMember? member, Type? fallbackType, out string? error) { if (StringUtils.IsNullOrEmpty(member?.typeName) && fallbackType == null) @@ -803,6 +974,14 @@ public static bool IsIEnumerable(Type type) error = null; return type; } + + /// + /// Resolves a type with priority given to the provided type parameter. + /// + /// The primary type to use (if not null). + /// The fallback serialized member to extract type name from. + /// Set to an error message if the type cannot be resolved; otherwise, null. + /// The resolved type, or null if resolution fails. public static Type? GetTypeWithValuePriority(Type? type, SerializedMember? fallbackMember, out string? error) { if (type == null)