瀏覽代碼

Allow JS class to extend CLR type (#1049)

Marko Lahma 3 年之前
父節點
當前提交
43d63be7a3

+ 17 - 0
Jint.Tests/Runtime/InteropTests.cs

@@ -787,6 +787,23 @@ namespace Jint.Tests.Runtime
             ");
         }
 
+        [Fact]
+        public void JavaScriptClassCanExtendClrType()
+        {
+            var engine = new Engine();
+            engine.SetValue("TestClass", TypeReference.CreateTypeReference<TestClass>(engine));
+
+            engine.Execute("class ExtendedType extends TestClass { constructor() { super(); this.a = 1; } }");
+            engine.Execute("class MyExtendedType extends ExtendedType { constructor() { super(); this.b = 2; } }");
+            engine.Evaluate("let obj = new MyExtendedType();");
+
+            engine.Evaluate("obj.setString('Hello World!');");
+
+            Assert.Equal("Hello World!", engine.Evaluate("obj.string"));
+            Assert.Equal(1, engine.Evaluate("obj.a"));
+            Assert.Equal(2, engine.Evaluate("obj.b"));
+        }
+
         private struct TestStruct
         {
             public int Value;

+ 5 - 5
Jint/Native/Function/ClassDefinition.cs

@@ -81,7 +81,7 @@ namespace Jint.Native.Function
                     protoParent = null;
                     constructorParent = engine.Realm.Intrinsics.Function.PrototypeObject;
                 }
-                else if (!superclass!.IsConstructor)
+                else if (!superclass.IsConstructor)
                 {
                     ExceptionHelper.ThrowTypeError(engine.Realm, "super class is not a constructor");
                 }
@@ -92,13 +92,13 @@ namespace Jint.Native.Function
                     {
                         protoParent = protoParentObject;
                     }
-                    else if (temp._type == InternalTypes.Null)
+                    else if (temp.IsNull())
                     {
                         // OK
                     }
                     else
                     {
-                        ExceptionHelper.ThrowTypeError(engine.Realm);
+                        ExceptionHelper.ThrowTypeError(engine.Realm, "cannot resolve super class prototype chain");
                         return null!;
                     }
 
@@ -138,7 +138,7 @@ namespace Jint.Native.Function
                     F.SetFunctionName(_className);
                 }
 
-                F.MakeConstructor(false, proto);
+                F.MakeConstructor(writableProperty: false, proto);
                 F._constructorKind = _superClass is null ? ConstructorKind.Base : ConstructorKind.Derived;
                 F.MakeClassConstructor();
                 proto.CreateMethodProperty(CommonProperties.Constructor, F);
@@ -209,4 +209,4 @@ namespace Jint.Native.Function
             }
         }
     }
-}
+}

+ 3 - 4
Jint/Native/Object/ObjectInstance.cs

@@ -463,7 +463,7 @@ namespace Jint.Native.Object
             if (ownDesc == PropertyDescriptor.Undefined)
             {
                 var parent = GetPrototypeOf();
-                if (!(parent is null))
+                if (parent is not null)
                 {
                     return parent.Set(property, value, receiver);
                 }
@@ -504,13 +504,12 @@ namespace Jint.Native.Object
                 }
             }
 
-            if (!(ownDesc.Set is ICallable setter))
+            if (ownDesc.Set is not FunctionInstance setter)
             {
                 return false;
             }
 
-            var functionInstance = (FunctionInstance) setter;
-            _engine.Call(functionInstance, receiver, new[] { value }, expression: null);
+            _engine.Call(setter, receiver, new[] { value }, expression: null);
 
             return true;
         }

+ 8 - 0
Jint/Runtime/Interop/ObjectWrapper.cs

@@ -18,6 +18,7 @@ namespace Jint.Runtime.Interop
 	public sealed class ObjectWrapper : ObjectInstance, IObjectWrapper, IEquatable<ObjectWrapper>
     {
         private readonly TypeDescriptor _typeDescriptor;
+        internal bool _allowAddingProperties;
 
         public ObjectWrapper(Engine engine, object obj)
             : base(engine)
@@ -52,6 +53,13 @@ namespace Jint.Runtime.Interop
                     // can try utilize fast path
                     var accessor = _engine.Options.Interop.TypeResolver.GetAccessor(_engine, Target.GetType(), member);
 
+                    if (ReferenceEquals(accessor, ConstantValueAccessor.NullAccessor) && _allowAddingProperties)
+                    {
+                        // there's no such property, but we can allow extending by calling base
+                        // which will add properties, this allows for example JS class to extend a CLR type
+                        return base.Set(property, value, receiver);
+                    }
+
                     // CanPut logic
                     if (!accessor.Writable || !_engine.Options.Interop.AllowWrite)
                     {

+ 29 - 14
Jint/Runtime/Interop/TypeReference.cs

@@ -50,24 +50,35 @@ namespace Jint.Runtime.Interop
 
         private ObjectInstance Construct(JsValue[] arguments)
         {
+            ObjectInstance result = null;
             if (arguments.Length == 0 && ReferenceType.IsValueType)
             {
                 var instance = Activator.CreateInstance(ReferenceType);
-                var result = TypeConverter.ToObject(_realm, FromObject(Engine, instance));
-
-                return result;
+                result = TypeConverter.ToObject(_realm, FromObject(Engine, instance));
             }
+            else
+            {
+                var constructors = _constructorCache.GetOrAdd(
+                    ReferenceType,
+                    t => MethodDescriptor.Build(t.GetConstructors(BindingFlags.Public | BindingFlags.Instance)));
 
-            var constructors = _constructorCache.GetOrAdd(
-                ReferenceType,
-                t => MethodDescriptor.Build(t.GetConstructors(BindingFlags.Public | BindingFlags.Instance)));
+                foreach (var (method, _, _) in TypeConverter.FindBestMatch(_engine, constructors, _ => arguments))
+                {
+                    var retVal = method.Call(Engine, null, arguments);
+                    result = TypeConverter.ToObject(_realm, retVal);
 
-            foreach (var (method, _, _) in TypeConverter.FindBestMatch(_engine, constructors, _ => arguments))
-            {
-                var retVal = method.Call(Engine, null, arguments);
-                var result = TypeConverter.ToObject(_realm, retVal);
+                    // todo: cache method info
+                    break;
+                }
+            }
 
-                // todo: cache method info
+            if (result is not null)
+            {
+                if (result is ObjectWrapper objectWrapper)
+                {
+                    // allow class extension
+                    objectWrapper._allowAddingProperties = true;
+                }
 
                 return result;
             }
@@ -127,11 +138,15 @@ namespace Jint.Runtime.Interop
             if (_properties?.TryGetValue(key, out descriptor) != true)
             {
                 descriptor = CreatePropertyDescriptor(key);
-                _properties ??= new PropertyDictionary();
-                _properties[key] = descriptor;
+                if (!ReferenceEquals(descriptor, PropertyDescriptor.Undefined))
+                {
+                    _properties ??= new PropertyDictionary();
+                    _properties[key] = descriptor;
+                    return descriptor;
+                }
             }
 
-            return descriptor;
+            return base.GetOwnProperty(property);
         }
 
         private PropertyDescriptor CreatePropertyDescriptor(string name)