Creating new linters

Jim Hester

2020-02-18

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.

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],
  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.

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.

Adding your linter to the default_linters

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.

Submit pull request

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!