Skip to content

Match V8 semantics for CallSite.getTypeName/getFunctionName/getMethodName#30941

Open
robobun wants to merge 1 commit into
mainfrom
farm/13e8b783/callsite-v8-semantics
Open

Match V8 semantics for CallSite.getTypeName/getFunctionName/getMethodName#30941
robobun wants to merge 1 commit into
mainfrom
farm/13e8b783/callsite-v8-semantics

Conversation

@robobun
Copy link
Copy Markdown
Collaborator

@robobun robobun commented May 17, 2026

Problem

Reported in #30938: async_hooks + process._fatalException + tap produces TAP subtest errors formatted as undefined.<anonymous> instead of the receiver's class name.

# Bun 1.3.14
stack: |
  undefined.<anonymous> (fail.cjs:29:12)
at:
  function: undefined.<anonymous>
# Node 24
stack: |
  Test.<anonymous> (fail.cjs:29:9)
at:
  function: Test.<anonymous>

The literal string "undefined" appears because source-map-support does typeName + "." when formatting method-call frames, and Bun's CallSite.getTypeName() was returning typeof this (so "undefined", "object", "function") where V8/Node returns the receiver's constructor name — or null when the receiver can't be identified.

Two neighbouring methods had the same shape of mismatch:

  • getFunctionName() returned "" for anonymous frames where V8 returns null.
  • getMethodName() delegated to getFunctionName (there's even a // TODO on it) so it never actually walked the prototype chain.

Fix

src/jsc/bindings/CallSitePrototype.cpp:

  • getTypeName — null/undefined receiver → null; otherwise JSObject::calculatedClassName, which already implements V8's cascade (own constructorproto.constructor@@toStringTag[[Class]]"Object"). Global object → null.
  • getFunctionName — empty stored name → null.
  • getMethodName — walk this + prototype chain, collect property names whose value is identity-equal to the frame's function, return the unique name or null. Matches V8's CallSiteInfo::GetMethodName.

Caveat

Bun still can't fully reproduce Node's Test.<anonymous> output because JSC::StackFrame doesn't persist the receiver through stack capture — by the time Error.prepareStackTrace runs the callframe is gone. getTypeName() / getMethodName() fall back to null in that case, so the TAP output goes from undefined.<anonymous> to null.<anonymous> (and from function: undefined.<anonymous> to function: null.<anonymous>). Still wrong vs. Node, but now consistent with what V8 itself returns for strict-mode frames where the receiver is unavailable, and the null return value unblocks ecosystem libraries that use typeName && typeName !== 'Object'-style guards. Fixing it the rest of the way needs this stashed on every captured frame — a separate, bigger change.

Verification

test/js/node/v8/capture-stack-trace.test.js:

bun bd test test/js/node/v8/capture-stack-trace.test.js
 41 pass
 0 fail

Fail-before (USE_SYSTEM_BUN=1):

CallSite.p.getTypeName ... Expected: null / Received: "undefined"
CallSite.p.getFunctionName ... Expected: null / Received: ""
CallSite.p.getMethodName ... Expected: null / Received: "topLevelFn"

Related test files still green:

bun bd test test/js/node/v8/                                   → 46 pass
bun bd test test/regression/issue/fix-bindings-stack-trace.test.ts
         test/regression/issue/prepare-stack-trace-crash.test.ts
         test/regression/issue/circular-error-stack.test.ts
         test/regression/issue/23022-stack-trace-iterator.test.ts
         test/regression/issue/circular-error-stack-edge-cases.test.ts → 12 pass

Fixes #30938

…Name

getTypeName returned the string "undefined" (typeof thisValue) instead of the
receiver's constructor name. getFunctionName returned "" for anonymous
frames. getMethodName delegated to getFunctionName and never walked the
prototype chain.

All three now match V8 / Node.js return values:
- getTypeName: null when the receiver is null/undefined/global/strict, otherwise
  the constructor name (via JSObject::calculatedClassName, which replicates V8's
  constructor -> proto.constructor -> @@toStringTag -> [[Class]] cascade).
- getFunctionName: null for frames whose function has no name.
- getMethodName: walks the receiver's own properties and its prototype chain,
  returning the unique property name whose value is identity-equal to the
  frame's function, or null.

Users on tap + source-map-support saw `undefined.<anonymous>` in TAP error
output because source-map-support does `typeName + "."` when formatting
method-call frames. The frame class comment remains that Bun can't always
recover the original receiver from captured stack frames — in that case both
getTypeName and getMethodName still return null, but never the literal string
"undefined".
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

Warning

Rate limit exceeded

@robobun has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 3 minutes and 14 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cc61c834-eb71-4250-8f1f-23dbefca4315

📥 Commits

Reviewing files that changed from the base of the PR and between 172afa5 and 03430ef.

📒 Files selected for processing (2)
  • src/jsc/bindings/CallSitePrototype.cpp
  • test/js/node/v8/capture-stack-trace.test.js

Comment @coderabbitai help to get the list of available commands and usage tips.

@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented May 17, 2026

Updated 2:05 PM PT - May 17th, 2026

@robobun, your commit 03430ef60ba81cf238edb1ba5572310968909e9a passed in Build #55559! 🎉


🧪   To try this PR locally:

bunx bun-pr 30941

That installs a local version of the PR into your bun-30941 executable, so you can run:

bun-30941 --bun

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

I didn't find any bugs, but this is non-trivial JSC bindings C++ (prototype-chain walk, nested exception scopes, user-visible return-value changes for getTypeName/getFunctionName/getMethodName), so it's worth a human pass on the exception-scope discipline and V8-semantics edge cases.

Extended reasoning...

Overview

This PR rewrites three CallSite prototype functions in src/jsc/bindings/CallSitePrototype.cpp to match V8 semantics: getTypeName now returns the receiver's constructor/class name (via JSObject::calculatedClassName) or null instead of typeof this; getFunctionName returns null instead of "" for anonymous frames; and getMethodName is a fresh ~80-line implementation that walks the receiver's prototype chain looking for a property identity-equal to the frame's function. Tests in capture-stack-trace.test.js add three regression cases and update two existing assertions that previously encoded the buggy behavior, plus bump two hard-coded line-number assertions.

Security risks

Low. The new code reads property names/values via VMInquiry slots (skipping accessors), bails on Proxy objects and on objects that override getPrototype, caps traversal at 128 levels, and swallows exceptions from enumeration via DECLARE_TOP_EXCEPTION_SCOPE + tryClearException(). No new attack surface beyond what Error.prepareStackTrace already exposes; no auth/crypto/permissions involved.

Level of scrutiny

Moderate-to-high. This is hand-written JSC bindings C++ on a hot-ish path (every prepareStackTrace consumer). The exception-scope nesting (an outer DECLARE_THROW_SCOPE from ENTER_PROTO_FUNC plus inner DECLARE_TOP_EXCEPTION_SCOPEs inside the loop) is the kind of thing JSC's debug-build verifier is picky about, and calculatedClassName / getOwnPropertyNames can both touch user-observable state. The return-value changes (""null, "undefined"null) are intentional V8-compat fixes but are user-visible and could affect downstream code that did .length on the old string returns.

Other factors

The PR description is thorough, the author ran the directly affected test file plus five related regression tests, and the bug-hunter pass found nothing. No CODEOWNERS cover these paths. Still, ~130 lines of new prototype-walk + exception-handling logic in core bindings is beyond what I'd auto-approve without a human confirming the JSC scope discipline and the acknowledged "still-not-quite-Node" caveat is acceptable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

async_hooks + process._fatalException: loss of function name and async context in TAP subtest errors

1 participant