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]
)
})
}
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.
#' @describeIn linters checks that '<-' is always used for assignment
#' @export
Next we define the name of the new linter. The convention is that all linter names are suffixed by _linter
.
assignment_linter <- function(source_file) {
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]
)
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).
context("assignment_linter")
You can then specify one or more test_that
functions. Most of the linters use the same default form.
test_that("returns the correct linting", {
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.
expect_lint("blah", NULL, assignment_linter)
expect_lint("blah=1",
rex("Use <-, not =, for assignment."),
assignment_linter)
expect_lint("fun((blah = fun(1)))",
rex("Use <-, not =, for assignment."),
assignment_linter)
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!