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.

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.6.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 ranges, and call 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 govern file discovery for both format and lint, which share one file walk.

KeyTypeDefaultDescription
excludearray of strings[]Additional gitignore-style patterns to skip, resolved relative to the directory containing arity.toml.
default-excludebooleantrueWhether to also apply the built-in default exclude set (below).

The built-in default exclude set (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. The CLI flag --exclude <PATTERN> (on format and lint) adds to the configured exclude for a single run.

exclude = ["vendor/", "*.gen.R"]
default-exclude = true

[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.

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

This document contains the help content for the arity command-line program.

arity

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

Usage: arity [OPTIONS] <COMMAND>

Subcommands:
  • parse — Parse and display the CST tree for debugging
  • format — Format .R files
  • lint — Lint .R files
  • index — Build or refresh the installed-package introspection index
  • lsp — Run the language server over stdio
  • completions — Generate a shell completion script (write it to stdout)
  • init — Write a starter arity.toml to the current directory
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

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

  • --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)
  • --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

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

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.

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.

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.

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.

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.

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.

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.

The rule fires only when the terminator is a direct statement of the block (a 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.

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.

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.

= 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)

shadowed-builtin

Flag a local binding whose name is exported by a default R package when that name is later used as a call in the same scope (c <- 1; c(2, 3)). The two-step trigger keeps false positives down.

Shadowing base c() and then calling it:

c <- 1
c(2, 3)
warning: shadowed-builtin
 --> example.R:1:1
  |
1 | c <- 1
  | ^ 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.

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).

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.

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.

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.

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.

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()

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.

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.

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()