Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow JS class to extend CLR type #1049

Merged
merged 1 commit into from
Jan 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions Jint.Tests/Runtime/InteropTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,23 @@ public void CanUseDelegateAsFunction()
");
}

[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;
Expand Down
10 changes: 5 additions & 5 deletions Jint/Native/Function/ClassDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ static MethodDefinition CreateConstructorMethodDefinition(string source)
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");
}
Expand All @@ -92,13 +92,13 @@ static MethodDefinition CreateConstructorMethodDefinition(string source)
{
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!;
}

Expand Down Expand Up @@ -138,7 +138,7 @@ static MethodDefinition CreateConstructorMethodDefinition(string source)
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);
Expand Down Expand Up @@ -209,4 +209,4 @@ static MethodDefinition CreateConstructorMethodDefinition(string source)
}
}
}
}
}
7 changes: 3 additions & 4 deletions Jint/Native/Object/ObjectInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ public override bool Set(JsValue property, JsValue value, JsValue receiver)
if (ownDesc == PropertyDescriptor.Undefined)
{
var parent = GetPrototypeOf();
if (!(parent is null))
if (parent is not null)
{
return parent.Set(property, value, receiver);
}
Expand Down Expand Up @@ -504,13 +504,12 @@ public override bool Set(JsValue property, JsValue value, JsValue receiver)
}
}

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;
}
Expand Down
8 changes: 8 additions & 0 deletions Jint/Runtime/Interop/ObjectWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -52,6 +53,13 @@ public override bool Set(JsValue property, JsValue value, JsValue receiver)
// 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)
{
Expand Down
43 changes: 29 additions & 14 deletions Jint/Runtime/Interop/TypeReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,24 +50,35 @@ public override JsValue Call(JsValue thisObject, JsValue[] arguments)

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;
}
Expand Down Expand Up @@ -127,11 +138,15 @@ public override PropertyDescriptor GetOwnProperty(JsValue property)
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)
Expand Down