This vignette describes the steps necessary to create a new linter.
A good example of a simple linter is the assignment_linter
.
#' @describeIn linters checks that '<-' is always used for assignment
#' @export
assignment_linter <- function(source_file) {
lapply(ids_with_token(source_file, "EQ_ASSIGN"),
function(id) {
parsed <- source_file$parsed_content[id, ]
Lint(
filename = source_file$filename,
line_number = parsed$line1,
column_number = parsed$col1,
type = "style",
message = "Use <-, not =, for assignment.",
line = source_file$lines[parsed$line1],
linter = "assignment_linter"
)
})
}
Lets walk through the parts of the linter individually.
The first two lines add the linter to the linters
documentation and export it for use outside the package.
Next we define the name of the new linter. The convention is that all linter names are suffixed by _linter
.
Your linter will be called by each top level expression in the file to be linted.
The raw text of the expression is available from source_file$content
. However it is recommended to work with the tokens from source_file$parsed_content
if possible, as they are tokenzied from the R
parser. These tokens are obtained from parse()
and getParseData()
calls done prior to calling the new linter. getParseData()
returns a data.frame
with information from the source parse tree of the file being linted. A list of tokens available from r-source/src/main/gram.y.
ids_with_token()
can be used to search for a specific token and return the associated id. Note that the rownames
for parsed_content
are set to the id
, so you can retrieve the rows for a given id with source_file$parsed_content[id, ]
.
lapply(ids_with_token(source_file, "EQ_ASSIGN"),
function(id) {
parsed <- source_file$parsed_content[id, ]
Lastly build a Lint
object which describes the issue. See ?Lint
for a description of the arguments.
Lint(
filename = source_file$filename,
line_number = parsed$line1,
column_number = parsed$col1,
type = "style",
message = "Use <-, not =, for assignment.",
line = source_file$lines[parsed$line1],
linter = "assignment_linter"
)
You do not have to return a Lint for every iteration of your loop. Feel free to return NULL
or empty lists() for tokens which do not need to be linted. You can even return a list
of Lint
objects if more than one Lint was found.
The linter
package uses testthat for testing. You can run all of the currently available tests using devtools::test()
. If you want to run only the tests in a given file use the filter
argument to devtools::test()
.
Linter tests should be put in the tests/testthat/ folder. The test filename should be the linter name prefixed by test-
, e.g. test-assignment_linter.R
.
The first line in the test file should be a line which defines the context of the text (the linter name).
You can then specify one or more test_that
functions. Most of the linters use the same default form.
You then test a series of expectations for the linter using expect_lint
. Please see ?expect_lint
for a full description of the parameters.
I try to test 3 main things.
It is always better to write too many tests rather than too few.
If your linter is non-project specific you can add it to default_linters
. This object is created in the file zzz.R
. The name ensures that it will always run after all the linters are defined. Simply add your linter name to the default_linters
list before the NULL
at the end.
Push your changes to a branch of your fork of the lintr repository, and submit a pull request to get your linter merged into lintr!