--- title: "Adding new rules" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Adding new rules} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} 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](https://ast-grep.github.io/guide/pattern-syntax.html) 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. ```{r, error=TRUE} stop("this is an error") rlang::abort("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: ```yaml 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()`: ```{r, error=TRUE} n <- 10 stop("Got ", n, " 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()`: ```{r, error=TRUE} n <- 10 rlang::abort(paste0("Got ", n, " 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 `~~`: ```yaml 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: ```{r, eval=FALSE} flint::lint_text( 'stop("Got ", n, " values instead of 1.")', linters = "stop_abort" ) ``` Running our example with `lint_text()` now shows the message: ```{.r} 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: ```{r, eval=FALSE} 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`: ```yaml keep: - any_duplicated [...] - unreachable_code - stop_abort ``` Running `lint_text()` or `fix_text()` without `linters` now works: ```{r, eval=FALSE} 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")`.