rguides

Code Quality with lintr and styler

Writing clean, consistent code is essential for any R project, especially when building packages for others to use. High code quality reduces bugs, speeds up code review, and makes collaboration smoother. Two packages from the R ecosystem help you achieve this: lintr for static code analysis and styler for automatic code formatting.

Installing lintr and styler

Both packages are available on CRAN and can be installed with a simple call to install.packages(). You can also install the development versions from GitHub if you need the latest features.

# Install lintr from CRAN
install.packages("lintr")

# Or install the development version from GitHub
remotes::install_github("r-lib/lintr")

# Install styler from CRAN
install.packages("styler")

# Or install the development version from GitHub
remotes::install_github("r-lib/styler")

lintr requires R version 3.5.0 or later, while styler requires R 3.6.0 or later. Most modern R installations will satisfy these requirements.

Basic linting with lint()

The lint() function analyzes a single R file and reports any style violations or potential issues. This is the simplest way to check your code for problems.

library(lintr)

# Lint a single file
lint("R/utils.R")

When you run this, lintr parses your code and returns a list of lint objects describing each issue. Each lint includes the file path, line number, column number, and a description of the problem. Lintr checks for a wide range of issues: code style violations, potential bugs like unused variables, and best-practice recommendations such as avoiding sapply() when vapply() is safer.

For example, if your code has lines longer than 80 characters, you might see output like this:

# Example output for a file with long lines
lint("R/utils.R")
# <text:3> line too long (85 > 80): x <- "This is a very long line that exceeds the recommended maximum length of eighty characters"
# <text:10> missing spacing around operator: x<-1

You can also lint code stored as a character vector using lint_text():

code <- "x<-function(a){a+1}"
lint(text = code)

This is useful for quickly testing snippets without creating files. When you have a code fragment and want to check for issues before committing, lint_text() provides immediate feedback without the overhead of saving a temporary file. It accepts any character vector, so you can pass code from the clipboard, a GitHub gist, or an interactive R session.

Linting entire packages

When developing an R package, you’ll want to check all source files at once. The lint_package() function lints all R files in standard package locations: R/, tests/, inst/, vignettes/, and data-raw/.

library(lintr)

# Lint the current package
lint_package(".")

# Lint a specific package directory
lint_package("path/to/your/package")

You can also use lint_dir() to lint all R files in any directory, not just packages:

# Lint all R files in a directory recursively
lint_dir("R/", recursive = TRUE)

The lint_dir() function accepts a pattern argument to filter files. The default pattern "\\.[Rr]$" matches files ending in .R or .r.

Basic styling with style_text()

The styler package formats R code automatically. The style_text() function takes a character vector of code and returns a styled version.

library(styler)

# Style a simple expression
code <- "x<-function(a,b){a+b}"
style_text(code)
# [1] "x <- function(a, b) {\n  a + b\n}"

Notice how styler added spaces around the assignment operator and between function arguments. The output follows the tidyverse style guide by default. Every transformation that styler applies — from spacing to indentation to line breaks — corresponds to a specific rule in that guide. Understanding what styler changes helps you trust the automated formatting and learn which manual habits to adjust.

You can control the styling behavior with several parameters. The strict argument determines how strictly rules are applied:

# Strict mode (default) - applies all rules
style_text("x   <-   1", strict = TRUE)
# [1] "x <- 1"

# Non-strict mode - allows some flexibility
style_text("x   <-   1", strict = FALSE)
# [1] "x <- 1"

The scope parameter controls how invasive the transformations are. Options include "spaces", "indentation", "line_breaks", and "tokens". Higher scopes include all transformations from lower scopes.

Styling entire packages

Similar to lintr, styler can process entire packages with style_pkg(). This styles all R source files in a package while respecting the package structure.

library(styler)

# Style the current package
style_pkg(".")

# Style without modifying files (preview only)
style_pkg(".", dry = "on")

The dry parameter is particularly useful. Setting it to "on" shows you what would change without actually modifying your files. This lets you review the transformations before committing them to version control. After reviewing the dry run output, switch to dry = "off" (the default) to apply the changes. This two-step workflow prevents surprises when styler reformats code you did not expect it to touch.

For non-package directories, use style_dir():

# Style all R files in a directory
style_dir("R/", recursive = TRUE)

Configuration via .lintr file

lintr reads configuration from a .lintr file in your project root. This file uses the Debian Control Format (DCF) and lets you customize which linters to use and which files to exclude.

Create a .lintr file in your project root:

linters: linters_with_defaults(
  line_length_linter(120),
  object_name_linter(styles = c("snake_case", "symbols")),
  commented_code_linter = NULL
)

exclusions: list(
  "inst/example/bad.R",
  "tests/testthat/exclusions-test" = 1
)

encoding: UTF-8

This configuration sets the maximum line length to 120 characters, configures object naming to allow snake_case and symbols, disables the commented code linter, and excludes specific files or lines from linting.

To exclude specific lines within a file, add a comment with # nolint:

# This line is excluded from linting # nolint
x <- 1  # nolint: line_length_linter

For specific linters, use the format # nolint:linter_name:

x<-1  # nolint:assignment_linter

You can also configure lintr programmatically using options():

options(lintr.linter_file = ".lintr")
options(lintr.cache_directory = "~/.cache/R/lintr")

The style_dir() function applies the same formatting rules as style_pkg() but to any directory, making it useful for scripts, analysis projects, and non-package codebases. The recursive = TRUE option traverses subdirectories so you can format an entire project tree in one command. Both style_pkg() and style_dir() respect .Rbuildignore and .gitignore patterns when deciding which files to process.

lintr uses a configuration file .lintr in the project root to customize which rules apply. Common customizations: increasing the line length limit (line_length_linter(120) instead of the default 80), disabling specific linters for files that follow a different convention, and adding custom linters for project-specific rules. usethis::use_lintr() creates a default .lintr file.

lint_package() lints all R files in a package and returns a data frame of issues. In CI/CD, lint_package() with a non-zero exit code on errors enforces linting as a build gate. GitHub Actions integration: r-lib/actions/lint-package runs lintr on every pull request and posts comments on lines with violations.

styler and automated formatting

styler::style_pkg() reformats all R source files in a package to match the tidyverse style guide. The formatter handles: consistent spacing around operators, consistent indentation (2 spaces), and consistent comma placement. Unlike lintr, which only reports issues, styler modifies files in place.

For teams adopting styler, the initial run produces a large diff. Commit the reformatting as a separate commit to make subsequent diffs easier to read. After the initial formatting pass, styler should produce no changes if every developer runs it before committing — enforce this with a pre-commit hook or CI check.

The air tool (an R equivalent to Python’s Black) provides opinionated, zero-config formatting with no style choices to debate. It is the most aggressive auto-formatter and produces the most consistent results across different contributors.

Common pitfalls to avoid

Exclusion syntax errors: When excluding lint warnings, use # nolint not # nolint:. For specific linters, the format is # nolint:linter_name with an underscore, not a colon after “nolint”.

Cache corruption: If lintr results seem wrong, clear the cache with lintr::clear_cache(). This resolves many intermittent issues.

Preview before styling with styler: Always use dry = "on" first to see what will change. Styling modifies your files, and it’s easy to make unintended changes.

Scope confusion with styler: The "tokens" scope changes assignment operators from = to <-. Make sure you understand what each scope level does before applying it to a large codebase.

Object usage linter requirements: The object_usage_linter() requires your package to be loaded. Use pkgload::load_all() or ensure the package is installed before running this linter.

Encoding issues: If your files use non-UTF-8 encoding, specify it in your .lintr file. Otherwise, lintr may misparse your code and report false positives.


Both lintr and styler integrate well with continuous integration systems. You can add them to your GitHub Actions workflow to automatically check code quality on every push. This ensures that all contributors follow your project’s style conventions.

See Also: building-r-packages, r-debugging, r-renv

Team adoption strategies

Adopting lintr on an existing project with many existing violations is challenging. Start by generating the baseline report: lintr::lint_package(), then commit the .lintr configuration with rules that match your current code quality. This prevents regressions while not requiring immediate cleanup of all existing issues.

Over time, tighten the configuration as the codebase improves. lintr::lint_diff() lints only the lines changed in a pull request, which is a low-friction way to enforce standards on new code without requiring teams to fix all existing issues at once.

For shared configuration across a monorepo or organization, publish the .lintr file in a shared location and reference it in each project’s .lintr with linters = lintr::default_linters. This ensures consistent rules across all projects without duplicating configuration.