Formatter
Documentation for Roughly's R code formatter.
Roughly includes a non-invasive R code formatter that emphasizes readability while respecting the existing structure of your code.
Format your R files using the command line:
roughly fmt # Format all files in the current directoryroughly fmt <path> # Format all files in <path>roughly fmt --check # Only check if files would be formattedroughly fmt --diff # Show a diff of formatting changes without applying them
Philosophy
Section titled “Philosophy”Roughly’s non-invasive approach means it respects your existing line breaks and won’t arbitrarily split expressions you’ve chosen to keep on one line. The formatter is guided by these principles:
- Single-line expressions remain single-line: The formatter only adds line breaks if the expression is already multi-line, and will never break single-line expressions into multiple lines (with one exception).
- Flexible style preservation: Both compact (“hugged”) and expanded forms for nested expressions are supported, so your preferred style is respected (see hugging behavior).
- Automatic braces only when needed: Braces are added automatically only where they prevent subtle bugs (see auto-bracing).
- Minimal configuration: The formatter works out of the box with sensible defaults, so you can use it immediately without extra setup (see configuration).
Formatting Rules
Section titled “Formatting Rules”Below is a comprehensive list of rules describing the behavior of the formatter for different kinds of expressions, including edge cases where special handling is applied.
Binary Operators
Section titled “Binary Operators”Assignment operators always have spaces around them:
# Before formattingx<-1data<<-compute()
# After formattingx <- 1data <<- compute()
Binary operators have spaces around them, except for the range (:
) and power (^
) operators:
# Before formattingresult=x+y*zpower=base^exponentsequence=1:10
# After formattingresult = x + y * zpower = base^exponentsequence = 1:10
Pipeline operators maintain proper indentation when expressions span multiple lines:
# Before formattingdata %>%filter(condition) %>%select(value)
# After formattingdata %>% filter(condition) %>% select(value)
Unary Operators
Section titled “Unary Operators”Unary operators receive appropriate spacing based on their type and context:
# Before formattingresult = ! conditionvalue = - 42formula = ~x + y
# After formattingresult = !conditionvalue = -42formula = ~ x + y
Special spacing rule: The ~
(formula) operator gets a space when followed by complex expressions, but not when followed by simple identifiers.
Blocks
Section titled “Blocks”Multiline blocks always have a newline after the opening brace and before the closing brace:
# Before formatting{ x <- 1 print(x)}
# After formatting{ x <- 1 print(x)}
Single-line blocks are allowed, including those with semicolons. The formatter adds a space after {
and before }
for readability:
# Before formatting{x <- 1; print(x)}
# After formatting{ x <- 1; print(x) }
Semicolons in multiline blocks are split into separate lines for clarity:
# Before formatting{ x <- 1; print(x)}
# After formatting{ x <- 1 print(x)}
Empty blocks have no space between the braces:
# Before formatting{ }
# After formatting{}
Parenthesized Expressions
Section titled “Parenthesized Expressions”Single-line parenthesized expressions are always formatted in a “hugging” style—there is no extra space between the opening parenthesis and the enclosed expression:
# Before formatting( x + y )
# After formatting(x + y)
Multiline parenthesized expressions can be formatted in either hugged or expanded style; the formatter preserves both.
# expanded( expression + other_part)
# hugged(expression + other_part)
If Expressions
Section titled “If Expressions”Single-line if-else: Single-line if-else
expressions are allowed and preserved, since if
is an expression in R and can be used as a ternary operator:
x <- if (condition) consequence else alternative
Nested if-else: Nested if-else
chains are formatted so each else if
and else
starts on its own line, with all branches aligned at the same indentation level—no extra indentation for nested cases.
if (a) { x} else if (b) { y} else { z}
Auto-bracing for multiline if-else: Whenever an if-else
spans multiple lines, all branches are always wrapped in braces for clarity and consistency:
# Before formattingif (condition) { consequence} else alternative
# After formattingif (condition) { consequence} else { alternative}
Auto-bracing for multiline conditions: If an if
expression has a multiline condition, the formatter ensures the body is wrapped in braces even if it’s a single expression:
# Before formattingif ( a && b) body
# After formattingif ( a && b) { body}
Loops are the only expressions that are not allowed on a single line.
Because for
, while
, and repeat
loops are used solely for their side effects and do not return meaningful values, the formatter always requires these loops to be written across multiple lines with explicit braces (see auto-bracing). This ensures that side-effecting code is visually distinct from pure expressions.
For loops always enforce braced blocks for the body, ensuring consistency:
# Before formattingfor(item in sequence) run_effect(item)
# After formattingfor (item in sequence) { run_effect(item)}
While loops follow similar block enforcement rules:
# Before formattingwhile(condition) action()
# After formattingwhile (condition) { action()}
Repeat loops also enforce braced blocks:
# Before formattingrepeat action()
# After formattingrepeat { action()}
Multiline for loop headers: The formatter allows both of the following styles for multiline for
loop headers, preserving your preferred structure:
for ( item in sequence) {}
for ( item in sequence) {}
Function Calls
Section titled “Function Calls”Function calls receive consistent formatting with proper spacing around argument separators and assignment operators.
# Before formattingcall(a,b=1,...)
# After formattingcall(a, b = 1, ...)
Multiline function calls: Once two arguments appear on different lines, the call is treated as multiline, and each argument is formatted on its own line for clarity.
# Before formattingcall( a = x, b = y, c = z)
# After formattingcall( a = x, b = y, c = z)
Nested function calls can use either a hugged style—where the inner call starts right after the outer call’s parenthesis—or an expanded style. Both are preserved by the formatter, letting you choose the most readable form for your code.
# Hugged format - both functions start on the same lineresult <- outer(inner( arg))
# Expanded format - also validresult <- outer( inner( arg ))
Trailing argument hugging is allowed when the last argument starts on the same line: this means the final argument of a function call can begin immediately after the opening parenthesis or previous argument, even if it itself is multiline.
# This format is preserved - last argument starts on same linecall(a = x, b = y, c = inner( expr))
This behavior is particularly useful for testing frameworks and S4 method definitions:
test_that("description", { expect_equal(result, expected)})
setMethod("method", "Class", function(x) { # ... implementation})
Function Definitions
Section titled “Function Definitions”Single-line functions: Functions with a simple, single-expression body can be written on one line, with or without braces.
add <- function(x, y) x + ydouble <- function(x) { x * 2 }
Multiline functions: If the function body spans multiple lines, braces are always added—even if the body starts on the same line as the function declaration.
# Before formattingfunction() call(a = x, b = y)
# After formattingfunction() { call(a = x, b = y)}
Exception – multiline call on same line: If the function body is a function call that starts on the same line and is itself multiline, braces are not required.
fn <- function() call( a = x, b = y)
Anonymous functions (lambda expressions): Anonymous functions using \
are formatted the same way as named functions, supporting both single-line and multiline bodies.
lapply(data, \(x) x + 1)
lapply(data, \(x) { y <- x * 2 y + 1})
Switch Statements
Section titled “Switch Statements”Switch statements are formatted like normal function calls. For fallthrough cases (e.g., case = ,
), an extra space is added after the =
to clearly indicate the fallthrough.
result <- switch( type, "a" = handle_a(), "b" = , "c" = handle_bc(), "default" = handle_default())
Subsetting
Section titled “Subsetting”Bracket subsetting follows the same formatting rules as function calls:
# Before formattingdata[ row,col ]data[[ "name" ]]
# After formattingdata[row, col]data[["name"]]
Extract & Namespace Operators
Section titled “Extract & Namespace Operators”Extract and namespace operators ($
, @
, ::
, :::
) are formatted without spaces around them:
# Before formattingcollection $ itemcollection @ itempkg :: processpkg ::: filter
# After formattingcollection$itemcollection@itempkg::processpkg:::filter
When chaining extract or namespace operators across multiple lines, the formatter indents each subsequent line to make the chain visually distinct and easy to follow:
# Before formattingobject$call(x)$call(x, y)
# After formattingobject$ call(x)$ call(x, y)
String Literals
Section titled “String Literals”String literals receive intelligent quote normalization. The formatter prefers double quotes ("
) unless the string contains unescaped double quotes:
# Before formattingmessage <- 'Hello world'quoted_content <- 'Say "hello"'
# After formattingmessage <- "Hello world"quoted_content <- 'Say "hello"'
Multi-line string literals always keep their original indentation and line breaks, no matter where they appear. Even if surrounding code is refactored or deleted, the formatter never changes the internal content of multi-line strings.
# Before formatting# { <- parent block gets deleted x <- "This is a multi-line string. It preserves indentation and line breaks."# }
# After formatting# { <- parent block gets deletedx <- "This is a multi-line string. It preserves indentation and line breaks."# }
R6 Class Definitions
Section titled “R6 Class Definitions”Class definitions with empty lines between methods are preserved:
PersonClass <- R6Class( "Person", public = list( initialize = function(name) { private$name <- name },
get_name = function() { return(private$name) } ))
Comments
Section titled “Comments”The formatter ensures that a space is inserted after the #
for standard comments. For special comment types like Roxygen (#'
) and plumber (#*
) comments, the space is placed after the initial two characters:
# Before formatting# comment with space#comment without space#'roxygen comment#*plumber comment#'string' <- commented out string#!/usr/bin/env Rscript
# After formatting# comment with space# comment without space#' roxygen comment#* plumber comment#'string' <- commented out string#!/usr/bin/env Rscript
Additional exceptions to this rule are:
- Commented-out strings such as
#'string'
are left unchanged, since inserting a space (e.g.,#' string'
) would alter the content of the string. - Shebangs, for example
#!/usr/bin/env Rscript
, remain unchanged.
Line Spacing
Section titled “Line Spacing”The formatter normalizes line spacing between expressions, allowing at most one empty line:
# Before formattingx <- 1y <- 2
z <- 3
# After formattingx <- 1y <- 2
z <- 3
Line Endings
Section titled “Line Endings”The formatter automatically detects and preserves the line ending style (LF
or CRLF
) used in the original file.
Format Suppression
Section titled “Format Suppression”You can disable formatting for specific code sections using the # fmt: skip
comment directive. This is useful when you want to preserve specific formatting for readability, such as aligned data structures.
The fmt: skip
directive can be placed before any expression to skip formatting for it:
matrix( # fmt: skip c( 1, 2, 3, 4 ), # only the c(..) call won't be reformatted nrow = 2)
Or, at the end of a line to skip the previous expression:
# the entire matrix(..) call won't be reformattedmatrix(c(1, 2, 3, 4), nrow=2) # fmt: skip
You can also skip formatting for an entire file by placing # fmt: skip-file
at the top of the file. This directive must be placed at the very beginning of the file to take effect.
Rationale
Section titled “Rationale”This section explains the key design decisions that guided the formatter’s implementation.
Auto-Bracing
Section titled “Auto-Bracing”Accidental bugs: It’s easy to accidentally introduce subtle bugs when omitting braces in loops, function definitions, or if
expressions. For example, if you later add a line after an unbraced if
, only the first line is controlled by the condition:
# unbraced conditionif (condition) line1 line2 # <- is meant to be in body
# how it is interpreted:if (condition) line1line2 # <- gets executed unconditionally
Therefore, the formatter always adds braces to if
expressions and function definitions whenever the body spans multiple lines.
# Before formattingif (condition) action()
# After formattingif (condition) { action()}
For control flow structures such as for
, while
, and repeat
loops, the formatter always adds braces around the body—regardless of its length—since single-line loops are not allowed (see Loops).
# Before formattingfor (item in sequence) action()
# After formattingfor (item in sequence) { action()}
Hugging Behavior
Section titled “Hugging Behavior”“Hugging” refers to how nested expressions are formatted in multiline contexts—keeping them compact by allowing inner expressions to start on the same line as the outer expression’s opening delimiter. This is part of Roughly’s non-invasive approach: both hugged and expanded formats are allowed.
Nested function calls can be formatted in a hugged style:
# Hugged format - both functions start on the same lineresult <- outer(inner( arg))
# Expanded format - also validresult <- outer( inner( arg ))
Parenthesized expressions can also use hugging:
(expression + other_part)
# Also allowed( expression + other_part)
See the trailing argument hugging section under Function Calls for more details. The formatter preserves this style, which is especially useful for S4 methods and testing frameworks.