Skip to content

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

Open
robobun wants to merge 2 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 2 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 several times; conflicts were always adjacent-insertion in run_command.rs (most recently main's disable_default_env_files line in exec_as_if_node, kept alongside the --check early return). exec_check is pub(crate) to satisfy main's -D unreachable-pub. An earlier wtf-bindings.cpp assert fix was dropped 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.


no test proof · iteration 16 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/run/check.test.ts

@robobun

robobun commented May 18, 2026

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

@robobun, your commit 205f030 is building: #99157

@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
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.
Title check ✅ Passed The title clearly and concisely describes the main change: adding a CLI flag for syntax checking without execution.
Description check ✅ Passed The description explains the behavior, implementation, compatibility decisions, and test coverage in sufficient detail.

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: 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 (head 205f030), 19 tests, all review threads resolved, docs entry added.

CI

Head 205f030 = fresh rebase onto main (2670e50) + node-argv0 test coverage (conflict: main added disable_default_env_files at the same spot as the --check early return in exec_as_if_node; both kept). exec_check made pub(crate) for main's new -D unreachable-pub. check.test.ts (19, incl. two argv0: "node" cases) and as-node.test.ts (11) pass locally on the rebased head; CI is running.

Prior builds on earlier heads only ever failed on lanes tagged [pre-existing]/[flaky] by bun run ci:errors (runner-outside-step infra, bun-upgrade ETXTBSY, darwin flakes), never on anything this PR touches.

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: pass the extension.
  • bun test --check accepts but ignores --check; inherited from RUNTIME_PARAMS_TEST_PARAMS sharing (same as --eval/--if-present under bun test).
  • bun-as-node node -c foo.js doesn't map -c to --check (it stays --config). -c, --config <PATH>? takes an optional value so the simple -i-style flag remap doesn't apply; would need a separate node-mode param table or positional recovery. Workaround: use --check.

@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
Comment thread src/runtime/cli/run_command.rs
Comment thread test/cli/run/check.test.ts Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs
Comment thread src/runtime/cli/run_command.rs
Comment thread src/options_types/context.rs Outdated
Comment thread src/runtime/cli/run_command.rs
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
@robobun
robobun force-pushed the farm/e67ccbc8/add-check-flag branch from 5bdf688 to 6dcc809 Compare July 25, 2026 18:58
Comment thread src/runtime/cli/run_command.rs
Comment thread src/runtime/cli/run_command.rs
Comment thread src/runtime/cli/Arguments.rs
bun --check file.js parses the entry point with Bun's JS/TS parser and
exits 0 (no output) on success or 1 with the syntax error, without
booting the JS VM. Matches node --check semantics: reads stdin when no
file is given, rejects combining with -e/-p, and honors --if-present.

Before this, --check was an unknown flag and was silently skipped, so
the file ran its top level. -c remains Bun's --config alias.
@robobun
robobun force-pushed the farm/e67ccbc8/add-check-flag branch from 6dcc809 to 2670e50 Compare August 16, 2026 04:44
Comment thread test/cli/run/check.test.ts
Exercises the exec_as_if_node dispatch and the node-mode guard on the
'run' positional strip (a file literally named run must be checked,
not stripped).

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

Thanks for adding the argv0: "node" tests — that closes out the last coverage gap I flagged. This run found no new issues.

Because this adds a new user-facing CLI flag and there are a couple of explicitly-deferred items awaiting a maintainer call (the node -c remap under RunAsNodeCommand, and whether bun test --check should be rejected), a human sign-off would still be worthwhile.

What was reviewed: the three dispatch sites (exec_auto_or_run for Auto/Run, exec_as_if_node) all route to exec_check before VM boot; file-read uses cursor read_to_end_into so pipes/FIFOs work and the fd is closed via File's Drop; loader selection consults ctx.args.loaders before DEFAULT_LOADERS; --if-present short-circuits only on ENOENT; the --eval/--print conflict exits 9 to match Node. Tests now cover all three entry points including the PRETEND_TO_BE_NODE gate on the run-strip.

Extended reasoning...

Overview

This PR adds bun --check <file> (Node's node --check equivalent): parse the entry point for syntax errors and exit without executing. It touches:

  • src/options_types/context.rs — new syntax_check: bool on RuntimeOptions
  • src/runtime/cli/Arguments.rs--check param entry, conflict check with --eval/--print (exit 9)
  • src/runtime/cli/mod.rs — early dispatch to exec_check from exec_auto_or_run
  • src/runtime/cli/run_command.rs — the ~100-line exec_check implementation, plus a 3-line early-return in exec_as_if_node
  • test/cli/run/check.test.ts — 17 tests (306 lines), docs/snippets/cli/run.mdx, completions/bun.zsh

Security risks

None identified. The command reads a file (or stdin) and runs it through the JS parser without booting the VM, resolving imports, or executing any code. The path is opened via sys::File::openat with O_RDONLY and no path traversal concerns beyond what bun <file> already has. Untrusted input reaches only the parser, which is already the attack surface for bun <file>.

Level of scrutiny

Medium-high. This is new user-facing CLI API surface, and the repo's REVIEW.md calls out "API design (adding or changing user-facing API surface)" as a situational section requiring extra care. The implementation itself is self-contained and well-tested, but two design questions were explicitly deferred by the author "for a maintainer call": (1) whether -c should remap to --check under RunAsNodeCommand (it currently stays --config, so node -c foo.js via the bun shim doesn't syntax-check), and (2) whether bun test --check should be explicitly rejected rather than silently accepted-and-ignored. Neither blocks the feature, but both are the kind of API-surface call a maintainer should make rather than a bot.

Other factors

This PR has been through many review iterations — every prior nit I raised (stdin -, --if-present, --loader override, ESPIPE on pipes, error-message wording, describe.concurrent, docs, and most recently argv0-node test coverage) has been addressed with a fix commit and a test. The current diff reflects all of those. The one still-unresolved inline comment (mine, on line 265, asking for argv0:"node" coverage) is now addressed by the two new tests at the end of check.test.ts (commit 205f030), so nothing is actually outstanding on my side. The remaining deferred items are the two maintainer-call questions above, which the author has explicitly flagged rather than silently dropped.

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