lintr 3.0.0


  Michael Chirico

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:


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:


This snippet generates 5 lints:

  1. object_name_linter() because the uppercase T and F in the function name do not match lower_snake_case.
  2. brace_linter() because { should be separated from ) by a space.
  3. paren_body_linter() because ) should be separated from the function body (starting at {) by a space.
  4. infix_spaces_linter() because = should be surrounded by spaces on both sides.
  5. 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.

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.

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:

  1. Inconsistency—some linters were designated as calls, like line_length_linter(120), while others were designated as names, like no_tab_linter.
  2. Brittleness—some linters evolve to gain (or lose) parameters over time, e.g. in this release assignment_linter gained two arguments, allow_cascading_assign and allow_right_assign, to fine-tune the handling of the cascading assignment operators <<-/->> and right assignment operators ->/->>, respectively.
  3. 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) { ... }


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:

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 of any(; anyNA(x) is nearly always a better choice, both for performance and for readability.
  • expect_named_linter() detects usage in testthat suites like expect_equal(names(x), c("a", "b", "c")); testthat also exports expect_named() which is tailor made to make more readable tests like expect_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 be if (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 of stringsAsFactors changed from TRUE to FALSE, by identifying usages of data.frame() that (1) have known string columns and (2) don’t declare a value for stringsAsFactors, 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 to sprintf() (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 by R CMD check.
  • namespace_linter(): a new linter to check for common mistakes in pkg::symbol usage, e.g. if symbol is not an exported object from pkg.

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.


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.