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

First install the pre_commit Python package.

pip install pre-commit

Install the {precommit} R package.

install.packages("precommit")

You’ll want to also install a couple of other R packages.

install.packages(c("styler", "lintr"))

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.

💡 The .pre-commit-config.yaml file should be staged and committed to the repository so that everybody who works on the code applies the same rules.

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.