Repository navigation
Fix gem source-block bang lost after x = if … end (#1476) - #1478
Conversation
Assisted-by: Claude Code:claude-opus-5-5
A gem declared in the user's own source "…" do block loses its DEPENDENCIES `!` on hosted rollback/remove when an assignment-form keyword construct (v = if/case/begin … end) precedes it, so frozen installs fail on Bundler 4.0.19+ (#1476). Add unit cases for the block scanner and the restore round trip, and an e2e Bundler case. Assisted-by: Claude Code:claude-opus-5-5
The hosted gem unwind only counted if/case/begin/… as opening a block when the keyword started its line. In `v = if …` nothing was opened, but the matching `end` still closed the user's source block, so the restored lock dropped its `!` pin and Bundler 4.0.19+ frozen installs failed after rollback or remove. The restored declaration also came back at column 0 after a blank line. Count keywords word by word wherever they start an expression (after =, (, ',', ;, …), treat `end` as closing one, net out one-line constructs, skip modifiers, names like x.class or if:, and endless defs. Derive the restored declaration's indent from the same block stack. Fixes #1476 Assisted-by: Claude Code:claude-opus-5-5
After an `end` indented deeper than the block's body (a case/when laid out under its assignment), the restored declaration copied that `end`'s indent, so the Gemfile did not come back byte for byte. Take the indent of the line that opened the enclosing block + 2 instead. Assisted-by: Claude Code:claude-opus-5-5
Review follow-ups for the source-block scanner (#1476): - a keyword after a label (require: if), =>, a ternary, and/or/not or an operator starts an expression, so its end is counted; - %w[]/%i[] arrays, regexps, backticks, =begin/=end comments and heredoc bodies are no longer read as code; - a ; ends a loop's statement, so a later do opens its own block; - def begin / x.then if treat the keyword-like word as a name; - the restored declaration keeps a blank line before a following declaration after an inner end, as before; - the stack is computed once per restore, with opener line offsets. Assisted-by: Claude Code:claude-opus-5-5
Second review pass on the source-block scanner (#1476): - mask strings and %-literals that span lines, and find the comment with the same literal rules (a ' in a regexp, # in %q()); - add %s() / %x() literals; - class <<self and list <<name are not heredocs; - a modifier on a backslash-continued line stays a modifier; - endless defs with operator names or nested parens; - a multi-line block opener indents from its first line. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ 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 9f2c94f. Configure here.
|
Burn-down agent: labeled Ready for review at head
Generated by Claude Code |
Final review brief (merge steward)What it does: Hosted gem Risk: medium. The change is only in the gem unwind path, but it replaces a small line-level heuristic with roughly 450 lines of hand-written Ruby lexing (literals, heredocs, Look here:
Verified:
Changes I made: none. Open questions: none blocking. Known lexing limits are listed in the PR description: nested-quote Auto-merge (squash) is armed, so approving sends this straight to the merge queue. Generated by Claude Code |
* ci(release): publish npm from release.yml, not publish-npm.yml Every socket-patch npm package's trusted publisher is registered against release.yml, and npm allows only one publisher per package. The 5.0.0 release dispatched publish-npm.yml, whose OIDC token named that file, so npm rejected it with E401 before staging anything. Move the npm leg back inline as release.yml's npm-publish job (same steps as publish-npm.yml) and delete publish-npm.yml, so the existing registration keeps working. Add an `npm-retry-version` input that skips the version gate, build, tag, GitHub release and crates.io jobs and re-stages npm from an existing release's assets; this retries 5.0.0 now and replaces the manual Publish npm dispatch. crates.io stays dispatched through publish-cargo.yml, since crates.io allows several publishers per crate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(release): fold 22 more PRs into the 5.0.0 notes Cover the PRs merged to main after #1432's base (1779317..4cb3643): Cargo source replacement and registry = "crates-io" (#338/#455, #386), pnpm lockfile=false (#1074), berry removed descriptors (#1435), yarnrc ${VAR} reads (#1100), CRLF .npmrc append (#1196), Composer vendor-dir cascade and global home (#439/#686, #586), Go +incompatible vex (#484), gem moved BUNDLE_GEMFILE and commented Gemfile revert (#952, #988), Poetry source = "pypi" (#1282) and Hatch path characters (#547). No entries for #1411 (macOS fix to code #1393 added in this release; the #1393 entry already describes the intended behavior), #1426 (perf regression introduced and fixed within 5.0.0) or the CI/test/bench PRs. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(release): cover #1419, #1434, #1440, #1442, #1444 in the 5.0.0 notes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(release): 5.0.1 Cut [Unreleased] into [5.0.1] — 2026-10-13 and stamp 5.0.1 with scripts/version-sync.sh. The 5.0.1 notes cover main through 501c6d9 (#1449), i.e. every PR after the v5.0.0 tag (6ad6add): the npm publish fix (5.0.0 never reached npm or crates.io, 5.0.1 is the first v5 there), #1465, #759, #1460, #1462, #1445, #869, #1447, #1450, #1467, and #1449 under Maintenance. CI/test-only PRs (#1448, #1452, #1454, #1457, #1459) have no entries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * chore(release): 5.0.1 notes for PRs merged since the cut Covers #1469, #1470, #1474, #1475, #1478, #1479, #1480 and #1482; #1484 is CI-only. The notes now cover main through 285a5e8. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1476
Summary
Hosted gem
rollback/removenow keep theDEPENDENCIES!(and thedeclaration's indent) for a gem declared in the user's own
source "…" doblock when an assignment-form keyword construct(
v = if … end,v = case … end,v = begin … end,x ||= unless …)precedes it. Before this, the restored lock dropped the
!and everyfrozen/deployment install on Bundler 4.0.19+ failed with exit 16, while the
unwind reported success.
Root cause
in_source_block(crates/socket-patch-core/src/patch/redirect/upstream/gem.rs)decides
DepEntry::KeepvsUnpinby counting block openers andendsline by line.
opens_keyword_blockonly countedif/case/begin/…when the keyword was the first token of the line. In
v = if …nothing waspushed, but the matching
endstill popped, removing the user'ssourceframe.
declaration_prefixused the same line-level heuristics, so therestored gem also came back at column 0 after a blank line.
Fix
One block scanner (
block_stack) now feeds bothin_source_blockanddeclaration_prefix:(line start, after
=,(,,,;, a labelrequire:,=>, aternary,
and/or/when/then/else, an operator, or acontinued previous line);
endcloses one; one-line constructs net out;gem "x" if c,x.save! if y,return if y, a modifier on a\\-continued line), names (x.class,:if,if:,begin?,def begin) and endless defs (def x = 1,def ==(o) = …) open nothing;a loop's own
dois not a second block;%w[]/%i()/%q{}/%x()/… and regexp literals(multi-line strings and
%-literals included), comments,=begin/=endblocks and heredoc bodies are not code;
line (+2); a blank line is still restored before a following declaration.
Tests (red → green)
v = if … endlock!+ Gemfile byte-identicalsource_block_declaration_keeps_its_bang_through_restore(new manifests:if,case+begin,unless/while, deeperend)\n\ngem "rails"at column 0,!dropped)if/unless/case/begin/while/until/for, labels, ternary, literals, heredocs,=begin)source_block_detectionendmanifest_blocks_restore_in_placev = if … end+w = case … end), rollback + remove, frozen install of the restored paire2e_redirect_gem_build::gem_hosted_unwind_keeps_a_source_block_bang(newScanVexSourceBlockAssignmentdriver)Commands run locally (Ruby 3.3.6):
cargo fmt --all -- --check✅cargo clippy --workspace --all-features -- -D warnings✅cargo test -p socket-patch-core --all-features --lib redirect::upstream::gem: 17 passed; with the fix reverted, 2 failedBUNDLER_VERSION={4.0.22,4.0.18} SOCKET_PATCH_BUNDLER_E2E_REQUIRED=1 cargo test -p socket-patch-cli --all-features --test e2e_redirect_gem_build --test e2e_vendor_gem_build -- --ignored: 29 + 8 passed on eachcargo test --workspace --all-features --no-fail-fast: 14337 passed, 13 failed. All 13 are environmental and outside gem code: 12 usechmodto force write failures, which root ignores (the sandbox runs as uid 0), ande2e_bun_lockb::binary_shared_bundled_record_hosted_pin_is_managedneeds the real patch API (tunnel error). CI runs these unprivileged.No wrapper (
npm/,pypi/,gem/) changes: this is core Rust logic only.Review notes (
/code-review high, two passes)All confirmed findings are fixed in c9f3945 and 9f2c94f. Not changed:
vex/discover/gem.rsparse_source_block_declskeeps its own linegrammar. It reads only Socket-written patch-registry blocks (which hold
plain
gemlines) and fails closed: an early close only dropsattribution and never adds a
!or an attestation. Sharing the lexerthere would be a separate refactor.
block_stackruns twice per restored direct gem (in_source_blockanddeclaration_prefix) over a small Gemfile prefix. Threading one stackthrough
restore_one→restore_manifestcosts more code than it saves.ruby_wordslooks likegem_line_tail_blocks_edit's classifier inredirect/mod.rs, but the two answer different questions (modifier vsargument position on a declaration line), so they stay separate.
#{}interpolationthat nests quotes, and percent literals after a bare method call
(
puts %w[…]).🤖 Generated with Claude Code
https://claude.ai/code/session_01HuKqLAEGGwH73DucozCrY6
Generated by Claude Code