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
#' @exportNext 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!