library(flint)
#>
#> Attaching package: 'flint'
#> The following object is masked from 'package:utils':
#>
#> fix
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 astgrepr
and
therefore of the Rust crate ast-grep
. This crate has great
documentation on creating new
rules so you should start there.
The objective for this vignette is to add support for is_numeric_linter()
.
Step 1: setup
We need three things to start experimenting with new rules:
an example: this can be taken from
lintr
’s examples for a specific linter. For instance we can already runlint_text("is.numeric(y) || is.integer(y)")
. This doesn’t return anything since we haven’t implemented the rule yet.a rule: this requires storing a new YAML file in
flint/rules
. We can call this oneis_numeric.yml
.store the rule name (
"is_numeric"
) inlist_linters()
.
Additionally, if the rules we want to add are part of
lintr
, we can copy the tests stored in lintr
test suite to ensure that our rules cover most corner cases.
Step 2: start exploring
In lintr::is_numeric_linter()
, we want to warn if one
uses is.numeric(y) || is.integer(y)
or
is.integer(y) || is.numeric(y)
since this equivalent to
is.numeric(y)
.
In our rule, we can already add those two patterns using
any
:
id: is_numeric_1
language: r
severity: warning
rule:
any:
- pattern: is.numeric($VAR) || is.integer($VAR)
- pattern: is.integer($VAR) || is.numeric($VAR)
message: is.numeric(x) is the same as is.numeric(x) || is.integer(x). Use
is.double(x) to test for objects stored as 64-bit floating point.
Running our example now shows the message:
lint_text("is.numeric(y) || is.integer(y)")
#> Original code: is.numeric(y) || is.integer(y)
#> Suggestion: is.numeric(x) || is.integer(x) can be simplified to is.numeric(x). Use is.double(x) to test for objects stored as 64-bit floating point.
But we can see from the lintr::is_numeric_linter()
examples that are more cases in which we should warn. For instance,
class(z) %in% c("numeric", "integer")
should warn since it
is equivalent to is.numeric(z)
,
class(z) %in% c("numeric", "integer", "factor")
shouldn’t.
We can add a second rule in our YAML file by adding a separator
---
:
id: is_numeric_1
language: r
severity: warning
rule:
any:
- pattern: is.numeric($VAR) || is.integer($VAR)
- pattern: is.integer($VAR) || is.numeric($VAR)
message: is.numeric(x) || is.integer(x) can be simplified to is.numeric(x). Use
is.double(x) to test for objects stored as 64-bit floating point.
---
id: is_numeric_2
language: r
severity: warning
rule:
any:
- pattern: class($VAR) %in% c("numeric", "integer")
- pattern: class($VAR) %in% c("integer", "numeric")
message: class(x) %in% c("numeric", "integer") can be simplified to is.numeric(x). Use
is.double(x) to test for objects stored as 64-bit floating point.
And this now works as well:
lint_text('class(z) %in% c("numeric", "integer")')
#> Original code: class(z) %in% c("numeric", "integer")
#> Suggestion: class(x) %in% c("numeric", "integer") can be simplified to is.numeric(x). Use is.double(x) to test for objects stored as 64-bit floating point.
lint_text('class(z) %in% c("numeric", "integer", "factor")')
Notice however that the patterns in our rule use double quotation
marks "
. So what happens when the code has single quotation
marks '
instead?
lint_text("class(z) %in% c('numeric', 'integer')")
Then it doesn’t work. To remedy this, we can use the strictness
parameter in ast-grep
, that comes as a sublevel of the
pattern
level. By default, strictness
is
smart
but we want to ignore quotation so we set it to
ast
(see here
for examples):
id: is_numeric_2
language: r
severity: warning
rule:
any:
- pattern:
context: class($VAR) %in% c("numeric", "integer")
strictness: ast
- pattern:
context: class($VAR) %in% c("integer", "numeric")
strictness: ast
message: class(x) %in% c("numeric", "integer") can be simplified to is.numeric(x). Use
is.double(x) to test for objects stored as 64-bit floating point.
We now have what we wanted:
lint_text("class(z) %in% c('numeric', 'integer')")
#> Original code: class(z) %in% c('numeric', 'integer')
#> Suggestion: class(x) %in% c("numeric", "integer") can be simplified to is.numeric(x). Use is.double(x) to test for objects stored as 64-bit floating point.
lint_text('class(z) %in% c("numeric", "integer")')
#> Original code: class(z) %in% c('numeric', 'integer')
#> Suggestion: class(x) %in% c("numeric", "integer") can be simplified to is.numeric(x). Use is.double(x) to test for objects stored as 64-bit floating point.
Step 3: corner cases
We want to avoid false positives and false negatives. If the rule
exists in lintr
then we can copy the section of their test
suite that tests this rule. In this case, it is this
file.