Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Arity

Arity is a language server, formatter, and linter for the R language. It is built in Rust on a lossless, incremental parser, providing a fast, deterministic development experience that integrates with popular code editors and IDEs.

Quick Start

Install with Cargo:

cargo install arity

Format your first document:

arity format file.R

Run lint checks:

arity lint file.R

For full installation options (prebuilt binaries, package managers, and source builds), see Getting Started.

Where to Go Next


arity v0.16.0

Getting Started

Installation

Cargo

The simplest way to install Arity is from crates.io with Cargo:

cargo install arity

From Source

Clone the repository and build a release binary:

git clone https://github.com/jolars/arity
cd arity
cargo build --release

The binary is written to target/release/arity.

First Run

Format a file in place:

arity format file.R

Check formatting without writing changes:

arity format --check file.R

Lint a file (or pipe from stdin):

arity lint file.R

Run the language server over stdio (for editor integration):

arity lsp

See the CLI Reference for the full set of commands and options.

Editor Setup

Arity ships a language server, started with arity lsp (stdio, JSON-RPC). It offers formatting, diagnostics with quick fixes, hover, completion, signature help, go-to-definition and find-references, rename, document and workspace symbols, semantic tokens, folding and selection ranges, document links, document color swatches, and call and type hierarchy.

Configuration is read from an arity.toml discovered from each file’s directory (see the configuration reference).

VS Code/Positron

Install the Arity extension from the VS Code Marketplace or Open VSX. It bundles the arity binary (falling back to a download) and starts the language server automatically for R files. Editors that support VS Code extensions, such as Positron, work the same way.

Neovim

With nvim-lspconfig installed, register arity as a server for R files:

vim.lsp.config("arity", {
  cmd = { "arity", "lsp" },
  filetypes = { "r" },
  root_markers = { "arity.toml", "DESCRIPTION", ".git" },
})
vim.lsp.enable("arity")

Format on save (optional):

vim.api.nvim_create_autocmd("BufWritePre", {
  pattern = "*.R",
  callback = function() vim.lsp.buf.format() end,
})

Helix

In ~/.config/helix/languages.toml:

[language-server.arity]
command = "arity"
args = ["lsp"]

[[language]]
name = "r"
language-servers = ["arity"]
formatter = { command = "arity", args = ["format"] }
auto-format = true

Other editors

Any LSP-capable editor can use arity by launching arity lsp over stdio for the r language. Point your client’s R language-server command at arity with the lsp argument.

Configuration

Arity is configured with a TOML file named arity.toml. All keys are optional; omitting a key uses its default. Keys are kebab-case, and unknown keys are rejected with an error (so a typo never silently falls back to a default).

Run arity init to write a commented starter file.

Discovery

For a given file, arity looks for arity.toml by walking up from the file’s directory through its ancestors, stopping at the first arity.toml it finds or at a directory containing a .git entry (the repository root), whichever comes first.

On the command line:

  • --config <PATH> loads an explicit file and skips discovery.
  • --no-config ignores any discovered file and uses the built-in defaults.

Top-level keys

These apply to both format and lint (the first two govern the shared file walk).

KeyTypeDefaultDescription
excludearray of stringsbuilt-in setgitignore-style patterns to skip, resolved relative to the directory containing arity.toml. Setting it replaces the built-in set (below).
extend-excludearray of strings[]Like exclude, but added to exclude rather than replacing it. Use this to skip extra paths while keeping the built-in defaults.
cachebooleantrueEnable the persistent result cache (currently the format --check already-formatted cache; the cache directory follows [index] cache-dir/$ARITY_CACHE_DIR). The --no-cache CLI flag overrides it per run.

The built-in default exclude set (the default value of exclude; generated or vendored files that should not be reformatted or linted) is:

.git/
renv/
revdep/
cpp11.R
RcppExports.R
extendr-wrappers.R
import-standalone-*.R

Excludes apply only to directory walks. A file named explicitly on the command line is always processed, even if it matches an exclude pattern. Pass --force-exclude (on format and lint) to apply the patterns to explicitly named files too—useful for runners like pre-commit that pass staged files as arguments. The CLI flag --exclude <PATTERN> (on format and lint) adds to the configured exclude/extend-exclude for a single run.

# Keep the built-in defaults and also skip these:
extend-exclude = ["vendor/", "*.gen.R"]

# Or replace the built-in defaults entirely:
# exclude = ["vendor/", "*.gen.R"]

[format]

KeyTypeDefaultDescription
line-widthinteger (1–1000)80The width the formatter tries to keep lines within. Not a hard cap.
indent-widthinteger (1–1000)2Number of spaces per indentation level.
line-endingstring"auto"Newline style: "auto", "lf", "crlf", or "native" (see below).

line-ending = "auto" mirrors the source file’s first line ending (defaulting to lf when the file has none); "native" is crlf on Windows and lf elsewhere; "lf" and "crlf" force that ending.

[format]
line-width = 80
indent-width = 2
line-ending = "auto"

line-width and indent-width can be overridden per run with the --line-width/--indent-width flags on arity format.

[lint]

KeyTypeDefaultDescription
selectarray of stringsunsetIf set, only these rule IDs run.
ignorearray of strings[]Rule IDs to disable (applied on top of select or the default set).

Rule IDs are the kebab-case names from the rule reference. Unknown IDs are reported when linting runs, not when the config is parsed. The --select/--ignore flags on arity lint override these for a single run.

[lint]
select = ["undefined-symbol", "equals-na"]
ignore = ["unused-binding"]

[index]

Controls the R-package symbol index used by the language server (and by namespace-aware lint rules) to resolve names.

KeyTypeDefaultDescription
library-pathsarray of paths[]Explicit R library directories, used when automatic discovery misses.
cache-dirpathunsetOverride the index cache directory (otherwise XDG/$ARITY_CACHE_DIR).
auto-buildbooleantrueLet the language server lazily index referenced-but-unindexed packages.
helpbooleantrueHarvest help titles while indexing. false stores names only (faster).

Note: the downloadable CRAN symbol sidecar is not configured here. Enabling network access is a per-user decision set via the ARITY_REMOTE_URL environment variable, never committed in a shared arity.toml.

Note: the same applies to the attach probe (arity index --attach-probe), which observes what a meta-package attaches by running library() in a fresh R session. Because that executes package attach hooks, it is enabled per run by the flag or per user via the ARITY_ATTACH_PROBE environment variable, never from arity.toml. Without it, attach sets are still captured for packages following the tidyverse core convention, with a built-in table as the offline fallback.

Reserved for future use

The following are not yet implemented but are reserved so the schema can grow without breaking changes (adding a key is always backward-compatible under the strict unknown-key check):

  • [format].indent-style ("space" or "tab")—tab indentation.
  • [format].skip and a # fmt: skip comment—opt specific calls out of formatting.
  • [lint.rules.<id>]—per-rule configuration tables (including per-rule severity).
  • Category names (e.g. "correctness") in select/ignore.

Command-Line Help for arity

Arity: a language server, formatter, and linter for R

Usage: arity [OPTIONS] <COMMAND>

Options

--config <PATH>

Path to an explicit arity.toml (skips discovery)

--no-config

Ignore any discovered arity.toml and use built-in defaults

--color <WHEN>

When to use color in output

Default value: auto

Possible values:

  • auto: Colorize when writing to a terminal and NO_COLOR is unset (default)
  • always: Always colorize
  • never: Never colorize
-q, --quiet

Suppress informational output (errors are still shown)

-v, --verbose

Print extra informational output (e.g. per-command summaries)

arity parse

Parse and display the CST tree for debugging

Usage: arity parse [OPTIONS] [FILE]

Arguments

<FILE>
Input file (stdin if not provided)

Options

--quiet
Suppress CST output to stdout
--verify
Verify parser losslessness (input must equal CST text)

arity format

Format .R files

Usage: arity format [OPTIONS] [PATH]...

Arguments

<PATH>...
Input file(s) or path(s) (stdin if omitted)

Options

--verify
Verify formatting idempotence for supported inputs (does not write files)
--check
Check formatting without writing changes; prints a diff for each file that would be reformatted and exits non-zero if any differ
--line-width <N>
Override the configured line width
--indent-width <N>
Override the configured indent width
--exclude <PATTERN>
Additional gitignore-style exclude patterns (repeatable or comma-separated); augments the configured exclude/extend-exclude
--force-exclude
Apply exclude patterns to files named explicitly on the command line too (they are normally always processed); for runners like pre-commit that pass staged files as arguments
--no-cache
Disable the persistent already-formatted cache (read and write) for this run; only affects --check

arity lint

Lint .R files

Reads stdin when no paths are given. Exit codes: 0 = no findings, 1 = findings (or files blocked by parse errors), 2 = usage/IO error.

Usage: arity lint [OPTIONS] [PATH]...

Arguments

<PATH>...
Input file(s) or path(s) (stdin if omitted)

Options

--stdin-filename <PATH>

Filename to report for stdin input (for diagnostics)

--fix

Apply safe autofixes in place and report what remains

--unsafe-fixes

Also apply fixes that may change behavior (requires –fix)

--select <RULE_ID>

Only run these rules (overrides config select); repeatable or comma-separated

--ignore <RULE_ID>

Disable these rules (overrides config ignore); repeatable or comma-separated

--exclude <PATTERN>

Additional gitignore-style exclude patterns (repeatable or comma-separated); augments the configured exclude/extend-exclude

--force-exclude

Apply exclude patterns to files named explicitly on the command line too (they are normally always processed); for runners like pre-commit that pass staged files as arguments

--output <OUTPUT>

Output format

Default value: pretty

Possible values:

  • pretty: Annotated multi-line snippets (default; matches jarl/rustc-style output)
  • concise: One finding per line (path:line:col: severity [rule] message)
  • json: JSON array of diagnostics, for editor integration

arity index

Build or refresh the installed-package introspection index

Usage: arity index [OPTIONS] [PATH]...

Arguments

<PATH>...
Project path(s) to scan for referenced packages (default: “.”)

Options

--force
Re-harvest even when the installed version is already indexed
--no-help
Skip harvesting help (names only; faster)
--attach-probe
Probe what each meta-package attaches by running library() in a fresh R session (spawns R and executes package attach hooks; also enabled by setting ARITY_ATTACH_PROBE)
--cache-dir <DIR>
Override the cache directory
--quiet
Suppress per-package progress output

arity lsp

Run the language server over stdio

Usage: arity lsp

arity completions

Generate a shell completion script (write it to stdout)

Usage: arity completions <SHELL>

Arguments

<SHELL>

Shell to generate completions for

Possible values: bash, elvish, fish, powershell, zsh

arity init

Write a starter arity.toml to the current directory

Usage: arity init [OPTIONS]

Options

--force
Overwrite an existing arity.toml

Benchmarks

Wall-clock speed of arity against other R tooling, measured with hyperfine. Two operations are covered:

  • the formatter, compared against air (with styler available opt-in);
  • the linter, compared against jarl.

Each operation is measured at two scopes: single files (synthetic corpus tiers) and a whole project (a real R package). arity is the baseline in every chart, and every other tool’s time is reported relative to it.

This is not a CI gate and not a parity target. Timings are machine- and run-dependent, and these numbers measure speed only, never output or finding equivalence (see AIR_COMPAT.md or task air-compat for formatter output comparison). The tools also pay very different startup floors: styler runs inside an R process, so a large part of its time on small inputs is interpreter startup rather than real work. Treat the ratios, not the absolute milliseconds, as the takeaway.

The figures below are regenerated manually with task bench and committed as a machine-readable artifact (benches/benchmark_results.json); they are never re-measured when this site is built or in CI.

How it is measured

For single files, each tool is invoked as a user would pipe (formatters) or point it at a file (linters):

ToolInvocation
arityarity format / arity lint FILE
airair format --stdin-file-path bench.R
stylerRscript -e 'styler::style_text(readLines(file("stdin")))'
jarljarl check FILE

For projects, each tool walks the package’s R/ source tree in one invocation. Formatters run in check mode so nothing is mutated, but the full formatting work is still done:

ToolInvocation
arityarity format --check R/ / arity lint R/
airair format --check R/
jarljarl check R/

arity is the baseline; every other tool’s time is reported relative to it. Comparison tools absent from the machine are skipped, so a run without jarl simply omits the linter comparison. The timing backend prefers hyperfine (warmup plus stddev/min/max); without hyperfine and jq it falls back to a mean-only shell loop and the min/max columns become blank.

styler is an R package: it pays an interpreter startup floor plus a steep per-line cost, so it is not measured by default and only ever appears on the formatter single-file tiers (never on projects, where style_dir would mutate the checkout). Opt in with ARITY_BENCH_STYLER=1 task bench; even then it is skipped on tiers too large to format in reasonable time. Because the tools do such different work, this is a rough scale comparison, not a like-for-like one.

Corpus

Single files are synthetic: every formatter fixture’s expected.R (crates/arity-formatter/tests/fixtures/formatter/*/expected.R) is concatenated (sorted, blank-line separated) into a base block, which is repeated to two size tiers. The content repeats, so it is cache-friendly and not fully representative of real code; it exists to amortize process startup and show rough scaling, not to model a real workload.

Projects use a real R package (the tidyr source tree by default), cloned once at a pinned tag into a local cache. Point the benchmark at your own checkout with ARITY_BENCH_PROJECT=/path/to/pkg task bench; only its R/ directory is measured.

Setup

  • arity: 0.11.0
  • air: 0.10.0
  • jarl: 0.5.0
  • backend: hyperfine (min runs: 3)
  • host: linux/x86_64, Intel(R) Core(TM) Ultra 7 155U
  • generated: 2026-07-11T02:55:45Z

Results

Each operation gets its own section below, split into single files and a whole project. The arity baseline sits on the dashed line at 1 in every chart; faster tools fall below it, slower tools rise above.

Formatter

Single files

Formatting speed on single files relative to arity, one dot per synthetic corpus tier. The vertical axis is mean wall-clock time as a ratio to arity on a log scale, so arity lies on the dashed baseline at 1; faster tools fall below it and slower tools rise above. Hover a dot for the exact figures.
Data table
small (123094 bytes, 8498 lines)
ToolMean (ms)Min (ms)Max (ms)Relative
arity26.346420.550736.0864baseline
air35.968029.441847.86331.4x slower
large (1477128 bytes, 101976 lines)
ToolMean (ms)Min (ms)Max (ms)Relative
arity791.3714773.7293810.6469baseline
air449.5672387.3011588.70861.8x faster

Projects

Formatting speed on a real R package (the tidyr source tree) relative to arity, on the same log-ratio axis.
Data table
tidyr (245685 bytes, 8774 lines)
ToolMean (ms)Min (ms)Max (ms)Relative
arity39.409732.705249.4364baseline
air56.743448.206570.02631.4x slower

Linter

Single files

Linting speed on single files relative to arity, one dot per synthetic corpus tier, on the same log-ratio axis as the formatter charts.
Data table
small (123094 bytes, 8498 lines)
ToolMean (ms)Min (ms)Max (ms)Relative
arity37.600630.257944.9339baseline
jarl44.502732.822055.50761.2x slower
large (1477128 bytes, 101976 lines)
ToolMean (ms)Min (ms)Max (ms)Relative
arity385.7874307.3075435.3728baseline
jarl673.2606628.6883760.40761.7x slower

Projects

Linting speed on a real R package (the tidyr source tree) relative to arity, on the same log-ratio axis.
Data table
tidyr (245685 bytes, 8774 lines)
ToolMean (ms)Min (ms)Max (ms)Relative
arity34.414528.200543.7791baseline
jarl23.137417.657230.67171.5x faster

Lint rules

Each rule’s reference page is generated from the rule’s own metadata by running the linter on worked examples. Regenerate with cargo run --example docgen.

Correctness

Suspicious

Readability

Performance

Documentation

undefined-symbol

Flag an identifier read that resolves to no in-scope binding and no known package export.

Gated for safety: the rule stays silent for a whole file unless every library()-attached package is indexed, since an un-indexed package could export the otherwise-unresolved name.

This rule is enabled by default.

subtotal resolves to nothing:

total <- subtotal
warning: undefined-symbol
 --> example.R:1:10
  |
1 | total <- subtotal
  |          ^^^^^^^^ no in-scope binding or attached package exports `subtotal`

unused-binding

Flag a local binding that is never read in the same file. Function parameters, for-loop variables, and names beginning with . are exempt, since those are meaningful even when unused.

This rule is enabled by default.

x is assigned but never used:

x <- 1
y <- 2
print(y)
warning: unused-binding
 --> example.R:1:1
  |
1 | x <- 1
  | ^ local binding `x` is assigned but never read
  = help: Remove the assignment, or prefix the name with `.` to mark it intentional.

duplicate-formal

Flag a function defined with two parameters of the same name. R raises a runtime error (repeated formal argument); this catches it statically.

This rule is enabled by default.

Two parameters named x:

f <- function(x, x) x
error: duplicate-formal
 --> example.R:1:18
  |
1 | f <- function(x, x) x
  |                  ^ parameter `x` is declared more than once in this function
  = help: Rename one of the parameters.

duplicated-arguments

Flag a call that supplies the same argument name more than once (f(a = 1, a = 2)). The call-side sibling of duplicate-formal; reported as a warning with no autofix, since it isn’t always a runtime error.

This rule is enabled by default.

The argument a is supplied twice:

list(a = 1, a = 2)
warning: duplicated-arguments
 --> example.R:1:13
  |
1 | list(a = 1, a = 2)
  |             ^ argument `a` is supplied more than once in this call
  = help: Remove or rename the duplicate argument.

equals-na

Flag x == NA, which is always NA rather than TRUE/FALSE—almost always a mistake for is.na(x), which is the autofix.

This rule is enabled by default.

Comparing to NA with ==:

x == NA
warning: equals-na
 --> example.R:1:1
  |
1 | x == NA
  | ^^^^^^^ comparison with `NA` is always `NA`; use `is.na()`
  = help: Use `is.na(x)`.

After applying the fix:

is.na(x)

vector-logic

Flag the vectorized &/| used directly in an if/while condition, where the scalar &&/|| is meant.

A condition needs a single TRUE/FALSE: R only looks at the first element (a length > 1 condition is an error since R 4.2), and &&/|| short-circuit. The fix doubles the operator. Operators inside a function call (if (any(a | b))) are left alone—a vector result is the point there.

This rule is enabled by default.

Vectorized & in an if condition:

if (a & b) {
  go()
}
warning: vector-logic
 --> example.R:1:7
  |
1 | if (a & b) {
  |       ^ `&` in a condition; use `&&`
  = help: Use the scalar `&&` in an `if`/`while` condition.

After applying the fix:

if (a && b) {
  go()
}

unreachable-code

Flag statements that follow an unconditional return() or stop() in a block—once either runs, nothing after it in the same block can be reached, so the trailing code is dead. A direct-statement if/else that exits in both branches likewise leaves its tail unreachable (a control-flow-graph verdict).

The rule fires only when the terminator is a direct statement of the block (a lone return()/stop() guarded by an if leaves the tail reachable) and only when the callee resolves to base R; a local redefinition is left alone. return is additionally required to sit inside a function. The deletion fix is unsafe, and withheld when it would drop a comment.

This rule is enabled by default.

A statement after return() can never run:

f <- function() {
  return(1)
  2
}
warning: unreachable-code
 --> example.R:3:3
  |
3 |   2
  |   ^ code after `return()` can never be reached
  = help: Remove the unreachable code, or fix the control flow.

An if/else that exits in both branches leaves its tail dead:

f <- function() {
  if (x) return(1) else return(2)
  3
}
warning: unreachable-code
 --> example.R:3:3
  |
3 |   3
  |   ^ code after this `if` can never be reached (both branches exit)
  = help: Remove the unreachable code, or fix the control flow.

is-numeric

Flag is.numeric(x) || is.integer(x) (and the vectorized | spelling), which is just is.numeric(x): is.numeric() already returns TRUE for integer vectors, so the disjunction adds nothing and suggests a misreading of what is.numeric() tests.

The rule fires only when both operands are single-argument calls on the same argument and both callees resolve to base R; a local redefinition of either is left alone.

This rule is enabled by default.

Testing for a numeric vector:

if (is.numeric(x) || is.integer(x)) mean(x)
warning: is-numeric
 --> example.R:1:5
  |
1 | if (is.numeric(x) || is.integer(x)) mean(x)
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `is.numeric(x) || is.integer(x)` is redundant—`is.numeric()` is already `TRUE` for integer vectors
  = help: Use `is.numeric(x)`.

After applying the fix:

if (is.numeric(x)) mean(x)

if-always-true

Flag an if whose condition is the literal TRUE or FALSE. The branch is decided statically, so the if is dead control flow. Only the bare literals are flagged—not folded constants (if (1 == 1)) or the rebindable symbols T/F.

This rule is enabled by default.

An if gated on a constant always takes the same branch:

if (TRUE) {
  f()
} else {
  g()
}
warning: if-always-true
 --> example.R:1:5
  |
1 | if (TRUE) {
  |     ^^^^ `if` condition is always `TRUE`
  = help: The condition always holds; the branch always runs.

empty-assignment

Flag an assignment whose value is an empty block (x <- {}). An empty block evaluates to NULL, so this is a roundabout x <- NULL—usually a leftover from deleting the block’s body. An empty function body or if branch is not flagged.

This rule is enabled by default.

Assigning an empty block is the same as assigning NULL:

x <- {}
warning: empty-assignment
 --> example.R:1:6
  |
1 | x <- {}
  |      ^^ assigning an empty block `{}` is the same as assigning `NULL`
  = help: Assign `NULL` or a meaningful value instead.

assignment-in-condition

Flag an assignment (<-, =, <<-, :=) used as the direct condition of an if/while. The bare = form (often a == typo) is autofixed to ==; the others are reported without a fix.

This rule is enabled by default.

= where == was meant:

if (x = 5) print(x)
warning: assignment-in-condition
 --> example.R:1:5
  |
1 | if (x = 5) print(x)
  |     ^^^^^ assignment used as a condition; did you mean `==`?
  = help: Replace `=` with `==` or move the assignment out.

After applying the fix:

if (x == 5) print(x)

implicit-assignment

Flag an assignment (<-, =, <<-, ->, ->>) nested inside a call or subscript argument, e.g. mean(x <- 1:10). The binding runs as a side effect of the argument and is easy to miss; assign on its own line instead. The if/while condition case is covered by assignment-in-condition, and the data.table / rlang walrus (:=) is left alone.

This rule is disabled by default; enable it with select.

An assignment hidden inside a call argument:

mean(x <- 1:10)
warning: implicit-assignment
 --> example.R:1:6
  |
1 | mean(x <- 1:10)
  |      ^^^^^^^^^ assignment nested in a call argument; assign on its own line
  = help: Move the assignment to its own statement.

browser

Flag a leftover browser() call. browser() opens R’s interactive debugger and is meant to be removed before code is committed; in a non-interactive session it silently does nothing, so it lingers unnoticed.

Only a call that resolves to base R’s browser is flagged—a same-named user function is left alone. The safe-delete fix is offered only for a browser() that is a direct statement (of a block or the file top level); in any other position it is withheld so the edit can’t break syntax.

This rule is enabled by default.

A browser() call left in after debugging:

f <- function(x) {
  browser()
  x + 1
}
warning: browser
 --> example.R:2:3
  |
2 |   browser()
  |   ^^^^^^^^^ leftover `browser()` debugging call
  = help: Remove the `browser()` call.

After applying the fix:

f <- function(x) {
  x + 1
}

shadowed-builtin

Flag a local binding to a function whose name is exported by a default R package when that name is later called in the same scope (c <- function(...) ...; c(2, 3)). A value binding (names <- names(x)) is exempt: R’s call-position lookup skips non-function locals, so it is not a hazard.

This rule is enabled by default.

Binding a function over base c() and then calling it:

c <- function(x, y) x
c(2, 3)
warning: shadowed-builtin
 --> example.R:1:1
  |
1 | c <- function(x, y) x
  | ^ local binding `c` shadows a base-R name later used in this scope
  = help: Rename the local, or fully qualify the base call (e.g. `base::c`).

redundant-equals

Flag comparison to a logical literal: x == TRUE is just x, and x == FALSE is !x.

This rule is enabled by default.

Comparing to TRUE:

if (ready == TRUE) go()
warning: redundant-equals
 --> example.R:1:5
  |
1 | if (ready == TRUE) go()
  |     ^^^^^^^^^^^^^ comparison with a logical literal is redundant
  = help: Use the expression directly, or negate it.

After applying the fix:

if (ready) go()

redundant-ifelse

Flag ifelse(c, TRUE, FALSE) (which is just c) and ifelse(c, FALSE, TRUE) (which is !c).

This rule is enabled by default.

An ifelse that returns its own condition:

flag <- ifelse(cond, TRUE, FALSE)
warning: redundant-ifelse
 --> example.R:1:9
  |
1 | flag <- ifelse(cond, TRUE, FALSE)
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^ `ifelse()` returning `TRUE`/`FALSE` is redundant
  = help: Use the condition directly, or negate it.

After applying the fix:

flag <- cond

repeat

Flag while (TRUE), an unconditional loop better written as repeat.

repeat states the intent—loop until a break/return—without the dummy TRUE condition. Only the reserved literal TRUE is matched; the rebindable T is left to true-false-symbol.

This rule is enabled by default.

An unconditional while loop:

while (TRUE) {
  poll()
}
warning: repeat
 --> example.R:1:1
  |
1 | while (TRUE) {
  | ^^^^^^^^^^^^ `while (TRUE)` is an unconditional loop; use `repeat`
  = help: Write `repeat` for a loop with no exit condition.

After applying the fix:

repeat {
  poll()
}

true-false-symbol

Prefer the reserved literals TRUE/FALSE over the rebindable base symbols T/F.

T and F are ordinary base-R bindings, not reserved words—T <- FALSE is legal—so relying on them as boolean shorthand is fragile. The fix is withheld when the name resolves to a local binding, since that is the user’s own variable rather than the shorthand.

This rule is enabled by default.

T and F used as boolean shorthand:

x <- T
y <- F
warning: true-false-symbol
 --> example.R:1:6
  |
1 | x <- T
  |      ^ use `TRUE` instead of `T`
  = help: `T`/`F` are rebindable; prefer the reserved literals.
warning: true-false-symbol
 --> example.R:2:6
  |
2 | y <- F
  |      ^ use `FALSE` instead of `F`
  = help: `T`/`F` are rebindable; prefer the reserved literals.

After applying the fix:

x <- TRUE
y <- FALSE

comparison-negation

Flag a negated comparison—!(a == b), !x < y—which reads more clearly as the opposite comparison (a != b, x >= y).

The fix is withheld when a comment in the operand would otherwise be lost.

This rule is enabled by default.

Negating an equality test:

if (!(a == b)) stop()
warning: comparison-negation
 --> example.R:1:5
  |
1 | if (!(a == b)) stop()
  |     ^^^^^^^^^ negated comparison is clearer as the opposite operator
  = help: Flip the comparison instead of negating it.

After applying the fix:

if (a != b) stop()

outer-negation

Flag any(!x)/all(!x), which by De Morgan’s law read more clearly with the negation pulled outside: !all(x) and !any(x).

The rule fires only when every positional argument is negated (a na.rm argument is allowed and preserved). The fix is withheld when the call sits in a context that binds tighter than !, where the rewrite would need parentheses.

This rule is enabled by default.

Negating every element of an aggregation:

if (any(!ok)) stop()
warning: outer-negation
 --> example.R:1:5
  |
1 | if (any(!ok)) stop()
  |     ^^^^^^^^ negating an aggregation is clearer with the negation outside
  = help: `any(!x)` is `!all(x)`; `all(!x)` is `!any(x)`.

After applying the fix:

if (!all(ok)) stop()

string-boundary

Flag grepl("^abc", x) and grepl("abc$", x), single-anchored fixed-string matches that are the clearer startsWith(x, "abc") and endsWith(x, "abc")—they state the prefix/suffix test directly and skip regex compilation.

The rule fires only on the clean shape (two positional arguments, a one-end-anchored plain-literal pattern) and only when grepl resolves to base R. The fix is unsafe: on NA or non-character input startsWith/endsWith diverge from grepl (NA vs FALSE, an error vs coercion).

This rule is enabled by default.

Anchored fixed-string matches:

grepl("^abc", x)
warning: string-boundary
 --> example.R:1:1
  |
1 | grepl("^abc", x)
  | ^^^^^^^^^^^^^^^^ single-anchor `grepl()` is the clearer `startsWith()`
  = help: Use `startsWith()`.

unnecessary-nesting

Flag an if whose entire body is a second if—the two could be a single if with the conditions joined by &&, dropping a needless level of nesting. It fires only when neither if has an else (an else on either side changes what runs) and the inner if is the sole statement of the outer one.

The fix joins the conditions with &&, parenthesizing each non-primary condition so the grouping is preserved. It is unsafe (collapsing dedents the body, so a reformat may follow) and withheld when it would drop a comment.

This rule is enabled by default.

An if whose only body is another if can be a single if:

if (a) {
  if (b) {
    do_thing()
  }
}
warning: unnecessary-nesting
 --> example.R:2:3
  |
2 |   if (b) {
  |   ^^ this `if` is nested in another `if` with no `else`
  = help: Combine the two conditions with `&&` to drop a level of nesting.

any-is-na

Flag any(is.na(x)), which is the purpose-built anyNA(x)—faster (it short-circuits and builds no intermediate logical vector) and clearer.

The rule fires only on the clean single-argument shape and only when both any and is.na resolve to base R; a local redefinition of either is left alone.

This rule is enabled by default.

Testing for any missing value:

if (any(is.na(x))) stop()
warning: any-is-na
 --> example.R:1:5
  |
1 | if (any(is.na(x))) stop()
  |     ^^^^^^^^^^^^^ `any(is.na(x))` is the faster, clearer `anyNA(x)`
  = help: Use `anyNA(x)`.

After applying the fix:

if (anyNA(x)) stop()

any-duplicated

Flag any(duplicated(x)), which is the purpose-built anyDuplicated(x) > 0—faster (it short-circuits and builds no intermediate logical vector) and clearer.

The rule fires only on the clean single-argument shape and only when both any and duplicated resolve to base R; a local redefinition of either is left alone. Because the replacement is a comparison, the fix is withheld in a context that binds tighter than a comparison, where the bare rewrite would need parentheses.

This rule is enabled by default.

Testing for any duplicate value:

if (any(duplicated(x))) stop()
warning: any-duplicated
 --> example.R:1:5
  |
1 | if (any(duplicated(x))) stop()
  |     ^^^^^^^^^^^^^^^^^^ `any(duplicated(x))` is the faster, clearer `anyDuplicated(x) > 0`
  = help: Use `anyDuplicated(x) > 0`.

After applying the fix:

if (anyDuplicated(x) > 0) stop()

coalesce

Flag if (is.null(x)) y else x (and its mirror if (!is.null(x)) x else y), which is the null-coalescing x %||% y—shorter, and it evaluates x once instead of twice.

The rule fires only when is.null resolves to base R; a local redefinition is left alone. The fix is unsafe: %||% needs R >= 4.4 (or rlang), and collapsing the two evaluations of x changes behavior when x has side effects.

This rule is enabled by default.

Falling back to a default when a value is NULL:

y <- if (is.null(x)) default else x
warning: coalesce
 --> example.R:1:6
  |
1 | y <- if (is.null(x)) default else x
  |      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `if (is.null(x)) y else x` is the null-coalescing `x %||% y`
  = help: Use `x %||% y`.

crossprod

Flag t(x) %*% y and x %*% t(y), which are the purpose-built crossprod(x, y) and tcrossprod(x, y)—faster (they go straight to BLAS and never materialize the transpose that t() builds) and clearer.

The rule fires only when one operand is a single-argument t() call and that t resolves to base R; a local redefinition is left alone. When both operands are the same symbol the single-argument form (crossprod(x)) is used.

This rule is enabled by default.

Transposed matrix products:

a <- t(x) %*% y
b <- x %*% t(y)
warning: crossprod
 --> example.R:1:6
  |
1 | a <- t(x) %*% y
  |      ^^^^^^^^^^ `t(x) %*% y` is the faster, clearer `crossprod(x, y)`
  = help: Use `crossprod(x, y)`.
warning: crossprod
 --> example.R:2:6
  |
2 | b <- x %*% t(y)
  |      ^^^^^^^^^^ `x %*% t(y)` is the faster, clearer `tcrossprod(x, y)`
  = help: Use `tcrossprod(x, y)`.

After applying the fix:

a <- crossprod(x, y)
b <- tcrossprod(x, y)

lengths

Flag sapply(x, length), which is the purpose-built lengths(x)—faster (a single C pass instead of an R call per element) and clearer. Both return an integer vector and keep x’s names by default.

The rule fires only on the clean two-positional-argument shape and only when sapply resolves to base R and length is not locally rebound; a redefinition of either is left alone.

This rule is enabled by default.

Per-element lengths of a list:

n <- sapply(x, length)
warning: lengths
 --> example.R:1:6
  |
1 | n <- sapply(x, length)
  |      ^^^^^^^^^^^^^^^^^ `sapply(x, length)` is the faster, clearer `lengths(x)`
  = help: Use `lengths(x)`.

After applying the fix:

n <- lengths(x)

nzchar

Flag comparisons of nchar(x) against zero—nchar(x) > 0, nchar(x) >= 1, nchar(x) != 0, and their mirrored and negated spellings—which are the purpose-built nzchar(x) (or !nzchar(x) for the empty test): faster, and it states the intent directly.

The rule fires only on the clean single-argument shape and only when nchar resolves to base R; a local redefinition is left alone. The fix is unsafe: on NA_character_ input nzchar yields TRUE where the nchar comparison yields NA (exact equivalence would need nzchar(x, keepNA = TRUE)).

This rule is enabled by default.

Testing for non-empty strings:

keep <- x[nchar(x) > 0]
warning: nzchar
 --> example.R:1:11
  |
1 | keep <- x[nchar(x) > 0]
  |           ^^^^^^^^^^^^ comparing `nchar()` to zero is the faster, clearer `nzchar()`
  = help: Use `nzchar(x)` for non-empty, `!nzchar(x)` for empty.

seq

Flag colon ranges from 1 up to a length—1:length(x), 1:nrow(x), or 1:n—which silently count down when that length is zero (1:0 is c(1L, 0L), not an empty sequence), a classic off-by-one bug for loops over possibly-empty input. seq_along(x) and seq_len(n) return a zero-length sequence instead, and agree with the colon form everywhere else.

The length/nrow/ncol/NROW/NCOL forms fire only when the callee resolves to base R; a redefinition is left alone. Literal ranges (1:10) and computed bounds (1:(n - 1)) are not flagged.

This rule is enabled by default.

Ranges over a vector’s indices and up to a count:

for (i in 1:length(x)) print(x[i])
for (j in 1:n) f(j)
warning: seq
 --> example.R:1:11
  |
1 | for (i in 1:length(x)) print(x[i])
  |           ^^^^^^^^^^^ `1:length(x)` counts down (`1:0`) when the length is zero; `seq_along(x)` handles empty input
  = help: Use `seq_along(x)`.
warning: seq
 --> example.R:2:11
  |
2 | for (j in 1:n) f(j)
  |           ^^^ `1:n` counts down (`1:0`) when the length is zero; `seq_len(n)` handles empty input
  = help: Use `seq_len(n)`.

After applying the fix:

for (i in seq_along(x)) print(x[i])
for (j in seq_len(n)) f(j)

class-equals

Flag comparisons of class(x) against a string literal—class(x) == "cls", class(x) != "cls", and the %in% membership spellings—which are inherits(x, "cls") (or its negation): class() returns the whole class vector, so the comparison is elementwise (an error in an if () condition on a multi-class object) and misses subclasses, while inherits() asks the intended question directly without materializing the vector.

The rule fires only on the clean single-argument shape and only when class resolves to base R; a local redefinition is left alone. The fix is unsafe: on a multi-class object the comparison yields an elementwise vector where inherits() yields a scalar, and for S4 objects inherits() follows the formal inheritance chain.

This rule is enabled by default.

Testing an object’s class:

if (class(x) == "factor") levels(x)
warning: class-equals
 --> example.R:1:5
  |
1 | if (class(x) == "factor") levels(x)
  |     ^^^^^^^^^^^^^^^^^^^^ comparing `class(x)` to a string is fragile—`class()` returns a vector; `inherits()` asks directly
  = help: Use `inherits(x, "cls")` (or `!inherits(x, "cls")`).

fixed-regex

Flag a base-R regex call (grepl, grep, sub, gsub, regexpr, gregexpr, regexec) whose pattern is a plain string literal with no regex metacharacter, and add fixed = TRUE—it skips regex compilation and states that the pattern is a literal.

The rule fires only when the callee resolves to base R and no fixed/ignore.case/perl argument is already present. Because a metacharacter-free pattern matches identically either way, the fix (inserting , fixed = TRUE) is safe.

This rule is enabled by default.

A literal pattern matched as a regex:

grepl("abc", x)
warning: fixed-regex
 --> example.R:1:7
  |
1 | grepl("abc", x)
  |       ^^^^^ `grepl()` with a literal pattern should use `fixed = TRUE`
  = help: Add `fixed = TRUE`.

After applying the fix:

grepl("abc", x, fixed = TRUE)

sort

Flag sort(x)[1], which sorts the whole vector just to read one extreme — the purpose-built min(x) (or max(x) for decreasing = TRUE) finds it in a single pass and states the intent directly.

The rule fires only on the clean shape — a [1] subset of a sort call with one positional argument and at most a literal decreasing flag — and only when sort resolves to base R; a local redefinition is left alone. The fix is unsafe: sort drops NAs by default while min/max propagate them (exact equivalence would need na.rm = TRUE), and on an empty vector sort(x)[1] is NA while min(x) warns and yields Inf.

This rule is enabled by default.

Reading one extreme off a full sort:

smallest <- sort(x)[1]
largest <- sort(x, decreasing = TRUE)[1]
warning: sort
 --> example.R:1:13
  |
1 | smallest <- sort(x)[1]
  |             ^^^^^^^^^^ `sort(x)[1]` sorts everything to read one extreme — use `min(x)`
  = help: Use `min(x)`.
warning: sort
 --> example.R:2:12
  |
2 | largest <- sort(x, decreasing = TRUE)[1]
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `sort(x)[1]` sorts everything to read one extreme — use `max(x)`
  = help: Use `max(x)`.

roxygen-unknown-tag

Flag roxygen tags that roxygen2 does not understand.

roxygen2 warns on an unknown tag and drops it from the generated .Rd, so a misspelled tag (@exprot, @parma) silently loses documentation—or worse, an intended @export never reaches the NAMESPACE. Custom tags from extension roclets can be suppressed with # arity-ignore roxygen-unknown-tag.

This rule is enabled by default.

A misspelled @export:

#' Add one
#' @exprot
add_one <- function(x) x + 1
warning: roxygen-unknown-tag
 --> example.R:2:4
  |
2 | #' @exprot
  |    ^^^^^^^ `@exprot` is not a tag roxygen2 understands
  = help: Check the spelling against the roxygen2 tag index; suppress with `# arity-ignore roxygen-unknown-tag` for extension-roclet tags.

roxygen-title

Flag a documented function whose roxygen block has no title.

The first untagged paragraph (or an explicit @title) becomes the topic title; without one, roxygen2 warns and R CMD check rejects the generated .Rd. An @export with no documentation at all is flagged too—R CMD check reports it as an undocumented export. Blocks that merge into or inherit another topic (@rdname, @describeIn, @inherit*, @template) and @noRd blocks are skipped.

This rule is enabled by default.

A documented, exported function with no title paragraph:

#' @param x A number.
#' @export
add_one <- function(x) x + 1
warning: roxygen-title
 --> example.R:1:1
  |
1 | #' @param x A number.
  | ^^ documentation block has no title
  = help: Add a leading prose line (the first paragraph becomes the title) or an explicit `@title`.

roxygen-return

Flag an @exported function documented without @return.

CRAN requires every exported function’s documentation to describe its return value (the .Rd \value section); roxygen2 itself stays silent, so the omission otherwise surfaces only at submission time. @returns is accepted as an alias. @noRd blocks and merged or inherited topics (@rdname, @inherit, …) are skipped.

This rule is enabled by default.

An exported function with no @return:

#' Add one
#' @param x A number.
#' @export
add_one <- function(x) x + 1
warning: roxygen-return
 --> example.R:3:4
  |
3 | #' @export
  |    ^^^^^^^ exported function is documented without `@return`
  = help: Add `@return` (or `@returns`) describing the value.

roxygen-param

Flag @param documentation that does not match the documented function.

Four shapes are reported: a formal argument with no @param, a @param naming a nonexistent formal (often a rename that never reached the docs), a name documented twice, and a @param missing its name or description. Blocks that inherit or merge documentation (@inheritParams, @rdname, @describeIn, @template) are exempt from the coverage checks; duplicates are always reported.

This rule is enabled by default.

y is undocumented and @param z matches nothing:

#' Add two numbers
#' @param x The first number.
#' @param z The other one.
#' @export
add <- function(x, y) x + y
warning: roxygen-param
 --> example.R:3:11
  |
3 | #' @param z The other one.
  |           ^ `@param z` does not match a formal argument of the documented function
  = help: Rename it to a formal argument or remove it.
warning: roxygen-param
 --> example.R:5:20
  |
5 | add <- function(x, y) x + y
  |                    ^ formal argument `y` is not documented with `@param`
  = help: Add `@param` for it (or `@inheritParams` a function that documents it).

roxygen-examples

Flag @examples code that does not parse.

R CMD check runs example code, so a syntax error in an @examples section (or an @examplesIf condition) fails the package at check time. The embedded code is reparsed with arity’s own parser and the first syntax error of each snippet is reported at its exact location in the comment. Rd wrappers like \dontrun{} are understood and their contents still checked.

This rule is enabled by default.

An unclosed call in the example:

#' Add one
#' @examples
#' add_one(1
#' @export
add_one <- function(x) x + 1
warning: roxygen-examples
 --> example.R:3:11
  |
3 | #' add_one(1
  |           ^ example code does not parse: expected ')' to close function call
  = help: `R CMD check` runs example code; fix the syntax error.