Skip to content

Commit 5f18b79

Browse files
Extend allow_filter in conjunct_test_linter() to allow linting only dplyr::filter() (i.e. qualified) (#2169)
* extend allow_dplyr in conjunct_test_linter() for finer tuning * issue in NEWS * Update test-conjunct_test_linter.R --------- Co-authored-by: AshesITR <alexander.rosenstock@web.de>
1 parent 8cdf26c commit 5f18b79

File tree

4 files changed

+86
-61
lines changed

4 files changed

+86
-61
lines changed

NEWS.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
+ `yoda_test_linter()`
3636
* `sprintf_linter()` is pipe-aware, so that `x %>% sprintf(fmt = "%s")` no longer lints (#1943, @MichaelChirico).
3737
* `line_length_linter()` helpfully includes the line length in the lint message (#2057, @MichaelChirico).
38-
* `conjunct_test_linter()` also lints usage like `dplyr::filter(x, A & B)` in favor of using `dplyr::filter(x, A, B)` unless `allow_filter = TRUE` (part of #884, @MichaelChirico; #2110, @salim-b).
38+
* `conjunct_test_linter()` also lints usage like `dplyr::filter(x, A & B)` in favor of using `dplyr::filter(x, A, B)` (part of #884; #2110 and #2078, @salim-b and @MichaelChirico). Option `allow_filter` toggles when this applies. `allow_filter = "always"` drops such lints entirely, while `"not_dplyr"` only lints calls explicitly qualified as `dplyr::filter()`. The default, `"never"`, assumes all unqualified calls to `filter()` are `dplyr::filter()`.
3939
* `sort_linter()` checks for code like `x == sort(x)` which is better served by using the function `is.unsorted()` (part of #884, @MichaelChirico).
4040
* `paste_linter()` gains detection for file paths that are better constructed with `file.path()`, e.g. `paste0(dir, "/", file)` would be better as `file.path(dir, file)` (part of #884, #2082, @MichaelChirico). What exactly gets linted here can be fine-tuned with the `allow_file_path` option (`"double_slash"` by default, with alternatives `"never"` and `"always"`). When `"always"`, these rules are ignored. When `"double_slash"`, paths appearing to construct a URL that have consecutive forward slashes (`/`) are skipped. When `"never"`, even URLs should be construced with `file.path()`.
4141
* `seq_linter()` recommends `rev()` in the lint message for lints like `nrow(x):1` (#1542, @MichaelChirico).

R/conjunct_test_linter.R

+31-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
#'
1515
#' @param allow_named_stopifnot Logical, `TRUE` by default. If `FALSE`, "named" calls to `stopifnot()`,
1616
#' available since R 4.0.0 to provide helpful messages for test failures, are also linted.
17-
#' @param allow_filter Logical, `FALSE` by default. If `TRUE`, `filter()` expressions are not linted.
17+
#' @param allow_filter Character naming the method for linting calls to `filter()`. The default, `"never"`, means
18+
#' `filter()` and `dplyr::filter()` calls are linted; `"not_dplyr"` means only `dplyr::filter()` calls are linted;
19+
#' and `"always"` means no calls to `filter()` are linted. Calls like `stats::filter()` are never linted.
1820
#'
1921
#' @examples
2022
#' # will produce lints
@@ -38,6 +40,11 @@
3840
#' linters = conjunct_test_linter()
3941
#' )
4042
#'
43+
#' lint(
44+
#' text = "filter(mtcars, mpg > 20 & vs == 0)",
45+
#' linters = conjunct_test_linter()
46+
#' )
47+
#'
4148
#' # okay
4249
#' lint(
4350
#' text = "expect_true(x || (y && z))",
@@ -51,14 +58,26 @@
5158
#'
5259
#' lint(
5360
#' text = "dplyr::filter(mtcars, mpg > 20 & vs == 0)",
54-
#' linters = conjunct_test_linter(allow_filter = TRUE)
61+
#' linters = conjunct_test_linter(allow_filter = "always")
62+
#' )
63+
#'
64+
#' lint(
65+
#' text = "filter(mtcars, mpg > 20 & vs == 0)",
66+
#' linters = conjunct_test_linter(allow_filter = "not_dplyr")
67+
#' )
68+
#'
69+
#' lint(
70+
#' text = "stats::filter(mtcars$cyl, mtcars$mpg > 20 & mtcars$vs == 0)",
71+
#' linters = conjunct_test_linter()
5572
#' )
5673
#'
5774
#' @evalRd rd_tags("conjunct_test_linter")
5875
#' @seealso [linters] for a complete list of linters available in lintr.
5976
#' @export
6077
conjunct_test_linter <- function(allow_named_stopifnot = TRUE,
61-
allow_filter = FALSE) {
78+
allow_filter = c("never", "not_dplyr", "always")) {
79+
allow_filter <- match.arg(allow_filter)
80+
6281
expect_true_assert_that_xpath <- "
6382
//SYMBOL_FUNCTION_CALL[text() = 'expect_true' or text() = 'assert_that']
6483
/parent::expr
@@ -85,12 +104,17 @@ conjunct_test_linter <- function(allow_named_stopifnot = TRUE,
85104
sep = " | "
86105
)
87106

88-
filter_xpath <- "
107+
filter_ns_cond <- switch(allow_filter,
108+
never = "not(SYMBOL_PACKAGE[text() != 'dplyr'])",
109+
not_dplyr = "SYMBOL_PACKAGE[text() = 'dplyr']",
110+
always = "true"
111+
)
112+
filter_xpath <- glue("
89113
//SYMBOL_FUNCTION_CALL[text() = 'filter']
90-
/parent::expr[not(SYMBOL_PACKAGE[text() != 'dplyr'])]
114+
/parent::expr[{ filter_ns_cond }]
91115
/parent::expr
92116
/expr[AND]
93-
"
117+
")
94118

95119
Linter(function(source_expression) {
96120
# need the full file to also catch usages at the top level
@@ -122,7 +146,7 @@ conjunct_test_linter <- function(allow_named_stopifnot = TRUE,
122146
type = "warning"
123147
)
124148

125-
if (!allow_filter) {
149+
if (allow_filter != "always") {
126150
filter_expr <- xml_find_all(xml, filter_xpath)
127151

128152
filter_lints <- xml_nodes_to_lints(

man/conjunct_test_linter.Rd

+23-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-conjunct_test_linter.R

+31-50
Original file line numberDiff line numberDiff line change
@@ -33,67 +33,40 @@ test_that("conjunct_test_linter blocks && conditions with expect_true()", {
3333
})
3434

3535
test_that("conjunct_test_linter blocks || conditions with expect_false()", {
36-
expect_lint(
37-
"expect_false(x || y)",
38-
rex::rex("Instead of expect_false(A || B), write multiple expectations"),
39-
conjunct_test_linter()
40-
)
36+
linter <- conjunct_test_linter()
37+
lint_msg <- rex::rex("Instead of expect_false(A || B), write multiple expectations")
4138

42-
expect_lint(
43-
"expect_false(x || y || z)",
44-
rex::rex("Instead of expect_false(A || B), write multiple expectations"),
45-
conjunct_test_linter()
46-
)
39+
expect_lint("expect_false(x || y)", lint_msg, linter)
40+
expect_lint("expect_false(x || y || z)", lint_msg, linter)
4741

4842
# these lint because `||` is always outer by operator precedence
49-
expect_lint(
50-
"expect_false(x || y && z)",
51-
rex::rex("Instead of expect_false(A || B), write multiple expectations"),
52-
conjunct_test_linter()
53-
)
54-
expect_lint(
55-
"expect_false(x && y || z)",
56-
rex::rex("Instead of expect_false(A || B), write multiple expectations"),
57-
conjunct_test_linter()
58-
)
43+
expect_lint("expect_false(x || y && z)", lint_msg, linter)
44+
expect_lint("expect_false(x && y || z)", lint_msg, linter)
5945
})
6046

6147
test_that("conjunct_test_linter skips allowed stopifnot() and assert_that() usages", {
62-
expect_lint("stopifnot(x)", NULL, conjunct_test_linter())
63-
expect_lint("assert_that(x, y, z)", NULL, conjunct_test_linter())
48+
linter <- conjunct_test_linter()
49+
50+
expect_lint("stopifnot(x)", NULL, linter)
51+
expect_lint("assert_that(x, y, z)", NULL, linter)
6452

6553
# more complicated expression
66-
expect_lint("stopifnot(x || (y && z))", NULL, conjunct_test_linter())
54+
expect_lint("stopifnot(x || (y && z))", NULL, linter)
6755
# the same by operator precedence, though not obvious a priori
68-
expect_lint("stopifnot(x || y && z)", NULL, conjunct_test_linter())
69-
expect_lint("assertthat::assert_that(x && y || z)", NULL, conjunct_test_linter())
56+
expect_lint("stopifnot(x || y && z)", NULL, linter)
57+
expect_lint("assertthat::assert_that(x && y || z)", NULL, linter)
7058
})
7159

7260
test_that("conjunct_test_linter blocks simple disallowed usages of stopifnot() and assert_that()", {
73-
expect_lint(
74-
"stopifnot(x && y)",
75-
rex::rex("Instead of stopifnot(A && B), write multiple conditions"),
76-
conjunct_test_linter()
77-
)
61+
linter <- conjunct_test_linter()
62+
lint_msg <- function(fun) rex::rex("Instead of ", fun, "(A && B), write multiple conditions")
7863

79-
expect_lint(
80-
"stopifnot(x && y && z)",
81-
rex::rex("Instead of stopifnot(A && B), write multiple conditions"),
82-
conjunct_test_linter()
83-
)
64+
expect_lint("stopifnot(x && y)", lint_msg("stopifnot"), linter)
65+
expect_lint("stopifnot(x && y && z)", lint_msg("stopifnot"), linter)
8466

8567
# assert_that() versions
86-
expect_lint(
87-
"assert_that(x && y)",
88-
rex::rex("Instead of assert_that(A && B), write multiple conditions"),
89-
conjunct_test_linter()
90-
)
91-
92-
expect_lint(
93-
"assertthat::assert_that(x && y && z)",
94-
rex::rex("Instead of assert_that(A && B), write multiple conditions"),
95-
conjunct_test_linter()
96-
)
68+
expect_lint("assert_that(x && y)", lint_msg("assert_that"), linter)
69+
expect_lint("assertthat::assert_that(x && y && z)", lint_msg("assert_that"), linter)
9770
})
9871

9972
test_that("conjunct_test_linter's allow_named_stopifnot argument works", {
@@ -132,11 +105,19 @@ test_that("conjunct_test_linter blocks simple disallowed usages", {
132105
})
133106

134107
test_that("conjunct_test_linter respects its allow_filter argument", {
135-
linter <- conjunct_test_linter(allow_filter = TRUE)
108+
linter_always <- conjunct_test_linter(allow_filter = "always")
109+
linter_dplyr <- conjunct_test_linter(allow_filter = "not_dplyr")
110+
lint_msg <- rex::rex("Use dplyr::filter(DF, A, B) instead of dplyr::filter(DF, A & B)")
136111

137-
expect_lint("dplyr::filter(DF, A & B)", NULL, linter)
138-
expect_lint("dplyr::filter(DF, A & B & C)", NULL, linter)
139-
expect_lint("DF %>% dplyr::filter(A & B)", NULL, linter)
112+
expect_lint("dplyr::filter(DF, A & B)", NULL, linter_always)
113+
expect_lint("dplyr::filter(DF, A & B & C)", NULL, linter_always)
114+
expect_lint("DF %>% dplyr::filter(A & B)", NULL, linter_always)
115+
expect_lint("dplyr::filter(DF, A & B)", lint_msg, linter_dplyr)
116+
expect_lint("dplyr::filter(DF, A & B & C)", lint_msg, linter_dplyr)
117+
expect_lint("DF %>% dplyr::filter(A & B)", lint_msg, linter_dplyr)
118+
expect_lint("filter(DF, A & B)", NULL, linter_dplyr)
119+
expect_lint("filter(DF, A & B & C)", NULL, linter_dplyr)
120+
expect_lint("DF %>% filter(A & B)", NULL, linter_dplyr)
140121
})
141122

142123
test_that("filter() is assumed to be dplyr::filter() by default, unless o/w specified", {

0 commit comments

Comments
 (0)