Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,12 @@ jobs:

- name: Test
run: ./run.sh run_tests

- name: Audit shared libraries
if: runner.os == 'Linux'
env:
GITHUB_PAT: ${{ github.token }}
run: |
Rscript -e 'remotes::install_github("cornball-ai/tinyelf")'
Rscript -e 'install.packages(".", repos = NULL, type = "source")'
Rscript -e 'tinyelf::audit_so(system.file("libs", package = "rformat"))'
14 changes: 4 additions & 10 deletions R/RcppExports.R
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
# Generated by using Rcpp::compileAttributes() -> do not edit by hand
# Generator token: 10BE3573-1514-4C36-9D1C-5A225CD40393

cpp_format_pipeline <- function(code, indent_str, wrap, expand_if,
brace_style, line_limit, function_space,
control_braces) {
.Call(`_rformat_cpp_format_pipeline`, code, indent_str, wrap,
expand_if, brace_style, line_limit, function_space,
control_braces)
cpp_format_pipeline <- function(code, indent_str, wrap, expand_if, brace_style, line_limit, function_space, control_braces, join_else) {
.Call(`_rformat_cpp_format_pipeline`, code, indent_str, wrap, expand_if, brace_style, line_limit, function_space, control_braces, join_else)
}

cpp_format_all <- function(code, indent_str, wrap, expand_if, brace_style,
line_limit, function_space, control_braces) {
.Call(`_rformat_cpp_format_all`, code, indent_str, wrap, expand_if,
brace_style, line_limit, function_space, control_braces)
cpp_format_all <- function(code, indent_str, wrap, expand_if, brace_style, line_limit, function_space, control_braces, join_else) {
.Call(`_rformat_cpp_format_all`, code, indent_str, wrap, expand_if, brace_style, line_limit, function_space, control_braces, join_else)
}

cpp_set_trace <- function(enable) {
Expand Down
63 changes: 63 additions & 0 deletions R/ast_else.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#' Join Else to Preceding Close Brace
#'
#' AST transform that moves ELSE tokens (and any following tokens on the
#' same line, like `if` in `else if`) to the same output line as the
#' preceding `}`. Skips if a COMMENT exists between `}` and `else`,
#' or if joining would exceed the line limit.
#'
#' @param terms Enriched terminal DataFrame.
#' @param indent_str Indent string for line width calculation.
#' @param line_limit Maximum line width.
#' @return Updated DataFrame.
#' @keywords internal
join_else_transform <- function(terms, indent_str, line_limit) {
terms <- terms[order(terms$out_line, terms$out_order),]
terms$out_order <- seq_len(nrow(terms))
n <- nrow(terms)
if (n < 2L) return(terms)

# Find all ELSE tokens
else_idx <- which(terms$token == "ELSE")
if (length(else_idx) == 0L) return(terms)

for (ei in else_idx) {
else_line <- terms$out_line[ei]

# Walk backwards to find preceding non-comment token
rbrace_idx <- NA_integer_
has_comment <- FALSE
j <- ei - 1L
while (j >= 1L) {
if (terms$token[j] == "COMMENT") {
has_comment <- TRUE
j <- j - 1L
next
}
if (terms$token[j] == "'}'") {
rbrace_idx <- j
}
break
}

# Skip if no preceding }, or comment between } and else
if (is.na(rbrace_idx) || has_comment) next

rbrace_line <- terms$out_line[rbrace_idx]
if (rbrace_line == else_line) next # already on same line

# Check if joining would exceed line_limit
rbrace_width <- ast_line_width(terms, rbrace_line, indent_str)
else_width <- ast_line_width(terms, else_line, indent_str)
if (rbrace_width + 1L + else_width > line_limit) next

# Move ELSE and all following tokens on the same line to the } line
# This handles "else if (y) {" — moving else, if, (, y, ), { together
k <- ei
while (k <= n && terms$out_line[k] == else_line) {
terms$out_line[k] <- rbrace_line
k <- k + 1L
}
}

terms
}
7 changes: 6 additions & 1 deletion R/ast_pipeline.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
#' @param line_limit Maximum line length.
#' @param function_space Add space after `function`.
#' @param control_braces Control brace mode.
#' @param join_else If TRUE, move else to same line as preceding `}`.
#' @return Formatted code string.
#' @keywords internal
format_pipeline <- function(code, indent, wrap, expand_if, brace_style,
line_limit, function_space = FALSE,
control_braces = FALSE) {
control_braces = FALSE, join_else = TRUE) {
if (is.numeric(indent)) {
indent_str <- strrep(" ", indent)
} else {
Expand Down Expand Up @@ -90,6 +91,10 @@ format_pipeline <- function(code, indent, wrap, expand_if, brace_style,
terms <- recompute_nesting(terms)
}

if (isTRUE(join_else)) {
terms <- join_else_transform(terms, indent_str, line_limit)
}

serialize_tokens(terms, indent_str, wrap, line_limit)
}

7 changes: 4 additions & 3 deletions R/format_tokens.R
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
#' @param line_limit Maximum line length before wrapping (default 80).
#' @param function_space If TRUE, add space before `(` in function definitions.
#' @param control_braces If TRUE, add braces to bare one-line control flow bodies.
#' @param join_else If TRUE, move else to same line as preceding `}`.
#' @return Formatted code as character string.
#' @importFrom utils getParseData
#' @keywords internal
format_tokens <- function(code, indent = 4L, wrap = "paren",
expand_if = FALSE, brace_style = "kr",
line_limit = 80L, function_space = FALSE,
control_braces = FALSE) {
control_braces = FALSE, join_else = TRUE) {
if (is.character(indent)) {
indent_str <- indent
} else {
Expand All @@ -43,7 +44,7 @@ format_tokens <- function(code, indent = 4L, wrap = "paren",
if (is.loaded("_rformat_cpp_format_all")) {
return(cpp_format_all(code, indent_str, wrap, expand_if,
brace_style, line_limit, function_space,
cb_str))
cb_str, join_else))
}

# Pure R fallback: split into top-level expressions, format each
Expand All @@ -57,7 +58,7 @@ format_tokens <- function(code, indent = 4L, wrap = "paren",
parts[i] <- format_pipeline(chunks[[i]]$text, indent_str,
wrap, expand_if, brace_style,
line_limit, function_space,
control_braces)
control_braces, join_else)
}
}
result <- paste(parts, collapse = "\n")
Expand Down
33 changes: 23 additions & 10 deletions R/rformat.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
#' bodies (e.g., `if (x) y` becomes `if (x) { y }`). Default FALSE
#' matches R Core source code where 59% of control flow bodies are bare.
#' @param expand_if Expand inline if-else to multi-line (default FALSE).
#' @param else_same_line If TRUE (default), fix `}\\nelse` to `} else {` on
#' the same line. Matches R Core source code where 70% use same-line else.
#' @param else_same_line If TRUE (default), repair top-level `}\\nelse`
#' (which is a parse error in R) by joining to `} else` before formatting.
#' When FALSE, unparseable input is returned unchanged with a warning.
#' @param function_space If TRUE, add space before `(` in function definitions:
#' `function (x)` instead of `function(x)`. Default FALSE matches 96% of
#' R Core source code.
#' @param join_else If TRUE (default), move `else` to the same line as the
#' preceding `}`: `} else {`. Matches R Core source code where 70%
#' use same-line else. When FALSE, `}\\nelse` on separate lines is preserved.
#' @return Formatted code as a character string.
#' @export
#' @examples
Expand All @@ -45,7 +49,7 @@
rformat <- function(code, indent = 4L, line_limit = 80L, wrap = "paren",
brace_style = "kr", control_braces = FALSE,
expand_if = FALSE, else_same_line = TRUE,
function_space = FALSE) {
function_space = FALSE, join_else = TRUE) {
if (!is.character(code)) {
stop("`code` must be a character string")
}
Expand All @@ -68,7 +72,8 @@ rformat <- function(code, indent = 4L, line_limit = 80L, wrap = "paren",
brace_style = brace_style,
line_limit = line_limit,
function_space = function_space,
control_braces = control_braces)
control_braces = control_braces,
join_else = join_else)

formatted <- format_blank_lines(formatted)
# Fix } else if formatter broke valid input
Expand Down Expand Up @@ -99,10 +104,13 @@ rformat <- function(code, indent = 4L, line_limit = 80L, wrap = "paren",
#' @param control_braces If TRUE, add braces to bare one-line control flow
#' bodies. Default FALSE matches R Core majority style.
#' @param expand_if Expand inline if-else to multi-line (default FALSE).
#' @param else_same_line If TRUE (default), fix `}\\nelse` to `} else {`.
#' @param else_same_line If TRUE (default), repair top-level `}\\nelse`
#' (which is a parse error in R) by joining to `} else` before formatting.
#' @param function_space If TRUE, add space before `(` in function definitions:
#' `function (x)` instead of `function(x)`. Default FALSE matches 96% of
#' R Core source code.
#' @param join_else If TRUE (default), move `else` to the same line as the
#' preceding `}`.
#' @return Invisibly returns formatted code.
#' @export
#' @examples
Expand All @@ -119,7 +127,7 @@ rformat_file <- function(path, output = NULL, dry_run = FALSE, indent = 4L,
line_limit = 80L, wrap = "paren",
brace_style = "kr", control_braces = FALSE,
expand_if = FALSE, else_same_line = TRUE,
function_space = FALSE) {
function_space = FALSE, join_else = TRUE) {
if (!file.exists(path)) {
stop("File not found: ", path)
}
Expand All @@ -130,7 +138,8 @@ rformat_file <- function(path, output = NULL, dry_run = FALSE, indent = 4L,
line_limit = line_limit,
function_space = function_space,
control_braces = control_braces,
else_same_line = else_same_line)
else_same_line = else_same_line,
join_else = join_else)

if (!dry_run) {
if (is.null(output)) {
Expand Down Expand Up @@ -161,10 +170,13 @@ rformat_file <- function(path, output = NULL, dry_run = FALSE, indent = 4L,
#' @param control_braces If TRUE, add braces to bare one-line control flow
#' bodies. Default FALSE matches R Core majority style.
#' @param expand_if Expand inline if-else to multi-line (default FALSE).
#' @param else_same_line If TRUE (default), fix `}\\nelse` to `} else {`.
#' @param else_same_line If TRUE (default), repair top-level `}\\nelse`
#' (which is a parse error in R) by joining to `} else` before formatting.
#' @param function_space If TRUE, add space before `(` in function definitions:
#' `function (x)` instead of `function(x)`. Default FALSE matches 96% of
#' R Core source code.
#' @param join_else If TRUE (default), move `else` to the same line as the
#' preceding `}`.
#' @return Invisibly returns vector of modified file paths.
#' @export
#' @examples
Expand All @@ -181,7 +193,7 @@ rformat_dir <- function(path = ".", recursive = TRUE, dry_run = FALSE,
indent = 4L, line_limit = 80L, wrap = "paren",
brace_style = "kr", control_braces = FALSE,
expand_if = FALSE, else_same_line = TRUE,
function_space = FALSE) {
function_space = FALSE, join_else = TRUE) {
if (!dir.exists(path)) {
stop("Directory not found: ", path)
}
Expand All @@ -198,7 +210,8 @@ rformat_dir <- function(path = ".", recursive = TRUE, dry_run = FALSE,
line_limit = line_limit,
function_space = function_space,
control_braces = control_braces,
else_same_line = else_same_line)
else_same_line = else_same_line,
join_else = join_else)

if (formatted != original) {
modified <- c(modified, f)
Expand Down
29 changes: 28 additions & 1 deletion inst/tinytest/test_rformat.R
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,38 @@ expect_equal(
)

# else_same_line = FALSE inside braces (where } \n else does parse)
# Also need join_else = FALSE to preserve the separate-line layout
expect_equal(
rformat("{\nif (x) {\n y\n}\nelse {\n z\n}\n}", else_same_line = FALSE),
rformat("{\nif (x) {\n y\n}\nelse {\n z\n}\n}",
else_same_line = FALSE, join_else = FALSE),
"{\n if (x) {\n y\n }\n else {\n z\n }\n}\n"
)

# join_else = TRUE (default) moves else to same line as }
expect_equal(
rformat("{\nif (x) {\n y\n}\nelse {\n z\n}\n}"),
"{\n if (x) {\n y\n } else {\n z\n }\n}\n"
)

# join_else = FALSE preserves } / else on separate lines
expect_equal(
rformat("{\nif (x) {\n y\n}\nelse {\n z\n}\n}", join_else = FALSE),
"{\n if (x) {\n y\n }\n else {\n z\n }\n}\n"
)

# join_else skips when comment between } and else
expect_equal(
rformat("{\nif (x) {\n y\n} # comment\nelse {\n z\n}\n}",
join_else = TRUE),
"{\n if (x) {\n y\n } # comment\n else {\n z\n }\n}\n"
)

# join_else with nested if-else chains
expect_equal(
rformat("{\nif (x) {\n 1\n}\nelse if (y) {\n 2\n}\nelse {\n 3\n}\n}"),
"{\n if (x) {\n 1\n } else if (y) {\n 2\n } else {\n 3\n }\n}\n"
)

# Trailing whitespace removal
expect_false(
grepl(" \n", rformat("x <- 1 \ny <- 2 ")),
Expand Down
4 changes: 3 additions & 1 deletion man/format_pipeline.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
\title{AST-Based Format Pipeline}
\usage{
format_pipeline(code, indent, wrap, expand_if, brace_style, line_limit,
function_space = FALSE, control_braces = FALSE)
function_space = FALSE, control_braces = FALSE, join_else = TRUE)
}
\arguments{
\item{code}{Code string for one top-level expression.}
Expand All @@ -22,6 +22,8 @@ format_pipeline(code, indent, wrap, expand_if, brace_style, line_limit,
\item{function_space}{Add space after `function`.}

\item{control_braces}{Control brace mode.}

\item{join_else}{If TRUE, move else to same line as preceding `\}`.}
}
\value{
Formatted code string.
Expand Down
4 changes: 3 additions & 1 deletion man/format_tokens.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
\usage{
format_tokens(code, indent = 4L, wrap = "paren", expand_if = FALSE,
brace_style = "kr", line_limit = 80L, function_space = FALSE,
control_braces = FALSE)
control_braces = FALSE, join_else = TRUE)
}
\arguments{
\item{code}{Character string of R code.}
Expand All @@ -25,6 +25,8 @@ parenthesis, `"fixed"` uses 8-space indent.}
\item{function_space}{If TRUE, add space before `(` in function definitions.}

\item{control_braces}{If TRUE, add braces to bare one-line control flow bodies.}

\item{join_else}{If TRUE, move else to same line as preceding `\}`.}
}
\value{
Formatted code as character string.
Expand Down
19 changes: 19 additions & 0 deletions man/join_else_transform.Rd
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
% tinyrox says don't edit this manually, but it can't stop you!
\name{join_else_transform}
\alias{join_else_transform}
\title{Join Else to Preceding Close Brace}
\usage{
join_else_transform(terms)
}
\arguments{
\item{terms}{Enriched terminal DataFrame.}
}
\value{
Updated DataFrame.
}
\description{
AST transform that moves ELSE tokens (and any following opening brace)
to the same output line as the preceding `\}`. Skips if a COMMENT
exists between `\}` and `else`.
}
\keyword{internal}
11 changes: 8 additions & 3 deletions man/rformat.Rd
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
\usage{
rformat(code, indent = 4L, line_limit = 80L, wrap = "paren",
brace_style = "kr", control_braces = FALSE, expand_if = FALSE,
else_same_line = TRUE, function_space = FALSE)
else_same_line = TRUE, function_space = FALSE, join_else = TRUE)
}
\arguments{
\item{code}{Character string of R code to format.}
Expand All @@ -27,12 +27,17 @@ matches R Core source code where 59\% of control flow bodies are bare.}

\item{expand_if}{Expand inline if-else to multi-line (default FALSE).}

\item{else_same_line}{If TRUE (default), fix `\}\\nelse` to `\} else \{` on
the same line. Matches R Core source code where 70\% use same-line else.}
\item{else_same_line}{If TRUE (default), repair top-level `\}\\nelse`
(which is a parse error in R) by joining to `\} else` before formatting.
When FALSE, unparseable input is returned unchanged with a warning.}

\item{function_space}{If TRUE, add space before `(` in function definitions:
`function (x)` instead of `function(x)`. Default FALSE matches 96\% of
R Core source code.}

\item{join_else}{If TRUE (default), move `else` to the same line as the
preceding `\}`: `\} else \{`. Matches R Core source code where 70\%
use same-line else. When FALSE, `\}\\nelse` on separate lines is preserved.}
}
\value{
Formatted code as a character string.
Expand Down
Loading