Skip to content

Commit dc1e515

Browse files
Merge branch 'main' into require_nzchar
2 parents ab8ebbe + 7970260 commit dc1e515

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+808
-670
lines changed

.lintr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ linters: linters_with_defaults(
2020
implicit_integer_linter(),
2121
keyword_quote_linter(),
2222
lengths_linter(),
23-
line_length_linter(120),
23+
line_length_linter(120L),
2424
missing_argument_linter(),
2525
nested_ifelse_linter(),
2626
numeric_leading_zero_linter(),

DESCRIPTION

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Imports:
3636
xmlparsedata (>= 1.0.5)
3737
Suggests:
3838
bookdown,
39-
crayon,
39+
cli,
4040
httr (>= 1.2.1),
4141
jsonlite,
4242
mockery,
@@ -128,6 +128,7 @@ Collate:
128128
'linter_tags.R'
129129
'lintr-deprecated.R'
130130
'lintr-package.R'
131+
'list_comparison_linter.R'
131132
'literal_coercion_linter.R'
132133
'make_linter_from_regex.R'
133134
'matrix_apply_linter.R'
@@ -151,6 +152,7 @@ Collate:
151152
'pipe_call_linter.R'
152153
'pipe_consistency_linter.R'
153154
'pipe_continuation_linter.R'
155+
'print_linter.R'
154156
'quotes_linter.R'
155157
'redundant_equals_linter.R'
156158
'redundant_ifelse_linter.R'

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export(lint_dir)
9090
export(lint_package)
9191
export(linters_with_defaults)
9292
export(linters_with_tags)
93+
export(list_comparison_linter)
9394
export(literal_coercion_linter)
9495
export(make_linter_from_xpath)
9596
export(matrix_apply_linter)
@@ -114,6 +115,7 @@ export(paste_linter)
114115
export(pipe_call_linter)
115116
export(pipe_consistency_linter)
116117
export(pipe_continuation_linter)
118+
export(print_linter)
117119
export(quotes_linter)
118120
export(redundant_equals_linter)
119121
export(redundant_ifelse_linter)

NEWS.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
# lintr (development version)
22

3+
## Deprecations & breaking changes
4+
5+
* Various things marked deprecated since {lintr} 3.0.0 have been fully deprecated. They will be completely removed in the subsequent release.
6+
+ `source_file=` argument to `ids_with_token()` and `with_id()`.
7+
+ Passing linters by name or as non-`"linter"`-classed functions.
8+
+ `linter=` argument of `Lint()`.
9+
+ Linters `closed_curly_linter()`, `open_curly_linter()`, `paren_brace_linter()`, and `semicolon_terminator_linter()`..
10+
+ `with_defaults()`.
11+
+ Linters `closed_curly_linter()`, `open_curly_linter()`, `paren_brace_linter()`, and `semicolon_terminator_linter()`.
12+
+ Helper `with_defaults()`.
13+
14+
## Bug fixes
15+
16+
* `object_name_linter()` no longer errors when user-supplied `regexes=` have capture groups (#2188, @MichaelChirico).
17+
318
## New and improved features
419

520
* More helpful errors for invalid configs (#2253, @MichaelChirico).
21+
* `library_call_linter()` is extended to encourage all packages to be attached with `library(symbol)`, not `library("symbol", character.only = TRUE)` or "vectorized" approaches looping over package names (part of #884, @MichaelChirico).
622

723
### New linters
824

@@ -11,6 +27,8 @@
1127
* `comparison_negation_linter()` for discouraging negated comparisons when a direct negation is preferable, e.g. `!(x == y)` could be `x != y` (part of #884, @MichaelChirico).
1228
* `nzchar_linter()` for encouraging `nzchar()` to test for empty strings, e.g. `nchar(x) > 0` can be `nzchar(x)` (part of #884, @MichaelChirico).
1329
* `terminal_close_linter()` for discouraging using `close()` to end functions (part of #884, @MichaelChirico). Such usages are not robust to errors, where `close()` will not be run as intended. Put `close()` in an `on.exit()` hook, or use {withr} to manage connections with proper cleanup.
30+
* `list_comparison_linter()` for discouraging comparisons on the output of `lapply()`, e.g. `lapply(x, sum) > 10` (part of #884, @MichaelChirico).
31+
* `print_linter()` for discouraging usage of `print()` on string literals like `print("Reached here")` or `print(paste("Found", nrow(DF), "rows."))` (#1894, @MichaelChirico).
1432

1533
### Lint accuracy fixes: removing false positives
1634

@@ -23,7 +41,7 @@
2341

2442
* `infix_spaces_linter()` distinguishes `<-`, `:=`, `<<-` and `->`, `->>`, i.e. `infix_spaces_linter(exclude_operators = "->")` will no longer exclude `->>` (#2115, @MichaelChirico). This change is breaking for users relying on manually-supplied `exclude_operators` containing `"<-"` to also exclude `:=` and `<<-`. The fix is to manually supply `":="` and `"<<-"` as well. We don't expect this change to affect many users, the fix is simple, and the new behavior is much more transparent, so we are including this breakage in a minor release.
2543
* Removed `find_line()` and `find_column()` entries from `get_source_expressions()` expression-level objects. These have been marked deprecated since version 3.0.0. No users were found on GitHub.
26-
* There is experimental support for writing config in plain R scripts (as opposed to DCF files; #1210, @MichaelChirico). The script is run in a new environment and variables matching settings (`?default_settings`) are copied over. In particular, this removes the need to write R code in a DCF-friendly way, and allows normal R syntax highlighting in the saved file. We may eventually deprecate the DCF approach in favor of this one; user feedback is welcome on strong preferences for either approach, or for a different approach like YAML. Generally you should be able to convert your existing `.lintr` file to an equivalent R config by replacing the `:` key-value separators with assignments (`<-`). By default, such a config is searched for in a file named '.lintr.R'. This is a mildly breaking change if you happened to be keeping a file '.lintr.R' around since that file is given precedence over '.lintr'.
44+
* There is experimental support for writing config in plain R scripts (as opposed to DCF files; #1210, @MichaelChirico). The script is run in a new environment and variables matching settings (`?default_settings`) are copied over. In particular, this removes the need to write R code in a DCF-friendly way, and allows normal R syntax highlighting in the saved file. We may eventually deprecate the DCF approach in favor of this one; user feedback is welcome on strong preferences for either approach, or for a different approach like YAML. Generally you should be able to convert your existing `.lintr` file to an equivalent R config by replacing the `:` key-value separators with assignments (`<-`). By default, such a config is searched for in a file named `.lintr.R`. This is a mildly breaking change if you happened to be keeping a file `.lintr.R` around since that file is given precedence over `.lintr`.
2745
+ We also validate config files up-front make it clearer when invalid configs are present (#2195, @MichaelChirico). There is a warning for "invalid" settings, i.e., settings not part of `?default_settings`. We think this is more likely to affect users declaring settings in R, since any variable defined in the config that's not a setting must be removed to make it clearer which variables are settings vs. ancillary.
2846

2947
## Bug fixes

R/deprecated.R

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
NULL
77

88
lintr_deprecated <- function(old, new = NULL, version = NULL,
9-
type = "Function") {
9+
type = "Function", signal = c("warning", "stop")) {
10+
signal <- match.arg(signal)
11+
signal <- match.fun(signal)
1012
msg <- c(
1113
c(type, " ", old, " was deprecated"),
1214
if (length(version) > 0L) {
@@ -18,5 +20,5 @@ lintr_deprecated <- function(old, new = NULL, version = NULL,
1820
}
1921
)
2022
msg <- paste0(msg, collapse = "")
21-
warning(msg, call. = FALSE, domain = NA)
23+
signal(msg, call. = FALSE, domain = NA)
2224
}

R/exclude.R

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,20 @@ exclude <- function(lints, exclusions = settings$exclusions, linter_names = NULL
3535
return(lints)
3636
}
3737

38-
df <- as.data.frame(lints)
38+
lint_df <- as.data.frame(lints)
3939

40-
filenames <- unique(df$filename)
40+
filenames <- unique(lint_df$filename)
4141
source_exclusions <- lapply(filenames, parse_exclusions, linter_names = linter_names, ...)
4242
names(source_exclusions) <- filenames
4343

4444

4545
exclusions <- normalize_exclusions(c(source_exclusions, exclusions))
4646
to_exclude <- vapply(
47-
seq_len(nrow(df)),
47+
seq_len(nrow(lint_df)),
4848
function(i) {
49-
file <- df$filename[i]
50-
file %in% names(exclusions) &&
51-
is_excluded(df$line_number[i], df$linter[i], exclusions[[file]])
49+
filename <- lint_df$filename[i]
50+
filename %in% names(exclusions) &&
51+
is_excluded(lint_df$line_number[i], lint_df$linter[i], exclusions[[filename]])
5252
},
5353
logical(1L)
5454
)
@@ -375,11 +375,11 @@ remove_linter_duplicates <- function(x) {
375375

376376
if (length(unique_linters) < length(ex)) {
377377
ex <- lapply(unique_linters, function(linter) {
378-
lines <- unlist(ex[names2(ex) == linter])
379-
if (Inf %in% lines) {
378+
excluded_lines <- unlist(ex[names2(ex) == linter])
379+
if (Inf %in% excluded_lines) {
380380
Inf
381381
} else {
382-
lines
382+
excluded_lines
383383
}
384384
})
385385

R/expect_lint.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,19 @@ expect_lint <- function(content, checks, ..., file = NULL, language = "en") {
9999
itr, field, deparse(value), deparse(check)
100100
)
101101
# deparse ensures that NULL, list(), etc are handled gracefully
102-
exp <- if (field == "message") {
102+
ok <- if (field == "message") {
103103
re_matches(value, check)
104104
} else {
105105
isTRUE(all.equal(value, check))
106106
}
107-
if (!is.logical(exp)) {
107+
if (!is.logical(ok)) {
108108
stop(
109109
"Invalid regex result, did you mistakenly have a capture group in the regex? ",
110110
"Be sure to escape parenthesis with `[]`",
111111
call. = FALSE
112112
)
113113
}
114-
testthat::expect(exp, msg)
114+
testthat::expect(ok, msg)
115115
})
116116
},
117117
lints,

R/extract.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ get_chunk_positions <- function(pattern, lines) {
9393

9494
filter_chunk_start_positions <- function(starts, lines) {
9595
# keep blocks that don't set a knitr engine (and so contain evaluated R code)
96-
drop <- defines_knitr_engine(lines[starts])
97-
starts[!drop]
96+
drop_idx <- defines_knitr_engine(lines[starts])
97+
starts[!drop_idx]
9898
}
9999

100100
filter_chunk_end_positions <- function(starts, ends) {

R/ids_with_token.R

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@
2929
#' @export
3030
ids_with_token <- function(source_expression, value, fun = `==`, source_file = NULL) {
3131
if (!missing(source_file)) {
32-
lintr_deprecated(old = "source_file", new = "source_expression", version = "3.0.0", type = "Argument")
33-
source_expression <- source_file
32+
lintr_deprecated(
33+
old = "source_file", new = "source_expression",
34+
version = "3.0.0", type = "Argument",
35+
signal = "stop"
36+
)
3437
}
3538
if (!is_lint_level(source_expression, "expression")) {
3639
return(integer())

R/library_call_linter.R

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,64 @@
11
#' Library call linter
22
#'
3-
#' Force library calls to all be at the top of the script.
3+
#' This linter covers several rules related to [library()] calls:
4+
#'
5+
#' - Enforce such calls to all be at the top of the script.
6+
#' - Block usage of argument `character.only`, in particular
7+
#' for loading packages in a loop.
48
#'
59
#' @param allow_preamble Logical, default `TRUE`. If `FALSE`,
610
#' no code is allowed to precede the first `library()` call,
711
#' otherwise some setup code is allowed, but all `library()`
812
#' calls must follow consecutively after the first one.
913
#' @examples
1014
#' # will produce lints
15+
#'
16+
#' code <- "library(dplyr)\nprint('test')\nlibrary(tidyr)"
17+
#' writeLines(code)
1118
#' lint(
12-
#' text = "
13-
#' library(dplyr)
14-
#' print('test')
15-
#' library(tidyr)
16-
#' ",
19+
#' text = code,
1720
#' linters = library_call_linter()
1821
#' )
1922
#'
2023
#' lint(
21-
#' text = "
22-
#' library(dplyr)
23-
#' print('test')
24-
#' library(tidyr)
25-
#' library(purrr)
26-
#' ",
24+
#' text = "library('dplyr', character.only = TRUE)",
25+
#' linters = library_call_linter()
26+
#' )
27+
#'
28+
#' code <- paste(
29+
#' "pkg <- c('dplyr', 'tibble')",
30+
#' "sapply(pkg, library, character.only = TRUE)",
31+
#' sep = "\n"
32+
#' )
33+
#' writeLines(code)
34+
#' lint(
35+
#' text = code,
2736
#' linters = library_call_linter()
2837
#' )
2938
#'
3039
#' # okay
40+
#' code <- "library(dplyr)\nprint('test')"
41+
#' writeLines(code)
3142
#' lint(
32-
#' text = "
33-
#' library(dplyr)
34-
#' print('test')
35-
#' ",
43+
#' text = code,
3644
#' linters = library_call_linter()
3745
#' )
3846
#'
47+
#' code <- "# comment\nlibrary(dplyr)"
3948
#' lint(
40-
#' text = "
41-
#' # comment
42-
#' library(dplyr)
43-
#' ",
49+
#' text = code,
50+
#' linters = library_call_linter()
51+
#' )
52+
#'
53+
#' code <- paste(
54+
#' "foo <- function(pkg) {",
55+
#' " sapply(pkg, library, character.only = TRUE)",
56+
#' "}",
57+
#' sep = "\n"
58+
#' )
59+
#' writeLines(code)
60+
#' lint(
61+
#' text = code,
4462
#' linters = library_call_linter()
4563
#' )
4664
#'
@@ -56,30 +74,93 @@ library_call_linter <- function(allow_preamble = TRUE) {
5674
glue("@line1 > //SYMBOL_FUNCTION_CALL[{ attach_call }][1]/@line1")
5775
)
5876
}
59-
xpath <- glue("
77+
upfront_call_xpath <- glue("
6078
//SYMBOL_FUNCTION_CALL[{ attach_call }][last()]
6179
/preceding::expr
6280
/SYMBOL_FUNCTION_CALL[{ unsuppressed_call }][last()]
6381
/following::expr[SYMBOL_FUNCTION_CALL[{ attach_call }]]
6482
/parent::expr
6583
")
6684

85+
# STR_CONST: block library|require("..."), i.e., supplying a string literal
86+
# ancestor::expr[FUNCTION]: Skip usages inside functions a la {knitr}
87+
char_only_direct_xpath <- "
88+
//SYMBOL_FUNCTION_CALL[text() = 'library' or text() = 'require']
89+
/parent::expr
90+
/parent::expr[
91+
expr[2][STR_CONST]
92+
or (
93+
SYMBOL_SUB[text() = 'character.only']
94+
and not(ancestor::expr[FUNCTION])
95+
)
96+
]
97+
"
98+
99+
bad_indirect_funs <- c("do.call", "lapply", "sapply", "map", "walk")
100+
call_symbol_cond <- "
101+
SYMBOL[text() = 'library' or text() = 'require']
102+
or STR_CONST[text() = '\"library\"' or text() = '\"require\"']
103+
"
104+
char_only_indirect_xpath <- glue("
105+
//SYMBOL_FUNCTION_CALL[{ xp_text_in_table(bad_indirect_funs) }]
106+
/parent::expr
107+
/parent::expr[
108+
not(ancestor::expr[FUNCTION])
109+
and expr[{ call_symbol_cond }]
110+
]
111+
")
112+
call_symbol_path <- glue("./expr[{call_symbol_cond}]")
113+
67114
Linter(function(source_expression) {
68115
if (!is_lint_level(source_expression, "file")) {
69116
return(list())
70117
}
71118

72119
xml <- source_expression$full_xml_parsed_content
73120

74-
bad_expr <- xml_find_all(xml, xpath)
121+
upfront_call_expr <- xml_find_all(xml, upfront_call_xpath)
75122

76-
call_name <- xp_call_name(bad_expr)
123+
call_name <- xp_call_name(upfront_call_expr)
77124

78-
xml_nodes_to_lints(
79-
bad_expr,
125+
upfront_call_lints <- xml_nodes_to_lints(
126+
upfront_call_expr,
80127
source_expression = source_expression,
81128
lint_message = sprintf("Move all %s calls to the top of the script.", call_name),
82129
type = "warning"
83130
)
131+
132+
char_only_direct_expr <- xml_find_all(xml, char_only_direct_xpath)
133+
char_only_direct_calls <- xp_call_name(char_only_direct_expr)
134+
character_only <-
135+
xml_find_first(char_only_direct_expr, "./SYMBOL_SUB[text() = 'character.only']")
136+
char_only_direct_msg_fmt <- ifelse(
137+
is.na(character_only),
138+
"Use symbols, not strings, in %s calls.",
139+
"Use symbols in %s calls to avoid the need for 'character.only'."
140+
)
141+
char_only_direct_msg <-
142+
sprintf(as.character(char_only_direct_msg_fmt), char_only_direct_calls)
143+
char_only_direct_lints <- xml_nodes_to_lints(
144+
char_only_direct_expr,
145+
source_expression = source_expression,
146+
lint_message = char_only_direct_msg,
147+
type = "warning"
148+
)
149+
150+
char_only_indirect_expr <- xml_find_all(xml, char_only_indirect_xpath)
151+
char_only_indirect_lib_calls <- get_r_string(char_only_indirect_expr, call_symbol_path)
152+
char_only_indirect_loop_calls <- xp_call_name(char_only_indirect_expr)
153+
char_only_indirect_msg <- sprintf(
154+
"Call %s() directly, not vectorized with %s().",
155+
char_only_indirect_lib_calls, char_only_indirect_loop_calls
156+
)
157+
char_only_indirect_lints <- xml_nodes_to_lints(
158+
char_only_indirect_expr,
159+
source_expression = source_expression,
160+
lint_message = char_only_indirect_msg,
161+
type = "warning"
162+
)
163+
164+
c(upfront_call_lints, char_only_direct_lints, char_only_indirect_lints)
84165
})
85166
}

0 commit comments

Comments
 (0)