Skip to content

Commit 7fa5e73

Browse files
Fix gem source-block bang lost after x = if … end (#1476) (#1478)
* Start fix for #1476 Assisted-by: Claude Code:claude-opus-5-5 * Test gem unwind after x = if … end in source block 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 * Keep gem source-block bang after x = if … end 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 * Indent restored gem from its enclosing block 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 * Skip literals, comments, heredocs in gem scan 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 * Carry literals across lines in gem block scan 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e6ef4c1 commit 7fa5e73

2 files changed

Lines changed: 637 additions & 64 deletions

File tree

‎crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,10 @@ enum Driver {
436436
/// `source "<upstream>" do` block (#1056): bundler locks it with a `!`
437437
/// source pin, which the unwind must keep.
438438
ScanVexSourceBlock,
439+
/// [`Driver::ScanVexSourceBlock`] with assignment-form keyword
440+
/// constructs (`v = if … end`, `w = case … end`) before the gem
441+
/// (#1476): their `end`s must not close the user's source block.
442+
ScanVexSourceBlockAssignment,
439443
/// A heredoc option continues beyond the declaration's physical line.
440444
ScanVexHeredocDeclaration,
441445
/// A double-quoted interpolation can itself contain a heredoc opener.
@@ -518,6 +522,9 @@ impl Driver {
518522
Driver::ScanVexTwin => "scan --mode hosted (Gemfile + gems.rb twin)",
519523
Driver::ScanVexGroupBlock => "scan --mode hosted (gem in a group block)",
520524
Driver::ScanVexSourceBlock => "scan --mode hosted (gem in a source block)",
525+
Driver::ScanVexSourceBlockAssignment => {
526+
"scan --mode hosted (gem in a source block after `v = if … end`)"
527+
}
521528
Driver::ScanVexSemicolonJoinedDeclaration => {
522529
"scan --mode hosted (two `;`-joined gem declarations)"
523530
}
@@ -815,6 +822,12 @@ async fn redirect_scanned_project(
815822
"source \"{up}/upstream\"\n\nsource \"{up}/upstream\" do\n gem \"{DEP}\", \"{DEP_VERSION}\"\nend\n",
816823
up = server.uri()
817824
),
825+
Driver::ScanVexSourceBlockAssignment => format!(
826+
"source \"{up}/upstream\"\n\nsource \"{up}/upstream\" do\n v = if ENV[\"ZZ\"]\n \"a\"\n \
827+
else\n \"b\"\n end\n w = case ENV[\"ZZ\"]\n when \"x\" then \"a\"\n \
828+
else \"b\"\n end\n gem \"{DEP}\", \"{DEP_VERSION}\"\nend\n",
829+
up = server.uri()
830+
),
818831
Driver::ScanVexEvalGemfile => {
819832
std::fs::write(proj.join("Gemfile.common"), format!("gem \"{DEP}\"\n")).unwrap();
820833
format!(
@@ -1063,6 +1076,7 @@ async fn redirect_scanned_project(
10631076
| Driver::ScanVexEvalGemfile
10641077
| Driver::ScanVexGroupBlock
10651078
| Driver::ScanVexSourceBlock
1079+
| Driver::ScanVexSourceBlockAssignment
10661080
| Driver::ScanVexCustomGitSource
10671081
| Driver::ScanVexMultiLineDeclaration
10681082
| Driver::ScanVexConditionalDeclaration
@@ -1267,6 +1281,7 @@ async fn redirect_scanned_project(
12671281
Driver::ScanVex
12681282
| Driver::ScanVexGroupBlock
12691283
| Driver::ScanVexSourceBlock
1284+
| Driver::ScanVexSourceBlockAssignment
12701285
| Driver::ScanVexTrailingSemicolonDeclaration => {
12711286
assert_eq!(env["vex"]["statements"], 1, "vex block: {env}");
12721287
assert!(
@@ -2334,18 +2349,29 @@ async fn gem_hosted_group_block_pin_survives_a_refused_vendored_takeover() {
23342349
/// pre-scan pair, `!` included, so a FROZEN install of the restored pair
23352350
/// still succeeds (it exited 16 when the unwind dropped the `!`).
23362351
/// Converged without CHECKSUMS so the restore needs no rubygems.org sha.
2352+
/// #1476: the same holds when assignment-form `v = if … end` /
2353+
/// `w = case … end` constructs precede the gem inside the block.
23372354
#[tokio::test(flavor = "multi_thread")]
23382355
#[ignore = "host capstone: shells out to a real ruby/gem/bundler (>= 2.2); \
23392356
the unpinned `test` job skips it, an e2e job with a pinned toolchain runs it via --ignored"]
23402357
async fn gem_hosted_unwind_keeps_a_source_block_bang() {
2341-
for command in ["rollback", "remove"] {
2358+
for (driver, command) in [
2359+
(Driver::ScanVexSourceBlock, "rollback"),
2360+
(Driver::ScanVexSourceBlock, "remove"),
2361+
(Driver::ScanVexSourceBlockAssignment, "rollback"),
2362+
(Driver::ScanVexSourceBlockAssignment, "remove"),
2363+
] {
2364+
let label = match driver {
2365+
Driver::ScanVexSourceBlockAssignment => "source-block-assignment",
2366+
_ => "source-block",
2367+
};
23422368
let Some(fx) = redirect_scanned_project(
2343-
&format!("source-block {command}"),
2369+
&format!("{label} {command}"),
23442370
Spelling::Gemfile,
23452371
false,
23462372
true,
23472373
None,
2348-
Driver::ScanVexSourceBlock,
2374+
driver,
23492375
)
23502376
.await
23512377
else {
@@ -2408,7 +2434,7 @@ async fn gem_hosted_unwind_keeps_a_source_block_bang() {
24082434
fx.pristine_gemfile,
24092435
"{command}: the Gemfile comes back byte for byte"
24102436
);
2411-
let fresh = stage_fresh_checkout(&fx, &format!("source-block-{command}"));
2437+
let fresh = stage_fresh_checkout(&fx, &format!("{label}-{command}"));
24122438
let install = bundle_env(&fresh, &["install"], &[("BUNDLE_FROZEN", "true")]);
24132439
assert!(
24142440
install.status.success(),

0 commit comments

Comments
 (0)