Skip to content

Commit acaa5c7

Browse files
Create all_equal_linter() (#2885)
* Create all_equal_linter() * Add all_equal_linter() in NEWS * Add lint message and vectorize test * Fix typo * Merge XPaths and cover while() case * Add tests with tolerance arg * Lint isFALSE(all.equal()) * Customize error message when tolerance is present * Fix long line lint * Add test for false positive * Ensure only all.equal() in if/while calls is linted * avoid paste() --------- Co-authored-by: Michael Chirico <[email protected]>
1 parent 16ae3dc commit acaa5c7

File tree

10 files changed

+201
-2
lines changed

10 files changed

+201
-2
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Collate:
6868
'absolute_path_linter.R'
6969
'actions.R'
7070
'addins.R'
71+
'all_equal_linter.R'
7172
'any_duplicated_linter.R'
7273
'any_is_na_linter.R'
7374
'assignment_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export(Lint)
1515
export(Linter)
1616
export(T_and_F_symbol_linter)
1717
export(absolute_path_linter)
18+
export(all_equal_linter)
1819
export(all_linters)
1920
export(all_undesirable_functions)
2021
export(all_undesirable_operators)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151

5252
### New linters
5353

54+
* `all_equal_linter()` warns about incorrect use of `all.equal()` in `if` clauses or preceded by `!` (#2885, @Bisaloo).
5455
* `download_file_linter()` encourages the use of `mode = "wb"` (or `mode = "ab"`) when using `download.file()`, rather than `mode = "w"` or `mode = "a"`, as the latter can produce broken
5556
files in Windows (#2882, @Bisaloo).
5657
* `lint2df_linter()` encourages the use of the `list2DF()` function, or the `data.frame()` function when recycling is required, over the slower and less readable `do.call(cbind.data.frame, )` alternative (#2834, @Bisaloo).

R/all_equal_linter.R

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#' Warn about invalid usage of `all.equal()`
2+
#'
3+
#' [all.equal()] returns `TRUE` in the absence of differences but return a
4+
#' character string (not `FALSE`) in the presence of differences.
5+
#' Usage of `all.equal()` without wrapping it in `isTRUE()` in `if` clauses, or
6+
#' preceded by the negation operator `!`, are thus likely to generate unexpected
7+
#' errors if the compared objects have differences.
8+
#' An alternative is to use `identical()` to compare vector of strings or when
9+
#' exact equality is expected.
10+
#'
11+
#' @examples
12+
#' # lints
13+
#' lint(
14+
#' text = 'if (all.equal(a, b)) message("equal")',
15+
#' linters = all_equal_linter()
16+
#' )
17+
#'
18+
#' lint(
19+
#' text = '!all.equal(a, b)',
20+
#' linters = all_equal_linter()
21+
#' )
22+
#'
23+
#' lint(
24+
#' text = 'isFALSE(all.equal(a, b))',
25+
#' linters = all_equal_linter()
26+
#' )
27+
#'
28+
#' # okay
29+
#' lint(
30+
#' text = 'if (isTRUE(all.equal(a, b))) message("equal")',
31+
#' linters = all_equal_linter()
32+
#' )
33+
#'
34+
#' lint(
35+
#' text = '!identical(a, b)',
36+
#' linters = all_equal_linter()
37+
#' )
38+
#'
39+
#' lint(
40+
#' text = "!isTRUE(all.equal(a, b))",
41+
#' linters = all_equal_linter()
42+
#' )
43+
#'
44+
#' @evalRd rd_tags("all_equal_linter")
45+
#' @seealso [linters] for a complete list of linters available in lintr.
46+
#' @export
47+
all_equal_linter <- function() {
48+
49+
Linter(linter_level = "expression", function(source_expression) {
50+
all_equal_calls <- source_expression$xml_find_function_calls("all.equal")
51+
52+
dangerous_unwrapped_all_equal <- xml_find_all(
53+
all_equal_calls,
54+
"parent::expr[
55+
preceding-sibling::*[not(self::COMMENT)][2][self::IF or self::WHILE]
56+
or preceding-sibling::*[not(self::COMMENT)][1][self::OP-EXCLAMATION]
57+
]"
58+
)
59+
60+
has_tolerance_arg <- !is.na(
61+
xml_find_first(dangerous_unwrapped_all_equal, "SYMBOL_SUB[text() = 'tolerance']")
62+
)
63+
64+
unwrapped_all_equal_lints <- xml_nodes_to_lints(
65+
dangerous_unwrapped_all_equal,
66+
source_expression = source_expression,
67+
lint_message = ifelse(
68+
has_tolerance_arg,
69+
"Wrap all.equal() in isTRUE().",
70+
"Wrap all.equal() in isTRUE(), or replace it by identical() if no tolerance is required."
71+
),
72+
type = "warning"
73+
)
74+
75+
is_false_all_equal <- xml_find_all(
76+
all_equal_calls,
77+
"parent::expr[preceding-sibling::expr[1]/SYMBOL_FUNCTION_CALL/text() = 'isFALSE']"
78+
)
79+
80+
is_false_all_equal_lints <- xml_nodes_to_lints(
81+
is_false_all_equal,
82+
source_expression = source_expression,
83+
lint_message =
84+
"Use !isTRUE() to check for differences in all.equal(). isFALSE(all.equal()) always returns FALSE.",
85+
type = "warning"
86+
)
87+
88+
c(unwrapped_all_equal_lints, is_false_all_equal_lints)
89+
})
90+
}

inst/lintr/linters.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
linter,tags
22
absolute_path_linter,robustness best_practices configurable
3+
all_equal_linter,common_mistakes robustness
34
any_duplicated_linter,efficiency best_practices
45
any_is_na_linter,efficiency best_practices
56
assignment_linter,style consistency default configurable

man/all_equal_linter.Rd

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

man/common_mistakes_linters.Rd

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

man/linters.Rd

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

man/robustness_linters.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
test_that("all_equal_linter() skips allowed usages", {
2+
linter <- all_equal_linter()
3+
4+
# Only when used in if
5+
expect_no_lint("all.equal(a, b)", linter)
6+
expect_no_lint("all.equal(a, b, tolerance = 1e-3)", linter)
7+
8+
expect_no_lint("if (isTRUE(all.equal(a, b))) message('equal')", linter)
9+
expect_no_lint("if (!isTRUE(all.equal(a, b))) message('different')", linter)
10+
11+
expect_no_lint("if (A) all.equal(x, y)", linter)
12+
})
13+
14+
test_that("all_equal_linter() blocks simple disallowed usages", {
15+
linter <- all_equal_linter()
16+
lint_message <- rex::rex("Wrap all.equal() in isTRUE().")
17+
expect_lint("if (all.equal(a, b, tolerance = 1e-3)) message('equal')", lint_message, linter)
18+
19+
lint_message <- rex::rex("Wrap all.equal() in isTRUE(), or replace it by identical()")
20+
expect_lint("if (all.equal(a, b)) message('equal')", lint_message, linter)
21+
expect_lint("!all.equal(a, b)", lint_message, linter)
22+
expect_lint("while (all.equal(a, b)) message('equal')", lint_message, linter)
23+
24+
lint_message <- rex::rex("!isTRUE()")
25+
expect_lint("isFALSE(all.equal(a, b))", lint_message, linter)
26+
})
27+
28+
test_that("lints vectorize", {
29+
lint_message <- rex::rex("Wrap all.equal() in isTRUE()")
30+
31+
expect_lint(
32+
trim_some("{
33+
all.equal(a, b)
34+
!all.equal(a, b)
35+
!isTRUE(all.equal(a, b))
36+
if ( # test
37+
all.equal(a, b)) message('equal')
38+
}"),
39+
list(
40+
list(lint_message, line_number = 3L),
41+
list(lint_message, line_number = 6L)
42+
),
43+
all_equal_linter()
44+
)
45+
})

0 commit comments

Comments
 (0)