Skip to content

Commit 4c88419

Browse files
allow_lazy argument for implicit_assignment_linter to skip lazy assignment (#2140)
* simplify implicit_assignment_linter logic, and catch false negatives * narrow line * add a simpler test of lints in all call args * catch parenthetical assignments * allow_lazy argument for implicit_assignment_linter to skip lazy assignments * + not * for bullet (consistency) --------- Co-authored-by: Indrajeet Patil <[email protected]>
1 parent 5f18b79 commit 4c88419

File tree

4 files changed

+55
-2
lines changed

4 files changed

+55
-2
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
* `implicit_assignment_linter()`
4949
+ finds assignments in call arguments besides the first one (#2136, @MichaelChirico).
5050
+ finds assignments in parenthetical expressions like `if (A && (B <- foo(A))) { }` (#2138, @MichaelChirico).
51+
+ gains an argument `allow_lazy` (default `FALSE`) that allows optionally skipping lazy assignments like `A && (B <- foo(A))` (#2016, @MichaelChirico).
5152
* `implicit_assignment_linter()` finds assignments in call arguments besides the first one (#2136, @MichaelChirico).
5253
* `inner_combine_linter()` no longer throws on length-1 calls to `c()` like `c(exp(2))` or `c(log(3))` (#2017, @MichaelChirico). Such usage is discouraged by `unnecessary_concatenation_linter()`, but `inner_combine_linter()` _per se_ does not apply.
5354
* `condition_message_linter()` ignores usages of extracted calls like `env$stop(paste(a, b))` (#1455, @MichaelChirico).

R/implicit_assignment_linter.R

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#' be avoided, except for functions that capture side-effects (e.g. [capture.output()]).
55
#'
66
#' @param except A character vector of functions to be excluded from linting.
7+
#' @param allow_lazy logical, default `FALSE`. If `TRUE`, assignments that only
8+
#' trigger conditionally (e.g. in the RHS of `&&` or `||` expressions) are skipped.
79
#'
810
#' @examples
911
#' # will produce lints
@@ -30,13 +32,19 @@
3032
#' linters = implicit_assignment_linter()
3133
#' )
3234
#'
35+
#' lint(
36+
#' text = "A && (B <- foo(A))",
37+
#' linters = implicit_assignment_linter(allow_lazy = TRUE)
38+
#' )
39+
#'
3340
#' @evalRd rd_tags("implicit_assignment_linter")
3441
#' @seealso
3542
#' - [linters] for a complete list of linters available in lintr.
3643
#' - <https://style.tidyverse.org/syntax.html#assignment>
3744
#'
3845
#' @export
39-
implicit_assignment_linter <- function(except = c("bquote", "expression", "expr", "quo", "quos", "quote")) {
46+
implicit_assignment_linter <- function(except = c("bquote", "expression", "expr", "quo", "quos", "quote"),
47+
allow_lazy = FALSE) {
4048
stopifnot(is.null(except) || is.character(except))
4149

4250
if (length(except) > 0L) {
@@ -64,6 +72,10 @@ implicit_assignment_linter <- function(except = c("bquote", "expression", "expr"
6472
]
6573
")
6674

75+
if (allow_lazy) {
76+
xpath <- paste0(xpath, "[not(ancestor::expr/preceding-sibling::*[self::AND2 or self::OR2])]")
77+
}
78+
6779
Linter(function(source_expression) {
6880
# need the full file to also catch usages at the top level
6981
if (!is_lint_level(source_expression, "file")) {

man/implicit_assignment_linter.Rd

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

tests/testthat/test-implicit_assignment_linter.R

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,37 @@ test_that("implicit_assignment_linter works as expected with pipes and walrus op
349349
test_that("parenthetical assignments are caught", {
350350
linter <- implicit_assignment_linter()
351351
lint_message <- rex::rex("Avoid implicit assignments in function calls.")
352+
352353
expect_lint("(x <- 1:10)", lint_message, linter)
353354
expect_lint("if (A && (B <- foo())) { }", lint_message, linter)
354355
})
356+
357+
test_that("allow_lazy lets lazy assignments through", {
358+
linter <- implicit_assignment_linter(allow_lazy = TRUE)
359+
lint_message <- rex::rex("Avoid implicit assignments in function calls.")
360+
361+
expect_lint("A && (B <- foo(A))", NULL, linter)
362+
# || also admits laziness
363+
expect_lint("A || (B <- foo(A))", NULL, linter)
364+
# & and |, however, do not
365+
expect_lint("A & (B <- foo(A))", lint_message, linter)
366+
expect_lint("A | (B <- foo(A))", lint_message, linter)
367+
expect_lint("A && foo(bar(idx <- baz()))", NULL, linter)
368+
# LHS _is_ linted
369+
expect_lint("(A <- foo()) && B", lint_message, linter)
370+
# however we skip on _any_ RHS (even if it's later an LHS)
371+
# test on all &&/|| combinations to stress test operator precedence
372+
expect_lint("A && (B <- foo(A)) && C", NULL, linter)
373+
expect_lint("A && (B <- foo(A)) || C", NULL, linter)
374+
expect_lint("A || (B <- foo(A)) && C", NULL, linter)
375+
expect_lint("A || (B <- foo(A)) || C", NULL, linter)
376+
# &&/|| elsewhere in the tree don't matter
377+
expect_lint(
378+
trim_some("
379+
A && B
380+
foo(C <- bar())
381+
"),
382+
lint_message,
383+
linter
384+
)
385+
})

0 commit comments

Comments
 (0)