Formatting rules
ry includes an R code formatter that changes as little as it can: it normalizes spacing, indentation, and bracing, and keeps the structure you wrote.
ry fmt # Format all files in the current directoryry fmt <path> # Format all files in <path>ry fmt --check # List files that would be reformatted, without writingry fmt --diff # Show a diff of formatting changes without applying them--check and --diff exit 1 when any file would change, which is what a CI job gates on, and an
error, such as a file that cannot be parsed, exits 2. The exit codes
have the details.
Philosophy
Section titled “Philosophy”The formatter keeps your line breaks and never splits an expression you wrote on one line. Everything else follows from that idea:
- Single-line expressions remain single-line: The formatter adds line breaks only where the expression is already multi-line, and never breaks a single-line expression into several (with one exception).
- Both nesting styles are preserved: Compact (“hugged”) and expanded forms for nested expressions are equally valid, and neither is rewritten into the other (see hugging behavior).
- Braces are added where omitting them invites a bug: a multi-line
ifor function body gets braces so that a line added later cannot fall outside it (see auto-bracing), and every loop body is braced (see loops).
Formatting Rules
Section titled “Formatting Rules”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:10Pipeline operators: continuation lines of a multi-line pipeline are indented one step:
# Before formattingdata %>%filter(condition) %>%select(value)
# After formattingdata %>% filter(condition) %>% select(value)Unary operators
Section titled “Unary operators”!, unary -, and unary + hug their operand:
# Before formattingresult = ! conditionvalue = - 42formula = ~x + y
# After formattingresult = !conditionvalue = -42formula = ~ x + y~ hugs a lone name (~x, ~.) and is spaced before anything else (~ x + y, ~ log(x), ~ 1).
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 }:
# Before formatting{x <- 1; print(x)}
# After formatting{ x <- 1; print(x) }Semicolons in multiline blocks are split into separate lines, so that each statement gets its own line in a diff and its own breakpoint:
# 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”A single-line parenthesized expression is always hugged, with no space between the parentheses and the expression inside:
# 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 alternativeNested if-else: A nested if-else chain is formatted so that each else if and each else starts on its own line, with every branch at the same indentation level and 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 wrapped in braces, so a statement added to one branch cannot end up outside it:
# 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.
for, while, and repeat loops are evaluated for their side effects and return no meaningful value, so the formatter writes them across multiple lines with explicit braces (see auto-bracing). This keeps side-effecting code visually distinct from expressions that produce a value.
For loops always get a braced body:
# 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: Both of the following styles for multiline for loop headers are preserved:
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 goes on its own line, so adding or removing one changes one line of the diff.
# Before formattingcall( a = x, b = y, c = z)
# After formattingcall( a = x, b = y, c = z)Nested function calls keep whichever of the hugged and expanded styles you wrote (see hugging behavior).
Compact multiline calls: When a call spans multiple lines solely because a single argument (often the final one) is itself multiline, the formatter keeps the other arguments on the original line rather than placing every argument on its own line, preserving a concise, compact layout:
# This format is preserved - only last argument spans multiple linescall(a = x, b = y, c = inner( expr))Two common instances of this layout are a test_that() block and an S4 method definition:
test_that("description", { expect_equal(result, expected)})
setMethod("method", "Class", function(x) { # ... implementation}, sealed = TRUE)In the setMethod example, sealed = TRUE sits on a different line from the other arguments, but only the function body is multiline, so the formatter keeps the layout.
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 body spans several lines, braces are always added, even when 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 calls
Section titled “Switch calls”switch() calls are formatted like any other function call. A fallthrough case keeps a spaced = with nothing after it: "b" = ,.
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 and namespace operators
Section titled “Extract and namespace operators”Extract and namespace operators ($, @, ::, :::) are formatted without spaces around them:
# Before formattingcollection $ itemcollection @ itempkg :: processpkg ::: filter
# After formattingcollection$itemcollection@itempkg::processpkg:::filterWhen an extract or namespace chain spans several lines, each subsequent line is indented one step:
# Before formattingobject$call(x)$call(x, y)
# After formattingobject$ call(x)$ call(x, y)String literals
Section titled “String literals”String literals are normalized to 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"'The inside of a multi-line string is never re-indented, because its whitespace is part of the value.
# 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."# }Raw string literals (r"(...)", R"[...]", and their custom-dash variants) are preserved byte-for-byte. Quote normalization never applies to them, since their whole purpose is to hold characters, such as quotes and backslashes, that would otherwise need escaping.
# Before formattingpath <- r"(C:\Users\me)"quoted <- r"(He said "hi")"
# After formattingpath <- r"(C:\Users\me)"quoted <- r"(He said "hi")"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 RscriptAdditional 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. - Quarto and knitr cell options, for example
#| echo: false, are kept verbatim, since theirkey: valuepayload is read by machines.
Type annotations
Section titled “Type annotations”ry’s type annotations are written in #: comments. The formatter treats each block of consecutive #: lines as one unit: the block is parsed with the same annotation grammar the type checker uses, and, only when it parses, re-rendered with the canonical spacing used throughout the typing reference: one space after #:, no space before , or : and one space after, spaces around | and ->, and no padding inside (, [, {, or generic <...>. A leading type-parameter binder such as <T> is followed by a space.
# Before formatting#:fn( x:integer,y:double )->characterrender <- function(x, y) as.character(x + y)#: list[ named : double ]weights <- list(a = 1.0)#: Either< E , A >|NULLoutcome <- NULL
# After formatting#: fn(x: integer, y: double) -> characterrender <- function(x, y) as.character(x + y)#: list[named: double]weights <- list(a = 1.0)#: Either<E, A> | NULLoutcome <- NULLAnything that does not parse as an annotation is left verbatim, beyond ensuring the single space after #:. Prose, a dotted type name, a pkg::fun reference, and a malformed type are all left alone, so the formatter never corrupts a comment it does not understand. Consecutive #: lines form one annotation block (exactly as the type checker groups them), so a block that is not a single valid annotation is also left as written. Two compact annotations with no blank line between them are one such block.
The reformat is deliberately non-invasive: token order, identifier casing, and your line breaks are preserved. A single-line annotation stays on one line, an expanded annotation (one written with @param / @return lines) keeps one directive per line rather than being collapsed into a compact fn(...), and content lines are never rejoined.
# Before formatting#: @param count { integer }#: @param render { fn(integer)->character }#: @returns { character }
# After formatting#: @param count {integer}#: @param render {fn(integer) -> character}#: @returns {character}When an annotation wraps across several #: lines, indentation and closing brackets are normalized from the shape of the opening brackets. An opening bracket followed by more content on its own line is hugged: its closer stays glued to the token before it. An opening bracket that ends its line is expanded: its closer gets a line of its own, aligned with the line that opened it. Each line is indented one step (indent-width spaces) per enclosing expanded bracket. Both canonical styles are stable:
# expanded#: @type Future {#: list{#: instrument: Instrument,#: maturity: integer#: }#: }
# hugged#: @type Future {list{#: instrument: Instrument,#: maturity: integer#: }}and mixed closer shapes normalize to the nearest consistent style:
# Before formatting#: @type Future {#: list{#: instrument: Instrument,#: maturity: integer#: }}
# After formatting#: @type Future {#: list{#: instrument: Instrument,#: maturity: integer#: }#: }A blank line, a non-#: comment, or ordinary code ends an annotation block, so unrelated comments are never pulled into one. Trailing empty #: lines at the end of a block are dropped.
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 <- 3A single blank line inside a call is kept too, so an R6 class keeps the blank lines between its methods:
PersonClass <- R6Class( "Person", public = list( initialize = function(name) { private$name <- name },
get_name = function() { return(private$name) } ))Line endings and indentation
Section titled “Line endings and indentation”The line ending follows [format] line-ending, whose default "auto" keeps the file’s own (LF or CRLF). The indent is [format] indent-width spaces, 2 by default.
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 skipped expression is preserved byte-exactly, including the column of its first line, so hand-aligned constructs keep their alignment.
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: skipFor a whole region, use # fmt: off and re-enable formatting with # fmt: on. The region between the directives is preserved byte-exactly, keeping its original indentation, columns, and blank lines. The directives work at the top level as well as inside functions and { ... } blocks; a region left open runs to the end of its enclosing block.
f <- function() { # fmt: off x <- c(1, 2, 30, 4) # fmt: on x}# fmt: skip-file before any other code or comment (a shebang included) leaves the whole file untouched. Blank lines before it are allowed.
Rationale
Section titled “Rationale”Auto-bracing
Section titled “Auto-bracing”Accidental bugs: Omitting braces in loops, function definitions, or if expressions makes a later edit change what the code means. Adding a line after an unbraced if leaves only the first line under the condition:
# unbraced conditionif (condition) line1 line2 # <- is meant to be in body
# how it is interpreted:if (condition) line1line2 # <- gets executed unconditionallyTherefore, 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 a control flow structure such as a for, while, or repeat loop, the formatter always adds braces around the body, whatever its length, because a single-line loop is not allowed. See Loops.
# Before formattingfor (item in sequence) action()
# After formattingfor (item in sequence) { action()}Hugging behavior
Section titled “Hugging behavior”“Hugging” is the compact layout for a nested expression in a multiline context: the inner expression starts on the same line as the outer expression’s opening delimiter. Both hugged and expanded forms are allowed, and the formatter preserves whichever was written.
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 compact multiline calls under Function calls.