Skip to content

fix(repl): tab completion now resolves the target object from dot expressions#27519

Open
robobun wants to merge 8 commits intomainfrom
claude/fix-repl-tab-completion-27518
Open

fix(repl): tab completion now resolves the target object from dot expressions#27519
robobun wants to merge 8 commits intomainfrom
claude/fix-repl-tab-completion-27518

Conversation

@robobun
Copy link
Collaborator

@robobun robobun commented Feb 27, 2026

Summary

  • Fix target object resolution: REPL tab completion now evaluates the expression before a dot to determine the correct completion target (e.g., {}.to[TAB] completes against Object.prototype, not globalThis)
  • Remove duplicate prototype traversal: getPropertyNames with DontEnumPropertiesMode::Include already walks the full prototype chain, so the manual prototype chain walk was producing duplicate completions
  • Side-effect-free evaluation: Added Bun__REPL__evaluateCompletionTarget which evaluates expressions silently without modifying _error or _

Test plan

  • New regression test in test/regression/issue/27518.test.ts with 4 test cases:
    • Object literal dot-completion shows Object.prototype methods
    • Variable dot-completion shows correct properties
    • Array dot-completion shows Array.prototype methods (not global properties)
    • Global completion still works for bare identifiers
  • All 105 existing REPL tests pass (test/js/bun/repl/repl.test.ts)
  • Test fails with system bun (USE_SYSTEM_BUN=1)

Closes #27518

🤖 Generated with Claude Code

…ressions

Previously, REPL tab completion always completed against `globalThis`,
even for dot expressions like `{}.to[TAB]` or `[1,2,3].pus[TAB]`. This
was because the target expression before the dot was never evaluated.

Now, when a dot is detected before the word being completed, the
expression before the dot is evaluated and used as the completion target.
Also removes a redundant prototype chain walk in `getCompletions` since
`getPropertyNames` with `DontEnumPropertiesMode::Include` already
traverses the full prototype chain.

Closes #27518

Co-Authored-By: Claude <noreply@anthropic.com>
@robobun
Copy link
Collaborator Author

robobun commented Feb 27, 2026

Updated 5:54 AM PT - Feb 28th, 2026

❌ Your commit 953c1562 has 4 failures in Build #38333 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 27519

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

bun-27519 --bun
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a C binding Bun__REPL__evaluateCompletionTarget, uses it from the REPL to evaluate the expression before a dot for accurate completion targets, refactors completion enumeration to use propertyNames traversal, and adds regression tests exercising REPL tab-completion cases.

Changes

Cohort / File(s) Summary
C++ Bindings
src/bun.js/bindings/bindings.cpp, src/bun.js/bindings/headers.h
Added Bun__REPL__evaluateCompletionTarget declaration and implementation that evaluates a source snippet and returns the result or undefined on exception without setting a global error. Refactored Bun__REPL__getCompletions to use propertyNames traversal (no manual prototype chain walking).
Zig REPL Integration
src/repl.zig
Declared external Bun__REPL__evaluateCompletionTarget and added logic to locate the dot-expression start, evaluate the expression before the last dot to obtain a completion target, clear pending exceptions as needed, and pass the evaluated target to Bun__REPL__getCompletions instead of always using undefined.
Tests
test/regression/issue/27518.test.ts
Added a regression test suite that spawns a terminal Bun REPL and asserts tab-completion behavior for object literals, variable-based objects, arrays, bare identifiers, and post-assignment cases; includes output capture, timeout/wait helpers, and cleanup.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: REPL tab completion now resolves the target object from dot expressions, which directly addresses the core issue.
Description check ✅ Passed The PR description provides comprehensive information covering what the PR does, how it was verified, and includes a detailed test plan with specific test cases and results.
Linked Issues check ✅ Passed The PR fully addresses the objective from #27518: evaluating the left-hand expression of a dot to determine the correct completion target, preventing global properties from appearing in completions.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing tab completion in the REPL: binding additions, helper functions, expression evaluation, and tests for the specific issue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/27518.test.ts`:
- Around line 110-122: The test destructures an unused allOutput from the
withTerminalRepl callback; in the test "array dot-completion does not show
global properties" remove allOutput from the parameter list (i.e., change async
({ send, waitFor, allOutput }) => to async ({ send, waitFor }) =>) so only used
helpers remain; update the callback signature in this test (inside
withTerminalRepl) and ensure no other references to allOutput exist in the test
body.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6e317c8 and 6e78fe0.

📒 Files selected for processing (4)
  • src/bun.js/bindings/bindings.cpp
  • src/bun.js/bindings/headers.h
  • src/repl.zig
  • test/regression/issue/27518.test.ts
Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/27518.test.ts`:
- Around line 76-79: The current cleanup masks REPL-exit regressions by racing
proc.exited with a fixed Bun.sleep and silently killing the process; replace
that pattern so the test explicitly waits for the child to exit within a short
timeout, fails if it doesn't, and asserts the exit code. Specifically, remove
the Promise.race([proc.exited, Bun.sleep(2000)]) usage and instead await
proc.exited with a timeout wrapper (use the same test harness timeout helper or
create one), after which assert proc.exitCode (or proc.status) equals the
expected value (e.g., 0); if the timeout expires and the process is still
running (proc.killed is false), call proc.kill but also cause the test to fail
with a clear message indicating the REPL did not exit. Use the identifiers send,
proc.exited, Bun.sleep only for timeout logic and proc.kill/proc.killed and
proc.exitCode in your assertions.
- Around line 46-67: The waitFor function can hang because it awaits a waiter
promise that only resolves on new data, so if no data comes it never re-checks
the deadline; modify the loop in waitFor to race the resolveWaiter promise
against a timeout: create the waiter Promise that assigns resolveWaiter, create
a timeout Promise (using setTimeout) that rejects (or resolves) after the
computed remaining ms, then await Promise.race([waiterPromise, timeoutPromise]);
ensure you clear the timer and null out resolveWaiter in both paths so
resolveWaiter is not leaked; reference the waitFor function, the resolveWaiter
variable, deadline/remaining computation, and received/cursor handling.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6e78fe0 and 7e8e05c.

📒 Files selected for processing (1)
  • test/regression/issue/27518.test.ts
Race the waiter promise against a setTimeout so the deadline check
fires even when no new terminal data arrives. Simplify cleanup to
force-kill without asserting exit code since PTY-mode REPL may not
terminate within the grace period.

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
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.

LGTM — standard REPL completion pattern, clean implementation with good test coverage.

Extended reasoning...

Overview

This PR fixes REPL tab completion to evaluate the expression before a dot (e.g., {}.to[TAB]) to determine the correct completion target, rather than always completing against globalThis. It adds a new C++ function Bun__REPL__evaluateCompletionTarget that silently evaluates expressions without modifying _ or _error, updates the Zig tab completion logic to use it, removes a duplicate prototype chain walk in getCompletions (since getPropertyNames with DontEnumPropertiesMode::Include already traverses prototypes), and adds a regression test with 4 cases.

Security risks

The new evaluateCompletionTarget function evaluates arbitrary JavaScript during tab completion, which could trigger side effects. However, this is the standard approach used by Node.js REPL and other REPLs — REPL users already have full code execution capability, so this introduces no new attack surface. The function properly clears exceptions on failure and returns undefined.

Level of scrutiny

Medium scrutiny. The changes touch production C++ and Zig code in the REPL subsystem, but the REPL is an interactive developer tool (not a server-side code path). The pattern of evaluating dot-expression targets for completion is well-established across JavaScript REPLs. The C++ code correctly uses exception scoping and the Zig code handles allocation failures gracefully.

Other factors

  • The CI failures shown in the timeline are all pre-existing vendor/tinycc/tcc.h clang warnings and CMakeLists.txt cmake warnings, unrelated to this PR.
  • CodeRabbit found only trivial nits (unused destructuring, waitFor timeout race), both already addressed in follow-up commits.
  • The bug hunting system found no issues.
  • The test file is correctly placed in test/regression/issue/27518.test.ts per CLAUDE.md conventions.
  • The parenthesization of expressions before evaluation (wrapping {} as ({})) is a standard technique to disambiguate object literals from block statements.
Address review feedback: the cleanup now sends Ctrl+C to clear pending
input before .exit, explicitly asserts the exit code is 0, and fails
the test with a clear message if the REPL doesn't exit in time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/regression/issue/27518.test.ts`:
- Around line 65-75: Replace the ad-hoc setTimeout-based race with Bun.sleep by
removing the timer variable and the wrapper promise and instead using
Bun.sleep(remaining) in the Promise.race; keep the existing resolveWaiter
assignment and post-race cleanup (set resolveWaiter = null) and remove
clearTimeout(timer) since Bun.sleep needs no explicit cancellation. Locate the
block that declares timer and calls setTimeout, and update the Promise.race to
use Bun.sleep(remaining) so the test uses Bun's deadline-based sleep
consistently with the other tests.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d02533b and 00d1ab3.

📒 Files selected for processing (1)
  • test/regression/issue/27518.test.ts
…d clear stale exceptions

Two bugs fixed based on review feedback:

1. Expression extraction now scans backwards from the dot to find only
   the dot-expression chain (identifiers, dots, balanced brackets and
   parens) instead of taking the entire line prefix. This prevents side
   effects from evaluating assignments like `x = obj.ba<TAB>` and
   avoids syntax errors from `let x = foo.ba<TAB>`.

2. Added global.clearException() before the early return when
   getCompletions returns undefined, preventing stale exceptions from
   leaking into subsequent evaluations (e.g. from a Proxy with a
   throwing ownKeys trap).

Also adds a regression test verifying tab completion doesn't cause
side effects when used after an assignment operator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
test/regression/issue/27518.test.ts (1)

65-75: 🛠️ Refactor suggestion | 🟠 Major

Replace setTimeout with Bun.sleep.

The timeout mechanism uses setTimeout which violates the coding guideline for test files. Bun.sleep(remaining) can be used directly in Promise.race without needing wrapper promises or explicit cleanup.

🔧 Suggested fix
-      let timer: ReturnType<typeof setTimeout> | undefined;
       await Promise.race([
         new Promise<void>(resolve => {
           resolveWaiter = resolve;
         }),
-        new Promise<void>(resolve => {
-          timer = setTimeout(resolve, remaining);
-        }),
+        Bun.sleep(remaining),
       ]);
-      clearTimeout(timer);
       resolveWaiter = null;

As per coding guidelines: "Do not use setTimeout in tests; instead await the condition to be met".
,

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/regression/issue/27518.test.ts` around lines 65 - 75, The test uses
setTimeout with timer, clearTimeout and resolveWaiter to race a wait condition;
replace that with Bun.sleep to follow test guidelines: use Promise.race([new
Promise(resolve => { resolveWaiter = resolve; }), Bun.sleep(remaining)]) and
remove the timer variable, clearTimeout call, and explicit timer cleanup so
resolveWaiter and remaining are the only symbols used for the race; ensure you
null out resolveWaiter after the race as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@test/regression/issue/27518.test.ts`:
- Around line 65-75: The test uses setTimeout with timer, clearTimeout and
resolveWaiter to race a wait condition; replace that with Bun.sleep to follow
test guidelines: use Promise.race([new Promise(resolve => { resolveWaiter =
resolve; }), Bun.sleep(remaining)]) and remove the timer variable, clearTimeout
call, and explicit timer cleanup so resolveWaiter and remaining are the only
symbols used for the race; ensure you null out resolveWaiter after the race as
before.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 00d1ab3 and 58ae884.

📒 Files selected for processing (2)
  • src/repl.zig
  • test/regression/issue/27518.test.ts
Claude Bot and others added 3 commits February 28, 2026 09:42
Two CI failures fixed:

1. ASAN: Changed getCompletions from DECLARE_THROW_SCOPE to
   DECLARE_TOP_EXCEPTION_SCOPE and explicitly clear exceptions on all
   error paths. The throw scope was causing assertion failures because
   the Zig caller doesn't have a JSC exception scope, so JSC's
   exception check verification would fire when getLength was called
   on the result.

2. macOS: The "variable dot-completion" test was matching on the
   terminal echo of the definition input rather than the completion
   output. Fixed by waiting for the REPL evaluation output (which
   contains the property values) before sending the tab completion,
   ensuring the cursor advances past all echoed characters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the ad-hoc setTimeout-based race with Bun.sleep(remaining)
for consistency with the rest of the test patterns. Bun.sleep needs
no explicit cancellation so the timer variable and clearTimeout call
are removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The waitFor helper was setting cursor = all.length which consumed ALL
received data on match, not just data up to the match point. When the
evaluation output and prompt arrived in the same chunk (common on fast
platforms like Alpine/Debian aarch64), the subsequent waitFor for the
prompt would find nothing because the cursor had already skipped past it.

Fix: advance cursor only to the end of the matched text. This also
fixes the ASAN failure where waitFor(/xyz(One|Two)/) matched on
xyzOne before xyzTwo arrived — now we wait for "xyzTwo" specifically
to ensure both completions are in the output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
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.

LGTM — all previously identified bugs were addressed, including the expression extraction fix and the clearException fix.

Extended reasoning...

Overview

This PR fixes REPL tab completion to resolve the target object from dot expressions rather than always completing against globalThis. It touches 4 files: bindings.cpp (new evaluateCompletionTarget function, refactored getCompletions), headers.h (declaration), repl.zig (new findExpressionStart helper, modified handleTab), and a new regression test file.

Previous Review

In my prior review I raised two bugs:

  1. [Red/Critical] Expression extraction using line[0 .. word_start - 1] caused side effects (e.g., x = obj.ba[TAB] would evaluate the assignment). This was fixed by adding findExpressionStart which scans backwards from the dot to extract only the dot-expression chain.
  2. [Yellow/Medium] Missing global.clearException() before the early return when completions.isUndefined(). This was fixed both in Zig (added global.clearException()) and C++ (switched from DECLARE_THROW_SCOPE to DECLARE_TOP_EXCEPTION_SCOPE with explicit scope.clearException() calls).

Both fixes are verified in the current diff, and a new test case (dot-completion after assignment does not cause side effects) explicitly validates the side-effect fix.

Security Risks

The evaluateCompletionTarget function evaluates arbitrary JS expressions as a side effect of pressing Tab. This is by design (matching Chrome DevTools behavior) and is scoped to the extracted dot-expression chain only. The findExpressionStart parser ensures only the minimal expression is evaluated. No new injection vectors or auth concerns.

Level of Scrutiny

Medium scrutiny. REPL tab completion is a developer-facing feature, not a core runtime path. The worst case for edge cases in findExpressionStart (e.g., strings inside brackets) is falling back to globalThis completions, not a crash or data loss.

Other Factors

All CodeRabbit issues (unused destructuring, waitFor deadlock, exit code assertions, setTimeout→Bun.sleep) were also addressed. The test file follows CLAUDE.md conventions (regression test path, bunExe/bunEnv, await using, exit code assertions). No outstanding reviewer comments remain.

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

Labels

1 participant