Skip to content

Deduplicate jsc bindings, runtime api, test matchers, and shell builtins - #32023

Open
alii wants to merge 2 commits into
mainfrom
claude/split/jsc-runtime
Open

Deduplicate jsc bindings, runtime api, test matchers, and shell builtins#32023
alii wants to merge 2 commits into
mainfrom
claude/split/jsc-runtime

Conversation

@alii

@alii alii commented Jun 9, 2026

Copy link
Copy Markdown
Member

What this does

Dedup pass over the jsc bindings, runtime APIs, bun:test matchers and shell builtins. Only moves and removes code; no intended behavior change. Net -1.7k lines (53 files, +3294 / -5031).

  • ConsoleObject: TagPayload (a full mirror of Tag that existed to carry one variant's payload) is gone; TagResult carries the custom-formatter payload as an Option. Shared with the macro evaluator, VirtualMachine::print_error_instance and the test-runner pretty printer. WrappedWriter::new replaces 32 struct literals and the eight per-function pf! macros go away.
  • URL: bun_jsc::URL is now a re-export of bun_url::whatwg::URL. The getters and destroy() that only the jsc copy had move down into bun_url (whose copy is now opaque_ffi! like the jsc one was), and the two entry points that need a JSValue (href_from_js, from_js) become the UrlJsc extension trait. src/CLAUDE.md updated accordingly.
  • VirtualMachine / jsc_hooks: one wait loop behind load_entry_point and load_entry_point_for_test_runner; auto_tick / auto_tick_active become one auto_tick<const ACTIVE: bool>.
  • bun:test: toEqual / toStrictEqual share one implementation (toStrictEqual.rs deleted); the toHave*With family shares argument comparison, mock.results parsing and its failure epilogues. The shared epilogues are written against the throw! macro from bun:test: stop expect() failure messages from consuming <...> spans in user data #34343, so user data still never goes through the color-tag rewrite.
  • Shell: basename / dirname share PathBuiltin; the identical OutputTaskVTable impls of cp/ls/mkdir/touch come from impl_output_task_vtable!; the lex/parse testing APIs share their argument prologue; subproc Writable::init / Readable::init are one match with per-arm cfg instead of two platform copies, and the Blob/ArrayBuffer stdin writer is shared with Bun.spawn's Writable.
  • cron: CronRegisterJob / CronRemoveJob share CronJobCommon plus a CronJobBase trait with hooks for the two places they actually differ (Windows non-zero-exit acceptance, the SID error message).
  • dns: one generic PendingCacheKey<Req> and one pending-chain drain (drain_chain_ok / drain_chain_err) replace the per-record-kind copies; CacheHit becomes an alias of LookupCacheHit<GetAddrInfoRequest> and the two get_or_put_into_pending_cache variants merge.
  • valkey: the seven command-shape macros collapse into cmd! and cmd_varargs! (required / skip_null / strict select the three varargs behaviors). All 133 commands keep their name, wire command, argument names, state requirement and meta.
  • Smaller ones: AsyncModule error builders, CryptoHasher digest argument handling, PasswordObject verify algorithm parsing, csrf option parsing, HashObject / pretty_format / ConsoleObject using JSType::is_array_buffer_like, MarkdownObject input pinning, JSON5Object delegating to the shared expr_to_js, napi create_string_* prologue, bake init_transpiler_impl and the DevServer field exhaustiveness macro, udp setsockopt helpers, ipc message draining, socket event-callback tail, ServerWebSocket publish prologue, CodeCoverage line mapping.

Rebase onto main

This was stacked on #32022 / #32000; #32000 is closed as superseded, so the PR now targets main directly and carries the one piece it needed from there (the whatwg::URL additions). Re-applied on top of ~790 commits of main; 30 of the files conflicted. Where main had changed one of the copies being merged, main's behavior is what the shared code does now. Notable:

Verification

  • cargo check on all 10 CI targets, cargo clippy --workspace, cargo fmt --check, and test/internal/source-lints (75 lints) pass.
  • Debug build runs of the touched suites: console (incl. console.table), expect.test.js, spyMatchers, toHaveReturnedWith, mock-fn, expect-failure-message-angle-brackets, shell commands + bunshell + lex/parse, cron, CryptoHasher, csrf, hash, password, JSON5, coverage, macros, markdown, SocketAddress, sourcemap, napi, spawn + ipc, websocket server, udp + dgram, dns, bake dev/prod, auto-install, valkey unit tests against a local redis. The only failures were environment or debug-build timing (root can't hit EACCES, no outbound DNS, the 300k-message websocket benchmark at the 30s limit) and fail identically on the release build.
  • Differential checks against the current canary (same inputs, byte-identical output from this build): 50 matcher failure messages in both color modes, every publish* argument error, mkdir/touch/ls/basename/dirname output and errors, Bun.resolveSync of file: URLs, fetch() of file: URLs, 14 redis URL shapes, dns pass/fail set, and a script verifying all 133 valkey commands map to the equivalent macro variant.
  • No new tests: the PR adds no behavior, so the existing suites are the regression coverage.

#32024 is still stacked on this branch and needs the same treatment separately.

@alii

alii commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

@robobun adopt

@robobun

robobun commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Retargeted at main by alii; rebased and mergeable at 306781e. Build 92360: green except an R2-side ServiceUnavailable in one s3 test (135/136 S3 tests pass on that lane); nothing traces to the diff. Ready for review. Awaiting alii's call on landing this as one PR vs. retargeting the eight splits (#37358-#37366) at main.

@robobun

robobun commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator
Updated 11:35 AM PT - Aug 11th, 2026

@robobun, your commit d474a28fcb3df45e40c18b0e5a77305307a8646c passed in Build #92371! 🎉


🧪   To try this PR locally:

bunx bun-pr 32023

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

bun-32023 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

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 a large refactor (53 files, net −5k lines) touching unsafe runtime internals across the VM event loop, IPC, NAPI, DNS, crypto, shell, and cron — worth a human pass to confirm the behavioral-equivalence claims hold across all the consolidations.

Extended reasoning...

Overview

This PR consolidates duplicated code across ~53 files in the Bun runtime: collapsing the TagPayload/Tag split in ConsoleObject, unifying the four PendingCacheKey types in DNS into one generic, merging auto_tick/auto_tick_active via a const generic, factoring shell builtins' OutputTaskVTable into a macro, restructuring CronRegisterJob/CronRemoveJob around a shared CronJobCommon base, deduping the CryptoHasher digest-output parsing, deleting the dead draft FFI implementation, and turning jsc::URL into a re-export of bun_url::whatwg::URL plus a UrlJsc extension trait. The diff is ~525k chars (truncated) with a net −5k lines.

Security risks

Several touched files are security-adjacent: csrf_jsc.rs (CSRF token generation/verification option parsing), CryptoHasher.rs and PasswordObject.rs (digest output handling, algorithm validation), and napi_body.rs (unsafe pointer/length handling for caller-supplied buffers). The changes look like straight extractions of existing logic into helpers with identical semantics, and the bug-hunting pass found nothing, but crypto/CSRF argument parsing and unsafe NAPI slice construction are exactly the kind of code where a subtle reordering or changed early-return can matter.

Level of scrutiny

High. Even granting the "zero intended behavior change" framing, this PR rewrites substantial unsafe Rust across the event loop (jsc_hooks.rs), intrusive-list DNS draining (drain_chain_ok/drain_chain_err), raw-pointer self-freeing state machines in cron.rs, and Stacked-Borrows-sensitive ManuallyDrop/ptr::read paths in subprocess Writable. The cron rework in particular merges two separate state enums and maybe_finished/finish paths into one generic — the kind of consolidation where a per-variant edge case (e.g. which states tolerate nonzero exit, which Drop runs) is easy to lose. These are production-critical hot paths, not sandbox code.

Other factors

CI is green across all lanes per the robobun comment, and the existing test suite is cited as regression coverage. The PR is also stacked (foundations → install-cli → this), so merge ordering matters. No prior human reviews are on the timeline. Given the breadth, the unsafe-heavy surface, and the security-adjacent files, this should get a human reviewer's eyes even if mechanically clean.

@robobun

robobun commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Re the specific high-scrutiny areas called out above, on the rebased head (b6193e2):

  • cron (CronRegisterJob/CronRemoveJobCronJobCommon): test/js/bun/cron/ 91 pass, 0 fail.
  • NAPI (napi_create_string_* prologue extraction into NapiStringUnit + napi_string_slice): napi.test.ts -t string 9/9 pass; napi-value-ffi.test.ts 0 fail.
  • DNS (PendingCacheKey generic + drain_chain_*): container has no outbound DNS so the external-host lookups fail identically under both the debug build and the baked release bun (52/80 and 39/69, same test names). CI lanes with network pass them (zero DNS annotations on build 64472).
  • Event loop / IPC / shell / crypto / csrf: in the PR description's verification list.

Build 64472 is at 281 jobs green with zero test-failure annotations; the only red is a darwin 26 lane that exited on a buildkite artifact-download timeout before running anything.

Agreed a human pass is warranted given the scope; flagging @alii since this was handed over via adopt.

@robobun
robobun force-pushed the claude/split/install-cli branch from a1c67b5 to 2f5dd36 Compare July 12, 2026 20:48
@robobun
robobun force-pushed the claude/split/jsc-runtime branch from b6193e2 to f29ce8f Compare July 13, 2026 14:59

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The automated bug hunt came back clean, but this needs a human pass — 52 files across the event loop, JSC GC/formatter, DNS, IPC, NAPI, sockets, cron, and shell, with two rounds of non-trivial conflict resolution against main.

Checked and ruled out: the merged auto_tick<ACTIVE> preserves both original tails (GC-timer / rejected-promises skipped only when ACTIVE); drain_chain_ok keeps the ensure_still_alive bracketing and per-global re-materialization from every original drain_pending_*; the cron CronJobCommon extraction preserves the raw-ptr / no-FnEntry-protector discipline and per-job accepts_nonzero_exit differences; digest_to_bytes still returns the freshly-written digest bytes after the output_digest extraction.

Extended reasoning...

Overview

Net −5k line deduplication refactor across 52 runtime files: collapses TagPayloadTag in the console formatter (shared with macro evaluator and test-runner pretty-printer), unifies four near-identical PendingCacheKey types in DNS behind a generic + HasTail/NameKeyed traits, extracts drain_chain_{ok,err} from five drain_pending_* copies, merges auto_tick/auto_tick_active into one const-generic fn, folds CronRegisterJob/CronRemoveJob state into a CronJobCommon base, macro-izes the shell builtins' OutputTaskVTable impl, extracts shared IPC drain loops, deletes the dead draft FFI implementation, and moves URL to a re-export with a UrlJsc extension trait. Two rebases onto main with 11+5 documented conflict resolutions.

Security risks

No new attack surface — this is code motion and helper extraction. The touched areas include NAPI string creation (unsafe slice construction from C pointers) and CSRF option parsing, but both are straight extractions of existing validation into helpers with identical guard order. No parsing/auth/crypto logic changed.

Level of scrutiny

High. This is production-critical native runtime code with pervasive unsafe, raw-pointer receivers whose contracts depend on Stacked-Borrows reasoning (cron's "local reborrow, no FnEntry protector" pattern), GC-rooting order (ensure_still_alive placement in DNS drain chains), and event-loop hot paths. The refactor's correctness hinges on every extracted helper preserving side-effect order and every conflict resolution correctly threading main's concurrent semantic changes (e.g. Rc<Handlers> in socket_body, transfer_to_js in napi) through the moved code. A subtle divergence in any one of the ~30 extractions could ship a UAF or a lost error path that the test suite doesn't cover.

Other factors

CI is green (281+ jobs) and the PR description documents per-file test verification, but the author's own bot already flagged that a human pass is warranted. The diff is stacked on another PR (install-cli) with a required merge order. Several extractions change control-flow shape in ways that are equivalent but not mechanically obvious — e.g. PasswordObject's verify_sync previously returned JSValue::ZERO on one dead-code branch and now returns Err(Thrown), and Mkdir::on_io_writer_chunk previously panicked on Idle/Done and now falls through to next(). These looked benign on inspection but are exactly the kind of thing a maintainer should confirm.

@robobun

robobun commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

On the two specific control-flow shape changes called out above:

PasswordObject verify_sync (Ok(JSValue::ZERO)Err(JsError::Thrown)): the branch is the algorithm_from_zig_stringNone path with has_exception() already true. At the host_fn boundary these are identical: src/jsc/host_fn.rs maps Err(JsError::Thrown)JSValue::ZERO (lines 114/123/681), so both sides hand C++ the same zero return with the same pending exception. The async verify twin already used Err(Thrown) here; the refactor's shared helper normalizes the sync twin to match.

Mkdir on_io_writer_chunk (Idle/Done panic → falls through to next()): mkdir was the only one of the six builtins with the explicit panic; touch/ls/cp/basename/dirname already used the if let State::Exec ... else None shape the macro emits. For Idle the behavior is unchanged (next() itself still panics on State::Idle). For Done it would now return Builtin::done(.., 0) instead of panicking, but that path is unreachable: chunks are only enqueued from write_err/write_out while in Exec, so the callback cannot fire after the state transitions to Done.

Build 72447 on the rebased head: 82 passed so far, one napi.test.ts GC-finalization-timing failure on windows x64-baseline that's fleet-wide (also in builds 72400/72410/72415/72420/72440).

@alii

alii commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

@robobun split this into a few prs. it's too big to land safely.

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

On it. Splitting by subsystem into separate PRs, each stacked on claude/split/install-cli like this one, so they can be reviewed and landed independently. Will link them here when they are up.

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Split into eight PRs, all stacked on claude/split/install-cli and independent of each other (each compiles in isolation; the union cherry-picks cleanly and is byte-identical to this PR's head f29ce8fe4, which CI ran at 285/286):

PR Scope Δ
#37358 Console formatter TagPayloadTag 5 files, −368
#37359 jsc::URL re-export + UrlJsc trait 3 files, −117
#37360 expect equality / mock-call matchers 10 files, −108
#37361 Subprocess stdin writer shared with shell; builtin vtable macro 10 files, −329
#37362 Delete draft FFI host_fns.rs 2 files, −601
#37363 Cron job state machines; DNS pending-cache drains 3 files, −430
#37365 auto_tick, IPC drains, async modules, entry-point wait 4 files, −242
#37366 Small per-file dedups (crypto, api objects, sockets, valkey, napi, bake, coverage) 17 files, −636

Each body carries the rebase-resolution notes relevant to its files and the suites run against it. Converting this one to a draft and leaving it open as the validated union for reference; happy to close it outright once the splits start landing.

@robobun
robobun marked this pull request as draft August 11, 2026 00:53
@robobun
robobun force-pushed the claude/split/install-cli branch from 2f5dd36 to 9ae3ff5 Compare August 11, 2026 02:09
@robobun
robobun force-pushed the claude/split/jsc-runtime branch from f29ce8f to 1aba1ea Compare August 11, 2026 04:20
@robobun
robobun changed the base branch from claude/split/install-cli to main August 11, 2026 04:20
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Re-landed directly on main (the stack underneath it, #32000 / #32022, is gone or stale): base retargeted to main, squashed to one commit (1aba1ea), 53 files, +3294 / -5031.

The PR description now lists what was dropped because main already has it or it depended on #32022, and which parts were re-derived against main (matchers on the throw! macro, cron after #35122 / #36571, dns after #36619 / #37075, the URL unification now carrying its bun_url half). Verification is in the description as well: check-all on all 10 targets, clippy, rustfmt, source lints, the touched suites on a debug build, and byte-for-byte differential runs against the current canary for the matcher messages, publish errors, shell builtins, file: URL handling, redis URL parsing and dns.

#32024 is still stacked on this branch and needs the same treatment on its own.

@alii

alii commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

@robobun get this mergeable — main moved and it conflicts again.

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

On it. Checking whether the base moved under the splits too; will rebase whatever conflicts and report back here.

Consolidates duplicated code across the jsc and runtime crates without
changing behavior:

- ConsoleObject: drop the TagPayload mirror of Tag; TagResult carries the
  custom-formatter payload as an Option. Shared with the macro evaluator
  and the test-runner pretty printer. WrappedWriter::new replaces 32
  struct-literal constructions; the per-function pf! macros go away.
- URL: bun_jsc::URL becomes a re-export of bun_url::whatwg::URL (which
  gains the getters and destroy() that only the jsc copy had); the
  JSValue entry points move to the UrlJsc extension trait.
- VirtualMachine: one wait loop for load_entry_point and the test-runner
  variant. jsc_hooks: auto_tick and auto_tick_active become one
  const-generic function.
- bun:test: toEqual/toStrictEqual share one implementation; the
  toHave*With matcher family shares argument comparison, mock.results
  parsing and the failure epilogues, written against the throw! macro so
  user data is never passed through the color-tag rewrite.
- Shell: basename/dirname share PathBuiltin; the OutputTaskVTable impls
  of cp/ls/mkdir/touch come from one macro; the lex/parse testing APIs
  share their argument prologue; subproc Writable/Readable use one match
  with per-arm cfg instead of two platform copies, and the Blob/ArrayBuffer
  stdin writer is shared with Bun.spawn.
- cron: the register and remove jobs share CronJobCommon and CronJobBase.
- dns: one generic PendingCacheKey and one pending-chain drain instead of
  a copy per record kind; CacheHit is now an alias of LookupCacheHit.
- valkey: the seven command-shape macros become cmd! and cmd_varargs!.
- Smaller dedups in AsyncModule, CryptoHasher, PasswordObject, csrf,
  HashObject, MarkdownObject, JSON5Object, napi string creation, bake
  transpiler setup and DevServer field exhaustiveness checks, udp
  setsockopt helpers, ipc message draining, socket event callbacks,
  ServerWebSocket publish prologue and CodeCoverage line mapping.
@robobun
robobun force-pushed the claude/split/jsc-runtime branch from 1aba1ea to 306781e Compare August 11, 2026 17:49
@robobun
robobun marked this pull request as ready for review August 11, 2026 17:50
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Mergeable again at 306781e. Only cron.rs conflicted this time, against #37411 (dropping the Done/Failed states); applied the same removal once to the unified CronJobState / finish. Clippy, rust:check-all (10/10), and the cron suite plus the other touched suites (matchers, console, shell, markdown, udp, ipc, bake, socket, napi) pass on the rebased head. Marked it ready for review since you retargeted it at main; flip it back if I misread.

One question: the eight splits (#37358-#37366) are still based on claude/split/install-cli, which is now a dead end with #32000 closed, so they are orphaned if this lands as one PR. Want me to close them, or retarget them at main as the way to land this instead? I have left them alone until you say.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed this PR and the bug-hunting pass found no issues. Given the scope — 53 files across the event loop (auto_tick), DNS pending-cache drains, cron process lifecycle, NAPI FFI, shell subprocess/builtin vtables, and the test-runner matchers, much of it restructuring unsafe raw-pointer code — a human review is still warranted, which lines up with the earlier call to split it.

What was reviewed: the auto_tick<const ACTIVE> merge preserves the three ACTIVE-gated skips and the debug sleep-timer logging path; the shared wait_for_entry_point_promise keeps the test-runner-only wakeup() outside the loop; the DNS drain_chain_ok/err skeletons preserve the head/tail consumption order and per-node next snapshotting; the cron CronJobCommon keeps Drop inside the enter/exit scope and the raw-ptr "borrow ends before free" discipline; the verify_sync Ok(ZERO)Err(Thrown) and mkdir Idle/Done shape changes were checked against the host_fn boundary and next() respectively.

Extended reasoning...

Overview

This PR is a large deduplication pass across 53 files (+3294 / −5031): ConsoleObject TagPayloadTag, bun_jsc::URL re-exported from bun_url, auto_tick/auto_tick_active merged behind a const generic, load_entry_point wait loop shared, the toHave*With matcher family sharing failure epilogues and mock-result parsing, shell builtins sharing PathBuiltin/impl_output_task_vtable!, subproc Writable/Readable::init collapsed to one match with per-arm cfg, cron register/remove sharing CronJobCommon, DNS PendingCacheKey<Req> + drain_chain_ok/err, valkey command macros collapsed to two, plus a long tail of smaller extractions (AsyncModule error builders, CryptoHasher digest args, csrf option parsing, napi string prologue, bake init_transpiler_impl, udp setsockopt helpers, socket callback tail, ServerWebSocket publish prologue). It has been rebased across ~800 main commits with 30 conflicting files re-derived.

Security risks

No new attack surface is introduced; this is a refactor. The security-adjacent files (csrf_jsc.rs, CryptoHasher.rs, PasswordObject.rs) only extract shared argument-parsing helpers with identical validation. No TLS/auth/permission logic is touched.

Level of scrutiny

High. The bulk of the change is in memory-safety-sensitive native code governed by the repo's most-blocked review category: intrusive raw-pointer chains in DNS (drain_chain_ok/err, HasTail::append_node), the cron *mut Selfheap::take state machines with their "borrow ends before free" invariants, NAPI FFI slice construction, subprocess Stdio payload extraction via ManuallyDrop+ptr::read, and the per-thread event-loop tick. Each of these was two nearly-identical copies before; merging them means any subtle divergence between the copies (intentional or accidental) is now a single behavior. The PR description documents byte-for-byte differential checks for the user-visible outputs, and the bug-hunting pass found nothing, but the sheer breadth of unsafe-code restructuring across a 30-file conflict rebase is exactly the kind of change the review guidelines say to treat as guilty until proven behavior-preserving.

Other factors

The maintainer explicitly said on this thread that it is "too big to land safely" and requested a split (eight split PRs exist at #37358#37366), then later asked to get it mergeable again — so there is an unresolved process question about whether this lands as one PR or via the splits. There is also an open question from robobun to the maintainer about closing vs. retargeting those splits. No new tests are added (by design, since no behavior change is intended), so regression coverage is entirely the existing suites plus the differential scripts described in the PR body. Given all of this, deferring to human review is the right call; approving would bypass a decision the maintainer has not yet made.

@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Agreed on all counts, and the process question is the real blocker here, not the code. To keep it in one place for @alii:

Either way the code is the same bytes; it is just a question of review granularity.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2981b827-b15e-4508-9925-f163fd1361ed

📥 Commits

Reviewing files that changed from the base of the PR and between 306781e and d474a28.

📒 Files selected for processing (1)
  • src/runtime/api/bun/subprocess/Writable.rs

Walkthrough

This PR consolidates shared helpers across URL handling, console formatting, runtime APIs, networking, shell execution, test matchers, Valkey commands, and coverage reporting.

Changes

Runtime refactors

Layer / File(s) Summary
URL contract and JSC integration
src/url/*, src/jsc/URL.rs, src/jsc/lib.rs, src/runtime/webcore/fetch.rs, src/CLAUDE.md
The shared WHATWG URL type now backs the JSC re-export. JSC conversions use the UrlJsc trait.
Console tags and entry-point waiting
src/jsc/ConsoleObject.rs, src/jsc/VirtualMachine.rs, src/js_parser_jsc/Macro.rs
Formatter tags and custom payloads are separated. Writer construction and entry-point promise waiting are centralized.
Runtime API helper consolidation
src/runtime/api/*, src/runtime/bake/*
Validation, input conversion, subprocess buffering, cron lifecycle, and transpiler initialization use shared implementations.
Network and event-loop paths
src/runtime/dns_jsc/*, src/runtime/ipc.rs, src/runtime/jsc_hooks.rs, src/runtime/napi/*, src/runtime/server/*, src/runtime/socket/*
DNS cache keys and waiter drains use generic abstractions. IPC, event-loop, N-API, WebSocket, and socket callback paths use shared helpers.
Crypto and coverage validation
src/runtime/crypto/*, src/sourcemap_jsc/CodeCoverage.rs
Digest argument handling and coverage offset mapping are centralized.
Shell builtin and subprocess abstractions
src/runtime/shell/*
Shell parsing, path transforms, output-task vtables, and stdio initialization use shared generic code.
Test matcher and Valkey command generation
src/runtime/test_runner/*, src/runtime/valkey_jsc/js_valkey_functions.rs
Mock matchers share comparison and failure helpers. Valkey command registration uses generic command macros.

Possibly related PRs

Suggested reviewers: jarred-sumner, robobun, dylan-conway

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main deduplication work across several major areas, although it does not list every affected subsystem.
Description check ✅ Passed The description explains the changes and provides detailed verification results, covering the template requirements despite using different section headings.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/runtime/shell/shell_body.rs (1)

782-796: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the argument-count error text identify the calling API.

shell_cmd_args_from_js is now shared by shell_lex and shell_parse. Line 785 hard-codes shell_parse in the message, so a shell_lex call with no arguments reports the wrong API name. Line 794 uses a third wording, shell, for the same failure class in the same function.

Pass the entry-point name into the helper and use one wording for both missing arguments.

As per coding guidelines: "Error messages must identify the failed resource, violated constraint, rejected value, cause, and concrete remedy while preserving rich underlying errors."

🐛 Proposed fix to name the calling API
     fn shell_cmd_args_from_js(
         global: &JSGlobalObject,
         callframe: &CallFrame,
+        api_name: &str,
         marked_argument_buffer: &mut MarkedArgumentBuffer,
     ) -> JsResult<(Bump, JsStrings, Vec<JSValue>, Vec<u8>)> {
         // SAFETY: bun_vm() is non-null for a Bun-owned global.
         let vm = global.bun_vm();
         let mut arguments = jsc::ArgumentsSlice::init(vm, callframe.arguments());
         let string_args: JSValue = match arguments.next_eat() {
             Some(s) => s,
             None => {
-                return Err(global.throw(format_args!("shell_parse: expected 2 arguments, got 0")));
+                return Err(global.throw(format_args!(
+                    "{api_name}: expected 2 arguments, got 0"
+                )));
             }
         };
 
         let arena = Bump::new();
 
         let template_args_js: JSValue = match arguments.next_eat() {
             Some(s) => s,
             None => {
-                return Err(global.throw(format_args!("shell: expected 2 arguments, got 0")));
+                return Err(global.throw(format_args!(
+                    "{api_name}: expected 2 arguments, got 1"
+                )));
             }
         };

Update both call sites:

-        let (arena, mut jsstrings, jsobjs, script) =
-            shell_cmd_args_from_js(global, callframe, marked_argument_buffer)?;
+        let (arena, mut jsstrings, jsobjs, script) =
+            shell_cmd_args_from_js(global, callframe, "shell_lex", marked_argument_buffer)?;
-        let (arena, mut jsstrings, mut jsobjs, script) =
-            shell_cmd_args_from_js(global, callframe, marked_argument_buffer)?;
+        let (arena, mut jsstrings, mut jsobjs, script) =
+            shell_cmd_args_from_js(global, callframe, "shell_parse", marked_argument_buffer)?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/shell/shell_body.rs` around lines 782 - 796, Update
shell_cmd_args_from_js to accept the calling entry-point name from shell_lex and
shell_parse, then use that name in both missing-argument errors. Replace the
inconsistent shell_parse and shell wording with one shared message that
identifies the API, expected argument count, and concrete remedy.

Source: Coding guidelines

src/runtime/valkey_jsc/js_valkey_functions.rs (2)

255-261: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Name the rejected argument position in the cmd_varargs! error.

The error reports "additional arguments" for every rejected element. The caller cannot tell which argument failed. The repository guideline requires that error messages identify the rejected value and the violated constraint.

Include the zero-based index of the failing argument in the message.

♻️ Proposed change to report the argument position
-            for arg in arguments {
+            for (arg_index, arg) in arguments.iter().enumerate() {
                 if $skip_null {
                     if arg.is_undefined_or_null() {
                         continue;
                     }
                 }
 
                 let Some(another) = from_js(global, *arg)? else {
                     return Err(global.throw_invalid_argument_type(
                         bname($name),
-                        "additional arguments",
+                        &format!("argument {arg_index}"),
                         "string or buffer",
                     ));
                 };
                 args.push(another);
             }

Adjust the second parameter to match the exact type throw_invalid_argument_type accepts; if it requires &'static str, add an overload or pass a formatted message through the existing error path.

As per coding guidelines: "Error messages must identify the failed resource, violated constraint, rejected value, cause, and concrete remedy while preserving rich underlying errors."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/valkey_jsc/js_valkey_functions.rs` around lines 255 - 261, Update
the rejected-argument handling in the cmd_varargs! expansion around from_js so
throw_invalid_argument_type identifies the failing zero-based argument index
instead of using the generic "additional arguments" label. Preserve the existing
string-or-buffer constraint and ensure the message uses the exact parameter type
accepted by throw_invalid_argument_type, extending the existing error path only
if needed to format the indexed label.

Source: Coding guidelines


215-263: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

The cmd_varargs! mode selection changes JS-visible null handling and error shape across the migrated Valkey commands. The root cause is the new three-mode contract (required / skip_null / strict) in the macro definition: it fixes both how undefined/null arguments are treated and which error the caller receives, and every registration below inherits that behavior from the mode it selects. The PR states no intended behavior change, so each mode assignment needs verification against the macro it replaced.

  • src/runtime/valkey_jsc/js_valkey_functions.rs#L215-L263: confirm that skip_null's continue on line 251 matches the previous break semantics used by the hand-written equivalents (set, srem, sadd, hmget), and that strict rejecting trailing undefined/null through the from_js None branch on line 255 matches the prior behavior for commands with optional trailing parameters.
  • src/runtime/valkey_jsc/js_valkey_functions.rs#L1041-L1641: audit each registration's mode choice; confirm the required mode's throw_missing_arguments_value error replaces the previous throw_invalid_argument_type without breaking tests, and re-check hscan on line 1049, where skip_null can drop a required cursor argument.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/valkey_jsc/js_valkey_functions.rs` around lines 215 - 263, The
cmd_varargs! mode choices may change null handling and error behavior from the
migrated hand-written commands. In
src/runtime/valkey_jsc/js_valkey_functions.rs:215-263, verify skip_null
preserves prior break semantics for set, srem, sadd, and hmget, while strict
preserves trailing null/undefined rejection for commands with optional
parameters. In src/runtime/valkey_jsc/js_valkey_functions.rs:1041-1641, audit
every registration’s mode, preserve required-argument validation behavior and
error compatibility, and ensure hscan does not use skip_null in a way that drops
its required cursor argument.
src/runtime/api/cron.rs (1)

2161-2212: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Install process ownership before reader startup

When reader startup fails, spawn_cmd_prepare returns before base!().process is set. T::finish then has no process to detach. On POSIX, the child is not watched or reaped. On Windows, WindowsSpawnResult::Drop cleans pipes but does not release its process pointer. Create and store the Process before reader startup, while extracting POSIX stdio fields before the consuming to_process call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/api/cron.rs` around lines 2161 - 2212, Update spawn_cmd_prepare
to construct and assign base!().process before starting either stdout_reader or
stderr_reader, ensuring T::finish can detach the process if reader startup
fails. For POSIX, extract the required spawned stdio fields before the consuming
to_process call, then store the resulting Process before invoking reader
startup; preserve the existing Windows pipe extraction and startup flow while
installing ownership beforehand.

Source: Coding guidelines

src/runtime/socket/udp_socket.rs (1)

944-959: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Do not retain a native socket reference across parse_addr.

parse_addr calls to_bun_string, which can run user JavaScript. That code can close the UDP socket after require_socket returns. The later membership call can then use a closed native socket. Parse all optional addresses first. Then call require_socket immediately before the native socket operation.

  • src/runtime/socket/udp_socket.rs#L944-L959: acquire the socket after parsing and validating the optional interface.
  • src/runtime/socket/udp_socket.rs#L1030-L1045: acquire the socket after parsing and validating the optional interface.

As per coding guidelines, “Assume any operation that can run user JavaScript can synchronously free state; perform coercions first, revalidate liveness after callbacks, and protect teardown-sensitive entry points.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/socket/udp_socket.rs` around lines 944 - 959, In the UDP
membership operation, move require_socket in the relevant method at
src/runtime/socket/udp_socket.rs lines 944-959 until after parse_addr and
interface-family validation, immediately before set_membership; apply the same
ordering in the corresponding method at src/runtime/socket/udp_socket.rs lines
1030-1045. Ensure no native socket reference is retained across parse_addr’s
JavaScript coercion, while preserving existing validation and error handling.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/api/bun/subprocess/Writable.rs`:
- Around line 19-48: Update the documentation for buffered_stdin_writer to state
that consuming Stdio::ArrayBuffer leaves
Stdio::ArrayBuffer(ArrayBufferStrong::default()), while Stdio::Blob is replaced
with Stdio::Ignore. Keep the implementation unchanged and explicitly note that
callers do not inspect the postcondition after Writable::init.

In `@src/runtime/server/ServerWebSocket.rs`:
- Around line 311-314: Update the closed-server log in publish_prologue to use
the log_name parameter instead of hardcoding “publish() closed”, preserving the
existing formatted per-method logging convention for publish and publishBinary.

In `@src/runtime/socket/udp_socket.rs`:
- Around line 838-844: Update require_socket to return
throw_setsockopt_errno(global_this, SystemErrno::EBADF) when self.socket.get()
is None, replacing the generic “Socket is closed” error; preserve the existing
socket dereference path for present sockets so all setsockopt callers retain the
required errno contract.

In `@src/runtime/valkey_jsc/js_valkey_functions.rs`:
- Around line 186-201: Update the argument handling in the cmd! macro to
validate frame.arguments().len() against the declared parameters and reject
extra arguments with the established arity error. For commands such as BITCOUNT
that support optional arguments, use cmd_varargs! so start, end, and BYTE|BIT
options are forwarded instead of silently discarded.
- Around line 1041-1079: Update the hscan command definition to require and
validate the cursor argument before dispatch, rather than relying on required
"key" alone, so missing or null cursors are rejected locally. Preserve the
existing key requirement and add coverage for both missing and null cursor
inputs.

In `@src/url/lib.rs`:
- Around line 187-200: Change URL::deinit to require unsafe cleanup semantics or
consume self, preventing callers from retaining a usable reference after C++
deletion. Keep destroy as the raw-pointer cleanup entry point with the same
live, uniquely owned, no-use-after-destruction contract, and update its call to
match the chosen deinit signature.

---

Outside diff comments:
In `@src/runtime/api/cron.rs`:
- Around line 2161-2212: Update spawn_cmd_prepare to construct and assign
base!().process before starting either stdout_reader or stderr_reader, ensuring
T::finish can detach the process if reader startup fails. For POSIX, extract the
required spawned stdio fields before the consuming to_process call, then store
the resulting Process before invoking reader startup; preserve the existing
Windows pipe extraction and startup flow while installing ownership beforehand.

In `@src/runtime/shell/shell_body.rs`:
- Around line 782-796: Update shell_cmd_args_from_js to accept the calling
entry-point name from shell_lex and shell_parse, then use that name in both
missing-argument errors. Replace the inconsistent shell_parse and shell wording
with one shared message that identifies the API, expected argument count, and
concrete remedy.

In `@src/runtime/socket/udp_socket.rs`:
- Around line 944-959: In the UDP membership operation, move require_socket in
the relevant method at src/runtime/socket/udp_socket.rs lines 944-959 until
after parse_addr and interface-family validation, immediately before
set_membership; apply the same ordering in the corresponding method at
src/runtime/socket/udp_socket.rs lines 1030-1045. Ensure no native socket
reference is retained across parse_addr’s JavaScript coercion, while preserving
existing validation and error handling.

In `@src/runtime/valkey_jsc/js_valkey_functions.rs`:
- Around line 255-261: Update the rejected-argument handling in the cmd_varargs!
expansion around from_js so throw_invalid_argument_type identifies the failing
zero-based argument index instead of using the generic "additional arguments"
label. Preserve the existing string-or-buffer constraint and ensure the message
uses the exact parameter type accepted by throw_invalid_argument_type, extending
the existing error path only if needed to format the indexed label.
- Around line 215-263: The cmd_varargs! mode choices may change null handling
and error behavior from the migrated hand-written commands. In
src/runtime/valkey_jsc/js_valkey_functions.rs:215-263, verify skip_null
preserves prior break semantics for set, srem, sadd, and hmget, while strict
preserves trailing null/undefined rejection for commands with optional
parameters. In src/runtime/valkey_jsc/js_valkey_functions.rs:1041-1641, audit
every registration’s mode, preserve required-argument validation behavior and
error compatibility, and ensure hscan does not use skip_null in a way that drops
its required cursor argument.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aca3c9ec-d6f6-4d57-a64b-f684edd8eefe

📥 Commits

Reviewing files that changed from the base of the PR and between da3851e and 306781e.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (52)
  • src/CLAUDE.md
  • src/js_parser_jsc/Macro.rs
  • src/jsc/AsyncModule.rs
  • src/jsc/ConsoleObject.rs
  • src/jsc/URL.rs
  • src/jsc/VirtualMachine.rs
  • src/jsc/lib.rs
  • src/runtime/api/HashObject.rs
  • src/runtime/api/MarkdownObject.rs
  • src/runtime/api/bun/subprocess/Writable.rs
  • src/runtime/api/cron.rs
  • src/runtime/api/csrf_jsc.rs
  • src/runtime/bake/DevServer.rs
  • src/runtime/bake/bake_body.rs
  • src/runtime/bake/dev_server/memory_cost.rs
  • src/runtime/bake/mod.rs
  • src/runtime/crypto/CryptoHasher.rs
  • src/runtime/crypto/PasswordObject.rs
  • src/runtime/dns_jsc/dns.rs
  • src/runtime/dns_jsc/dns_sd.rs
  • src/runtime/dns_jsc/mod.rs
  • src/runtime/ipc.rs
  • src/runtime/jsc_hooks.rs
  • src/runtime/napi/napi_body.rs
  • src/runtime/server/ServerWebSocket.rs
  • src/runtime/shell/builtin/basename.rs
  • src/runtime/shell/builtin/cp.rs
  • src/runtime/shell/builtin/dirname.rs
  • src/runtime/shell/builtin/ls.rs
  • src/runtime/shell/builtin/mkdir.rs
  • src/runtime/shell/builtin/touch.rs
  • src/runtime/shell/interpreter.rs
  • src/runtime/shell/shell_body.rs
  • src/runtime/shell/subproc.rs
  • src/runtime/socket/socket_body.rs
  • src/runtime/socket/udp_socket.rs
  • src/runtime/test_runner/expect.rs
  • src/runtime/test_runner/expect/toEqual.rs
  • src/runtime/test_runner/expect/toHaveBeenCalledWith.rs
  • src/runtime/test_runner/expect/toHaveBeenLastCalledWith.rs
  • src/runtime/test_runner/expect/toHaveBeenNthCalledWith.rs
  • src/runtime/test_runner/expect/toHaveLastReturnedWith.rs
  • src/runtime/test_runner/expect/toHaveNthReturnedWith.rs
  • src/runtime/test_runner/expect/toHaveReturnedWith.rs
  • src/runtime/test_runner/expect/toStrictEqual.rs
  • src/runtime/test_runner/mod.rs
  • src/runtime/test_runner/pretty_format.rs
  • src/runtime/valkey_jsc/js_valkey_functions.rs
  • src/runtime/webcore/fetch.rs
  • src/sourcemap_jsc/CodeCoverage.rs
  • src/url/Cargo.toml
  • src/url/lib.rs
💤 Files with no reviewable changes (2)
  • src/runtime/test_runner/mod.rs
  • src/runtime/test_runner/expect/toStrictEqual.rs

Comment thread src/runtime/api/bun/subprocess/Writable.rs
Comment thread src/runtime/server/ServerWebSocket.rs
Comment thread src/runtime/socket/udp_socket.rs
Comment thread src/runtime/valkey_jsc/js_valkey_functions.rs
Comment thread src/runtime/valkey_jsc/js_valkey_functions.rs
Comment thread src/url/lib.rs
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Review pass summary, so the thread history is skimmable:

coderabbit raised six line findings. One was ours and is fixed (d474a28, a doc comment on the new buffered_stdin_writer stating the per-variant postcondition; no code change). The other five are pre-existing behavior that the dedup preserves 1:1, each confirmed against the pre-PR copies on main and since withdrawn by coderabbit: the "publish() closed" log string (all three original copies used it), the udp missing-socket error (the two EBADF methods and the four generic-error methods on main map onto set_bool_opt and require_socket respectively, so both pre-existing behaviors survive unchanged), bitcount ignoring optional args, hscan skipping a null cursor, and bun_url's safe deinit(&mut self). The last three are real but predate this PR, so I have handed them off as separate fixes rather than widening a zero-behavior-change diff.

The three "possibly related" PRs in the walkthrough (#36051, #36404, #36603) are all still open, so there is nothing here to have mis-merged; they are just future rebase surface for whichever side lands second, which mildly favors landing this sooner.

Net: mergeable at d474a28, zero open threads, CI green apart from the R2 outage noted above. Still waiting on the one-PR-vs-splits call.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants