Adding new rules

library(flint, warn.conflicts = FALSE)

flint comes with an extensive set of rules taken from lintr, but if necessary one can also extend it relatively easily. This will require some knowledge of the Rust crate ast-grep to write. This crate has great documentation on creating new rules so you should start there.

In this vignette, our objective is to replace calls to stop() by rlang::abort(), for instance because we prefer the formatting of the output with the latter.

stop("this is an error")
#> Error: this is an error

rlang::abort("this is an error")
#> Error:
#> ! this is an error

Step 1: setup

First, we need to set up flint using setup_flint(). This will create a flint folder that contains (among other things) a rules folder where all rules are stored. This is divided between builtin rules (that shouldn’t modified manually) and custom rules (where we will store our custom rule).

Then, we can create the structure of a new rule by copying an existing rule, say flint/rules/builtin/any_is_na.yml for instance. After removing the stuff that is specific to this rule, we end up with this structure:

id: 
language: r
severity: warning
rule:
  pattern: 
fix: 
message: 

Step 2: start exploring

When we use stop(), we can pass multiple elements, like in paste0():

n <- 10
stop("Got ", n, " values instead of 1.")
#> Error: Got 10 values instead of 1.

This is not possible with rlang::abort(), which needs everything to be in the argument message, meaning that we need to manually put all those elements in paste0():

n <- 10
rlang::abort(paste0("Got ", n, " values instead of 1."))
#> Error:
#> ! Got 10 values instead of 1.

Therefore, we can look for the pattern stop(...) and replace it by rlang::abort(paste0(...)). Capturing all elements in a pattern is done with $$$ and those elements can be used in the fix or message arguments using by wrapping them in ~~:

id: stop_abort-1
language: r
severity: warning
rule:
  pattern: stop($$$ELEMS)
fix: rlang::abort(paste0(~~ELEMS~~))
message: Use `rlang::abort()` instead of `stop()`.

After storing this rule in flint/rules/custom/stop_abort.yml, we can call:

flint::lint_text(
  'stop("Got ", n, " values instead of 1.")', 
  linters = "stop_abort"
)

Running our example with lint_text() now shows the message:

flint::lint_text(
  'stop("Got ", n, " values instead of 1.")', 
  linters = "stop_abort"
)
#> Original code: stop("Got ", n, " values instead of 1.") 
#> Suggestion: Use `rlang::abort()` instead of `stop()`. 
#> Rule ID: stop_abort-1 

And fix_text() correctly applies the fix:

flint::fix_text(
  'stop("Got ", n, " values instead of 1.")', 
  linters = "stop_abort"
)
#> Old code: stop("Got ", n, " values instead of 1.") 
#> New code: rlang::abort(paste0("Got ", n, " values instead of 1.")) 

Note that there are still some corner cases to address, such as ignoring the arguments call. and domain of stop() if they are specified, but this is not in the scope of this vignette.

Step 3 (optional): add to config

To automatically use this new linter without having to specify it manually, we can add it to flint/config.yaml:

keep:
  - any_duplicated
  [...]
  - unreachable_code
  - stop_abort

Running lint_text() or fix_text() without linters now works:

flint::fix_text('stop("Got ", n, " values instead of 1.")')
#> Old code: stop("Got ", n, " values instead of 1.") 
#> New code: rlang::abort(paste0("Got ", n, " values instead of 1.")) 

Step 4: enjoy

The new linter is now set up, you can use flint as before to lint or fix specific files or entire folders, e.g. flint::fix_dir("R").