aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js/bindings/bindings.cpp
diff options
context:
space:
mode:
authorGravatar Dylan Conway <35280289+dylan-conway@users.noreply.github.com> 2023-06-20 19:06:58 -0700
committerGravatar GitHub <noreply@github.com> 2023-06-20 19:06:58 -0700
commit50064352346f48da74f66caca9d30b81a77c89b9 (patch)
treebe2738fbad1b0d0208daa18cb40d247b41c4d0dd /src/bun.js/bindings/bindings.cpp
parentadb451eec6b8286a4ee18b16b5b87644d5ef3020 (diff)
downloadbun-50064352346f48da74f66caca9d30b81a77c89b9.tar.gz
bun-50064352346f48da74f66caca9d30b81a77c89b9.tar.zst
bun-50064352346f48da74f66caca9d30b81a77c89b9.zip
enable asymmetric matchers in `expect.toEqual`, `expect.toStrictEqual`, and `expect.toHaveProperty` (#3367)
* add asymmetric matchers to `deepEquals` * fix comparison of a few jstypes * clean up and tests * fix merge * improve `expect.any` for primitives
Diffstat (limited to 'src/bun.js/bindings/bindings.cpp')
-rw-r--r--src/bun.js/bindings/bindings.cpp347
1 files changed, 218 insertions, 129 deletions
diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp
index 2ec1bd902..4eee81f4d 100644
--- a/src/bun.js/bindings/bindings.cpp
+++ b/src/bun.js/bindings/bindings.cpp
@@ -123,6 +123,115 @@ static void copyToUWS(WebCore::FetchHeaders* headers, UWSResponse* res)
using namespace JSC;
+using namespace WebCore;
+
+enum class AsymmetricMatcherResult : uint8_t {
+ PASS,
+ FAIL,
+ NOT_MATCHER,
+};
+
+AsymmetricMatcherResult matchAsymmetricMatcher(JSGlobalObject* globalObject, JSCell* matcherPropCell, JSValue otherProp, ThrowScope* throwScope)
+{
+ VM& vm = globalObject->vm();
+
+ if (auto* expectAnything = jsDynamicCast<JSExpectAnything*>(matcherPropCell)) {
+ if (otherProp.isUndefinedOrNull()) {
+ return AsymmetricMatcherResult::FAIL;
+ }
+
+ return AsymmetricMatcherResult::PASS;
+ } else if (auto* expectAny = jsDynamicCast<JSExpectAny*>(matcherPropCell)) {
+ JSValue constructorValue = expectAny->m_constructorValue.get();
+ JSObject* constructorObject = constructorValue.getObject();
+
+ if (otherProp.isPrimitive()) {
+ if (otherProp.isNumber() && globalObject->numberObjectConstructor() == constructorObject) {
+ return AsymmetricMatcherResult::PASS;
+ } else if (otherProp.isBoolean() && globalObject->booleanObjectConstructor() == constructorObject) {
+ return AsymmetricMatcherResult::PASS;
+ } else if (otherProp.isSymbol() && globalObject->symbolObjectConstructor() == constructorObject) {
+ return AsymmetricMatcherResult::PASS;
+ } else if (otherProp.isString()) {
+ if (auto* constructorFunction = jsDynamicCast<JSFunction*>(constructorObject)) {
+ String name = constructorFunction->name(vm);
+ if (name == "String"_s) {
+ return AsymmetricMatcherResult::PASS;
+ }
+ } else if (auto* internalConstructorFunction = jsDynamicCast<InternalFunction*>(constructorObject)) {
+ String name = internalConstructorFunction->name();
+ if (name == "String"_s) {
+ return AsymmetricMatcherResult::PASS;
+ }
+ }
+ } else if (otherProp.isBigInt()) {
+ if (auto* constructorFunction = jsDynamicCast<JSFunction*>(constructorObject)) {
+ String name = constructorFunction->name(vm);
+ if (name == "BigInt"_s) {
+ return AsymmetricMatcherResult::PASS;
+ }
+ } else if (auto* internalConstructorFunction = jsDynamicCast<InternalFunction*>(constructorObject)) {
+ String name = internalConstructorFunction->name();
+ if (name == "BigInt"_s) {
+ return AsymmetricMatcherResult::PASS;
+ }
+ }
+ }
+
+ return AsymmetricMatcherResult::FAIL;
+ }
+
+ if (constructorObject->hasInstance(globalObject, otherProp)) {
+ return AsymmetricMatcherResult::PASS;
+ }
+
+ return AsymmetricMatcherResult::FAIL;
+ } else if (auto* expectStringContaining = jsDynamicCast<JSExpectStringContaining*>(matcherPropCell)) {
+ JSValue expectedSubstring = expectStringContaining->m_stringValue.get();
+
+ if (otherProp.isString()) {
+ String otherString = otherProp.toWTFString(globalObject);
+ RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL);
+
+ String substring = expectedSubstring.toWTFString(globalObject);
+ RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL);
+
+ if (otherString.find(substring) != WTF::notFound) {
+ return AsymmetricMatcherResult::PASS;
+ }
+ }
+
+ return AsymmetricMatcherResult::FAIL;
+ } else if (auto* expectStringMatching = jsDynamicCast<JSExpectStringMatching*>(matcherPropCell)) {
+ JSValue expectedTestValue = expectStringMatching->m_testValue.get();
+
+ if (otherProp.isString()) {
+ if (expectedTestValue.isString()) {
+ String otherString = otherProp.toWTFString(globalObject);
+ RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL);
+
+ String substring = expectedTestValue.toWTFString(globalObject);
+ RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL);
+
+ if (otherString.find(substring) != WTF::notFound) {
+ return AsymmetricMatcherResult::PASS;
+ }
+ } else if (expectedTestValue.isCell() and expectedTestValue.asCell()->type() == RegExpObjectType) {
+ if (auto* regex = jsDynamicCast<RegExpObject*>(expectedTestValue)) {
+ JSString* otherString = otherProp.toString(globalObject);
+ if (regex->match(globalObject, otherString)) {
+ return AsymmetricMatcherResult::PASS;
+ }
+ }
+ }
+ }
+
+ return AsymmetricMatcherResult::FAIL;
+ }
+
+ return AsymmetricMatcherResult::NOT_MATCHER;
+}
+
template<typename PromiseType, bool isInternal>
static void handlePromise(PromiseType* promise, JSC__JSGlobalObject* globalObject, JSC::EncodedJSValue ctx, JSC__JSValue (*resolverFunction)(JSC__JSGlobalObject* arg0, JSC__CallFrame* callFrame), JSC__JSValue (*rejecterFunction)(JSC__JSGlobalObject* arg0, JSC__CallFrame* callFrame))
{
@@ -183,10 +292,39 @@ JSValue getIndexWithoutAccessors(JSGlobalObject* globalObject, JSObject* obj, ui
return JSValue();
}
-template<bool isStrict>
+template<bool isStrict, bool enableAsymmetricMatchers>
bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2, Vector<std::pair<JSC::JSValue, JSC::JSValue>, 16>& stack, ThrowScope* scope, bool addToStack)
{
VM& vm = globalObject->vm();
+
+ // need to check this before primitives, asymmetric matchers
+ // can match against any type of value.
+ if constexpr (enableAsymmetricMatchers) {
+ JSCell* c1 = v1.asCell();
+ JSCell* c2 = v2.asCell();
+ if (v2.isCell() && !v2.isEmpty() && c2->type() == JSC::JSType(JSDOMWrapperType)) {
+ switch (matchAsymmetricMatcher(globalObject, c2, v1, scope)) {
+ case AsymmetricMatcherResult::FAIL:
+ return false;
+ case AsymmetricMatcherResult::PASS:
+ return true;
+ case AsymmetricMatcherResult::NOT_MATCHER:
+ // continue comparison
+ break;
+ }
+ } else if (v1.isCell() && !v1.isEmpty() && c1->type() == JSC::JSType(JSDOMWrapperType)) {
+ switch (matchAsymmetricMatcher(globalObject, c1, v2, scope)) {
+ case AsymmetricMatcherResult::FAIL:
+ return false;
+ case AsymmetricMatcherResult::PASS:
+ return true;
+ case AsymmetricMatcherResult::NOT_MATCHER:
+ // continue comparison
+ break;
+ }
+ }
+ }
+
if (!v1.isEmpty() && !v2.isEmpty() && JSC::sameValue(globalObject, v1, v2)) {
return true;
}
@@ -257,7 +395,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
RETURN_IF_EXCEPTION(*scope, false);
// set has unique values, no need to count
- if (Bun__deepEquals<isStrict>(globalObject, nextValue1, nextValue2, stack, scope, false)) {
+ if (Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, nextValue1, nextValue2, stack, scope, false)) {
found = true;
if (!nextValue1.isPrimitive()) {
stack.append({ nextValue1, nextValue2 });
@@ -276,7 +414,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return false;
}
- break;
+ return true;
}
case JSMapType: {
if (c2Type != JSMapType) {
@@ -337,8 +475,8 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
JSValue value2 = nextValueObject2->getIndex(globalObject, static_cast<unsigned>(1));
RETURN_IF_EXCEPTION(*scope, false);
- if (Bun__deepEquals<isStrict>(globalObject, key1, key2, stack, scope, false)) {
- if (Bun__deepEquals<isStrict>(globalObject, nextValue1, nextValue2, stack, scope, false)) {
+ if (Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, key1, key2, stack, scope, false)) {
+ if (Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, nextValue1, nextValue2, stack, scope, false)) {
found = true;
if (!nextValue1.isPrimitive()) {
stack.append({ nextValue1, nextValue2 });
@@ -358,7 +496,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return false;
}
- break;
+ return true;
}
case ArrayBufferType: {
if (c2Type != ArrayBufferType) {
@@ -399,10 +537,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
JSC::DateInstance* left = jsCast<DateInstance*>(v1);
JSC::DateInstance* right = jsCast<DateInstance*>(v2);
- if (left->structureID() == right->structureID()) {
- return left->internalNumber() == right->internalNumber();
- }
- break;
+ return left->internalNumber() == right->internalNumber();
}
case RegExpObjectType: {
if (c2Type != RegExpObjectType) {
@@ -463,10 +598,18 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return (memcmp(vector, rightVector, byteLength) == 0);
}
case StringObjectType: {
+ if (c2Type != StringObjectType) {
+ return false;
+ }
+
if (!equal(JSObject::calculatedClassName(o1), JSObject::calculatedClassName(o2))) {
return false;
}
- break;
+
+ JSString* s1 = c1->toStringInline(globalObject);
+ JSString* s2 = c2->toStringInline(globalObject);
+
+ return s1->equal(globalObject, s2);
}
case JSFunctionType: {
return false;
@@ -518,7 +661,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
}
}
- if (!Bun__deepEquals<isStrict>(globalObject, left, right, stack, scope, true)) {
+ if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, stack, scope, true)) {
return false;
}
@@ -573,7 +716,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return false;
}
- if (!Bun__deepEquals<isStrict>(globalObject, prop1, prop2, stack, scope, true)) {
+ if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, stack, scope, true)) {
return false;
}
@@ -633,7 +776,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return true;
}
- if (!Bun__deepEquals<isStrict>(globalObject, left, right, stack, scope, true)) {
+ if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, left, right, stack, scope, true)) {
result = false;
return false;
}
@@ -707,7 +850,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return false;
}
- if (!Bun__deepEquals<isStrict>(globalObject, prop1, prop2, stack, scope, true)) {
+ if (!Bun__deepEquals<isStrict, enableAsymmetricMatchers>(globalObject, prop1, prop2, stack, scope, true)) {
return false;
}
@@ -721,93 +864,7 @@ bool Bun__deepEquals(JSC__JSGlobalObject* globalObject, JSValue v1, JSValue v2,
return true;
}
-using namespace WebCore;
-
-enum class AsymmetricMatcherResult : uint8_t {
- PASS,
- FAIL,
- NOT_MATCHER,
-};
-
-AsymmetricMatcherResult matchAsymmetricMatcher(JSGlobalObject* globalObject, JSCell* matcherPropCell, JSValue otherProp, ThrowScope* throwScope)
-{
- if (auto* expectAnything = jsDynamicCast<JSExpectAnything*>(matcherPropCell)) {
- if (otherProp.isUndefinedOrNull()) {
- return AsymmetricMatcherResult::FAIL;
- }
-
- return AsymmetricMatcherResult::PASS;
- } else if (auto* expectAny = jsDynamicCast<JSExpectAny*>(matcherPropCell)) {
- JSValue constructorValue = expectAny->m_constructorValue.get();
- JSObject* constructorObject = constructorValue.getObject();
-
- if (constructorObject->hasInstance(globalObject, otherProp)) {
- return AsymmetricMatcherResult::PASS;
- }
-
- // check for basic types
- VM& vm = globalObject->vm();
- ZigString name = {};
- JSC__JSValue__getNameProperty(JSValue::encode(constructorValue), globalObject, &name);
- StringView nameView(name.ptr, name.len);
-
- if (otherProp.isNumber() && nameView == "Number"_s) {
- return AsymmetricMatcherResult::PASS;
- } else if (otherProp.isBoolean() && nameView == "Boolean"_s) {
- return AsymmetricMatcherResult::PASS;
- } else if (otherProp.isString() && nameView == "String"_s) {
- return AsymmetricMatcherResult::PASS;
- } else if (otherProp.isBigInt() && nameView == "BigInt"_s) {
- return AsymmetricMatcherResult::PASS;
- }
-
- return AsymmetricMatcherResult::FAIL;
- } else if (auto* expectStringContaining = jsDynamicCast<JSExpectStringContaining*>(matcherPropCell)) {
- JSValue expectedSubstring = expectStringContaining->m_stringValue.get();
-
- if (otherProp.isString()) {
- String otherString = otherProp.toWTFString(globalObject);
- RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL);
-
- String substring = expectedSubstring.toWTFString(globalObject);
- RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL);
-
- if (otherString.find(substring) != WTF::notFound) {
- return AsymmetricMatcherResult::PASS;
- }
- }
-
- return AsymmetricMatcherResult::FAIL;
- } else if (auto* expectStringMatching = jsDynamicCast<JSExpectStringMatching*>(matcherPropCell)) {
- JSValue expectedTestValue = expectStringMatching->m_testValue.get();
-
- if (otherProp.isString()) {
- if (expectedTestValue.isString()) {
- String otherString = otherProp.toWTFString(globalObject);
- RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL);
-
- String substring = expectedTestValue.toWTFString(globalObject);
- RETURN_IF_EXCEPTION(*throwScope, AsymmetricMatcherResult::FAIL);
-
- if (otherString.find(substring) != WTF::notFound) {
- return AsymmetricMatcherResult::PASS;
- }
- } else if (expectedTestValue.isCell() and expectedTestValue.asCell()->type() == RegExpObjectType) {
- if (auto* regex = jsDynamicCast<RegExpObject*>(expectedTestValue)) {
- JSString* otherString = otherProp.toString(globalObject);
- if (regex->match(globalObject, otherString)) {
- return AsymmetricMatcherResult::PASS;
- }
- }
- }
- }
-
- return AsymmetricMatcherResult::FAIL;
- }
-
- return AsymmetricMatcherResult::NOT_MATCHER;
-}
-
+template<bool enableAsymmetricMatchers>
bool Bun__deepMatch(JSValue objValue, JSValue subsetValue, JSGlobalObject* globalObject, ThrowScope* throwScope, bool replacePropsWithAsymmetricMatchers)
{
VM& vm = globalObject->vm();
@@ -848,36 +905,38 @@ bool Bun__deepMatch(JSValue objValue, JSValue subsetValue, JSGlobalObject* globa
JSCell* subsetPropCell = subsetProp.asCell();
JSCell* propCell = prop.asCell();
- if (subsetProp.isCell() and subsetPropCell->type() == JSC::JSType(JSDOMWrapperType)) {
- switch (matchAsymmetricMatcher(globalObject, subsetPropCell, prop, throwScope)) {
- case AsymmetricMatcherResult::FAIL:
- return false;
- case AsymmetricMatcherResult::PASS:
- if (replacePropsWithAsymmetricMatchers) {
- obj->putDirect(vm, subsetProps[i], subsetProp);
+ if constexpr (enableAsymmetricMatchers) {
+ if (subsetProp.isCell() && !subsetProp.isEmpty() && subsetPropCell->type() == JSC::JSType(JSDOMWrapperType)) {
+ switch (matchAsymmetricMatcher(globalObject, subsetPropCell, prop, throwScope)) {
+ case AsymmetricMatcherResult::FAIL:
+ return false;
+ case AsymmetricMatcherResult::PASS:
+ if (replacePropsWithAsymmetricMatchers) {
+ obj->putDirect(vm, subsetProps[i], subsetProp);
+ }
+ // continue to next subset prop
+ continue;
+ case AsymmetricMatcherResult::NOT_MATCHER:
+ break;
}
- // continue to next subset prop
- continue;
- case AsymmetricMatcherResult::NOT_MATCHER:
- break;
- }
- } else if (prop.isCell() and propCell->type() == JSC::JSType(JSDOMWrapperType)) {
- switch (matchAsymmetricMatcher(globalObject, propCell, subsetProp, throwScope)) {
- case AsymmetricMatcherResult::FAIL:
- return false;
- case AsymmetricMatcherResult::PASS:
- if (replacePropsWithAsymmetricMatchers) {
- subsetObj->putDirect(vm, subsetProps[i], prop);
+ } else if (prop.isCell() && !prop.isEmpty() && propCell->type() == JSC::JSType(JSDOMWrapperType)) {
+ switch (matchAsymmetricMatcher(globalObject, propCell, subsetProp, throwScope)) {
+ case AsymmetricMatcherResult::FAIL:
+ return false;
+ case AsymmetricMatcherResult::PASS:
+ if (replacePropsWithAsymmetricMatchers) {
+ subsetObj->putDirect(vm, subsetProps[i], prop);
+ }
+ // continue to next subset prop
+ continue;
+ case AsymmetricMatcherResult::NOT_MATCHER:
+ break;
}
- // continue to next subset prop
- continue;
- case AsymmetricMatcherResult::NOT_MATCHER:
- break;
}
}
if (subsetProp.isObject() and prop.isObject()) {
- if (!Bun__deepMatch(prop, subsetProp, globalObject, throwScope, replacePropsWithAsymmetricMatchers)) {
+ if (!Bun__deepMatch<enableAsymmetricMatchers>(prop, subsetProp, globalObject, throwScope, replacePropsWithAsymmetricMatchers)) {
return false;
}
} else {
@@ -1622,7 +1681,17 @@ bool JSC__JSValue__deepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC_
ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
Vector<std::pair<JSValue, JSValue>, 16> stack;
- return Bun__deepEquals<false>(globalObject, v1, v2, stack, &scope, true);
+ return Bun__deepEquals<false, false>(globalObject, v1, v2, stack, &scope, true);
+}
+
+bool JSC__JSValue__jestDeepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject)
+{
+ JSValue v1 = JSValue::decode(JSValue0);
+ JSValue v2 = JSValue::decode(JSValue1);
+
+ ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
+ Vector<std::pair<JSValue, JSValue>, 16> stack;
+ return Bun__deepEquals<false, true>(globalObject, v1, v2, stack, &scope, true);
}
bool JSC__JSValue__strictDeepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject)
@@ -1632,7 +1701,17 @@ bool JSC__JSValue__strictDeepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1
ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
Vector<std::pair<JSValue, JSValue>, 16> stack;
- return Bun__deepEquals<true>(globalObject, v1, v2, stack, &scope, true);
+ return Bun__deepEquals<true, false>(globalObject, v1, v2, stack, &scope, true);
+}
+
+bool JSC__JSValue__jestStrictDeepEquals(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject)
+{
+ JSValue v1 = JSValue::decode(JSValue0);
+ JSValue v2 = JSValue::decode(JSValue1);
+
+ ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
+ Vector<std::pair<JSValue, JSValue>, 16> stack;
+ return Bun__deepEquals<true, true>(globalObject, v1, v2, stack, &scope, true);
}
bool JSC__JSValue__deepMatch(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject, bool replacePropsWithAsymmetricMatchers)
@@ -1642,7 +1721,17 @@ bool JSC__JSValue__deepMatch(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__
ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
- return Bun__deepMatch(obj, subset, globalObject, &scope, replacePropsWithAsymmetricMatchers);
+ return Bun__deepMatch<false>(obj, subset, globalObject, &scope, replacePropsWithAsymmetricMatchers);
+}
+
+bool JSC__JSValue__jestDeepMatch(JSC__JSValue JSValue0, JSC__JSValue JSValue1, JSC__JSGlobalObject* globalObject, bool replacePropsWithAsymmetricMatchers)
+{
+ JSValue obj = JSValue::decode(JSValue0);
+ JSValue subset = JSValue::decode(JSValue1);
+
+ ThrowScope scope = DECLARE_THROW_SCOPE(globalObject->vm());
+
+ return Bun__deepMatch<true>(obj, subset, globalObject, &scope, replacePropsWithAsymmetricMatchers);
}
// This is the same as the C API version, except it returns a JSValue which may be a *Exception