From b87e76a688338484b9d5f5fa44d2cfe5d2f3a3b8 Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Fri, 10 Apr 2026 20:12:19 -0500 Subject: [PATCH] Close gap between collapsed signature and bare body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When reformat_function_defs collapses a multi-line function signature onto one line, a braceless body that originally sat on the closing paren's line was left on its original line number. The serializer then emitted blank lines for the gap, making `f <- function(x, y)` look visually detached from `if (is.null(x)) y else x` even though the expression still parses as one function. Track the old signature end line and, for bare-body functions, shift trailing tokens up so the body stays adjacent to the collapsed signature. Only the single-line collapse branch needed the fix — the wrapping branch already moves bare body tokens to last_line. Regression test covers the `%||%`-style reproducer with control_braces and expand_if enabled. --- inst/tinytest/test_rformat.R | 14 ++++++++++++++ src/funcdef.cpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/inst/tinytest/test_rformat.R b/inst/tinytest/test_rformat.R index d46718d..b097e44 100644 --- a/inst/tinytest/test_rformat.R +++ b/inst/tinytest/test_rformat.R @@ -50,6 +50,20 @@ expect_true( info = "Inline function body should be preserved" ) +# Multi-line function header + bare if body + expand_if (regression) +# The signature collapse used to leave blank lines between function(...) +# and the body, making it look like two separate statements. +code <- "`%||%` <- function(\n x,\n y\n) if (is.null(x)) y else x" +result <- rformat(code, control_braces = "multi", expand_if = TRUE) +expect_true( + !is.null(tryCatch(parse(text = result), error = function(e) NULL)), + info = "Collapsed signature with bare if body should still parse" +) +expect_false( + grepl("function\\(x, y\\)\\s*\\n\\s*\\n", result), + info = "No blank line should separate collapsed signature from its bare body" +) + # Nested parentheses collapse (regression test) # Short nested calls should collapse to a single line code <- "tryCatch( diff --git a/src/funcdef.cpp b/src/funcdef.cpp index a244977..aa4f0fa 100644 --- a/src/funcdef.cpp +++ b/src/funcdef.cpp @@ -134,6 +134,19 @@ void reformat_function_defs(std::vector& tokens, need_change = true; if (!need_change) continue; + // Record the last line the signature currently occupies so we + // can close any gap between the collapsed signature and a + // bare (braceless) body. + int old_sig_end_line = tokens[fi].out_line; + for (int k = fi; k <= close_idx; k++) { + if (tokens[k].out_line > old_sig_end_line) + old_sig_end_line = tokens[k].out_line; + } + for (int c : comma_indices) { + if (tokens[c].out_line > old_sig_end_line) + old_sig_end_line = tokens[c].out_line; + } + for (int k = fi; k <= close_idx; k++) tokens[k].out_line = func_line; for (int c : comma_indices) @@ -141,6 +154,22 @@ void reformat_function_defs(std::vector& tokens, if (has_brace) tokens[brace_idx].out_line = func_line; + // Close the gap for a bare-body function: shift any tokens + // that lived on the old signature's trailing lines up so the + // body stays adjacent to the collapsed signature. + if (!has_brace && old_sig_end_line > func_line) { + int shift = old_sig_end_line - func_line; + for (int k = 0; k < n; k++) { + if (tokens[k].out_line >= old_sig_end_line + 1) { + tokens[k].out_line -= shift; + } else if (tokens[k].out_line == old_sig_end_line) { + // Body tokens that shared the old close-paren + // line now belong on func_line. + tokens[k].out_line = func_line; + } + } + } + reorder_tokens(tokens); changed = true; break;