# Creating new linters

#### 2019-10-01

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.

## Writing the linter

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.

## Writing linter tests

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.

1. Linter returns no lints when there is nothing to lint. e.g.
expect_lint("blah", NULL, assignment_linter)
1. Linter returns a lint when there is something to lint. e.g.
expect_lint("blah=1",
rex("Use <-, not =, for assignment."),
assignment_linter)
1. As many edge cases as you can think of that might break it. e.g.
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.