Skip to content

cli: add --check flag to syntax-check a file without executing#30981

Open
robobun wants to merge 17 commits into
mainfrom
farm/e67ccbc8/add-check-flag
Open

cli: add --check flag to syntax-check a file without executing#30981
robobun wants to merge 17 commits into
mainfrom
farm/e67ccbc8/add-check-flag

Conversation

@robobun

@robobun robobun commented May 18, 2026

Copy link
Copy Markdown
Collaborator

What

Adds bun --check <file>, equivalent to Node's node --check / node -c: parse the entry point for syntax errors and exit without ever executing it.

$ bun --check ok.js
$ echo $?
0

$ bun --check bad.js
1 | const x = ;
              ^
error: Unexpected ;
    at bad.js:1:11
$ echo $?
1

Behavior

  • bun --check <file> — parses <file>; exits 0 silently on success, prints the syntax error and exits 1 on failure.
  • bun --check with no file — reads from stdin (like node --check).
  • bun run --check <file> — same.
  • Works with JS, TS, JSX and TSX (loader is picked from the file extension, defaulting to TSX).
  • Top-level await and ESM import/export are accepted.
  • Never boots the JS VM or resolves imports — it only parses.
  • --check combined with --eval/--print is rejected with either --check or --eval can be used, not both and exit code 9, matching Node.

The short -c spelling is not provided because -c is already --config in Bun.

How

  • New syntax_check: bool on RuntimeOptions, populated from a --check entry in RUNTIME_PARAMS_.
  • RunCommand::exec_check() reads the file (or stdin via a streaming read() loop — pread fails on pipes), picks a loader from the extension, and runs bun_js_parser::Parser directly against an empty Define. If the parse errors or the log has errors, it prints them and exits 1; otherwise exits 0.
  • Wired into exec_auto_or_run (for bun / bun run) and exec_as_if_node (for bun-as-node) before VM boot.

Rebased onto main after #31783 and related refactors; the only conflict was a doc comment in run_command.rs. An earlier wtf-bindings.cpp assert fix was dropped from this branch because main fixed it independently.

Tests

test/cli/run/check.test.ts covers: valid/invalid files, no-execute guarantee, TS syntax, ESM syntax, top-level await, stdin, missing file, --eval conflict, and bun run --check.

@robobun

robobun commented May 18, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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 --check syntax-only CLI mode that parses a file or stdin for syntax errors without executing, including runtime option, CLI parsing/validation, early dispatch, parser-only implementation, tests, zsh completion, and a small TTY assertion update.

Changes

Syntax check flag implementation

Layer / File(s) Summary
Runtime option definition and CLI parsing
src/options_types/context.rs, src/runtime/cli/Arguments.rs
Add pub syntax_check: bool to RuntimeOptions, default it to false, add --check to the CLI table, and wire parsing with mutual-exclusion validation against --eval/--print.
CLI dispatch routing
src/runtime/cli/mod.rs, src/runtime/cli/run_command.rs
Add early-exit branches that route to RunCommand::exec_check when syntax_check is enabled, preventing normal VM boot/execution paths.
Syntax check implementation
src/runtime/cli/run_command.rs
Implement RunCommand::exec_check to read the target (file or stdin), select a JS-capable loader by extension, construct a bun_ast::Source, parse with bun_js_parser (top-level await enabled), emit diagnostics, flush outputs, and exit 0/1 without starting the JS VM.
Comprehensive test suite
test/cli/run/check.test.ts
Add integration tests validating exit codes and stdout/stderr for valid/invalid syntax, TypeScript/ESM/top-level-await handling, stdin mode, missing files, incompatible flag rejections, and bun run --check parity.
Shell completions and TTY assertion update
completions/bun.zsh, src/jsc/bindings/wtf-bindings.cpp
Add --check to _bun_run_completion options and replace a plain assert(tio != NULL) with the project ASSERT(tio != NULL) in the TTY binding.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: adding a --check flag for syntax-checking files without execution, matching the main objective across all modified files.
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.
Description check ✅ Passed The pull request description fully covers the required template sections with comprehensive details on functionality, behavior, implementation, and test coverage.

✏️ 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.

@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: 1

🤖 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/cli/Arguments.rs`:
- Around line 1134-1141: The conflict check incorrectly tests
!ctx.runtime_options.eval.script.is_empty() which misses cases like --eval "" or
--print ""; update the condition that follows ctx.runtime_options.syntax_check
to detect flag presence instead of script content by checking
args.flag(b"--eval") || args.flag(b"--print") (or any equivalent runtime
presence indicator), and keep the existing error path (Output::err_generic and
Global::exit(9)) so that any use of --check together with either --eval or
--print triggers the conflict error.
🪄 Autofix (Beta)

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: 12987e53-0833-49e6-a0c4-23f1647a1ae1

📥 Commits

Reviewing files that changed from the base of the PR and between 4c8a21b and b35a2bd.

📒 Files selected for processing (7)
  • completions/bun.zsh
  • src/jsc/bindings/wtf-bindings.cpp
  • src/options_types/context.rs
  • src/runtime/cli/Arguments.rs
  • src/runtime/cli/mod.rs
  • src/runtime/cli/run_command.rs
  • test/cli/run/check.test.ts

Comment thread src/runtime/cli/Arguments.rs
Comment thread src/runtime/cli/Arguments.rs
Comment thread test/cli/run/check.test.ts Outdated
Comment thread completions/bun.zsh Outdated

@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: 2

🤖 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 `@test/cli/run/check.test.ts`:
- Around line 161-174: Refactor the test "rejects combining --check with --eval
or --print" to use Jest's parameterized API (test.each) instead of a for-loop:
create a test.each([...]) that enumerates the flag variants
["-e","--eval","-p","--print"] and for each case spawn Bun with cmd [bunExe(),
"--check", flag, "1 + 1"], capture stdout/stderr/exit and assert the same three
expectations (stderr contains "either --check or --eval", stdout is empty,
exitCode !== 0); keep the existing Bun.spawn usage and variable names (proc,
bunExe, bunEnv) and preserve async handling so failures report per-case in test
output.
- Around line 58-61: The test currently asserts exitCode before guarding stderr
and verifying filesystem state; move the filesystem assertion using
Bun.file(`${dir}/ran.txt`).exists() to run before the exitCode assertion, insert
the guard pattern if (exitCode !== 0) { expect(stderr).toBe(""); } immediately
before asserting exitCode, and finally assert expect(exitCode).toBe(0);—keep the
existing stdout and stderr assertions (and the new guard) above the final
exitCode check so exitCode is asserted last.
🪄 Autofix (Beta)

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: 19364783-2846-4b8a-8206-2100edd0d18f

📥 Commits

Reviewing files that changed from the base of the PR and between 59118d8 and bdf6ef8.

📒 Files selected for processing (2)
  • completions/bun.zsh
  • test/cli/run/check.test.ts

Comment thread test/cli/run/check.test.ts
Comment thread test/cli/run/check.test.ts Outdated

@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: 1

🤖 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 `@test/cli/run/check.test.ts`:
- Around line 161-172: The test in test.each([...]) currently asserts any
non-zero exit code for the conflicting "--check" + eval/print flags; change the
final assertion from expect(exitCode).not.toBe(0) to expect(exitCode).toBe(9) so
the test verifies the exact contract for the conflict exit code; update the
assertion in the async test body that awaits proc.exited (the exitCode variable)
to assert equality with 9 while leaving the existing stdout/stderr checks and
Bun.spawn/using setup intact.
🪄 Autofix (Beta)

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: eeb6082a-974e-4c59-9310-04be509dc2be

📥 Commits

Reviewing files that changed from the base of the PR and between bdf6ef8 and a1c0263.

📒 Files selected for processing (1)
  • test/cli/run/check.test.ts

Comment thread test/cli/run/check.test.ts

@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: 1

🤖 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 `@test/cli/run/check.test.ts`:
- Line 20: The test currently asserts raw stderr is empty via
expect(stderr).toBe(""), which is flaky because ASAN can emit a one-time startup
warning; before the assertion, normalize stderr by splitting into lines and
filtering out any lines that start with "WARNING: ASAN interferes" (e.g., use
stderr.split(...).filter(line => !line.startsWith("WARNING: ASAN
interferes")).join(...)), then assert the filtered stderr is empty; apply the
same change for the other assertions in this file that use
expect(stderr).toBe("") (the occurrences around the other checks noted).
🪄 Autofix (Beta)

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: ae402ef5-cabf-425c-b006-5bd31e4c885f

📥 Commits

Reviewing files that changed from the base of the PR and between a1c0263 and 0987e28.

📒 Files selected for processing (1)
  • test/cli/run/check.test.ts

Comment thread test/cli/run/check.test.ts
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/mod.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/Arguments.rs

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

All my earlier nits have been addressed or reasonably deferred and I didn't find anything new, but since this adds a new user-facing CLI flag wired into the core bun/bun run/node-mode dispatch (plus a drive-by wtf-bindings.cpp fix), it's worth a maintainer sign-off.

Extended reasoning...

Overview

Adds bun --check <file> (Node's --check/-c equivalent): a new syntax_check field on RuntimeOptions, a --check entry in RUNTIME_PARAMS_, an ~80-line RunCommand::exec_check that reads the file/stdin and runs bun_js_parser::Parser directly without booting the VM, early-exit dispatch hooks in exec_auto_or_run and exec_as_if_node, a 200-line test suite, a zsh completion entry, and an unrelated one-line assertASSERT fix in wtf-bindings.cpp.

Security risks

None identified. The new path only reads a user-specified file (or stdin) and parses it; it never executes code, resolves imports, or touches the network. The unsafe { ctx.log() } blocks follow the existing documented pattern for the process-lifetime CLI log.

Level of scrutiny

Medium. This is a new user-facing feature threaded through the core CLI dispatch chain — an area where ordering and positional-handling edge cases are subtle (this PR itself went through five follow-up commits during review to fix -e "" detection, - → stdin, the run-positional strip under node-argv0, and --parallel/--filter precedence). The implementation now looks correct and is well-tested, but it is not a mechanical change.

Other factors

  • All prior inline feedback (mine and CodeRabbit's) is resolved; the two remaining items (no extension resolution for bun --check foofoo.js; bun test --check accepted-but-ignored) were explicitly deferred by the author with reasonable justification, and a maintainer may want to confirm those deferrals.
  • The bundled wtf-bindings.cpp change is trivially correct but is an unrelated build-fix riding along.
  • Good test coverage (12 cases including no-execute guarantee, stdin, -, TS/ESM/TLA, missing file, --eval conflict with exit 9, bun run --check).
  • No CODEOWNERS match for the touched paths.

@robobun

robobun commented May 18, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Ready for maintainer review. Feature complete, rebased onto current main, 14/14 tests pass locally, all 13 review threads resolved, docs entry added.

CI

Latest head (11203f8, adds --if-present support per review): Build #60836 shows 44+ pass and the same single failing lane: windows-x64-baseline-verify-baseline (exit status 2).

That same lane fails identically on main's tip (a1dd647d5b, the exact commit this branch is rebased onto), so it's pre-existing main breakage, not this diff. No failure in any run has touched test/cli/run/check.test.ts or any file this PR modifies.

Earlier flakes across runs, for the record: a v8-heap-snapshot SIGKILL (OOM-prone test, unrelated), an expired macOS runner, and two build-rust infra failures on a commit whose only delta from a green local build was docs + test files.

Deferred nits (for maintainer to confirm)

  • bun --check foo doesn't probe extensions (foofoo.js) — would require initializing the resolver for a syntax-only check; workaround is passing the extension.
  • bun test --check accepts but ignores --check — inherited from RUNTIME_PARAMS_TEST_PARAMS sharing (same as --eval/--if-present under bun test).

robobun and others added 10 commits June 5, 2026 20:31
Equivalent to `node --check` / `node -c`: parses the entry point (or
stdin if none given) with Bun's JS/TS parser and exits 0 if valid, or
prints the syntax error and exits 1. Never boots the JS VM.

`-c` is already taken by `--config` in Bun, so only the long `--check`
spelling is provided. Combining `--check` with `--eval`/`--print` is
rejected, matching Node's behavior.

Also fixes a unified-build include-order break in wtf-bindings.cpp
where a bare `assert()` was used without including <cassert>; switched
to WTF's `ASSERT()` to match the rest of the file.
Detect the conflict by flag presence (args.option().is_some()) instead
of checking whether the eval script is non-empty, so `bun --check -e ''`
is rejected the same way Node rejects `node --check --eval ''`.
exec_check is called from both exec_auto_or_run (where positionals[0]
may be the 'run' subcommand) and exec_as_if_node (where positionals[0]
is always the script name). Only strip the leading 'run' in the former
so bun-as-node `node --check run` checks a file named 'run' instead
of reading stdin.
Otherwise `bun --check --filter='*' foo` routes into filter_run and
executes the script, dropping the syntax-only guarantee. --check now
takes precedence over multi-run/filter dispatch.
@robobun robobun force-pushed the farm/e67ccbc8/add-check-flag branch from 2ab1374 to bfbba29 Compare June 5, 2026 20:38
Comment thread src/runtime/cli/Arguments.rs
Comment thread test/cli/run/check.test.ts Outdated
robobun and others added 2 commits June 5, 2026 20:54
The not.toContain assertion was vacuous after expect(stdout).toBe("") -
reordered so it can fail independently with a clearer message.
Comment thread src/runtime/cli/run_command.rs
bun run --if-present missing.js exits 0 via exec_with_cfg, but the
--check dispatch bypassed that path and always exited 1 on ENOENT.
Exit 0 on ENOENT when --if-present is set, matching the documented
"Exit without an error if the entrypoint does not exist" semantics.
Comment thread test/cli/run/check.test.ts Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
read_from pread()s from offset 0 which fails with ESPIPE on
non-seekable fds, breaking process substitution and FIFOs
(bun --check <(gen-js)). Open + stream-read instead, matching the
stdin branch and node --check.
Comment thread src/runtime/cli/run_command.rs
exec_check only consulted DEFAULT_LOADERS, so -l .js:ts had no effect
on the syntax dialect. Check ctx.args.loaders first, matching the run
path's precedence.
Comment thread src/runtime/cli/run_command.rs
'Cannot find module' was wrong for EISDIR/EACCES where the path was
found but unreadable. ENOENT keeps the familiar wording.
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.

1 participant