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
## ^