Skip to content

Commit 9d7b3b3

Browse files
Merge branch 'main' into require_nzchar
2 parents 91b1910 + 08d7396 commit 9d7b3b3

30 files changed

+690
-30
lines changed

DESCRIPTION

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ Collate:
157157
'regex_subset_linter.R'
158158
'repeat_linter.R'
159159
'routine_registration_linter.R'
160+
'sample_int_linter.R'
160161
'scalar_in_linter.R'
161162
'semicolon_linter.R'
162163
'seq_linter.R'
@@ -167,9 +168,11 @@ Collate:
167168
'spaces_inside_linter.R'
168169
'spaces_left_parentheses_linter.R'
169170
'sprintf_linter.R'
171+
'stopifnot_all_linter.R'
170172
'string_boundary_linter.R'
171173
'strings_as_factors_linter.R'
172174
'system_file_linter.R'
175+
'terminal_close_linter.R'
173176
'trailing_blank_lines_linter.R'
174177
'trailing_whitespace_linter.R'
175178
'tree_utils.R'

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export(redundant_ifelse_linter)
120120
export(regex_subset_linter)
121121
export(repeat_linter)
122122
export(routine_registration_linter)
123+
export(sample_int_linter)
123124
export(sarif_output)
124125
export(scalar_in_linter)
125126
export(semicolon_linter)
@@ -130,9 +131,11 @@ export(sort_linter)
130131
export(spaces_inside_linter)
131132
export(spaces_left_parentheses_linter)
132133
export(sprintf_linter)
134+
export(stopifnot_all_linter)
133135
export(string_boundary_linter)
134136
export(strings_as_factors_linter)
135137
export(system_file_linter)
138+
export(terminal_close_linter)
136139
export(todo_comment_linter)
137140
export(trailing_blank_lines_linter)
138141
export(trailing_whitespace_linter)

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@
66

77
### New linters
88

9+
* `sample_int_linter()` for encouraging `sample.int(n, ...)` over equivalents like `sample(1:n, ...)` (part of #884, @MichaelChirico).
10+
* `stopifnot_all_linter()` discourages tests with `all()` like `stopifnot(all(x > 0))`; `stopifnot()` runs `all()` itself, and uses a better error message (part of #884, @MichaelChirico).
911
* `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).
1012
* `nzchar_linter()` for encouraging `nzchar()` to test for empty strings, e.g. `nchar(x) > 0` can be `nzchar(x)` (part of #884, @MichaelChirico).
13+
* `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.
1114

1215
### Lint accuracy fixes: removing false positives
1316

1417
* `unreachable_code_linter()` ignores reachable code in inline functions like `function(x) if (x > 2) stop() else x` (#2259, @MEO265).
18+
* `unnecessary_lambda_linter()` ignores extractions with explicit returns like `lapply(l, function(x) foo(x)$bar)` (#2258, @MichaelChirico).
1519

1620
# lintr 3.1.1
1721

R/comparison_negation_linter.R

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,34 @@
33
#' `!(x == y)` is more readably expressed as `x != y`. The same is true of
44
#' other negations of simple comparisons like `!(x > y)` and `!(x <= y)`.
55
#'
6+
#' @examples
7+
#' # will produce lints
8+
#' lint(
9+
#' text = "!x == 2",
10+
#' linters = comparison_negation_linter()
11+
#' )
12+
#'
13+
#' lint(
14+
#' text = "!(x > 2)",
15+
#' linters = comparison_negation_linter()
16+
#' )
17+
#'
18+
#' # okay
19+
#' lint(
20+
#' text = "!(x == 2 & y > 2)",
21+
#' linters = comparison_negation_linter()
22+
#' )
23+
#'
24+
#' lint(
25+
#' text = "!(x & y)",
26+
#' linters = comparison_negation_linter()
27+
#' )
28+
#'
29+
#' lint(
30+
#' text = "x != 2",
31+
#' linters = comparison_negation_linter()
32+
#' )
33+
#'
634
#' @evalRd rd_tags("comparison_negation_linter")
735
#' @seealso [linters] for a complete list of linters available in lintr.
836
#' @export

R/get_source_expressions.R

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ get_source_expressions <- function(filename, lines = NULL) {
6868
}
6969

7070
# Only regard explicit attribute terminal_newline=FALSE as FALSE and all other cases (e.g. NULL or TRUE) as TRUE.
71-
# We don't use isFALSE since it is introduced in R 3.5.0.
72-
terminal_newline <- !identical(attr(source_expression$lines, "terminal_newline", exact = TRUE), FALSE)
71+
terminal_newline <- !isFALSE(attr(source_expression$lines, "terminal_newline", exact = TRUE))
7372

7473
e <- NULL
7574
source_expression$lines <- extract_r_source(
@@ -493,19 +492,6 @@ get_source_expression <- function(source_expression, error = identity) {
493492
error = error
494493
)
495494

496-
# TODO: Remove when minimum R version is bumped to > 3.5
497-
#
498-
# This needs to be done twice to avoid a bug fixed in R 3.4.4
499-
# https://bugs.r-project.org/bugzilla/show_bug.cgi?id=16041
500-
parsed_content <- tryCatch(
501-
parse(
502-
text = source_expression$content,
503-
srcfile = source_expression,
504-
keep.source = TRUE
505-
),
506-
error = error
507-
)
508-
509495
if (inherits(parsed_content, c("error", "lint"))) {
510496
assign("e", parsed_content, envir = parent.frame())
511497
parse_error <- TRUE

R/namespace.R

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,7 @@ is_s3_generic <- function(fun) {
8585

8686
.base_s3_generics <- unique(c(
8787
names(.knownS3Generics),
88-
if (getRversion() >= "3.5.0") {
89-
.S3_methods_table[, 1L]
90-
} else {
91-
# R < 3.5.0 doesn't provide .S3_methods_table
92-
# fallback: search baseenv() for generic methods
93-
imported_s3_generics(data.frame(pkg = "base", fun = ls(baseenv()), stringsAsFactors = FALSE))$fun
94-
},
88+
.S3_methods_table[, 1L],
9589
# Contains S3 generic groups, see ?base::groupGeneric and src/library/base/R/zzz.R
9690
ls(.GenericArgsEnv)
9791
))

R/nonportable_path_linter.R

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
#'
33
#' Check that [file.path()] is used to construct safe and portable paths.
44
#'
5+
#' @examples
6+
#' # will produce lints
7+
#' lint(
8+
#' text = "'abcdefg/hijklmnop/qrst/uv/wxyz'",
9+
#' linters = nonportable_path_linter
10+
#' )
11+
#'
12+
#' # okay
13+
#' lint(
14+
#' text = "file.path('abcdefg', 'hijklmnop', 'qrst', 'uv', 'wxyz')",
15+
#' linters = nonportable_path_linter()
16+
#' )
17+
#'
518
#' @inheritParams absolute_path_linter
619
#' @evalRd rd_tags("nonportable_path_linter")
720
#' @seealso

R/sample_int_linter.R

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#' Require usage of sample.int(n, m, ...) over sample(1:n, m, ...)
2+
#'
3+
#' [sample.int()] is preferable to `sample()` for the case of sampling numbers
4+
#' between 1 and `n`. `sample` calls `sample.int()` "under the hood".
5+
#'
6+
#' @examples
7+
#' # will produce lints
8+
#' lint(
9+
#' text = "sample(1:10, 2)",
10+
#' linters = sample_int_linter()
11+
#' )
12+
#'
13+
#' lint(
14+
#' text = "sample(seq(4), 2)",
15+
#' linters = sample_int_linter()
16+
#' )
17+
#'
18+
#' lint(
19+
#' text = "sample(seq_len(8), 2)",
20+
#' linters = sample_int_linter()
21+
#' )
22+
#'
23+
#' # okay
24+
#' lint(
25+
#' text = "sample(seq(1, 5, by = 2), 2)",
26+
#' linters = sample_int_linter()
27+
#' )
28+
#'
29+
#' lint(
30+
#' text = "sample(letters, 2)",
31+
#' linters = sample_int_linter()
32+
#' )
33+
#'
34+
#' @evalRd rd_tags("sample_int_linter")
35+
#' @seealso [linters] for a complete list of linters available in lintr.
36+
#' @export
37+
sample_int_linter <- function() {
38+
# looking for anything like sample(1: that doesn't come after a $ extraction
39+
# exclude TRUE/FALSE for sample(replace = TRUE, ...) usage. better
40+
# would be match.arg() but this also works.
41+
xpath <- glue("
42+
//SYMBOL_FUNCTION_CALL[text() = 'sample']
43+
/parent::expr[not(OP-DOLLAR or OP-AT)]
44+
/following-sibling::expr[1][
45+
(
46+
expr[1]/NUM_CONST[text() = '1' or text() = '1L']
47+
and OP-COLON
48+
)
49+
or expr/SYMBOL_FUNCTION_CALL[text() = 'seq_len']
50+
or (
51+
expr/SYMBOL_FUNCTION_CALL[text() = 'seq']
52+
and (
53+
count(expr) = 2
54+
or (
55+
expr[2]/NUM_CONST[text() = '1' or text() = '1L']
56+
and not(SYMBOL_SUB[
57+
text() = 'by'
58+
and not(following-sibling::expr[1]/NUM_CONST[text() = '1' or text() = '1L'])
59+
])
60+
)
61+
)
62+
)
63+
or NUM_CONST[not(text() = 'TRUE' or text() = 'FALSE')]
64+
]
65+
/parent::expr
66+
")
67+
68+
Linter(function(source_expression) {
69+
if (!is_lint_level(source_expression, "expression")) {
70+
return(list())
71+
}
72+
73+
xml <- source_expression$xml_parsed_content
74+
75+
bad_expr <- xml_find_all(xml, xpath)
76+
first_call <- xp_call_name(bad_expr, depth = 2L)
77+
original <- sprintf("%s(n)", first_call)
78+
original[!is.na(xml_find_first(bad_expr, "expr[2]/OP-COLON"))] <- "1:n"
79+
original[!is.na(xml_find_first(bad_expr, "expr[2]/NUM_CONST"))] <- "n"
80+
81+
xml_nodes_to_lints(
82+
bad_expr,
83+
source_expression = source_expression,
84+
lint_message = glue("sample.int(n, m, ...) is preferable to sample({original}, m, ...)."),
85+
type = "warning"
86+
)
87+
})
88+
}

R/scalar_in_linter.R

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@
77
#' `scalar %in% vector` is OK, because the alternative (`any(vector == scalar)`)
88
#' is more circuitous & potentially less clear.
99
#'
10+
#' @examples
11+
#' # will produce lints
12+
#' lint(
13+
#' text = "x %in% 1L",
14+
#' linters = scalar_in_linter()
15+
#' )
16+
#'
17+
#' lint(
18+
#' text = "x %chin% 'a'",
19+
#' linters = scalar_in_linter()
20+
#' )
21+
#'
22+
#' # okay
23+
#' lint(
24+
#' text = "x %in% 1:10",
25+
#' linters = scalar_in_linter()
26+
#' )
27+
#'
1028
#' @evalRd rd_tags("scalar_in_linter")
1129
#' @seealso [linters] for a complete list of linters available in lintr.
1230
#' @export

R/stopifnot_all_linter.R

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#' Block usage of all() within stopifnot()
2+
#'
3+
#' `stopifnot(A)` actually checks `all(A)` "under the hood" if `A` is a vector,
4+
#' and produces a better error message than `stopifnot(all(A))` does.
5+
#'
6+
#' @examples
7+
#' # will produce lints
8+
#' lint(
9+
#' text = "stopifnot(all(x > 0))",
10+
#' linters = stopifnot_all_linter()
11+
#' )
12+
#'
13+
#' lint(
14+
#' text = "stopifnot(y > 3, all(x < 0))",
15+
#' linters = stopifnot_all_linter()
16+
#' )
17+
#'
18+
#' # okay
19+
#' lint(
20+
#' text = "stopifnot(is.null(x) || all(x > 0))",
21+
#' linters = stopifnot_all_linter()
22+
#' )
23+
#'
24+
#' lint(
25+
#' text = "assert_that(all(x > 0))",
26+
#' linters = stopifnot_all_linter()
27+
#' )
28+
#'
29+
#' @evalRd rd_tags("stopifnot_all_linter")
30+
#' @seealso [linters] for a complete list of linters available in lintr.
31+
#' @export
32+
stopifnot_all_linter <- make_linter_from_xpath(
33+
xpath = "
34+
//SYMBOL_FUNCTION_CALL[text() = 'stopifnot']
35+
/parent::expr
36+
/parent::expr
37+
/expr[expr/SYMBOL_FUNCTION_CALL[text() = 'all']]
38+
",
39+
lint_message = paste(
40+
"Calling stopifnot(all(x)) is redundant. stopifnot(x) runs all()",
41+
"'under the hood' and provides a better error message in case of failure."
42+
)
43+
)

R/terminal_close_linter.R

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#' Prohibit close() from terminating a function definition
2+
#'
3+
#' Functions that end in `close(x)` are almost always better written by using
4+
#' `on.exit(close(x))` close to where `x` is defined and/or opened.
5+
#'
6+
#' @examples
7+
#' # will produce lints
8+
#' code <- paste(
9+
#' "f <- function(fl) {",
10+
#' " conn <- file(fl, open = 'r')",
11+
#' " readLines(conn)",
12+
#' " close(conn)",
13+
#' "}",
14+
#' sep = "\n"
15+
#' )
16+
#' writeLines(code)
17+
#' lint(
18+
#' text = code,
19+
#' linters = terminal_close_linter()
20+
#' )
21+
#'
22+
#' # okay
23+
#' code <- paste(
24+
#' "f <- function(fl) {",
25+
#' " conn <- file(fl, open = 'r')",
26+
#' " on.exit(close(conn))",
27+
#' " readLines(conn)",
28+
#' "}",
29+
#' sep = "\n"
30+
#' )
31+
#' writeLines(code)
32+
#' lint(
33+
#' text = code,
34+
#' linters = terminal_close_linter()
35+
#' )
36+
#'
37+
#' @evalRd rd_tags("terminal_close_linter")
38+
#' @seealso [linters] for a complete list of linters available in lintr.
39+
#' @export
40+
terminal_close_linter <- make_linter_from_xpath(
41+
xpath = "
42+
//FUNCTION
43+
/following-sibling::expr
44+
/expr[last()][
45+
expr/SYMBOL_FUNCTION_CALL[text() = 'close']
46+
or expr[
47+
SYMBOL_FUNCTION_CALL[text() = 'return']
48+
and following-sibling::expr/expr/SYMBOL_FUNCTION_CALL[text() = 'close']
49+
]
50+
]
51+
",
52+
lint_message = paste(
53+
"Use on.exit(close(x)) to close connections instead of",
54+
"running it as the last call in a function."
55+
)
56+
)

R/unnecessary_lambda_linter.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ unnecessary_lambda_linter <- function() {
6363
# c. that call's _first_ argument is just the function argument (a SYMBOL)
6464
# - and it has to be passed positionally (not as a keyword)
6565
# d. the function argument doesn't appear elsewhere in the call
66-
# TODO(#1703): handle explicit returns too: function(x) return(x)
6766
default_fun_xpath <- glue("
6867
//SYMBOL_FUNCTION_CALL[ {apply_funs} ]
6968
/parent::expr
@@ -76,8 +75,8 @@ unnecessary_lambda_linter <- function() {
7675
position() = 2
7776
and preceding-sibling::expr/SYMBOL_FUNCTION_CALL
7877
and not(preceding-sibling::*[1][self::EQ_SUB])
78+
and not(parent::expr/following-sibling::*[not(self::OP-RIGHT-PAREN or self::OP-RIGHT-BRACE)])
7979
]/SYMBOL
80-
and count(OP-LEFT-PAREN) + count(OP-LEFT-BRACE/following-sibling::expr/OP-LEFT-PAREN) = 1
8180
]
8281
/parent::expr
8382
")

0 commit comments

Comments
 (0)