Skip to content

Commit 57db3b9

Browse files
unnecessary_lambda_linter() finds explicit return cases (#2193)
* condense XPath * whitespace * add tests * stray paren * better naming * simplify * improved naming (again) * condense test * revert bad simplification * fix bad simplification, add new tests * less repetitive/slightly simpler * no nested glue * much simpler (but maybe too simple) * new test for unhandled case * remove repetitive expr[last()] * handle return * note about non-position-1 lambdas in docs
1 parent d93437f commit 57db3b9

File tree

5 files changed

+53
-8
lines changed

5 files changed

+53
-8
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
* `unused_import_linter()` gains an argument `interpret_glue` (default `TRUE`) paralleling that in `object_usage_linter()` to toggle whether `glue::glue()` expressions should be inspected for exported object usage (#2042, @MichaelChirico).
6868
* `sort_linter()` only lints on `order()` of a single vector, excluding e.g. `x[order(x, y)]` and `x[order(y, x)]` (#2156, @MichaelChirico).
6969
* `redundant_ifelse_linter()` is aware of `dplyr::if_else()`'s `missing=` argument, so that `if_else(A, TRUE, FALSE, missing = FALSE)` doesn't lint, but `if_else(A, TRUE, FALSE, NA)` does (#1941, @MichaelChirico). Note that `dplyr::coalesce()` or `tidyr::replace_na()` may still be preferable.
70+
* `unnecessary_lambda_linter()` checks for cases using explicit returns, e.g. `lapply(x, \(xi) return(sum(xi)))` (#1567, @MichaelChirico).
7071
* `default_undesirable_functions` is updated to also include `Sys.unsetenv()` (#2192, @IndrajeetPatil).
7172

7273
### New linters

R/unnecessary_lambda_linter.R

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
#' e.g. `lapply(DF, sum)` is the same as `lapply(DF, function(x) sum(x))` and
55
#' the former is more readable.
66
#'
7+
#' Cases like `lapply(x, \(xi) grep("ptn", xi))` are excluded because, though
8+
#' the anonymous function _can_ be avoided, doing so is not always more
9+
#' readable.
10+
#'
711
#' @examples
812
#' # will produce lints
913
#' lint(
@@ -66,7 +70,7 @@ unnecessary_lambda_linter <- function() {
6670
/following-sibling::expr[(FUNCTION or OP-LAMBDA) and count(SYMBOL_FORMALS) = 1]
6771
/expr[last()][
6872
count(.//SYMBOL[self::* = preceding::SYMBOL_FORMALS[1]]) = 1
69-
and count(.//SYMBOL_FUNCTION_CALL) = 1
73+
and count(.//SYMBOL_FUNCTION_CALL[text() != 'return']) = 1
7074
and preceding-sibling::SYMBOL_FORMALS =
7175
//expr[
7276
position() = 2
@@ -95,7 +99,7 @@ unnecessary_lambda_linter <- function() {
9599
# path to calling function symbol from the matched expressions
96100
fun_xpath <- "./parent::expr/expr/SYMBOL_FUNCTION_CALL"
97101
# path to the symbol of the simpler function that avoids a lambda
98-
symbol_xpath <- glue("(expr|expr[OP-LEFT-BRACE]/expr[1])/expr[SYMBOL_FUNCTION_CALL]")
102+
symbol_xpath <- "expr[last()]//expr[SYMBOL_FUNCTION_CALL[text() != 'return']]"
99103

100104
Linter(function(source_expression) {
101105
if (!is_lint_level(source_expression, "expression")) {

man/lint.Rd

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/unnecessary_lambda_linter.Rd

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-unnecessary_lambda_linter.R

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ test_that("unnecessary_lambda_linter skips allowed usages", {
66
# the first argument may be ... or have a cumbersome name, so an anonymous
77
# function may be preferable (e.g. this is often the case for grep() calls)
88
expect_lint("sapply(x, function(xi) foo(1, xi))", NULL, linter)
9+
expect_lint("sapply(x, function(xi) return(foo(1, xi)))", NULL, linter)
910

1011
# if the argument is re-used, that's also a no-go
1112
expect_lint("dendrapply(x, function(xi) foo(xi, xi))", NULL, linter)
@@ -41,10 +42,7 @@ test_that("unnecessary_lambda_linter skips allowed usages", {
4142
expect_lint("lapply(x, function(xi) tbl %in% xi)", NULL, linter)
4243
# would require multiple lapply() loops
4344
expect_lint("lapply(x, function(xi) foo(bar(xi)))", NULL, linter)
44-
45-
# TODO(michaelchirico): I believe there's cases where it's impossible to avoid an anonymous function due to a
46-
# conflict where you have to pass FUN= to an inner *apply function but it gets interpreted as the outer *apply's FUN
47-
# argument... but it's escaping me now.
45+
expect_lint("lapply(x, function(xi) return(foo(bar(xi))))", NULL, linter)
4846
})
4947

5048
test_that("unnecessary_lambda_linter blocks simple disallowed usage", {
@@ -55,6 +53,11 @@ test_that("unnecessary_lambda_linter blocks simple disallowed usage", {
5553
rex::rex("Pass sum directly as a symbol to lapply()"),
5654
linter
5755
)
56+
expect_lint(
57+
"lapply(DF, function(x) return(sum(x)))",
58+
rex::rex("Pass sum directly as a symbol to lapply()"),
59+
linter
60+
)
5861

5962
expect_lint(
6063
"rapply(l, function(x) is.data.frame(x))",
@@ -67,10 +70,16 @@ test_that("unnecessary_lambda_linter blocks simple disallowed usage", {
6770
rex::rex("Pass sum directly as a symbol to eapply()"),
6871
linter
6972
)
73+
expect_lint(
74+
"eapply(env, function(x) return(sum(x, na.rm = TRUE)))",
75+
rex::rex("Pass sum directly as a symbol to eapply()"),
76+
linter
77+
)
7078
})
7179

7280
test_that("unnecessary_lambda_linter doesn't apply to keyword args", {
7381
expect_lint("lapply(x, function(xi) data.frame(nm = xi))", NULL, unnecessary_lambda_linter())
82+
expect_lint("lapply(x, function(xi) return(data.frame(nm = xi)))", NULL, unnecessary_lambda_linter())
7483
})
7584

7685
test_that("purrr-style anonymous functions are also caught", {
@@ -119,6 +128,32 @@ test_that("cases with braces are caught", {
119128
linter
120129
)
121130

131+
expect_lint(
132+
"lapply(x, function(xi) { return(print(xi)) })",
133+
lint_msg,
134+
linter
135+
)
136+
137+
expect_lint(
138+
trim_some("
139+
lapply(x, function(xi) {
140+
print(xi)
141+
})
142+
"),
143+
lint_msg,
144+
linter
145+
)
146+
147+
expect_lint(
148+
trim_some("
149+
lapply(x, function(xi) {
150+
return(print(xi))
151+
})
152+
"),
153+
lint_msg,
154+
linter
155+
)
156+
122157
expect_lint(
123158
trim_some("
124159
lapply(x, function(xi) {

0 commit comments

Comments
 (0)