Kaynağa Gözat

Support interop against System.Text.Json types on NET8+ (#1826)

Marko Lahma 1 yıl önce
ebeveyn
işleme
198f4f73c4

+ 13 - 3
Jint.Tests.PublicInterface/InteropTests.SystemTextJson.cs

@@ -1,4 +1,3 @@
-using System.Reflection;
 using System.Text.Json.Nodes;
 using Jint.Native;
 using Jint.Runtime.Interop;
@@ -8,6 +7,12 @@ namespace Jint.Tests.PublicInterface;
 
 public sealed class SystemTextJsonValueConverter : IObjectConverter
 {
+    public static readonly SystemTextJsonValueConverter Instance = new();
+
+    private SystemTextJsonValueConverter()
+    {
+    }
+
     public bool TryConvert(Engine engine, object value, out JsValue result)
     {
         if (value is JsonValue jsonValue)
@@ -90,6 +95,9 @@ public partial class InteropTests
 
         var engine = new Engine(options =>
         {
+#if !NET8_0_OR_GREATER
+            // Jint doesn't know about the types statically as they are not part of the out-of-the-box experience
+
             // make JsonArray behave like JS array
             options.Interop.WrapObjectHandler = static (e, target, type) =>
             {
@@ -103,13 +111,14 @@ public partial class InteropTests
                 return ObjectWrapper.Create(e, target);
             };
 
-            options.AddObjectConverter(new SystemTextJsonValueConverter());
+            options.AddObjectConverter(SystemTextJsonValueConverter.Instance);
+
             // we cannot access this[string] with anything else than JsonObject, otherwise itw will throw
             options.Interop.TypeResolver = new TypeResolver
             {
                 MemberFilter = static info =>
                 {
-                    if (info.ReflectedType != typeof(JsonObject) && info.Name == "Item" && info is PropertyInfo p)
+                    if (info.ReflectedType != typeof(JsonObject) && info.Name == "Item" && info is System.Reflection.PropertyInfo p)
                     {
                         var parameters = p.GetIndexParameters();
                         return parameters.Length != 1 || parameters[0].ParameterType != typeof(string);
@@ -118,6 +127,7 @@ public partial class InteropTests
                     return true;
                 }
             };
+#endif
         });
 
         engine

+ 1 - 1
Jint.Tests.PublicInterface/Jint.Tests.PublicInterface.csproj

@@ -27,7 +27,7 @@
     <PackageReference Include="MongoDB.Bson.signed" />
     <PackageReference Include="NodaTime" />
     <PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
-    <PackageReference Include="System.Text.Json" />
+    <PackageReference Include="System.Text.Json" Condition="!$([MSBuild]::IsTargetFrameworkCompatible($(TargetFramework), 'net8.0'))" />
     <PackageReference Include="xunit" />
     <PackageReference Include="xunit.runner.visualstudio" />
   </ItemGroup>

+ 5 - 0
Jint/Options.cs

@@ -355,6 +355,11 @@ public class Options
         /// Defaults to <see cref="System.DateTimeKind.Utc"/>.
         /// </summary>
         public DateTimeKind DateTimeKind { get; set; } = DateTimeKind.Utc;
+
+        /// <summary>
+        /// Should the Array prototype be attached instead of Object prototype to the wrapped interop objects when type looks suitable. Defaults to true.
+        /// </summary>
+        public bool AttachArrayPrototype { get; set; } = true;
     }
 
     public class ConstraintOptions

+ 26 - 0
Jint/Runtime/Interop/DefaultObjectConverter.cs

@@ -86,6 +86,14 @@ namespace Jint
                 }
 #endif
 
+#if NET8_0_OR_GREATER
+                if (value is System.Text.Json.Nodes.JsonValue jsonValue)
+                {
+                    result = ConvertSystemTextJsonValue(engine, jsonValue);
+                    return result is not null;
+                }
+#endif
+
                 var t = value.GetType();
 
                 if (!engine.Options.Interop.AllowSystemReflection
@@ -148,6 +156,24 @@ namespace Jint
             return result is not null;
         }
 
+#if NET8_0_OR_GREATER
+        private static JsValue? ConvertSystemTextJsonValue(Engine engine, System.Text.Json.Nodes.JsonValue value)
+        {
+            return value.GetValueKind() switch
+            {
+                System.Text.Json.JsonValueKind.Object => JsValue.FromObject(engine, value),
+                System.Text.Json.JsonValueKind.Array => JsValue.FromObject(engine, value),
+                System.Text.Json.JsonValueKind.String => JsString.Create(value.ToString()),
+                System.Text.Json.JsonValueKind.Number => value.TryGetValue<double>(out var doubleValue) ? JsNumber.Create(doubleValue) : JsValue.Undefined,
+                System.Text.Json.JsonValueKind.True => JsBoolean.True,
+                System.Text.Json.JsonValueKind.False => JsBoolean.False,
+                System.Text.Json.JsonValueKind.Undefined => JsValue.Undefined,
+                System.Text.Json.JsonValueKind.Null => JsValue.Null,
+                _ => null
+            };
+        }
+#endif
+
         private static bool TryConvertConvertible(Engine engine, IConvertible convertible, [NotNullWhen(true)] out JsValue? result)
         {
             result = convertible.GetTypeCode() switch

+ 2 - 1
Jint/Runtime/Interop/DefaultTypeConverter.cs

@@ -56,7 +56,8 @@ namespace Jint.Runtime.Interop
             return converted;
         }
 
-        public virtual bool TryConvert(object? value,
+        public virtual bool TryConvert(
+            object? value,
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicFields)] Type type,
             IFormatProvider formatProvider,
             [NotNullWhen(true)] out object? converted)

+ 1 - 3
Jint/Runtime/Interop/MethodInfoFunction.cs

@@ -276,9 +276,7 @@ namespace Jint.Runtime.Interop
                 return jsArguments;
             }
 
-            var jsArray = Engine.Realm.Intrinsics.Array.Construct(Arguments.Empty);
-            Engine.Realm.Intrinsics.Array.PrototypeObject.Push(jsArray, argsToTransform);
-
+            var jsArray = new JsArray(_engine, argsToTransform);
             var newArgumentsCollection = new JsValue[nonParamsArgumentsCount + 1];
             for (var j = 0; j < nonParamsArgumentsCount; ++j)
             {

+ 19 - 1
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -31,12 +31,19 @@ namespace Jint.Runtime.Interop
             Target = obj;
             ClrType = GetClrType(obj, type);
             _typeDescriptor = TypeDescriptor.Get(ClrType);
+
             if (_typeDescriptor.LengthProperty is not null)
             {
                 // create a forwarder to produce length from Count or Length if one of them is present
                 var functionInstance = new ClrFunction(engine, "length", GetLength);
                 var descriptor = new GetSetPropertyDescriptor(functionInstance, Undefined, PropertyFlag.Configurable);
                 SetProperty(KnownKeys.Length, descriptor);
+
+                if (_typeDescriptor.IsArrayLike && engine.Options.Interop.AttachArrayPrototype)
+                {
+                    // if we have array-like object, we can attach array prototype
+                    SetPrototypeOf(engine.Intrinsics.Array.PrototypeObject);
+                }
             }
         }
 
@@ -45,7 +52,18 @@ namespace Jint.Runtime.Interop
         /// </summary>
         public static ObjectInstance Create(Engine engine, object obj, Type? type = null)
 #pragma warning disable CS0618 // Type or member is obsolete
-            => new ObjectWrapper(engine, obj, type);
+        {
+
+#if NET8_0_OR_GREATER
+            if (type == typeof(System.Text.Json.Nodes.JsonNode))
+            {
+                // we need to always expose the actual type instead of the type nodes provide
+                type = obj.GetType();
+            }
+#endif
+
+            return new ObjectWrapper(engine, obj, type);
+        }
 #pragma warning restore CS0618 // Type or member is obsolete
 
         public object Target { get; }

+ 60 - 37
Jint/Runtime/Interop/Reflection/IndexerAccessor.cs

@@ -48,46 +48,13 @@ internal sealed class IndexerAccessor : ReflectionAccessor
             integerKey = intKeyTemp;
         }
 
-        IndexerAccessor? ComposeIndexerFactory(PropertyInfo candidate, Type paramType)
-        {
-            object? key = null;
-            // int key is quite common case
-            if (paramType == typeof(int) && integerKey is not null)
-            {
-                key = integerKey;
-            }
-            else
-            {
-                engine.TypeConverter.TryConvert(propertyName, paramType, CultureInfo.InvariantCulture, out key);
-            }
-
-            if (key is not null)
-            {
-                // the key can be converted for this indexer
-                var indexerProperty = candidate;
-                // get contains key method to avoid index exception being thrown in dictionaries
-                paramTypeArray[0] = paramType;
-                var containsKeyMethod = targetType.GetMethod(nameof(IDictionary<string, string>.ContainsKey), paramTypeArray);
-                if (containsKeyMethod is null && targetType.IsAssignableFrom(typeof(IDictionary)))
-                {
-                    paramTypeArray[0] = typeof(object);
-                    containsKeyMethod = targetType.GetMethod(nameof(IDictionary.Contains), paramTypeArray);
-                }
-
-                return new IndexerAccessor(indexerProperty, containsKeyMethod, key);
-            }
-
-            // the key type doesn't work for this indexer
-            return null;
-        }
-
         var filter = new Func<MemberInfo, bool>(m => engine.Options.Interop.TypeResolver.Filter(engine, m));
 
         // default indexer wins
         var descriptor = TypeDescriptor.Get(targetType);
-        if (descriptor.IntegerIndexerProperty is not null && filter(descriptor.IntegerIndexerProperty))
+        if (descriptor.IntegerIndexerProperty is not null && !filter(descriptor.IntegerIndexerProperty))
         {
-            indexerAccessor = ComposeIndexerFactory(descriptor.IntegerIndexerProperty, typeof(int));
+            indexerAccessor = ComposeIndexerFactory(engine, targetType, descriptor.IntegerIndexerProperty, paramType: typeof(int), propertyName, integerKey, paramTypeArray);
             if (indexerAccessor != null)
             {
                 indexer = descriptor.IntegerIndexerProperty;
@@ -113,7 +80,7 @@ internal sealed class IndexerAccessor : ReflectionAccessor
             if (candidate.GetGetMethod() != null || candidate.GetSetMethod() != null)
             {
                 var paramType = indexParameters[0].ParameterType;
-                indexerAccessor = ComposeIndexerFactory(candidate, paramType);
+                indexerAccessor = ComposeIndexerFactory(engine, targetType, candidate, paramType, propertyName, integerKey, paramTypeArray);
                 if (indexerAccessor != null)
                 {
                     if (paramType != typeof(string) ||  integerKey is null)
@@ -145,6 +112,62 @@ internal sealed class IndexerAccessor : ReflectionAccessor
         return false;
     }
 
+    private static IndexerAccessor? ComposeIndexerFactory(
+        Engine engine,
+        [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.PublicProperties)] Type targetType,
+        PropertyInfo candidate,
+        Type paramType,
+        string propertyName,
+        int? integerKey,
+        Type[] paramTypeArray)
+    {
+        // check for known incompatible types
+#if NET8_0_OR_GREATER
+        if (typeof(System.Text.Json.Nodes.JsonNode).IsAssignableFrom(targetType)
+            && (targetType != typeof(System.Text.Json.Nodes.JsonArray) || paramType != typeof(int))
+            && (targetType != typeof(System.Text.Json.Nodes.JsonObject) || paramType != typeof(string)))
+        {
+            // we cannot access this[string] with anything else than JsonObject, otherwise itw will throw
+            // we cannot access this[int] with anything else than JsonArray, otherwise itw will throw
+            return null;
+        }
+#endif
+
+        object? key = null;
+        // int key is quite common case
+        if (paramType == typeof(int))
+        {
+            if (integerKey is not null)
+            {
+                key = integerKey;
+            }
+        }
+        else
+        {
+            engine.TypeConverter.TryConvert(propertyName, paramType, CultureInfo.InvariantCulture, out key);
+        }
+
+        if (key is not null)
+        {
+            // the key can be converted for this indexer
+            var indexerProperty = candidate;
+            // get contains key method to avoid index exception being thrown in dictionaries
+            paramTypeArray[0] = paramType;
+            var containsKeyMethod = targetType.GetMethod(nameof(IDictionary<string, string>.ContainsKey), paramTypeArray);
+            if (containsKeyMethod is null && targetType.IsAssignableFrom(typeof(IDictionary)))
+            {
+                paramTypeArray[0] = typeof(object);
+                containsKeyMethod = targetType.GetMethod(nameof(IDictionary.Contains), paramTypeArray);
+            }
+
+            return new IndexerAccessor(indexerProperty, containsKeyMethod, key);
+        }
+
+        // the key type doesn't work for this indexer
+        return null;
+    }
+
+
     public override bool Readable => _indexer.CanRead;
 
     public override bool Writable => _indexer.CanWrite;
@@ -157,7 +180,7 @@ internal sealed class IndexerAccessor : ReflectionAccessor
             return null;
         }
 
-        object[] parameters = { _key };
+        object[] parameters = [_key];
 
         if (_containsKey != null)
         {