Skip to content

Fix gem source block after a ; on the line (#1531) - #1537

Merged
Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-gem-source-block-after-semicolon
Oct 14, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 4 commits into
mainfrom
agent/fix-gem-source-block-after-semicolon

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #1531

What users saw

With a Gemfile like this:

source "https://rubygems.org"

zz = 1; source "https://rubygems.org" do
  gem "vuln-gem", "1.0.0"
end

Bundler locks vuln-gem (= 1.0.0)!. After scan --mode hosted, bundle install, and then rollback or remove, socket-patch restored the Gemfile byte for byte but wrote the DEPENDENCIES line without the !. On Bundler 4.0.19+ every frozen install (BUNDLE_FROZEN=true, deployment, CI) then failed with exit 16 until someone ran an unfrozen bundle install by hand.

Root cause

block_stack in crates/socket-patch-core/src/patch/redirect/upstream/gem.rs is the Gemfile scanner behind in_source_block. It only recognised a source call at the very start of a line. In the masked.split(';') loop it reset source_pending to false for every later statement, without checking whether that statement was itself a source call. So zz = 1; source "…" do was read as a plain do block, and the unwind chose Unpin for the gem.

Fix

The scanner already treats keywords (if, case, …) as openers wherever they start an expression (#1476). source now gets the same treatment: it is a call wherever starts_expression holds at depth 0, including after ;, =, then and so on. In detail:

  • source = … and source == … are not calls (source_call_args). .source, :source and source: were already excluded by ruby_words.
  • source_pending now records how many brace blocks were open around the call. A } that closes a block the call sat inside ends the call (urls.each { |u| source u }.then do is not a source block). A ; inside a brace block within the call's own arguments no longer ends it (source ENV.fetch("S") { warn "x"; "https://x" } do).
  • A source call or do block inside the brace block of an outer call's own arguments doesn't replace that call. Only a do or { at the call's brace level is its block (take_source, from Bugbot's review).
  • A top-level ; starts a new statement, so statement_line moves to that line. A restored declaration then takes its indent from the line the source statement is on, even when the earlier statement started on a previous line.

No wrapper changes: npm/, pypi/ and gem/ only dispatch to the Rust binary.

Tests

Issue Test Red on main Green here
#1531 source_block_opener_mid_line (unit, core): 14 opener shapes × inside/after the block, plus 9 non-call shapes ✅ FAILED. 3 passed; 1 failed ✅
#1531 source_block_declaration_keeps_its_bang_through_restore (unit, full restore round-trip): zz = 1; source … do and the continued-line case n/a (case added) ✅
#1531 gem_hosted_unwind_keeps_a_source_block_bang (e2e, real Bundler 4.0.21): new ScanVexSourceBlockAfterSemicolon driver, rollback and remove ✅ lock came back as vuln-gem (= 1.0.0) without ! ✅

Commands run locally (Ruby 3.3.6, Bundler 4.0.21 as in the CI leg):

  • cargo fmt --all -- --check: ok
  • cargo clippy --workspace --all-features -- -D warnings: ok
  • cargo test -p socket-patch-core --all-features --lib gem: 358 passed
  • BUNDLER_VERSION=4.0.21 SOCKET_PATCH_BUNDLER_E2E_VERSION=4.0.21 SOCKET_PATCH_BUNDLER_E2E_REQUIRED=1 cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build --test e2e_vendor_gem_build -- --include-ignored: 51 + 43 passed
  • cargo test --workspace --all-features --no-fail-fast: 246 binaries ok. 13 tests failed, all for sandbox reasons in code this PR doesn't touch. The sandbox runs as root, so the chmod 0o555 write-failure tests (vendor/reconcile/revert state writes, unremovable lock files, copy_tree symlink root, vlt heal, poetry/requirements wire failures, repair cleanup) can't fail. e2e_bun_lockb::binary_shared_bundled_record_hosted_pin_is_managed needs patches-api.socket.dev, which the sandbox can't reach. CI runs these as non-root with network access.

Code review

I ran /code-review high on the first fix and folded every finding into the second commit except one:

🤖 Generated with Claude Code

https://claude.ai/code/session_01JMZecNRUYcZ8pNUFxKbrhB


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
`rollback` and `remove` dropped the `!` source pin from a gem's
DEPENDENCIES line when its `source "..." do` block followed another
statement and a `;` on the same line (`zz = 1; source "..." do`).
Bundler 4.0.19+ then failed every frozen install with exit 16.

The Gemfile block scanner only checked for a `source` call at the
start of a line. It now checks each top-level `;` statement too, so
the gem is seen as inside its source block and keeps its pin.

Fixes #1531

Assisted-by: Claude Code:claude-opus-5-5
The first fix only looked for `source` right after a `;`. Bundler
also runs a source block opened after `then`, `=` and other
expression starts, so rollback could still drop the gem's `!` pin
there. The scanner now treats `source` as a call wherever a keyword
would start an expression, skips `source = ...` assignments, ends a
call made inside a brace block when that block closes, and takes a
restored gem's indent from the line its source statement is on.

Refs #1531

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) marked this pull request as ready for review October 14, 2026 15:30
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/patch/redirect/upstream/gem.rs
A `source` call or `do` block inside the brace block of a source
call's own arguments (`source ENV.fetch("S") { source "y" } do`)
took over the outer call, so its real `do` block was not seen as a
source block and rollback could drop the gem's `!` pin. Only a block
opened at the call's own brace level now belongs to it.

Refs #1531

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 6fff5ae. Configure here.

@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Tanmay Singla (@Tanmay182003) one commit landed after your approval (on c51944e), so I'm holding this out of the merge queue until you take another look:

  • 6fff5ae Keep an outer gem source call over inner ones: fixes the Bugbot finding on gem.rs. A source call inside a brace block in another source call's arguments (source ENV.fetch("S") { source "https://y" } do) replaced the outer pending call, so the outer do was missed and restore would drop the lock's ! pin. Now a nested source doesn't replace a pending one, and only a do/{ at the outer call's brace level counts as its block (new take_source helper). Two new cases in source_block_opener_mid_line.

I reproduced the original bug independently before this commit landed. CI is running on the new head. Once you re-approve, it can go into the queue.


Generated by Claude Code

Merged via the queue into main with commit 140ee15 Oct 14, 2026
54 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the agent/fix-gem-source-block-after-semicolon branch October 14, 2026 17:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants