diff --git a/Jint.Tests/Runtime/ProxyTests.cs b/Jint.Tests/Runtime/ProxyTests.cs index d97530ee5a..6cbc615a9f 100644 --- a/Jint.Tests/Runtime/ProxyTests.cs +++ b/Jint.Tests/Runtime/ProxyTests.cs @@ -205,6 +205,43 @@ public void ConstructHandlerInvariant() Assert.True(_engine.Evaluate(Script).AsBoolean()); } + // https://tc39.es/ecma262/#sec-proxycreate + // A proxy only has [[Construct]] if its target is a constructor at creation time; + // a handler "construct" trap cannot confer constructability. + [Fact] + public void ProxyWithNonConstructorTargetIsNotConstructor() + { + var ex = Assert.Throws(() => _engine.Evaluate("new (new Proxy(() => {}, { construct: () => ({}) }))()")); + Assert.Same(TypeErrorPrototype(_engine), ex.Error.AsObject().Prototype); + } + + [Fact] + public void ProxyWithConstructorTargetUsesConstructTrap() + { + var result = _engine.Evaluate("new (new Proxy(function(){}, { construct: () => ({ x: 1 }) }))().x"); + Assert.Equal(1, result.AsInteger()); + } + + [Fact] + public void ReflectConstructRequiresConstructorNewTarget() + { + var ex = Assert.Throws(() => _engine.Evaluate("Reflect.construct(function(){ return; }, [], new Proxy(() => {}, { construct: () => ({}) }))")); + Assert.Same(TypeErrorPrototype(_engine), ex.Error.AsObject().Prototype); + } + + [Fact] + public void RevokedConstructorProxyKeepsTypeofFunctionButConstructThrowsRevokedError() + { + _engine.Execute("var r = Proxy.revocable(function(){}, {}); r.revoke();"); + + // typeof relies on the [[Call]] slot captured at creation, revocation does not remove it + Assert.Equal("function", _engine.Evaluate("typeof r.proxy").AsString()); + + var ex = Assert.Throws(() => _engine.Evaluate("new r.proxy()")); + Assert.Same(TypeErrorPrototype(_engine), ex.Error.AsObject().Prototype); + Assert.Contains("revoked", ex.Message); + } + [Fact] public void ProxyHandlerGetDataPropertyShouldNotUseReferenceEquals() { diff --git a/Jint/Native/JsProxy.cs b/Jint/Native/JsProxy.cs index ccb0a3e710..5fcefd7fdd 100644 --- a/Jint/Native/JsProxy.cs +++ b/Jint/Native/JsProxy.cs @@ -10,6 +10,13 @@ internal sealed class JsProxy : ObjectInstance, IConstructor, ICallable internal ObjectInstance _target; internal ObjectInstance? _handler; + // https://tc39.es/ecma262/#sec-proxycreate + // A proxy has [[Construct]] iff its target is a constructor at creation time; + // a handler "construct" trap cannot confer constructability. Captured here so + // IsConstructor probes are side-effect free (no handler getter invocation) and + // remain correct after revocation nulls the target. + private readonly bool _isConstructor; + private static readonly JsString TrapApply = new JsString("apply"); private static readonly JsString TrapGet = new JsString("get"); private static readonly JsString TrapSet = new JsString("set"); @@ -36,6 +43,7 @@ public JsProxy( _target = target; _handler = handler; IsCallable = target.IsCallable; + _isConstructor = target.IsConstructor; } /// @@ -43,9 +51,11 @@ public JsProxy( /// JsValue ICallable.Call(JsValue thisObject, params JsCallArguments arguments) { - if (_target is not ICallable) + AssertNotRevoked(TrapApply); + + if (!IsCallable) { - Throw.TypeError(_engine.Realm, "(intermediate value) is not a function"); + Throw.TypeError(_engine.Realm, "Proxy target is not a function"); } var jsValues = new[] { _target, thisObject, _engine.Realm.Intrinsics.Array.ConstructFast(arguments) }; @@ -68,9 +78,11 @@ JsValue ICallable.Call(JsValue thisObject, params JsCallArguments arguments) /// ObjectInstance IConstructor.Construct(JsCallArguments arguments, JsValue newTarget) { - if (_target is not ICallable) + AssertNotRevoked(TrapConstruct); + + if (!_isConstructor) { - Throw.TypeError(_engine.Realm, "(intermediate value) is not a constructor"); + Throw.TypeError(_engine.Realm, "Proxy target is not a constructor"); } var argArray = _engine.Realm.Intrinsics.Array.Construct(arguments, _engine.Realm.Intrinsics.Array); @@ -102,23 +114,7 @@ internal override bool IsArray() public override object ToObject() => _target.ToObject(); - internal override bool IsConstructor - { - get - { - if (_target is not null && _target.IsConstructor) - { - return true; - } - - if (_handler is not null && _handler.TryGetValue(TrapConstruct, out var handlerFunction) && handlerFunction is IConstructor) - { - return true; - } - - return false; - } - } + internal override bool IsConstructor => _isConstructor; /// /// https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver