diff --git a/Jint.Tests/Runtime/ProxyTests.cs b/Jint.Tests/Runtime/ProxyTests.cs index 1cd8cd422..5b2c8efcc 100644 --- a/Jint.Tests/Runtime/ProxyTests.cs +++ b/Jint.Tests/Runtime/ProxyTests.cs @@ -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(() => _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(() => _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(() => _engine.Evaluate("Object.preventExtensions(pPrevent)")); + AssertJsTypeError(_engine, ex, "'preventExtensions' on proxy: trap returned truish but the proxy target is extensible"); + + ex = Assert.Throws(() => _engine.Evaluate("new Proxy({}, { get: 1 }).x")); + AssertJsTypeError(_engine, ex, "'get' trap of proxy handler is not a function"); + } + [Fact] public void ProxyToStringUseTarget() { diff --git a/Jint/Native/JsProxy.cs b/Jint/Native/JsProxy.cs index cd1a56cb0..3c61dd1b5 100644 --- a/Jint/Native/JsProxy.cs +++ b/Jint/Native/JsProxy.cs @@ -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); @@ -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; @@ -195,7 +195,7 @@ private List 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); } @@ -228,7 +228,7 @@ private List 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"); } } } @@ -245,14 +245,14 @@ private List 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; @@ -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); @@ -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; @@ -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"); } } } @@ -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"); } } } @@ -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"); } } } @@ -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"); } } @@ -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; @@ -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; @@ -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; @@ -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; @@ -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;