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
109 changes: 109 additions & 0 deletions JSTests/stress/function-toString-native-one-line.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Every kind of native function prints the same single-line NativeFunction
// source: `function name() { [native code] }`. This is the shape V8 uses.
//
// Before this, InternalFunction subclasses (Map, Set, WeakMap, DataView, ...)
// printed one line while host functions (NativeExecutable), builtin functions
// (FunctionExecutable), bound functions and remote functions printed
// function name() {\n [native code]\n}
// lodash's isNative() builds a RegExp from Object.prototype.hasOwnProperty's
// source and tests Map against it, so the two shapes made lodash believe Map
// was not native and fall back to a linear-scan cache in cloneDeep/memoize.

function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error(`bad value: ${JSON.stringify(actual)}, expected ${JSON.stringify(expected)}`);
}

const toString = Function.prototype.toString;
const source = (fn) => toString.call(fn);
const nativeFunction = /^function [^(]*\(\) \{ \[native code\] \}$/;

// Host functions (NativeExecutable).
shouldBe(source(Object.prototype.hasOwnProperty), "function hasOwnProperty() { [native code] }");
shouldBe(source(Array.prototype.push), "function push() { [native code] }");
shouldBe(source(Object.create), "function create() { [native code] }");
shouldBe(source(Math.max), "function max() { [native code] }");
shouldBe(source(Symbol.for), "function for() { [native code] }");

// Constructors that are InternalFunction subclasses.
shouldBe(source(Map), "function Map() { [native code] }");
shouldBe(source(Set), "function Set() { [native code] }");
shouldBe(source(WeakMap), "function WeakMap() { [native code] }");
shouldBe(source(DataView), "function DataView() { [native code] }");
shouldBe(source(Array), "function Array() { [native code] }");
shouldBe(source(Function), "function Function() { [native code] }");

// Constructors that are JSFunction subclasses with a NativeExecutable.
shouldBe(source(Promise), "function Promise() { [native code] }");
shouldBe(source(Number), "function Number() { [native code] }");
shouldBe(source(String), "function String() { [native code] }");
shouldBe(source(Boolean), "function Boolean() { [native code] }");

// Builtin functions written in JavaScript (FunctionExecutable).
shouldBe(source(Array.prototype.map), "function map() { [native code] }");
shouldBe(source(Array.from), "function from() { [native code] }");
shouldBe(source(Promise.prototype.then), "function then() { [native code] }");
shouldBe(source(Function.prototype.call), "function call() { [native code] }");

// Native accessors keep the `get `/`set ` prefix in the name.
shouldBe(source(Object.getOwnPropertyDescriptor(Map.prototype, "size").get), "function get size() { [native code] }");
shouldBe(source(Object.getOwnPropertyDescriptor(RegExp.prototype, "flags").get), "function get flags() { [native code] }");

// Bound functions.
shouldBe(source(function foo() {}.bind(null)), "function foo() { [native code] }");
shouldBe(source(Map.bind(null)), "function Map() { [native code] }");
shouldBe(source(Object.prototype.hasOwnProperty.bind({})), "function hasOwnProperty() { [native code] }");

// Callable objects that are neither JSFunction nor InternalFunction.
shouldBe(nativeFunction.test(source(new Proxy(function () {}, {}))), true);
shouldBe(nativeFunction.test(source(new Proxy(Map, {}))), true);

// The result is cached per executable. The cached string has the same shape.
shouldBe(source(Object.prototype.hasOwnProperty), source(Object.prototype.hasOwnProperty));
shouldBe(source(Array.prototype.map), source(Array.prototype.map));

// lodash's isNative(): a RegExp built from hasOwnProperty's source must match
// every other native function.
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
const reIsNative = RegExp("^" + source(Object.prototype.hasOwnProperty).replace(reRegExpChar, "\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, "$1.*?") + "$");
for (const fn of [Map, Set, WeakMap, WeakRef, DataView, Promise, Symbol, Object.create, Array.prototype.map, Function.prototype.bind, Map.prototype.get, Object.prototype.hasOwnProperty.bind({})]) {
if (!reIsNative.test(source(fn)))
throw new Error(`${fn.name} did not pass the lodash isNative RegExp: ${JSON.stringify(source(fn))}`);
}

// Everything reachable from the global object that is a native function prints one line.
const seen = new Set();
function visit(object, path, depth) {
if (object === null || (typeof object !== "object" && typeof object !== "function") || seen.has(object) || depth > 3)
return;
seen.add(object);
if (typeof object === "function") {
const text = source(object);
if (text.includes("[native code]") && !nativeFunction.test(text))
throw new Error(`${path} prints a native function on more than one line: ${JSON.stringify(text)}`);
}
let keys;
try {
keys = Reflect.ownKeys(object);
} catch {
return;
}
for (const key of keys) {
let descriptor;
try {
descriptor = Reflect.getOwnPropertyDescriptor(object, key);
} catch {
continue;
}
if (!descriptor)
continue;
const name = typeof key === "symbol" ? `[${key.description}]` : key;
if ("value" in descriptor)
visit(descriptor.value, `${path}.${name}`, depth + 1);
if (descriptor.get)
visit(descriptor.get, `${path}.${name}[get]`, depth + 1);
if (descriptor.set)
visit(descriptor.set, `${path}.${name}[set]`, depth + 1);
}
}
visit(globalThis, "globalThis", 0);
2 changes: 1 addition & 1 deletion Source/JavaScriptCore/API/tests/testapi.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1042,7 +1042,7 @@ static void testObjectiveCAPIMain()
JSContext *context = [[JSContext alloc] init];
context[@"TestObject"] = [TestObject class];
JSValue *result = [context evaluateScript:@"String(TestObject)"];
checkResult(@"String(TestObject)", [result isEqualToObject:@"function TestObject() {\n [native code]\n}"]);
checkResult(@"String(TestObject)", [result isEqualToObject:@"function TestObject() { [native code] }"]);
}

@autoreleasepool {
Expand Down
2 changes: 1 addition & 1 deletion Source/JavaScriptCore/inspector/InjectedScriptSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ let InjectedScript = class InjectedScript extends PrototypelessObjectBase
}

if (nativeGettersAsValues) {
if (@String(descriptor.get).@endsWith("[native code]\n}") || (!descriptor.get && @Object.@hasOwn(descriptor, "get") && !descriptor.set && @Object.@hasOwn(descriptor, "set"))) {
if (@String(descriptor.get).@endsWith("[native code] }") || (!descriptor.get && @Object.@hasOwn(descriptor, "get") && !descriptor.set && @Object.@hasOwn(descriptor, "set"))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) The injected-script native sniff was updated to "[native code] }" but the frontend counterpart isFunctionStringNativeCode in Source/WebInspectorUI/UserInterface/Base/Utilities.js:1690 still checks endsWith("{\n [native code]\n}"); after this change no native function's toString ends that way, so the Web Inspector object tree stops recognizing any native function (previously host/builtin/bound functions still matched). Fix: update isFunctionStringNativeCode to test the single-line suffix (e.g. endsWith("{ [native code] }")) so both inspector halves agree. Pre-existing for InternalFunction only; this diff extends the breakage to every native function.

Extended reasoning...

Before this diff, Object.prototype.hasOwnProperty.toString() and other NativeExecutable/FunctionExecutable builtins produced function name() {\n [native code]\n}, so isFunctionStringNativeCode (WebInspectorUI/UserInterface/Base/Utilities.js:1688-1691) returned true for them and ObjectTreePropertyTreeElement.js:299/312 and ObjectTreeBaseTreeElement.js:222 rendered them as native (simplified function, no jump-to-source context menu). After this diff every native path emits function name() { [native code] }, which never ends with {\n [native code]\n}, so isFunctionStringNativeCode returns false for all functions and the inspector frontend treats every native function as user code. The PR updated the backend sniff in InjectedScriptSource.js but not the matching frontend helper. Note: the JSCOnly port Bun builds does not ship WebInspectorUI, so this only affects consumers of this fork that build the full inspector UI.

Verification: nit — The mismatch is real. Source/WebInspectorUI/UserInterface/Base/Utilities.js:1688-1691 still reads return str.endsWith("{\n [native code]\n}");, and its callers at Source/WebInspectorUI/UserInterface/Views/ObjectTreePropertyTreeElement.js:299,312 and Source/WebInspectorUI/UserInterface/Views/ObjectTreeBaseTreeElement.js:222 gate the native-parameter-string display and the…

// Developers may create such a descriptor, so we should be resilient:
// let x = {}; Object.defineProperty(x, "p", {get:undefined}); Object.getOwnPropertyDescriptor(x, "p")
let fakeDescriptor = createFakeValueDescriptor(name, symbol, descriptor, isOwnProperty, true);
Expand Down
2 changes: 1 addition & 1 deletion Source/JavaScriptCore/runtime/FunctionExecutable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ JSString* FunctionExecutable::toStringSlow(JSGlobalObject* globalObject)
#else
if (isBuiltinFunction())
#endif
return cacheIfNoException(jsMakeNontrivialString(globalObject, "function "_s, name().string(), "() {\n [native code]\n}"_s));
return cacheIfNoException(jsMakeNontrivialString(globalObject, "function "_s, name().string(), "() { [native code] }"_s));

if (isClass())
return cache(jsString(vm, classSource().view()));
Expand Down
4 changes: 2 additions & 2 deletions Source/JavaScriptCore/runtime/JSFunction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,13 @@ JSString* JSFunction::toString(JSGlobalObject* globalObject)
if (inherits<JSBoundFunction>()) {
JSBoundFunction* function = uncheckedDowncast<JSBoundFunction>(this);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue string = jsMakeNontrivialString(globalObject, "function "_s, function->nameString(vm), "() {\n [native code]\n}"_s);
JSValue string = jsMakeNontrivialString(globalObject, "function "_s, function->nameString(vm), "() { [native code] }"_s);
RETURN_IF_EXCEPTION(scope, nullptr);
return asString(string);
} else if (inherits<JSRemoteFunction>()) {
JSRemoteFunction* function = uncheckedDowncast<JSRemoteFunction>(this);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue string = jsMakeNontrivialString(globalObject, "function "_s, function->nameString(), "() {\n [native code]\n}"_s);
JSValue string = jsMakeNontrivialString(globalObject, "function "_s, function->nameString(), "() { [native code] }"_s);
RETURN_IF_EXCEPTION(scope, nullptr);
return asString(string);
}
Expand Down
2 changes: 1 addition & 1 deletion Source/JavaScriptCore/runtime/NativeExecutable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ JSString* NativeExecutable::toStringSlow(JSGlobalObject *globalObject)

auto throwScope = DECLARE_THROW_SCOPE(vm);

JSValue value = jsMakeNontrivialString(globalObject, "function "_s, name(), "() {\n [native code]\n}"_s);
JSValue value = jsMakeNontrivialString(globalObject, "function "_s, name(), "() { [native code] }"_s);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Switching host-function toString to one line breaks several enabled ChakraCore baseline tests that pass on the base branch (they print only NativeExecutable-backed functions, so #155 did not touch them): Function/toString.js, strict/05.arguments_sm.js, Array/protoLookup.js, es6/letconst_global_shadow_builtins.js. run-javascriptcore-tests will report new failures after merging. Fix: rebaseline every .baseline-jsc still containing the multi-line shape — sweep:\{\n \[native code\]\n\} under JSTests/ChakraCore — replacing each three-line block with { [native code] }.

Extended reasoning...

On the base commit d71031a, parseFloat, String.prototype.indexOf, Array.prototype.shift, JSON.stringify and the strict-mode arguments.callee throw-type-error accessor are all JSFunctions backed by a NativeExecutable, so their toString still goes through NativeExecutable::toStringSlow and prints the three-line form. Their ChakraCore baselines (toString.baseline-jsc lines 3-6/34-41, 05.arguments_sm.baseline-jsc lines 4-9, protoLookup.baseline-jsc lines 41-43, letconst_global_shadow_builtins.baseline-jsc lines 14-16) contain only these host functions and no InternalFunction constructors, so PR #155 left them green. After this diff NativeExecutable::toStringSlow emits function name() { [native code] }, the printed output no longer matches the checked-in baselines, and runChakra :baseline in ChakraCore.yaml (lines 138, 608, 1585, 2151) reports each as a new failure. Baselines that also print an InternalFunction (e.g. Function/prototype.baseline-jsc, typedarray/set.baseline-jsc, ErrorCtorProps_v3.baseline-jsc) were already failing before this PR and…

Verification: normal — The change at Source/JavaScriptCore/runtime/NativeExecutable.cpp:111 switches host-function toString from "() {\n [native code]\n}" to "() { [native code] }", but the enabled ChakraCore baseline tests were not rebaselined and still expect the multi-line form. Base branch (d71031a) still emits multi-line for NativeExecutable (git show d71031a973:...NativeExecutable.cpp


RETURN_IF_EXCEPTION(throwScope, nullptr);

Expand Down
Loading