Enforcing Style in an R Project

In the previous post we looked at how to apply a linter and styler to a Python Project. Now we’re going to do the same for an R project. We’ll use the {precommit} R package to make the setup a breeze.

Install

Install the {precommit} package.

install.packages("precommit")

Setup

Setup precommit for a project.

precommit::use_precommit()

That will create the .pre-commit-config.yaml configuration file and, if present, also add it to .Rbuildignore.

The content of the .pre-commit-config.yaml file should look something like this (I’ve stripped out comment for brevity):

repos:
-   repo: https://github.com/lorenzwalthert/precommit
    rev: v0.3.2.9001
    hooks: 
    -   id: style-files
        args: [--style_pkg=styler, --style_fun=tidyverse_style]
    -   id: roxygenize
    -   id: use-tidy-description
    -   id: spell-check
        exclude: >
          (?x)^(
          .*\.[rR]|
          .*\.feather|
          .*\.jpeg|
          .*\.pdf|
          .*\.png|
          .*\.py|
          .*\.RData|
          .*\.rds|
          .*\.Rds|
          .*\.Rproj|
          .*\.sh|
          (.*/|)\.gitignore|
          (.*/|)\.gitlab-ci\.yml|
          (.*/|)\.lintr|
          (.*/|)\.pre-commit-.*|
          (.*/|)\.Rbuildignore|
          (.*/|)\.Renviron|
          (.*/|)\.Rprofile|
          (.*/|)\.travis\.yml|
          (.*/|)appveyor\.yml|
          (.*/|)NAMESPACE|
          (.*/|)renv/settings\.dcf|
          (.*/|)renv\.lock|
          (.*/|)WORDLIST|
          \.github/workflows/.*|
          data/.*|
          )$
    -   id: lintr
    -   id: readme-rmd-rendered
    -   id: parsable-R
    -   id: no-browser-statement
    -   id: no-debug-statement
    -   id: deps-in-desc
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.3.0
    hooks: 
    -   id: check-added-large-files
        args: ['--maxkb=200']
    -   id: file-contents-sorter
        files: '^\.Rbuildignore$'
    -   id: end-of-file-fixer
        exclude: '\.Rd'
-   repo: https://github.com/pre-commit-ci/pre-commit-ci-config
    rev: v1.5.1
    hooks:
    -   id: check-pre-commit-ci-config
-   repo: local
    hooks:
    -   id: forbid-to-commit
        name: Don't commit common R artifacts
        entry: Cannot commit .Rhistory, .RData, .Rds or .rds.
        language: fail
        files: '\.(Rhistory|RData|Rds|rds)$'
        # `exclude: <regex>` to allow committing specific files

ci:
    autoupdate_schedule: monthly

If you’re using {roxygen2} then you might be prompted to run the following:

precommit::snippet_generate('additional-deps-roxygenize')

That will probably generate additional instructions about changes you need to make to the id: roxygenize key in the .pre-commit-config.yaml file. Apply those.

📢 I actually ended up removing the roxygenize entry because I found that it occasionally sent me into a never ending commit loop.

Extra Hooks

You might also want to add the trailing-whitespace and check-yaml rules to .pre-commit-config.yaml under the https://github.com/pre-commit/pre-commit-hooks repository.

Commit

Once you’ve configured {precommit}, stage the .pre-commit-config.yaml and .Rbuildignore files and then try to commit. You might need to work fairly hard to get all of the checks passing, especially if there’s quite a lot of code in the repository. Just be systematic in addressing each of the errors raised by the hook processes.

You might get an error about /usr/lib/R/Rscript not being found. This means that pre-commit is looking in the wrong place for Rscript. I simply made a link from /usr/lib/R/Rscript to the actual location of Rscript.

The {lintr} Package

The {lintr} package will perform static analysis on your R code and help identify syntatic problems.

Install

Install {lintr}.

install.packages("lintr")

Configure

Create a .lintr file in the project root.

lintr::use_lintr()

You can update the .lintr file to tweak the way that {lintr} will treat your files.

linters: linters_with_defaults(
    line_length_linter(120),
    object_usage_linter = NULL,
    object_name_linter(c("snake_case", "SNAKE_CASE")),
    commented_code_linter = NULL
  )
exclusions: list()
encoding: "UTF-8"

You might want to add in "dotted.case" as another argument to object_name_linter().

Excluding Code

You can exclude chunks of code from linting by adding nolint hints as comments.

# The following line of code will be ignored by {lintr}.
n <- 42 # nolint

# The following block of code will be ignored by {lintr}.
# nolint start
n <- 41
n <- n + 1
# nolint end

The {styler} Package

By default the {precommit} package will invoke the {styler} package and apply the Tidyverse style (via tidyverse_style).

Install

Install {styler}.

install.packages("styler")

Flourish

With the {lintr} and {styler} packages installed and kicked off on every commit via the pre-commit framework you can be confident that the code you push is both syntactically correct and consistently formatted.