We are very excited to announce the release of lintr 3.0.0! lintr is maintained by Jim Hester and contributors, including three new package authors: Alexander Rosenstock, Kun Ren, and Michael Chirico. lintr provides both a framework for static analysis of R packages and scripts and a variety of linters, e.g. to enforce the tidyverse style guide.
You can install it from CRAN with:
install.packages("lintr")
Check our vignettes for a quick introduction to the package:
- Getting started (
vignette("lintr")
) - Integrating lintr with your preferred IDE (
vignette("editors")
) - Integrating lintr with your preferred CI tools (
vignette("continuous-integration")
)
We’ve also added lintr::use_lintr()
for a usethis-inspired interactive tool to configure lintr for your package/repo.
This blog post will highlight the biggest changes coming in this update which drove us to declare it a major release.
Selective exclusions
lintr now supports targeted exclusions of specific linters through an extension of the # nolint
syntax.
Consider the following example:
T_and_F_symbol_linter=function(){
list()
}
This snippet generates 5 lints:
object_name_linter()
because the uppercaseT
andF
in the function name do not matchlower_snake_case
.brace_linter()
because{
should be separated from)
by a space.paren_body_linter()
because)
should be separated from the function body (starting at{
) by a space.infix_spaces_linter()
because=
should be surrounded by spaces on both sides.assignment_linter()
because<-
should be used for assignment.
The first lint is spurious because t
and f
do not correctly convey that this linter targets
the symbols T
and F
, so we want to ignore it. Prior to this release, we would have to
throw the baby out with the bathwater by suppressing all five lints like so:
T_and_F_symbol_linter=function(){ # nolint. T and F are OK here.
list()
}
This hides the other four lints and prevents any new lints from being detected on this line in the future, which on average allows the overall quality of your projects/scripts to dip.
With the new feature, you’d write the exclusion like this instead:
T_and_F_symbol_linter=function(){ # nolint: object_name_linter. T and F are OK here.
list()
}
By qualifying the exclusion, the other 4 lints will be detected and exposed by lint()
so
that you can fix them! See ?exclude
for more details.
Linter factories
As of lintr 3.0.0, all linters must be function factories.
Previously, only parameterizable linters (such as line_length_linter()
, which takes a parameter controlling how
wide lines are allowed to be without triggering a lint) were factories, but this led to some problems:
- Inconsistency—some linters were designated as calls, like
line_length_linter(120)
, while others were designated as names, likeno_tab_linter
. - Brittleness—some linters evolve to gain (or lose) parameters over time, e.g. in this release
assignment_linter
gained two arguments,allow_cascading_assign
andallow_right_assign
, to fine-tune the handling of the cascading assignment operators<<-
/->>
and right assignment operators->
/->>
, respectively. - Performance—factories can run some fixed computations at declaration and store them in the function environment, whereas previously the calculation would need to be repeated on every expression of every file being linted.
This has two significant practical implications and are the main reason this is a major release.
First, lintr invocations should always use the call form, so old usages like:
lint_package(linters = assignment_linter)
should be replaced with:
lint_package(linters = assignment_linter())
We expect this to show up in most cases through users’ .lintr
configuration files.
Second, users implementing custom linters need to convert to function factories.
That means replacing:
my_custom_linter <- function(source_expression) { ... }
With:
my_custom_linter <- function() Linter(function(source_expression) { ... }))
Linter()
is a wrapper to construct the linter
S3 class.
Linter metadatabase, linter documentation, and pkgdown
We have also overhauled how linters are documented. Previously, all linters were documented on a single page and described in a quick blurb. This has gotten unwieldy as lintr has grown to export 72 linters! Now, each linter gets its own page, which will make it easier to document any parameters, enumerate edge cases/ known false positives, add links to external resources, etc.
To make linter discovery even more navigable, we’ve also added available_linters()
, a
database with known linters and some associated metadata tags for each.
For example, brace_linter
has tags style
, readability
, default
, and configurable
.
Each tag also gets its own documentation page (e.g. ?readability_linters
) which describes the tag
and lists all of the known associated linters. The tags are available in another database:
available_tags()
. These databases can be extended to include custom linters in your package;
see ?available_linters
.
Moreover, lintr’s documentation is now available as a website thanks to Hadley Wickham’s contribution to create a pkgdown website for the package: lintr.r-lib.org.
Google linters
This release also features more than 30 new linters originally authored by Google developers. Google adheres mostly to the tidyverse style guide and uses lintr to improve the quality of its considerable internal R code base. These linters detect common issues with readability, consistency, and performance. Here are some examples:
any_is_na_linter()
detects the usage ofany(is.na(x))
;anyNA(x)
is nearly always a better choice, both for performance and for readability.expect_named_linter()
detects usage in testthat suites likeexpect_equal(names(x), c("a", "b", "c"))
;testthat
also exportsexpect_named()
which is tailor made to make more readable tests likeexpect_named(x, c("a", "b", "c"))
.vector_logic_linter()
detects usage of vector logic operators|
and&
in situations where scalar logic applies, e.g.if (x | y) { ... }
should beif (x || y) { ... }
. The latter is more efficient and less error-prone.strings_as_factors_linter()
helps developers maintaining code that straddles the R 4.0.0 boundary, where the default value ofstringsAsFactors
changed fromTRUE
toFALSE
, by identifying usages ofdata.frame()
that (1) have known string columns and (2) don’t declare a value forstringsAsFactors
, and thus rely on the R version-dependent default.
See the NEWS for the complete list.
Other improvements
This is a big release—almost 2 years in the making—and includes a plethora of smaller but nonetheless important changes to lintr. Please check the NEWS for a complete enumeration of these. Here are a few more new linters as a highlight:
sprintf_linter()
: a new linter for detecting potentially problematic calls tosprintf()
(e.g. using too many or too few arguments as compared to the number of template fields).package_hooks_linter()
: a new linter to check consistency of.onLoad()
functions and other namespace hooks, as required byR CMD check
.namespace_linter()
: a new linter to check for common mistakes inpkg::symbol
usage, e.g. ifsymbol
is not an exported object frompkg
.
Google has developed and tested many more broad-purpose linters that it plans to share, e.g. for
detecting length(which(x == y)) > 0
(i.e., any(x == y)
), lapply(x, function(xi) sum(xi))
(i.e., lapply(x, sum)
), c("key_name" = "value_name")
(i.e., c(key_name = "value_name")
),
and more! Follow
#884 for updates.
Moreover, with the decision to accept a bevy of linters from Google that are not strictly related to the tidyverse style guide, we also opened the door to hosting linters for enforcing other style guides, for example the Bioconductor R code guide. We look forward to community contributions in this vein.
Acknowledgements
A great big thanks to the 97 people who have contributed to this release of lintr:
@1beb, @albert-ying, @aronatkins, @AshesITR, @assignUser, @barryrowlingson, @belokoch, @bersbersbers, @bsolomon1124, @chrisumphlett, @csgillespie, @danielinteractive, @dankessler, @dgkf, @dinakar29, @dmurdoch, @dpprdan, @dragosmg, @dschlaep, @eitsupi, @ElsLommelen, @f-ritter, @fabian-s, @fdlk, @fornaeffe, @frederic-mahe, @GiuseppeTT, @hadley, @hhoeflin, @hrvg, @huisman, @iago-pssjd, @IndrajeetPatil, @inventionate, @ishaar226, @jabenninghoff, @jameslamb, @jennybc, @jeremymiles, @jhgoebbert, @jimhester, @johanneswerner, @jonkeane, @JSchoenbachler, @JWiley, @karlvurdst, @klmr, @Kotsakis, @kpagacz, @kpj, @latot, @leogama, @liar666, @logstar, @lorenzwalthert, @maelle, @markromanmiller, @mattwarkentin, @maxheld83, @MichaelChirico, @michaelquinn32, @mikekaminsky, @milanglacier, @minimenchmuncher, @mjsteinbaugh, @nathaneastwood, @nlarusstone, @nsoranzo, @nvuillam, @pakjiddat, @pat-s, @prncevince, @QiStats-Joel, @rahulrachh, @razz-matazz, @renkun-ken, @rfalke, @richfitz, @russHyde, @salim-b, @schaffstein, @scottmmjackson, @sgvignali, @shaopeng-gh, @StefanBRas, @stefaneng, @stefanocoretta, @stufield, @TCABJ, @telegott, @ThierryO, @thisisnic, @tonyk7440, @wfmueller29, @wibeasley, @yannickwurm, and @yutannihilation.