Checking and formatting
ry check finds the mistakes R would only show you when a line runs, without running anything and
without any configuration. Run it in the project directory:
$ ry checkunresolved
! I could not resolve `ratee` in this package, its imports, or builtins. Did you mean `rate`? --[R/discount.R:2:11] 1 | apply_discount <- function(price, rate) { 2 | price * ratee | ^^^^^ 3 | }
1 problem in 1 fileIt reads every .R file in the project, plus the R chunks of .Rmd, .qmd, and .Rnw documents.
Your editor shows the same findings as you type (Editors). This page covers what
ry check finds with no configuration; type errors are opt-in and have
their own section.
A misspelled name is the most common finding, and R reports it only when execution reaches it. ry resolves every name by R’s scoping rules, so the typo is reported where you wrote it, as above.
Files in a package’s R/ share one namespace, a script is read from top to bottom, and library()
calls, DESCRIPTION dependencies, and NAMESPACE imports bring names into scope. The same analysis
reports an assignment that nothing reads, a top-level name defined twice in a package (R silently
keeps one of them), pkg::name where the package does not export name, and a NAMESPACE that
imports or exports a name that does not exist.
Every file outside R/ is analyzed on its own, seeing its own definitions plus the package’s.
A script also sees the top-level definitions of every file it source()s by a literal path, and of
the files those source in turn. The path is tried relative to the sourcing file’s directory, then
relative to the project root, where R usually runs. A sourced file reports no unused assignments,
because the script that sources it is what reads them. A computed path such as
source(file.path(dir, "utils.R")) is not followed, so its names stay unresolved.
Files directly under tests/testthat/ see the package’s definitions and the helper-*.R and
setup-*.R files’ definitions, but add nothing to the package, so a variable a test assigns never
changes what a name means under R/. Each test file’s own bindings stay in that file, as they do
when testthat runs it.
A .Rmd, .qmd, or .Rnw document is read as one script made of its R chunks in order, so a chunk
sees what earlier chunks define, and an inline `r expr` sees everything defined above it.
Chunks in other languages are treated as prose.
One gap matters from the start: after library() of a package ry knows nothing about, any bare name
might be one of its exports, so ry stops reporting unresolved names in the whole project.
Package stubs shows how to turn the check back on.
Inside data-masking functions such as dplyr’s filter() and mutate(), data.table’s [, and
base with(), subset(), and transform(), a bare name refers to a column, so this code reports
nothing:
library(dplyr)
by_region <- function(sales) { sales |> filter(amount > 0) |> mutate(net = amount * (1 - fee))}Syntax errors
Section titled “Syntax errors”R’s parser stops at the first error and names the token it choked on, so for this code it says
unexpected symbol and points at subtitle. ry names what is missing and keeps analyzing the rest
of the file:
config <- list( title = "Revenue" subtitle = "by quarter")syntax-error
x missing `,` between these arguments --[R/config.R:2:20] 1 | config <- list( 2 | title = "Revenue" | ^Eleven lints are on by default. Three of them catch a habit that causes bugs, and here they are in a
script, with the unused finding for each assignment left out:
total = 2Lflag <- Tx <- c(1, 2, )assignment-operator
! Use <-, not =, for assignment
boolean-shorthand
! Use TRUE, not T, for Boolean values
trailing-comma
! Unexpected comma after last argumentT is an ordinary variable that any code can overwrite. The trailing comma passes an empty
argument, which breaks c(1, 2, ) when it runs but is harmless to a function that never evaluates
it, so it is a warning. The fourth, object-system, warns on each S4, R6 or
Reference Class definition, because records and tags model data
with every field checked.
Five more catch code that reads right but does something else: x == NA is always NA, & in
an if condition evaluates both sides, 1:length(x) counts down to zero when x is empty,
class(x) == "data.frame" misses a tibble, and length(x == 0) measures the comparison instead of
comparing the length. Each fix changes what the code does, which is the point,
so it is unsafe and runs only with --unsafe-fixes. The last two, unreachable-code and
sprintf-arguments, flag code after a return() and a sprintf() format that wants more or fewer
values than it gets. Style lints, such as redundant-return, single-quotes and paste-sep, are off until
you turn them on. Diagnostic codes lists every code with its
reason, and configuration shows how to turn each one on or off.
Fixing findings
Section titled “Fixing findings”A finding with an obvious rewrite says so under its snippet. ry check --fix applies the safe ones
to your files, which are the lints above, and then reports what is left:
$ ry check --fixunresolved
! I could not resolve `totl` in this package, its imports, or builtins. Did you mean `total`? --[R/rates.R:4:7] 3 | x <- c(1, 2) 4 | print(totl) | ^^^^ help: Change to `total` (`ry check --fix --unsafe-fixes` applies it)
applied 3 fixes1 problem in 1 file1 fixable with `ry check --fix --unsafe-fixes`A spelling suggestion is a guess, so it needs --unsafe-fixes as well. Your editor offers the same
fixes as quick fixes, and Fixes lists which findings have one.
Silencing one finding
Section titled “Silencing one finding”Every finding carries a code, and naming it in a comment silences it:
y = 3L # ry: allow(assignment-operator)Type findings work the same way, as in # ry: allow(type-mismatch).
A suppression covers its own line and the line below, so it can sit at the end of the line or above
it. To turn a check off everywhere, set it in ry.toml.
lintr’s comments work too, so code written for lintr keeps its suppressions. # nolint silences
every finding on its line except a syntax error, # nolint start and # nolint end do the same for
the lines between them, and either can name lintr linters, as in # nolint: seq_linter.. A named
linter silences the ry code that reports the same thing, such as length-sequence for
seq_linter, and a linter ry has no counterpart for silences nothing. A .lintr file’s linters field carries
over too, as configuration describes.
Formatting
Section titled “Formatting”ry fmt gives every file in the project the same spacing, indentation and braces, so a review shows
what changed instead of whose style it is. It formats files in place:
x<-c(1,2,3)if(x>1){y<-2}for (i in 1:3) print(i)$ ry fmt1 file reformatted, 0 files left unchangedx <- c(1, 2, 3)if (x > 1) { y <- 2 }for (i in 1:3) { print(i)}Every loop body, and every if or function body that spans lines, gets braces, so a line added
later cannot fall outside the block it belongs to.
ry fmt never reflows code to a column limit, because a formatter that does rewrites lines you did
not touch and buries your change in layout churn. Your line breaks decide the layout instead: a call
written on one line stays on one line, and a call you broke across lines gets one argument per line:
report <- summarise(sales, total = sum(amount), n = n())report <- summarise(sales, total = sum(amount), n = n())report <- summarise(sales, total = sum(amount), n = n())report <- summarise( sales, total = sum(amount), n = n())ry fmt --range 2-3 R/report.R formats only those lines (a line 3, or a span with columns
2:1-3:10, works too). A statement is the smallest unit, so a range that reaches into a multi-line
call reformats the whole call. Editors use the same operation for “Format Selection” and for
format-on-type. The only settings are indent width and line endings, in
ry.toml, and Formatting rules has every
rule with examples.
ry check exits with status 1 when it finds anything, and ry fmt --check does when a file is not
formatted, listing each one:
$ ry fmt --checkWould reformat: ./R/report.R1 file would be reformatted, 0 files already formattedSo a CI job needs only those two commands. Pin the ry version, because a newer release can report
findings an older one did not. ry check --output json gives machine-readable output, as the
command reference describes.