Skip to content

Commit c315baf

Browse files
Extend boolean_arithmetic_linter() to other known-logical sum(<lgl>) cases (#2814)
* expect_no_lint * extensions for other known-logical vector cases * streamline & expand tests --------- Co-authored-by: AshesITR <[email protected]>
1 parent cf9758a commit c315baf

File tree

3 files changed

+45
-14
lines changed

3 files changed

+45
-14
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* `get_source_expression()` captures warnings emitted by the R parser (currently always for mis-specified literal integers like `1.1L`) and `lint()` returns them as lints (#2065, @MichaelChirico).
3030
* `object_name_linter()` and `object_length_linter()` apply to objects assigned with `assign()` or generics created with `setGeneric()` (#1665, @MichaelChirico).
3131
* `object_usage_linter()` gains argument `interpret_extensions` to govern which false positive-prone common syntaxes should be checked for used objects (#1472, @MichaelChirico). Currently `"glue"` (renamed from earlier argument `interpret_glue`) and `"rlang"` are supported. The latter newly covers usage of the `.env` pronoun like `.env$key`, where `key` was previously missed as being a used variable.
32+
* `boolean_arithmetic_linter()` finds many more cases like `sum(x | y) == 0` where the total of a known-logical vector is compared to 0 (#1580, @MichaelChirico).
3233

3334
### New linters
3435

R/boolean_arithmetic_linter.R

+12-6
Original file line numberDiff line numberDiff line change
@@ -30,22 +30,28 @@
3030
#' @seealso [linters] for a complete list of linters available in lintr.
3131
#' @export
3232
boolean_arithmetic_linter <- function() {
33-
# TODO(#1580): sum() cases x %in% y, A [&|] B, !A, is.na/is.nan/is.finite/is.infinite/is.element
3433
# TODO(#1581): extend to include all()-alike expressions
35-
zero_expr <- "(EQ or NE or GT or LE) and expr[NUM_CONST[text() = '0' or text() = '0L']]"
36-
one_expr <- "(LT or GE) and expr[NUM_CONST[text() = '1' or text() = '1L']]"
34+
zero_expr <- "(EQ or NE or GT or LE) and expr/NUM_CONST[text() = '0' or text() = '0L']"
35+
one_expr <- "(LT or GE) and expr/NUM_CONST[text() = '1' or text() = '1L']"
3736
length_xpath <- glue("
3837
parent::expr
3938
/parent::expr[
40-
expr[SYMBOL_FUNCTION_CALL[text() = 'length']]
39+
expr/SYMBOL_FUNCTION_CALL[text() = 'length']
4140
and parent::expr[ ({zero_expr}) or ({one_expr})]
4241
]
4342
")
43+
known_logical_calls <- c(
44+
"grepl", "str_detect", "nzchar", "startsWith", "endsWith",
45+
"xor", "is.element", "duplicated",
46+
"is.na", "is.nan", "is.finite", "is.infinite",
47+
NULL
48+
)
4449
sum_xpath <- glue("
4550
parent::expr[
4651
expr[
47-
expr[SYMBOL_FUNCTION_CALL[text() = 'grepl']]
48-
or (EQ or NE or GT or LT or GE or LE)
52+
expr/SYMBOL_FUNCTION_CALL[{xp_text_in_table(known_logical_calls)}]
53+
or (EQ or NE or GT or LT or GE or LE or AND or OR or OP-EXCLAMATION)
54+
or SPECIAL[text() = '%in%' or text() = '%chin%']
4955
]
5056
and parent::expr[ ({zero_expr}) or ({one_expr})]
5157
]")

tests/testthat/test-boolean_arithmetic_linter.R

+32-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
test_that("boolean_arithmetic_linter doesn't block allowed usages", {
22
linter <- boolean_arithmetic_linter()
33

4-
expect_lint("!any(x == y)", NULL, linter)
5-
expect_lint("!any(grepl(pattern, x))", NULL, linter)
4+
expect_no_lint("!any(x == y)", linter)
5+
expect_no_lint("!any(grepl(pattern, x))", linter)
66
})
77

8-
test_that("boolean_arithmetic_linter requires use of any() or !any()", {
8+
test_that("boolean_arithmetic_linter requires use of any() or !any() over length(.(<logical>))", {
99
linter <- boolean_arithmetic_linter()
1010
lint_msg <- rex::rex("Use any() to express logical aggregations.")
1111

@@ -14,16 +14,40 @@ test_that("boolean_arithmetic_linter requires use of any() or !any()", {
1414
expect_lint("length(which(is_treatment)) == 0L", lint_msg, linter)
1515
# regex version
1616
expect_lint("length(grep(pattern, x)) == 0", lint_msg, linter)
17-
# sum version
18-
expect_lint("sum(x == y) == 0L", lint_msg, linter)
19-
expect_lint("sum(grepl(pattern, x)) == 0", lint_msg, linter)
2017

2118
# non-== comparisons
2219
expect_lint("length(which(x == y)) > 0L", lint_msg, linter)
2320
expect_lint("length(which(is_treatment)) < 1", lint_msg, linter)
2421
expect_lint("length(grep(pattern, x)) >= 1L", lint_msg, linter)
25-
expect_lint("sum(x == y) != 0", lint_msg, linter)
26-
expect_lint("sum(grepl(pattern, x)) > 0L", lint_msg, linter)
22+
})
23+
24+
local({
25+
linter <- boolean_arithmetic_linter()
26+
lint_msg <- rex::rex("Use any() to express logical aggregations.")
27+
28+
outer_comparisons <- c("== 0", "== 0L", "> 0L", "> 0L", ">= 1", ">= 1L")
29+
30+
patrick::with_parameters_test_that(
31+
"sum(x {op} y) {outer} lints",
32+
expect_lint(sprintf("sum(x %s y) %s", op, outer), lint_msg, linter),
33+
.cases = expand.grid(
34+
op = c("==", "!=", ">", "<", ">=", "<=", "&", "|", "%in%", "%chin%"),
35+
outer = outer_comparisons
36+
)
37+
)
38+
39+
patrick::with_parameters_test_that(
40+
"sum({op}(x)) == 0 lints",
41+
expect_lint(sprintf("sum(%s(x)) == 0", op), lint_msg, linter),
42+
.cases = expand.grid(
43+
op = c(
44+
"!", "xor", "grepl", "str_detect", "is.element",
45+
"is.na", "is.finite", "is.infinite", "is.nan",
46+
"duplicated", "nzchar", "startsWith", "endsWith"
47+
),
48+
outer = outer_comparisons
49+
)
50+
)
2751
})
2852

2953
test_that("lints vectorize", {

0 commit comments

Comments
 (0)