Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Jint.Tests/Runtime/ProxyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,29 @@ public void ConstructTrapAcceptsSingleFractionalArgument()
Assert.Equal(1.5, result.AsNumber());
}

[Fact]
public void ProxyInvariantViolationsHaveDescriptiveMessages()
{
_engine.Execute("""
var frozen = Object.freeze({ a: 1 });
var pHas = new Proxy(frozen, { has: () => false });
var pOwnKeys = new Proxy(frozen, { ownKeys: () => ['b'] });
var pPrevent = new Proxy({}, { preventExtensions: () => true });
""");

var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("'a' in pHas"));
AssertJsTypeError(_engine, ex, "'has' on proxy: trap returned falsish for property 'a' which exists in the proxy target as non-configurable");

ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("Object.keys(pOwnKeys)"));
AssertJsTypeError(_engine, ex, "'ownKeys' on proxy: trap result did not include non-configurable property 'a' of the proxy target");

ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("Object.preventExtensions(pPrevent)"));
AssertJsTypeError(_engine, ex, "'preventExtensions' on proxy: trap returned truish but the proxy target is extensible");

ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("new Proxy({}, { get: 1 }).x"));
AssertJsTypeError(_engine, ex, "'get' trap of proxy handler is not a function");
}

[Fact]
public void ProxyToStringUseTarget()
{
Expand Down
59 changes: 34 additions & 25 deletions Jint/Native/JsProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ ObjectInstance IConstructor.Construct(JsCallArguments arguments, JsValue newTarg
var constructor = target as IConstructor;
if (constructor is null)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, "Proxy target is not a constructor");
}

return constructor.Construct(arguments, newTarget);
Expand All @@ -111,7 +111,7 @@ ObjectInstance IConstructor.Construct(JsCallArguments arguments, JsValue newTarg
var oi = result as ObjectInstance;
if (oi is null)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'construct' on proxy: trap returned non-object ('{result}')");
}

return oi;
Expand Down Expand Up @@ -195,7 +195,7 @@ private List<JsValue> ValidateOwnKeysTrapResult(ObjectInstance target, JsValue r
{
if (!uncheckedResultKeys.Add(key))
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'ownKeys' on proxy: trap returned duplicate entries ('{key}')");
}
trapResult.Add(key);
}
Expand Down Expand Up @@ -228,7 +228,7 @@ private List<JsValue> ValidateOwnKeysTrapResult(ObjectInstance target, JsValue r
var key = targetNonconfigurableKeys[i];
if (!uncheckedResultKeys.Remove(key))
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'ownKeys' on proxy: trap result did not include non-configurable property '{key}' of the proxy target");
}
}
}
Expand All @@ -245,14 +245,14 @@ private List<JsValue> ValidateOwnKeysTrapResult(ObjectInstance target, JsValue r
var key = targetConfigurableKeys[i];
if (!uncheckedResultKeys.Remove(key))
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'ownKeys' on proxy: trap result did not include property '{key}' of the non-extensible proxy target");
}
}
}

if (uncheckedResultKeys.Count > 0)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, "'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible");
}

return trapResult;
Expand All @@ -278,7 +278,7 @@ private PropertyDescriptor ValidateGetOwnPropertyTrapResult(ObjectInstance targe
{
if (!trapResultObj.IsObject() && !trapResultObj.IsUndefined())
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'getOwnPropertyDescriptor' on proxy: trap returned neither object nor undefined for property '{property}'");
}

var targetDesc = target.GetOwnProperty(property);
Expand All @@ -290,9 +290,14 @@ private PropertyDescriptor ValidateGetOwnPropertyTrapResult(ObjectInstance targe
return targetDesc;
}

if (!targetDesc.Configurable || !target.Extensible)
if (!targetDesc.Configurable)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'getOwnPropertyDescriptor' on proxy: trap returned undefined for property '{property}' which is non-configurable in the proxy target");
}

if (!target.Extensible)
{
Throw.TypeError(_engine.Realm, $"'getOwnPropertyDescriptor' on proxy: trap returned undefined for property '{property}' which exists in the non-extensible proxy target");
}

return PropertyDescriptor.Undefined;
Expand All @@ -305,21 +310,21 @@ private PropertyDescriptor ValidateGetOwnPropertyTrapResult(ObjectInstance targe
var valid = IsCompatiblePropertyDescriptor(extensibleTarget, resultDesc, targetDesc);
if (!valid)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property '{property}' that is incompatible with the existing property in the proxy target");
}

if (!resultDesc.Configurable)
{
if (targetDesc == PropertyDescriptor.Undefined || targetDesc.Configurable)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '{property}' which is either non-existent or configurable in the proxy target");
}

if (resultDesc.WritableSet && !resultDesc.Writable)
{
if (targetDesc.Writable)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'getOwnPropertyDescriptor' on proxy: trap reported non-configurable and writable: false for property '{property}' which is non-configurable and writable in the proxy target");
}
}
}
Expand Down Expand Up @@ -434,27 +439,31 @@ private void ValidateDefinePropertyTrapResult(ObjectInstance target, JsValue pro

if (targetDesc == PropertyDescriptor.Undefined)
{
if (!extensibleTarget || settingConfigFalse)
if (!extensibleTarget)
{
Throw.TypeError(_engine.Realm, $"'defineProperty' on proxy: trap returned truish for adding property '{property}' to the non-extensible proxy target");
}
if (settingConfigFalse)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'defineProperty' on proxy: trap returned truish for defining non-configurable property '{property}' which is non-existent in the proxy target");
}
}
else
{
if (!IsCompatiblePropertyDescriptor(extensibleTarget, desc, targetDesc))
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'defineProperty' on proxy: trap returned truish for adding property '{property}' that is incompatible with the existing property in the proxy target");
}
if (targetDesc.Configurable && settingConfigFalse)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'defineProperty' on proxy: trap returned truish for defining non-configurable property '{property}' which is configurable in the proxy target");
}

if (targetDesc.IsDataDescriptor() && !targetDesc.Configurable && targetDesc.Writable)
{
if (desc.WritableSet && !desc.Writable)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'defineProperty' on proxy: trap returned truish for defining non-writable property '{property}' which is writable in the proxy target");
}
}
}
Expand Down Expand Up @@ -493,12 +502,12 @@ private void ValidateHasTrapResult(ObjectInstance target, JsValue property)
{
if (!targetDesc.Configurable)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'has' on proxy: trap returned falsish for property '{property}' which exists in the proxy target as non-configurable");
}

if (!target.Extensible)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'has' on proxy: trap returned falsish for property '{property}' but the proxy target is not extensible");
}
}
}
Expand Down Expand Up @@ -540,7 +549,7 @@ private void ValidateDeleteTrapResult(ObjectInstance target, JsValue property)

if (!target.Extensible)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'deleteProperty' on proxy: trap returned truish for property '{property}' but the proxy target is non-extensible");
}
}

Expand All @@ -559,7 +568,7 @@ public override bool PreventExtensions()
var success = TypeConverter.ToBoolean(CallTrap(trap, target));
if (success && target.Extensible)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, "'preventExtensions' on proxy: trap returned truish but the proxy target is extensible");
}

return success;
Expand All @@ -583,7 +592,7 @@ public override bool Extensible
var targetResult = target.Extensible;
if (booleanTrapResult != targetResult)
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, $"'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is '{(targetResult ? "true" : "false")}')");
}

return booleanTrapResult;
Expand Down Expand Up @@ -617,7 +626,7 @@ public override bool Extensible

if (!ReferenceEquals(proto, target.Prototype))
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, "'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype");
}

return proto;
Expand Down Expand Up @@ -650,7 +659,7 @@ internal override bool SetPrototypeOf(JsValue value)
var proto = value.IsNull() ? null : value as ObjectInstance;
if (!ReferenceEquals(proto, target.Prototype))
{
Throw.TypeError(_engine.Realm);
Throw.TypeError(_engine.Realm, "'setPrototypeOf' on proxy: trap returned truish for setting a new prototype on the non-extensible proxy target");
}

return true;
Expand Down Expand Up @@ -678,7 +687,7 @@ internal override bool SetPrototypeOf(JsValue value)
var callable = handlerFunction as ICallable;
if (callable is null)
{
Throw.TypeError(_engine.Realm, $"{_handler} returned for property '{trapName}' of object '{_target}' is not a function");
Throw.TypeError(_engine.Realm, $"'{trapName}' trap of proxy handler is not a function");
}

return callable;
Expand Down
Loading