diff --git a/Jint.Tests/Runtime/InteropTests.cs b/Jint.Tests/Runtime/InteropTests.cs index 7b2a3efa04..afc1b1e5ed 100644 --- a/Jint.Tests/Runtime/InteropTests.cs +++ b/Jint.Tests/Runtime/InteropTests.cs @@ -4194,4 +4194,83 @@ public void ShouldSetDerivedTypePropertyWhenDeclaredTypeHasIndexer() var geometry = (GeometryWrapperWithProperty) feature.Geometry; Assert.Equal(99.9, geometry.X); } + + public class TypeWithListConstructor + { + public TypeWithListConstructor(List items) + { + Items = items; + } + + public List Items { get; } + } + + public class TypeWithCollectionParameters + { + public IList IListItems { get; set; } = []; + public ICollection ICollectionItems { get; set; } = []; + public IEnumerable IEnumerableItems { get; set; } = []; + public IReadOnlyList IReadOnlyListItems { get; set; } = []; + public IReadOnlyCollection IReadOnlyCollectionItems { get; set; } = []; + + public void SetIListItems(IList items) => IListItems = items; + public void SetICollectionItems(ICollection items) => ICollectionItems = items; + public void SetIEnumerableItems(IEnumerable items) => IEnumerableItems = items; + public void SetIReadOnlyListItems(IReadOnlyList items) => IReadOnlyListItems = items; + public void SetIReadOnlyCollectionItems(IReadOnlyCollection items) => IReadOnlyCollectionItems = items; + } + + [Fact] + public void ShouldConvertJsArrayToListWhenPassedToConstructor() + { + var engine = new Engine(options => options.AllowClr(GetType().Assembly)); + engine.SetValue("TypeWithListConstructor", TypeReference.CreateTypeReference(engine, typeof(TypeWithListConstructor))); + + var result = engine.Evaluate("new TypeWithListConstructor(['a', 'b', 'c'])"); + var obj = result.ToObject() as TypeWithListConstructor; + + Assert.NotNull(obj); + Assert.Equal(3, obj.Items.Count); + Assert.Equal("a", obj.Items[0]); + Assert.Equal("b", obj.Items[1]); + Assert.Equal("c", obj.Items[2]); + } + + [Fact] + public void ShouldConvertJsArrayToEmptyListWhenPassedToConstructor() + { + var engine = new Engine(options => options.AllowClr(GetType().Assembly)); + engine.SetValue("TypeWithListConstructor", TypeReference.CreateTypeReference(engine, typeof(TypeWithListConstructor))); + + var result = engine.Evaluate("new TypeWithListConstructor([])"); + var obj = result.ToObject() as TypeWithListConstructor; + + Assert.NotNull(obj); + Assert.Empty(obj.Items); + } + + [Fact] + public void ShouldConvertJsArrayToGenericCollectionTypes() + { + var engine = new Engine(options => options.AllowClr(GetType().Assembly)); + var target = new TypeWithCollectionParameters(); + engine.SetValue("target", target); + + engine.Evaluate("target.SetIListItems(['a', 'b'])"); + Assert.Equal(2, target.IListItems.Count); + Assert.Equal("a", target.IListItems[0]); + + engine.Evaluate("target.SetICollectionItems(['c', 'd'])"); + Assert.Equal(2, target.ICollectionItems.Count); + + engine.Evaluate("target.SetIEnumerableItems(['e', 'f'])"); + Assert.Equal(2, target.IEnumerableItems.Count()); + + engine.Evaluate("target.SetIReadOnlyListItems(['g', 'h'])"); + Assert.Equal(2, target.IReadOnlyListItems.Count); + Assert.Equal("g", target.IReadOnlyListItems[0]); + + engine.Evaluate("target.SetIReadOnlyCollectionItems(['i', 'j'])"); + Assert.Equal(2, target.IReadOnlyCollectionItems.Count); + } } diff --git a/Jint/Runtime/Interop/DefaultTypeConverter.cs b/Jint/Runtime/Interop/DefaultTypeConverter.cs index a951366859..b06f158d50 100644 --- a/Jint/Runtime/Interop/DefaultTypeConverter.cs +++ b/Jint/Runtime/Interop/DefaultTypeConverter.cs @@ -107,6 +107,43 @@ private bool TryConvert( return true; } + // Handle conversion from object[] (JS array) to generic collection types like List, IList, IEnumerable, etc. + // This must come before the generic assignability check because object[] incorrectly satisfies + // the assignability check for IList etc. (since object[] implements IList). + if (value is object?[] sourceArray && type.IsGenericType) + { + var genericArgs = type.GetGenericArguments(); + + if (genericArgs.Length == 1) + { + var genericTypeDef = type.GetGenericTypeDefinition(); + var elementType = genericArgs[0]; + + if (genericTypeDef != typeof(Collection<>) && InteropHelper.GenericCollectionTypeDefinitions.Contains(genericTypeDef)) + { + var targetList = (IList) Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType))!; + foreach (var item in sourceArray) + { + targetList.Add(item is null ? null : Convert(item, elementType, formatProvider)); + } + converted = targetList; + return true; + } + + if (genericTypeDef == typeof(Collection<>)) + { + var innerListType = typeof(List<>).MakeGenericType(elementType); + var innerList = (IList) Activator.CreateInstance(innerListType)!; + foreach (var item in sourceArray) + { + innerList.Add(item is null ? null : Convert(item, elementType, formatProvider)); + } + converted = Activator.CreateInstance(type, innerList)!; + return true; + } + } + } + if (type.IsGenericType) { var result = InteropHelper.IsAssignableToGenericType(value.GetType(), type); diff --git a/Jint/Runtime/Interop/InteropHelper.cs b/Jint/Runtime/Interop/InteropHelper.cs index cada0caec4..745be062b9 100644 --- a/Jint/Runtime/Interop/InteropHelper.cs +++ b/Jint/Runtime/Interop/InteropHelper.cs @@ -210,7 +210,7 @@ parameterValue is JsNumber jsNumber return 1; } - if (parameterValue.IsArray() && paramType.IsArray) + if (parameterValue.IsArray() && (paramType.IsArray || IsGenericCollectionType(paramType))) { // we have potential, TODO if we'd know JS array's internal type we could have exact match return 2; @@ -304,6 +304,24 @@ private static bool CanChangeType(object value, Type targetType) } } + internal static readonly HashSet GenericCollectionTypeDefinitions = + [ + typeof(List<>), + typeof(IList<>), + typeof(ICollection<>), + typeof(IEnumerable<>), + typeof(IReadOnlyList<>), + typeof(IReadOnlyCollection<>), + typeof(System.Collections.ObjectModel.Collection<>), + ]; + + internal static bool IsGenericCollectionType(Type type) + { + return type.IsGenericType + && type.GetGenericArguments().Length == 1 + && GenericCollectionTypeDefinitions.Contains(type.GetGenericTypeDefinition()); + } + internal static bool TypeIsNullable(Type type) { return !type.IsValueType || Nullable.GetUnderlyingType(type) != null; diff --git a/Jint/Runtime/Interop/MethodInfoFunction.cs b/Jint/Runtime/Interop/MethodInfoFunction.cs index 72a6f9c5bc..7fc72d0c39 100644 --- a/Jint/Runtime/Interop/MethodInfoFunction.cs +++ b/Jint/Runtime/Interop/MethodInfoFunction.cs @@ -51,6 +51,13 @@ private static bool IsGenericParameter(object? argObj, Type parameterType) if (parameterType.IsGenericParameter || parameterType.IsGenericType) { + // For fully concrete generic types (no open type parameters), verify the argument + // is actually assignable to prevent incorrect direct assignment when type arguments + // differ (e.g., object[] should not be directly assigned to IList) + if (!parameterType.ContainsGenericParameters && !parameterType.IsAssignableFrom(argObj.GetType())) + { + return false; + } return true; } return false;