Using lintr

Alexander Rosenstock

This vignette describes how to set up and configure lintr for use with projects or packages.

Running lintr on a project

Checking an R project for lints can be done with three different functions:

For more information about the assumed package structure, see R Packages.

Note that some linters (e.g. object_usage_linter()) require the package to be installed to function properly. pkgload::load_all() will also suffice. See ?executing_linters for more details.

Configuring linters

The .lintr file

The canonical way to configure R projects and packages for linting is to create a .lintr file in the project root. This is a file in debian control format (?read.dcf), each value of which is evaluated as R code by lintr when reading the settings. A minimal .lintr file can be generated by running use_lintr() in the project directory. Lintr supports per-project configuration of the following fields.

.lintr File Example

Below is an example .lintr file that uses 120 character line lengths, disables commented_code_linter, excludes a couple of files.

linters: linters_with_defaults(
    line_length_linter(120),
    commented_code_linter = NULL
  )
exclusions: list(
    "inst/doc/creating_linters.R" = 1,
    "inst/example/bad.R",
    "tests/testthat/exclusions-test"
  )

Other configuration options

More generally, lintr searches for a settings file according to following prioritized list. The first one found, if any, will be used:

  1. If options("lintr.linter_file") is an absolute path, this file will be used. The default for this option is ".lintr" or the value of the environment variable R_LINTR_LINTER_FILE, if set.
  2. A project-local linter file; that is, either
    1. a linter file (that is, a file named like lintr.linter_file) in the currently-searched directory, i.e. the directory of the file passed to lint(); or
    2. a linter file in the .github/linters child directory of the currently-searched directory.
  3. A project-local linter file in the closest parent directory of the currently-searched directory, starting from the deepest path, moving upwards one level at a time. When run from lint_package(), this directory can differ for each linted file.
  4. A linter file in the user’s HOME directory.
  5. A linter file called config in the user’s configuration path (given by tools::R_user_dir("lintr", which = "config")).

If no linter file is found, only default settings take effect (see defaults).

Using options()

Values in options(), if they are not NULL, take precedence over those in the linter file (e.g. .lintr). Note that the key option_name in the linter file translates to an R option lintr.option_name. For example, options(lintr.exclude = "# skip lint") will take precedence over exclude: # nolint in the linter file.

Using arguments to lint()

The settings can also be passed as arguments to linting functions directly. In case of exclusions, these will be combined with the globally parsed settings. Other settings will be overridden.

If only the specified settings should be changed, and the remaining settings should be taken directly from the defaults, the argument parse_settings = FALSE can be added to the function calls. This will suppress reading of the .lintr configuration. This is particularly useful for tests which should not exclude example files containing lints while the package-level .lintr excludes those files because the lints are intentional.

Defaults

The default settings of lintr are intended to conform to the tidyverse style guide. However, the behavior can be customized using different methods.

Apart from lintr.linter_file, which defaults to ".lintr", there are the following settings:

default
linters lintr::default_linters
encoding UTF-8
exclude regex: #[[:space:]]*nolint
exclude_next regex: #[[:space:]]*nolint next
exclude_start regex: #[[:space:]]*nolint start
exclude_end regex: #[[:space:]]*nolint end
exclude_linter regex: ^[[:space:]]*:[[:space:]]*(?<linters>(?:(?:[^,.])+[[:space:]]*,[[:space:]]*)*(?:[^,.])+)\.
exclude_linter_sep regex: [[:space:]]*,[[:space:]]*
exclusions (empty)
cache_directory /home/michael/.cache/R/lintr
comment_token (lintr-bot comment token for automatic GitHub comments)
comment_bot TRUE
error_on_lint FALSE

Note that the default encoding setting depends on the file to be linted. If an Encoding is found in a .Rproj file or a DESCRIPTION file, that encoding overrides the default of UTF-8.

Customizing active linters

If you only want to customize some linters, you can use the helper function linters_with_defaults(), which will keep all unnamed linters with the default settings. Disable a linter by passing NULL.

For example, to set the line length limit to 120 characters and globally disable the whitespace_linter(), you can put this into your .lintr:

linters: linters_with_defaults(
    line_length_linter = line_length_linter(120L),
    whitespace_linter = NULL
  )

By default, the following linters are enabled. Where applicable, the default settings are also shown.

settings
assignment_linter allow_cascading_assign = TRUE, allow_right_assign = FALSE, allow_trailing = TRUE, allow_pipe_assign = FALSE
brace_linter allow_single_line = FALSE
commas_linter allow_trailing = FALSE
commented_code_linter
cyclocomp_linter complexity_limit = 15L
equals_na_linter
function_left_parentheses_linter
indentation_linter indent = 2L, hanging_indent_style = “tidy”, assignment_as_infix = TRUE
infix_spaces_linter exclude_operators = NULL, allow_multiple_spaces = TRUE
line_length_linter length = 80L
object_length_linter length = 30L
object_name_linter styles = c(“snake_case”, “symbols”), regexes = character(0)
object_usage_linter interpret_glue = TRUE, skip_with = TRUE
paren_body_linter
pipe_continuation_linter
quotes_linter delimiter = “"”
semicolon_linter allow_compound = FALSE, allow_trailing = FALSE
seq_linter
spaces_inside_linter
spaces_left_parentheses_linter
T_and_F_symbol_linter
trailing_blank_lines_linter
trailing_whitespace_linter allow_empty_lines = FALSE, allow_in_strings = TRUE
vector_logic_linter
whitespace_linter

Another way to customize linters is by specifying tags in linters_with_tags(). The available tags are listed below:

lintr::available_tags(packages = "lintr")
#>  [1] "best_practices"      "common_mistakes"     "configurable"       
#>  [4] "consistency"         "correctness"         "default"            
#>  [7] "deprecated"          "efficiency"          "executing"          
#> [10] "package_development" "pkg_testthat"        "readability"        
#> [13] "robustness"          "style"

You can select tags of interest to see which linters are included:

linters <- lintr::linters_with_tags(tags = c("package_development", "readability"))
names(linters)
#>  [1] "backport_linter"                  "boolean_arithmetic_linter"       
#>  [3] "brace_linter"                     "commas_linter"                   
#>  [5] "commented_code_linter"            "conjunct_test_linter"            
#>  [7] "consecutive_assertion_linter"     "cyclocomp_linter"                
#>  [9] "empty_assignment_linter"          "expect_comparison_linter"        
#> [11] "expect_identical_linter"          "expect_length_linter"            
#> [13] "expect_named_linter"              "expect_not_linter"               
#> [15] "expect_null_linter"               "expect_s3_class_linter"          
#> [17] "expect_s4_class_linter"           "expect_true_false_linter"        
#> [19] "expect_type_linter"               "fixed_regex_linter"              
#> [21] "for_loop_index_linter"            "function_left_parentheses_linter"
#> [23] "function_return_linter"           "if_not_else_linter"              
#> [25] "implicit_assignment_linter"       "indentation_linter"              
#> [27] "infix_spaces_linter"              "inner_combine_linter"            
#> [29] "is_numeric_linter"                "keyword_quote_linter"            
#> [31] "length_levels_linter"             "lengths_linter"                  
#> [33] "library_call_linter"              "line_length_linter"              
#> [35] "matrix_apply_linter"              "nested_ifelse_linter"            
#> [37] "numeric_leading_zero_linter"      "object_length_linter"            
#> [39] "object_usage_linter"              "outer_negation_linter"           
#> [41] "package_hooks_linter"             "paren_body_linter"               
#> [43] "pipe_call_linter"                 "pipe_consistency_linter"         
#> [45] "pipe_continuation_linter"         "quotes_linter"                   
#> [47] "redundant_equals_linter"          "repeat_linter"                   
#> [49] "scalar_in_linter"                 "semicolon_linter"                
#> [51] "sort_linter"                      "spaces_inside_linter"            
#> [53] "spaces_left_parentheses_linter"   "string_boundary_linter"          
#> [55] "system_file_linter"               "T_and_F_symbol_linter"           
#> [57] "unnecessary_concatenation_linter" "unnecessary_lambda_linter"       
#> [59] "unnecessary_nested_if_linter"     "unnecessary_placeholder_linter"  
#> [61] "unreachable_code_linter"          "yoda_test_linter"

You can include tag-based linters in the configuration file, and customize them further:

linters: linters_with_tags(
    tags = c("package_development", "readability"),
    yoda_test_linter = NULL
  )

Using all available linters

The default lintr configuration includes only linters relevant to the tidyverse style guide, but there are many other linters available in {lintr}. You can see a list of all available linters using

names(lintr::all_linters())
#>  [1] "absolute_path_linter"             "any_duplicated_linter"           
#>  [3] "any_is_na_linter"                 "assignment_linter"               
#>  [5] "backport_linter"                  "boolean_arithmetic_linter"       
#>  [7] "brace_linter"                     "class_equals_linter"             
#>  [9] "commas_linter"                    "commented_code_linter"           
#> [11] "condition_message_linter"         "conjunct_test_linter"            
#> [13] "consecutive_assertion_linter"     "cyclocomp_linter"                
#> [15] "duplicate_argument_linter"        "empty_assignment_linter"         
#> [17] "equals_na_linter"                 "expect_comparison_linter"        
#> [19] "expect_identical_linter"          "expect_length_linter"            
#> [21] "expect_named_linter"              "expect_not_linter"               
#> [23] "expect_null_linter"               "expect_s3_class_linter"          
#> [25] "expect_s4_class_linter"           "expect_true_false_linter"        
#> [27] "expect_type_linter"               "extraction_operator_linter"      
#> [29] "fixed_regex_linter"               "for_loop_index_linter"           
#> [31] "function_argument_linter"         "function_left_parentheses_linter"
#> [33] "function_return_linter"           "if_not_else_linter"              
#> [35] "ifelse_censor_linter"             "implicit_assignment_linter"      
#> [37] "implicit_integer_linter"          "indentation_linter"              
#> [39] "infix_spaces_linter"              "inner_combine_linter"            
#> [41] "is_numeric_linter"                "keyword_quote_linter"            
#> [43] "length_levels_linter"             "length_test_linter"              
#> [45] "lengths_linter"                   "library_call_linter"             
#> [47] "line_length_linter"               "literal_coercion_linter"         
#> [49] "matrix_apply_linter"              "missing_argument_linter"         
#> [51] "missing_package_linter"           "namespace_linter"                
#> [53] "nested_ifelse_linter"             "nonportable_path_linter"         
#> [55] "numeric_leading_zero_linter"      "object_length_linter"            
#> [57] "object_name_linter"               "object_usage_linter"             
#> [59] "outer_negation_linter"            "package_hooks_linter"            
#> [61] "paren_body_linter"                "paste_linter"                    
#> [63] "pipe_call_linter"                 "pipe_consistency_linter"         
#> [65] "pipe_continuation_linter"         "quotes_linter"                   
#> [67] "redundant_equals_linter"          "redundant_ifelse_linter"         
#> [69] "regex_subset_linter"              "repeat_linter"                   
#> [71] "routine_registration_linter"      "scalar_in_linter"                
#> [73] "semicolon_linter"                 "seq_linter"                      
#> [75] "sort_linter"                      "spaces_inside_linter"            
#> [77] "spaces_left_parentheses_linter"   "sprintf_linter"                  
#> [79] "string_boundary_linter"           "strings_as_factors_linter"       
#> [81] "system_file_linter"               "T_and_F_symbol_linter"           
#> [83] "todo_comment_linter"              "trailing_blank_lines_linter"     
#> [85] "trailing_whitespace_linter"       "undesirable_function_linter"     
#> [87] "undesirable_operator_linter"      "unnecessary_concatenation_linter"
#> [89] "unnecessary_lambda_linter"        "unnecessary_nested_if_linter"    
#> [91] "unnecessary_placeholder_linter"   "unreachable_code_linter"         
#> [93] "unused_import_linter"             "vector_logic_linter"             
#> [95] "whitespace_linter"                "yoda_test_linter"

If you want to use all available linters, you can include this in your .lintr file:

linters: all_linters()

If you want to use all available linters except a few, you can exclude them using NULL:

linters: all_linters(
    commented_code_linter = NULL,
    implicit_integer_linter = NULL
  )

Advanced: programmatic retrieval of linters

For some use cases, it may be useful to specify linters by string instead of by name, i.e. "assignment_linter" instead of writing out assignment_linter().

Beware that in such cases, a simple get() is not enough:

library(lintr)
linter_name <- "assignment_linter"

show_lint <- function(l) {
  lint_df <- as.data.frame(l)
  print(lint_df[, c("line_number", "message", "linter")])
}
hline <- function() cat(strrep("-", getOption("width") - 5L), "\n", sep = "")

withr::with_tempfile("tmp", {
  writeLines("a = 1", tmp)

  # linter column is just 'get'
  show_lint(lint(tmp, linters = get(linter_name)()))
  hline()

  this_linter <- get(linter_name)()
  attr(this_linter, "name") <- linter_name
  # linter column is 'assignment_linter'
  show_lint(lint(tmp, linters = this_linter))
  hline()

  # more concise alternative for this case: use eval(call(.))
  show_lint(lint(tmp, linters = eval(call(linter_name))))
})
#>   line_number                        message linter
#> 1           1 Use <-, not =, for assignment.    get
#> ---------------------------------------------------------------------------
#>   line_number                        message            linter
#> 1           1 Use <-, not =, for assignment. assignment_linter
#> ---------------------------------------------------------------------------
#>   line_number                        message            linter
#> 1           1 Use <-, not =, for assignment. assignment_linter

Exclusions

Sometimes, linters should not be globally disabled. Instead, one might want to exclude some code from linting altogether or selectively disable some linters on some part of the code.

Note that the preferred way of excluding lints from source code is to use the narrowest possible scope and specify exactly which linters should not throw a lint on a marked line. This prevents accidental suppression of justified lints that happen to be on the same line as a lint that needs to be suppressed.

Excluding lines of code

Within source files, special comments can be used to exclude single lines of code from linting. All lints produced on the marked line are excluded from the results.

By default, this special comment is # nolint:

file.R

X = 42L # -------------- this comment overflows the default 80 chars line length.

> lint("file.R")

#> <text>:1:1: style: [object_name_linter] Variable and function name style should match snake_case or symbols.
#> X = 42L # -------------- this comment overflows the default 80 chars line length.
#> ^
#> <text>:1:3: style: [assignment_linter] Use <-, not =, for assignment.
#> X = 42L # -------------- this comment overflows the default 80 chars line length.
#>   ^
#> <text>:1:81: style: [line_length_linter] Lines should not be more than 80 characters. This line is 81 characters.
#> X = 42L # -------------- this comment overflows the default 80 chars line length.
#> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^

file2.R

X = 42L # nolint ------ this comment overflows the default 80 chars line length.

> lint("file2.R")

Observe how all lints were suppressed and no output is shown. Sometimes, only a specific linter needs to be excluded. In this case, the name of the linter can be appended to the # nolint comment preceded by a colon and terminated by a dot.

Excluding only some linters

file3.R

X = 42L # nolint: object_name_linter. this comment overflows the default 80 chars line length.

> lint("file3.R")

#> <text>:1:3: style: [assignment_linter] Use <-, not =, for assignment.
#> X = 42L # nolint: object_name_linter. this comment overflows the default 80 chars line length.
#>   ^
#> <text>:1:81: style: [line_length_linter] Lines should not be more than 80 characters. This line is 94 characters.
#> X = 42L # nolint: object_name_linter. this comment overflows the default 80 chars line length.
#> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~

Observe how only the object_name_linter was suppressed. This is preferable to blanket # nolint statements because blanket exclusions may accidentally silence a linter that was not intentionally suppressed.

Multiple linters can be specified by listing them with a comma as a separator:

file4.R

X = 42L # nolint: object_name_linter, line_length_linter. this comment overflows the default 80 chars line length.

> lint("file4.R")

#> <text>:1:3: style: [assignment_linter] Use <-, not =, for assignment.
#> X = 42L # nolint: object_name_linter, line_length_linter. this comment overflows the default 80 chars line length.
#>   ^

You can also specify the linter names by a unique prefix instead of their full name:

file5.R

X = 42L # nolint: object_name, line_len. this comment still overflows the default 80 chars line length.

> lint("file5.R")

#> <text>:1:3: style: [assignment_linter] Use <-, not =, for assignment.
#> X = 42L # nolint: object_name, line_len. this comment still overflows the default 80 chars line length.
#>   ^

Excluding multiple lines of codes

If any or all linters should be disabled for a contiguous block of code, the exclude_start and exclude_end patterns can be used. They default to # nolint start and # nolint end respectively.

# nolint start accepts the same syntax as # nolint to disable specific linters in the following lines until a # nolint end is encountered.

# x <- 42L
# print(x)
#> <text>:1:3: style: [commented_code_linter] Commented code should be removed.
#> # x <- 42L
#>   ^~~~~~~~
#> <text>:2:3: style: [commented_code_linter] Commented code should be removed.
#> # print(x)
#>   ^~~~~~~~
# nolint start: commented_code_linter.
# x <- 42L
# print(x)
# nolint end

(No lints)

Excluding lines via the config file

Individual lines can also be excluded via the config file by setting the key exclusions to a list with elements corresponding to different files. To exclude all lints for line 1 of file R/bad.R and line_length_linter for lines 4 to 6 of the same file, one can set

exclusions: list(
    "R/bad.R" = list(
      1, # global exclusions are unnamed
      line_length_linter = 4:6
    )
  )

All paths are interpreted relative to the location of the .lintr file.

Excluding files completely

The linter configuration can also be used to exclude a file entirely, or a linter for a file entirely. Use the sentinel line number Inf to mark all lines as excluded within a file. If a file is only given as a character vector, full exclusion is implied.

exclusions: list(
    # excluded from all lints:
    "R/excluded1.R",
    "R/excluded2.R" = Inf,
    "R/excluded3.R" = list(Inf),
    # excluded from line_length_linter:
    "R/no-line-length.R" = list(
      line_length_linter = Inf
    )
  )

Excluding directories completely

Entire directories are also recognized when supplied as strings in the exclusions key. For example, to exclude the renv folder from linting in a R project using renv, set

exclusions: list(
    "renv"
  )

This is particularly useful for projects which include external code in subdirectories.