Skip to content

Fix gem source-block detection for multi-line and brace openers (#1509) - #1520

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

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

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 #1509

Summary

A gem declared inside the user's own source block keeps its ! pin
through hosted rollback / remove (#1056). That only worked for the
one-line source "…" do spelling. Two other valid spellings lost the
!, and Bundler 4.0.19+ then refused every frozen install of the
restored pair (exit 16):

  • a source( opener whose arguments span lines before ) do
  • a source("…") { … } brace block, which also came back with the gem
    at column 0 after an extra blank line

Root cause

block_stack in crates/socket-patch-core/src/patch/redirect/upstream/gem.rs
is the Gemfile scanner behind both the ! check (in_source_block) and the
restored declaration's indent (declaration_prefix). It ended a statement
at the end of any line not ending in an operator, so an unclosed ( didn't
keep the source statement open until its do. It also never opened a
frame for { … }.

Fix

  • A statement stays open while a (, [ or hash { is unclosed, and
    remembers that it began with source until its block opens. The do
    at depth 0 is the source's block.
  • { opens a frame that } closes. A { in expression position is a
    hash, which counts toward depth and has no frame. Only the brace block
    directly after the source(…) argument list is the source's, so in
    source ENV.fetch("S") { … } do the do is still the source block. A
    stray } never closes a do block.
  • Character literals (?(, ?}) are masked so they aren't read as
    brackets.
  • Frame records the line its opener sits on, so a gem restored first
    in a multi-line source( block gets no blank line after ) do.
  • Only the end of an unfinished line is carried forward, since that is
    all starts_expression reads. This keeps a long open bracket linear.

The second and third commits apply the /code-review high findings: the brace
binding, hash braces in arguments, a ; inside the argument list, stray
} and ?( literals, the multi-line opener's blank line, quadratic
carry, and a duplicate ident helper. The third commit fixes a review finding: a leading if inside an open bracket or -> { lambda now opens its own frame, and an open bracket only carries a source statement.

Tests (red → green)

Issue shape Test
#1509 multi-line source(…) do source_block_detection, source_block_declaration_keeps_its_bang_through_restore, e2e gem_hosted_unwind_keeps_a_source_block_bang (ScanVexSourceBlockMultiLineOpener, rollback + remove)
#1509 source(…) { } (! and indent) same unit tests, e2e ScanVexSourceBlockBraces (rollback + remove, lock and Gemfile byte-restored, frozen install)
  • Red, before the fix: both unit tests failed (assertion failed: at("source(\n \"https://x\"\n) do …"), and the restore lost the !: rails (= 7.0.0) vs rails (= 7.0.0)!). The e2e test failed on Bundler 4.0.22 with rollback: the lock comes back byte for byte, ! included and DEPENDENCIES\n vuln-gem (= 1.0.0) without the !.
  • Green, after the fix:
    • cargo test -p socket-patch-core --all-features --lib -- gem: 357 passed
    • cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build -- --ignored (Ruby 3.3.6, Bundler 4.0.22): 30 passed
    • cargo fmt --all -- --check and cargo clippy --workspace --all-features -- -D warnings: clean
    • cargo test --workspace --all-features: couldn't finish locally: the session's disk allowance ran out linking every integration-test binary, so CI runs it. cargo test -p socket-patch-core --all-features --lib (all 6472) passes except 4 chmod-based permission tests (copy_tree::relax_loop_must_not_traverse_symlinked_root, vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry, pypi_poetry::wire_write_failure_…, pypi_requirements::wire_failure_rolls_back_…). Those can't fail as expected under uid 0 in this sandbox, and this diff doesn't touch them.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hsu76uJiHbbPSV1iSeEukP


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
Hosted rollback and remove dropped the `!` source pin from a gem
declared inside a `source(...)` block whose arguments span lines, or
inside a `source("...") { ... }` brace block. Bundler 4.0.19+ then
refused every frozen install of the restored Gemfile.lock, and the
brace form also came back with the gem at the wrong indent.

The Gemfile block scanner now keeps a statement open while a `(` or
`[` is unclosed, remembers that the statement began with `source`
until its `do` or `{`, and tracks `{ }` blocks alongside `do ... end`.

Fixes #1509

Assisted-by: Claude Code:claude-opus-5-5
A brace block now counts as the source's own block only right after
the `source(...)` argument list, so `source ENV.fetch(...) { ... } do`
keeps its `!` pin. Hash braces keep a statement open like parens, a
stray `}` no longer closes a `do` block, and `?(`-style character
literals are no longer read as brackets. A restored first gem in a
multi-line `source(` block no longer gets a blank line after the
opener. Only the end of an unfinished line is carried forward, so a
long open bracket no longer costs quadratic time.

Refs #1509

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

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

Comment thread crates/socket-patch-core/src/patch/redirect/upstream/gem.rs Outdated
An `if` at the start of a line inside an open `(`, `[` or hash was
read as a modifier, so it opened nothing while its `end` still closed
the enclosing `source` block, and the gem lost its `!` pin on
rollback or remove. A stabby lambda's `-> {` was read as a hash.

A line that leaves only a bracket open now starts a fresh expression
on the next line, `-> {` opens a block, and an open bracket carries
only a `source` statement on to its block, so a bracket the scanner
misses elsewhere no longer hides every later source block.

Refs #1509

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 943ec44. Configure here.

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