`lintr`

Lint is the fluff on your clothes. Aside from all that fluff, you look fine.

lintr (Author: Jim Hester) compares the code in your files / packages against a style-guide. This helps ensure your source code looks pretty consistent across your package(s).

Why is that useful?

It might not be. I couldn’t find many objective studies of code readability amongst the thousands of opinion pieces that are online, so I can’t tell you whether consistent styling is all that valuable. Hell, writing R code may dull your consistency desires: in a typical script I might have

  • a suppressPackageStartupMessages() (camelCase);

  • a file.exists() (lower.dot.separated);

  • a read_tsv() (snake_case);

  • and a Heatmap() (UpperCamelCase / PascalCase)

all present. It might be nice if names were less surprising (in R generally), and it always feels nicer to contribute to projects that are neatly formatted.

But this is your project we’re talking about. If you like your lines to be at-most 100 characters wide, get in early and specify that the lines must be at-most 100 characters wide. Similarly, if you like your object names to be snake_case, write up a .lintr file that ensures any contributors have to follow in your wake.

lintr can be used on individual files (.R, .Rmd, .Rnw etc), directories or packages. Rstudio, vim, atom etc can be integrated with lintr (see the details at the lintr github page). For example, through integration with syntastic, vim can run lintr whenever you save a file, and if you run lintr within Rstudio, any lints that are identified are put into the Markers pane.

To run lintr call:

lintr::lint(some_file)

As an example, some code has been added to a temporary file. The file, f, looks like this:

# -- My important script
abc <- 123

def = 456

c(0);

# some unparseable code:
FALSE <- TRUE

# some <- "commented code"

ghi = 789

my_unportable_file <- "~/hello/world"

snakeCase <- runif(10)

dotty.variable.name <- Sys.time()

my_s3_class.print <- print

# - `lintr` defaults will disallow CamelCase and single-quotes
# but we can wrap code in `# nolint start` / `# nolint end` to bypass

# nolint start
MyClassName <- function() {
  my_df <- data.frame()
  class(my_df) <- 'MyClassName'
  my_df
}
# nolint end

# Right assignment isn't caught yet
"A most perilous" -> assignment

We can now run lintr on that script, with the default choice of linting functions, and view the style-issues that are returned.

lintr::lint(filename = f)
/tmp/RtmprnDZzY/189631cf5f2a.R:4:5: style: Use <-, not =, for assignment.
def = 456
    ^
/tmp/RtmprnDZzY/189631cf5f2a.R:11:3: style: Commented code should be removed.
# some <- "commented code"
  ^~~~~~~~~~~~~~~~~~~~~~~~
/tmp/RtmprnDZzY/189631cf5f2a.R:13:5: style: Use <-, not =, for assignment.
ghi = 789
    ^
/tmp/RtmprnDZzY/189631cf5f2a.R:15:24: warning: Do not use absolute paths.
my_unportable_file <- "~/hello/world"
                       ^~~~~~~~~~~~~
/tmp/RtmprnDZzY/189631cf5f2a.R:17:1: style: Variable and function names should be all lowercase.
snakeCase <- runif(10)
^~~~~~~~~
/tmp/RtmprnDZzY/189631cf5f2a.R:19:1: style: Words within variable and function names should be separated by '_' rather than '.'.
dotty.variable.name <- Sys.time()
^~~~~~~~~~~~~~~~~~~

So we’ve caught style issues (choice of assignment operator, commented code, use of something other than snake_case etc) and some warnings ()

The default linters are as follows:

names(lintr::default_linters)
##  [1] "assignment_linter"              "single_quotes_linter"          
##  [3] "absolute_paths_linter"          "no_tab_linter"                 
##  [5] "line_length_linter"             "commas_linter"                 
##  [7] "infix_spaces_linter"            "spaces_left_parentheses_linter"
##  [9] "spaces_inside_linter"           "open_curly_linter"             
## [11] "closed_curly_linter"            "camel_case_linter"             
## [13] "multiple_dots_linter"           "object_length_linter"          
## [15] "object_usage_linter"            "trailing_whitespace_linter"    
## [17] "trailing_blank_lines_linter"    "commented_code_linter"

Note that we are using a lintr version from CRAN (version 1.0.3 ).

The github development version has more linters, can deal with a wider variety of document-types, and has better integration with Rstudio (eg, an Addin).

Also, there are linters beyond those in the defaults (more so in the development version). And you can write your own linters. To use just these linters, add them to a named list and pass them in as the linters argument to lint.

# Not real code
lintr::lint(
  f,
  linters = list(some_linter = some_linting_function)
)

To add extra linters to, or the modify the linters in the default linters, you can use with_defaults() and pass that as the linters argument. If you want to turn-off one of the default linters, give that linter a NULL value in the call to with_defaults.

Here we turn off a few of the default linters, modify the line-length and object-length linters, and leave all other default linters untouched (eg absolute_paths_linter still catches)

lintr::lint(
  filename = f,
  linters = lintr::with_defaults(
    # turn off a few linters
    assignment_linter = NULL,
    camel_case_linter = NULL,
    multiple_dots_linter = NULL,
    commented_code_linter = NULL,
    # make the line-length linter more restrictive
    line_length_linter = lintr::line_length_linter(length = 36),
    object_length_linter = lintr::object_length_linter(length = 16)
  )
)
## /tmp/RtmprnDZzY/189631cf5f2a.R:15:1: style: lines should not be more than 36 characters.
## my_unportable_file <- "~/hello/world"
## ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
## /tmp/RtmprnDZzY/189631cf5f2a.R:15:1: style: Variable and function names should not be longer than 16 characters.
## my_unportable_file <- "~/hello/world"
## ^~~~~~~~~~~~~~~~~~
## /tmp/RtmprnDZzY/189631cf5f2a.R:15:24: warning: Do not use absolute paths.
## my_unportable_file <- "~/hello/world"
##                        ^~~~~~~~~~~~~
## /tmp/RtmprnDZzY/189631cf5f2a.R:19:1: style: Variable and function names should not be longer than 16 characters.
## dotty.variable.name <- Sys.time()
## ^~~~~~~~~~~~~~~~~~~
## /tmp/RtmprnDZzY/189631cf5f2a.R:21:1: style: Variable and function names should not be longer than 16 characters.
## my_s3_class.print <- print
## ^~~~~~~~~~~~~~~~~

When working in an R project or package, you can define a .lintr configuration file that specifies which linters you want to apply to your code. These files use a related syntax to lintr::lint, but you specify the arguments in “key: value” format. My config for linting dupree looks like this (note how the value is an R expression; there’s a more detailed example on the lintr github page).

linters: with_defaults(
  commented_code_linter = NULL,
  line_length_linter(80),
  object_length_linter(40),
  open_curly_linter = NULL,
  spaces_left_parentheses_linter = NULL
  )

You can implement new linters if you want. This is a bit more complex than simply using lintr in your projects. There’s a write-up here.

In the temp script we wrote above, the use of the right-assignment operator -> wasn’t caught. But, surely it’s hideous? We can modify the code of assignment_linter to catch right-assignment (I’ve also had to explicitly call some lintr internal functions but we’ll gloss over that):

right_assignment_linter <- function(source_file) {
  lapply(
    # ids_with_token finds any right-assignments in the file `->`
    lintr:::ids_with_token(source_file, "RIGHT_ASSIGN"), function(id) {
      parsed <- lintr:::with_id(source_file, id)
      # The `Lint` function flags up any miscreant lines
      lintr:::Lint(
        filename = source_file$filename, line_number = parsed$line1,
        column_number = parsed$col1, type = "style",
        message = "Use <-, not ->, for assignment.",
        line = source_file$lines[as.character(parsed$line1)],
        linter = "right_assignment_linter"
      )
    }
  )
}

Running lint with our newly defined linter catches that filthy right-assignment.

lintr::lint(
  f,
  linters = list(right_assignment_linter = right_assignment_linter)
)
## /tmp/RtmprnDZzY/189631cf5f2a.R:35:19: style: Use <-, not ->, for assignment.
## "A most perilous" -> assignment
##                   ^
Code Analysis in R R-function Pokemon