Skip to content

shell: return which's PATH and cwd lookup as a struct instead of a tuple - #39131

Merged
alii merged 4 commits into
mainfrom
farm/f3d5f868/shell-which-search-path
Aug 15, 2026
Merged

alii merged 4 commits into
mainfrom
farm/f3d5f868/shell-which-search-path

Conversation

@robobun

@robobun robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • mordant's tuple_wants_struct flags Which::path_and_cwd in src/runtime/shell/builtin/which.rs: it returned (Vec<u8>, Vec<u8>), and both callers (Which::start and Which::next) unpacked it as (path_env, cwd) and passed the pair straight to Which::resolve(path_env, cwd, arg).
  • The two members have the same type, so a transposition at either end (the return, the destructuring, or the resolve call) compiles. The names existed at every call site but not in the type.
  • Nothing in the test suite would have noticed such a transposition either: the existing which tests only look up a bare name on the real $PATH, and they still pass with the two members swapped.

Fix

  • Replace the two helpers with a file-private SearchEnv { path_env, cwd } struct: SearchEnv::load(interp, cmd) does the same $PATH / cwd lookup (including the EnvMap::get ref/deref balance), and SearchEnv::resolve(&self, arg) wraps bun_which::which so the two slices are never passed positionally again.
  • Both callers become SearchEnv::load(..) + .resolve(&arg). The sync path in start still loads once per command and the streaming path in next still loads once per argument, as before. No behavior change.
  • Remove the cleared tuple_wants_struct:src/runtime/shell/builtin/which.rs entry from mordant-baseline.toml. bun run rust:mordant:baseline on this branch drops that entry and adds none, so the new struct does not trade one finding for another. It also drops two entries that are already stale on main and unrelated to this change (always_unwrapped_option:src/install/PackageInstall.rs, narrowed_two_ways:src/runtime/node/node_crypto_binding.rs, the latter cleared by crypto: store PBKDF2's key length as usize #37648); those are left as they are here.
  • Test (test/js/bun/shell/commands/which.test.ts): two fixture directories each hold an executable named tool; one is the shell's PATH, the other its cwd, and which tool ./tool must print them in that order. One case captures stdout (the start path), the other streams it from a child process (the next path). Because the change is a refactor the test passes before and after it; what it pins down is the role of each member: with the two slices passed to bun_which::which in the other order, both cases fail with the two directories swapped in the output, while the pre-existing tests stay green.
  • Verified: bun bd test test/js/bun/shell/commands/which.test.ts (5 pass, and 2 fail as described with the members deliberately swapped); bun bd test test/js/bun/shell/bunshell.test.ts -t which (2 pass); the new cases also pass on Windows (tool.exe fixture) with the current canary.

Background

  • The shell's which builtin has two output paths: when stdout is captured it resolves every argument synchronously in start; when stdout needs real IO it resolves one argument per next call and waits for each write. Both need $PATH (from the shell's export env) and the shell's cwd, which bun_which::which(buf, path, cwd, bin) takes as two positional byte slices: a bare name is looked up in path only, a name containing a slash is resolved against cwd only.
  • mordant-baseline.toml is the ratchet for the mordant lint pack: it records the per-(lint, file) finding counts that predate the CI job, so CI only fails on new findings. Fixing a site means deleting (or decrementing) its entry.

no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/shell/commands/which.test.ts

path_and_cwd returned (Vec<u8>, Vec<u8>) and both callers unpacked it as
(path_env, cwd) before handing the pair to resolve(path_env, cwd, arg),
where the two same-typed slices could be swapped without a compile error.
Fold both helpers into a SearchEnv { path_env, cwd } struct with a
resolve(&self, arg) method so the names live in the type. No behavior
change.

Drop the cleared tuple_wants_struct entry for which.rs from
mordant-baseline.toml.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 11 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: 35ecd666-dd89-4ec3-b162-9209e7ad6692

📥 Commits

Reviewing files that changed from the base of the PR and between c21d95a and 5adbff9.

📒 Files selected for processing (3)
  • mordant-baseline.toml
  • src/runtime/shell/builtin/which.rs
  • test/js/bun/shell/commands/which.test.ts

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

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:04 PM PT - Aug 15th, 2026

@alii, your commit 5adbff9 is building: #98566

@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.

LGTM — mechanical tuple→struct refactor to clear a mordant lint.

What was reviewed:

  • SearchEnv::load is byte-for-byte the old path_and_cwd body (including the EnvMap::get ref/deref balance) and resolve passes path_env, cwd to bun_which::which in the same order.
  • Both call sites preserve their prior load frequency: once per command in start's sync loop, once per arg in next.
  • The baseline drop removes exactly the one tuple_wants_struct:…/which.rs entry the change clears; the two stale entries the description mentions are intentionally left for main.
Extended reasoning...

Overview

This PR refactors src/runtime/shell/builtin/which.rs to replace a (Vec<u8>, Vec<u8>)-returning helper (Which::path_and_cwd) and its positional consumer (Which::resolve(path_env, cwd, arg)) with a file-private SearchEnv { path_env, cwd } struct that carries load and resolve methods. The only other change is deleting the corresponding tuple_wants_struct line from mordant-baseline.toml. The Which::arg helper moves a few lines up but is unchanged.

Security risks

None. This is a type-shape refactor of a shell builtin's internal helper; no parsing, no boundary crossing, no new inputs. The values ($PATH, cwd, arg bytes) flow to bun_which::which exactly as before.

Level of scrutiny

Low. The diff is a pure behavior-preserving reshape driven by a lint ratchet. I diffed each moved block against its original: SearchEnv::load reproduces the export_env.get(b"PATH") → .slice().to_vec() → s.deref() → unwrap_or_default() sequence and the shell.cwd().to_vec() verbatim; resolve still acquires a path buffer from the pool and calls bun_which::which(buf, path_env, cwd, arg) in the same argument order. The two callers in start (sync loop, load once) and next (streaming, load per arg) keep their prior semantics — start binds search before the loop and reuses it, matching the old single (path_env, cwd) destructure.

Other factors

Existing test coverage (test/js/bun/shell/commands/which.test.ts and the which cases in bunshell.test.ts) exercises both the found and not-found paths, and the PR description reports both suites passing under bun bd. No new test is expected for a no-behavior-change lint fix. The baseline edit is minimal and scoped to exactly the entry this change clears.

…names

Two fixture directories hold an executable of the same name; one is put on
the shell's PATH and the other is made its cwd. `which tool ./tool` has to
print them in that order, on both the captured-stdout path and the
streamed path, so feeding the two SearchEnv members to bun_which::which in
the wrong order shows up as the directories swapping in the output. The
existing which tests pass with the members transposed.
Comment thread src/runtime/shell/builtin/which.rs Outdated

@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.

LGTM — mechanical tuple→struct refactor with no behavior change.

What was reviewed:

  • SearchEnv::load/resolve preserve the exact $PATH/cwd lookup and bun_which::which(buf, path, cwd, bin) argument order from the old path_and_cwd/resolve pair, including the EnvMap::get ref/deref balance.
  • Both call sites (start sync loop, next per-arg) keep their original load frequency; Which::arg is unchanged, only relocated.
  • New tests follow harness conventions (tempDir + using, bunEnv/bunExe, concurrent pipe drain, stdout asserted before exit code, Windows .exe branch); the comment-cop feedback was addressed in 8acff7e.
Extended reasoning...

Overview

This PR clears one tuple_wants_struct mordant finding in the shell which builtin. Which::path_and_cwd returned (Vec<u8>, Vec<u8>) and Which::resolve took the two slices positionally; both are replaced by a file-private SearchEnv { path_env, cwd } with load (same body as path_and_cwd) and resolve(&self, arg) (same call to bun_which::which). The two call sites in start and next are updated in place, the baseline entry is removed from mordant-baseline.toml, and two tests are added that pin down which input feeds bare-name vs ./name lookups on both the captured and streamed stdout paths.

Security risks

None. No new inputs, no parsing changes, no permission or filesystem semantics touched — the same bytes reach bun_which::which in the same order as before. The struct is private to the file.

Level of scrutiny

Low. This is a type-shape refactor the lint tool asked for; I diffed the old and new helper bodies line-by-line and confirmed against src/which/lib.rs:100 that the (path, cwd, bin) positional order is preserved. Which::arg and the BuiltinState-provided state_mut are untouched. The mordant baseline change is the single expected deletion.

Other factors

The one piece of automated feedback (comment-cop on a long doc comment) was resolved by dropping the comment in 8acff7e; the thread is marked resolved. The new tests use tempDir/using, test.concurrent, Promise.all for pipe draining, assert stdout before exit code, and branch on isWindows for the fixture name and chmod — all in line with the repo's test guidance. No CODEOWNERS cover these paths. No prior claude[bot] review exists on this PR.

@alii
alii merged commit b1e6c59 into main Aug 15, 2026
9 of 10 checks passed
@alii
alii deleted the farm/f3d5f868/shell-which-search-path branch August 15, 2026 19:04

@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.

LGTM — mechanical tuple→struct refactor that clears the tuple_wants_struct mordant finding with no behavior change.

What was reviewed:

  • SearchEnv::load/resolve preserve the exact body of the old path_and_cwd/resolve, including the EnvMap::get ref/deref balance and the bun_which::which(buf, path, cwd, bin) argument order (checked against src/which/lib.rs:100).
  • Both callers keep their prior load cadence: once per command in the sync start path, once per arg in the streaming next path.
  • New tests follow harness conventions (tempDir with using, test.concurrent, concurrent pipe drain, stdout asserted before exit code) and are hermetic; the comment-cop feedback on the struct doc comment was already addressed and resolved.
Extended reasoning...

Overview

This PR replaces Which::path_and_cwd() -> (Vec<u8>, Vec<u8>) and Which::resolve(path_env, cwd, arg) in src/runtime/shell/builtin/which.rs with a file-private SearchEnv { path_env, cwd } struct exposing load(interp, cmd) and resolve(&self, arg). It removes the corresponding tuple_wants_struct:src/runtime/shell/builtin/which.rs entry from mordant-baseline.toml and adds two tests to test/js/bun/shell/commands/which.test.ts that pin down which of the two byte slices ($PATH vs cwd) each lookup reads.

Security risks

None. This is an internal refactor of a shell builtin's helper functions with no change to input handling, no new syscalls, and no change to how bun_which::which is invoked. The tests write only to per-test temp directories.

Level of scrutiny

Low. The Rust change is a pure shape refactor: the body of SearchEnv::load is byte-for-byte the old path_and_cwd (including the s.deref() refcount balance) with a struct return instead of a tuple, and resolve passes &self.path_env, &self.cwd to bun_which::which in the same order the old positional call did — verified against the signature at src/which/lib.rs:100 (which(buf, path, cwd, bin)). Both call sites (start loading once outside the loop, next loading per-arg) keep their prior semantics. The baseline file change removes exactly the one cleared entry and nothing else.

Other factors

The new tests are well-constructed per REVIEW.md: hermetic fixture dirs via tempDir with using, chmodSync gated on !isWindows, test.concurrent for the independent subprocess case, all three pipes drained concurrently via Promise.all, and stdout/stderr asserted before the exit code. They cover both the captured-stdout (sync start) and streamed-stdout (async next) code paths, and the PR description explains they fail if the two slices are swapped — so they guard the exact property the struct is meant to make un-transposable. The one prior bot comment (comment-cop about a paragraph-long doc comment on the struct) was addressed in a follow-up commit and the thread is resolved. No prior reviews from me on this PR.

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.

2 participants