Skip to content

Commit dc734b7

Browse files
New expect_identical_linter (#958)
* New expect_identical_linter * comment about double-lint test * simply test for any named argument * shorten message; put details in man page * @section Co-authored-by: AshesITR <alexander.rosenstock@web.de>
1 parent 9be262c commit dc734b7

File tree

9 files changed

+184
-1
lines changed

9 files changed

+184
-1
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Collate:
6363
'equals_na_linter.R'
6464
'exclude.R'
6565
'expect_comparison_linter.R'
66+
'expect_identical_linter.R'
6667
'expect_length_linter.R'
6768
'expect_lint.R'
6869
'expect_named_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export(default_undesirable_operators)
2929
export(duplicate_argument_linter)
3030
export(equals_na_linter)
3131
export(expect_comparison_linter)
32+
export(expect_identical_linter)
3233
export(expect_length_linter)
3334
export(expect_lint)
3435
export(expect_lint_free)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ function calls. (#850, #851, @renkun-ken)
9494
+ `expect_true_false_linter()` Require usage of `expect_true(x)` over `expect_equal(x, TRUE)` and similar
9595
+ `expect_named_linter()` Require usage of `expect_named(x, n)` over `expect_equal(names(x), n)` and similar
9696
* `expect_length_linter()` Require usage of `expect_length(x, n)` over `expect_equal(length(x), n)` and similar
97+
* `expect_identical_linter()` Require usage of `expect_identical()` by default, and `expect_equal()` only by exception
9798
* `expect_comparison_linter()` Require usage of `expect_gt(x, y)` over `expect_true(x > y)` and similar
9899
* `assignment_linter()` now lints right assignment (`->` and `->>`) and gains two arguments. `allow_cascading_assign` (`TRUE` by default) toggles whether to lint `<<-` and `->>`; `allow_right_assign` toggles whether to lint `->` and `->>` (#915, @michaelchirico)
99100
* `infix_spaces_linter()` gains argument `exclude_operators` to disable lints on selected infix operators. By default, all "low-precedence" operators throw lints; see `?infix_spaces_linter` for an enumeration of these. (#914 @michaelchirico)

R/expect_identical_linter.R

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#' Require usage of expect_identical(x, y) where appropriate
2+
#'
3+
#' At Google, [testthat::expect_identical()] should be the default/go-to function for
4+
#' comparing an output to an expected value. `expect_true(identical(x, y))`
5+
#' is an equivalent but unadvised method of the same test. Further,
6+
#' [testthat::expect_equal()] should only be used when `expect_identical()`
7+
#' is inappropriate, i.e., when `x` and `y` need only be *numerically
8+
#' equivalent* instead of fully identical (in which case, provide the
9+
#' `tolerance=` argument to `expect_equal()` explicitly). This also applies
10+
#' when it's inconvenient to check full equality (e.g., names can be ignored,
11+
#' in which case `ignore_attr = "names"` should be supplied to
12+
#' `expect_equal()` (or, for 2nd edition, `check.attributes = FALSE`).
13+
#'
14+
#' @section Exceptions:
15+
#'
16+
#' The linter allows `expect_equal()` in three circumstances:
17+
#' 1. A named argument is set (e.g. `ignore_attr` or `tolerance`)
18+
#' 2. Comparison is made to an explicit decimal, e.g.
19+
#' `expect_equal(x, 1.0)` (implicitly setting `tolerance`)
20+
#' 3. `...` is passed (wrapper functions whcih might set
21+
#' arguments such as `ignore_attr` or `tolerance`)
22+
#'
23+
#' @evalRd rd_tags("expect_identical_linter")
24+
#' @seealso [linters] for a complete list of linters available in lintr.
25+
#' @export
26+
expect_identical_linter <- function() {
27+
Linter(function(source_file) {
28+
if (length(source_file$parsed_content) == 0L) {
29+
return(list())
30+
}
31+
32+
xml <- source_file$xml_parsed_content
33+
34+
# outline:
35+
# 1. conditions for expect_equal()
36+
# - skip when any named argument is set. most commonly this
37+
# is check.attributes (for 2e tests) or one of the ignore_*
38+
# arguments (for 3e tests). This will generate some false
39+
# negatives, but will be much easier to maintain.
40+
# - skip cases like expect_equal(x, 1.02) or the constant vector version
41+
# where a numeric constant indicates inexact testing is preferable
42+
# - skip calls using dots (`...`); see tests
43+
# 2. conditions for expect_true()
44+
xpath <- glue::glue("//expr[
45+
(
46+
SYMBOL_FUNCTION_CALL[text() = 'expect_equal']
47+
and not(
48+
following-sibling::SYMBOL_SUB
49+
or following-sibling::expr[
50+
expr[SYMBOL_FUNCTION_CALL[text() = 'c']]
51+
and expr[NUM_CONST[contains(text(), '.')]]
52+
]
53+
or following-sibling::expr[NUM_CONST[contains(text(), '.')]]
54+
or following-sibling::expr[SYMBOL[text() = '...']]
55+
)
56+
) or (
57+
SYMBOL_FUNCTION_CALL[text() = 'expect_true']
58+
and following-sibling::expr[1][
59+
expr[SYMBOL_FUNCTION_CALL[text() = 'identical']]
60+
]
61+
)
62+
]")
63+
64+
bad_expr <- xml2::xml_find_all(xml, xpath)
65+
return(lapply(
66+
bad_expr,
67+
xml_nodes_to_lint,
68+
source_file = source_file,
69+
lint_message = paste(
70+
"Use expect_identical(x, y) by default; resort to expect_equal() only when needed,",
71+
"e.g. when setting ignore_attr= or tolerance=."
72+
),
73+
type = "warning"
74+
))
75+
})
76+
}

inst/lintr/linters.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ cyclocomp_linter,style readability best_practices default configurable
99
duplicate_argument_linter,correctness common_mistakes configurable
1010
equals_na_linter,robustness correctness common_mistakes default
1111
expect_comparison_linter,package_development best_practices
12+
expect_identical_linter,package_development
1213
expect_length_linter,package_development best_practices readability
1314
expect_named_linter,package_development best_practices readability
1415
expect_not_linter,package_development best_practices readability

man/expect_identical_linter.Rd

Lines changed: 39 additions & 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: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/package_development_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: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
test_that("expect_identical_linter skips allowed usages", {
2+
# expect_type doesn't have an inverted version
3+
expect_lint("expect_true(identical(x, y) || identical(y, z))", NULL, expect_identical_linter())
4+
# NB: also applies to tinytest, but it's sufficient to test testthat
5+
expect_lint("testthat::expect_true(identical(x, y) || identical(y, z))", NULL, expect_identical_linter())
6+
7+
# expect_equal calls with explicit tolerance= are OK
8+
expect_lint("expect_equal(x, y, tolerance = 1e-6)", NULL, expect_identical_linter())
9+
10+
# ditto for check.attributes = FALSE
11+
expect_lint("expect_equal(x, y, check.attributes = FALSE)", NULL, expect_identical_linter())
12+
})
13+
14+
test_that("expect_identical_linter blocks simple disallowed usages", {
15+
expect_lint(
16+
"expect_equal(x, y)",
17+
rex::rex("Use expect_identical(x, y) by default; resort to expect_equal() only when needed"),
18+
expect_identical_linter()
19+
)
20+
21+
# different usage to redirect to expect_identical
22+
expect_lint(
23+
"expect_true(identical(x, y))",
24+
rex::rex("Use expect_identical(x, y) by default; resort to expect_equal() only when needed"),
25+
expect_identical_linter()
26+
)
27+
})
28+
29+
test_that("expect_identical_linter skips cases likely testing numeric equality", {
30+
expect_lint("expect_equal(x, 1.034)", NULL, expect_identical_linter())
31+
expect_lint("expect_equal(x, c(1.01, 1.02))", NULL, expect_identical_linter())
32+
# whole numbers with explicit decimals are OK, even in mixed scenarios
33+
expect_lint("expect_equal(x, c(1.0, 2))", NULL, expect_identical_linter())
34+
# plain numbers are still caught, however
35+
expect_lint(
36+
"expect_equal(x, 1L)",
37+
rex::rex("Use expect_identical(x, y) by default; resort to expect_equal() only when needed"),
38+
expect_identical_linter()
39+
)
40+
expect_lint(
41+
"expect_equal(x, 1)",
42+
rex::rex("Use expect_identical(x, y) by default; resort to expect_equal() only when needed"),
43+
expect_identical_linter()
44+
)
45+
# NB: TRUE is a NUM_CONST so we want test matching it, even though this test is
46+
# also a violation of expect_true_false_linter()
47+
expect_lint(
48+
"expect_equal(x, TRUE)",
49+
rex::rex("Use expect_identical(x, y) by default; resort to expect_equal() only when needed"),
50+
expect_identical_linter()
51+
)
52+
})
53+
54+
test_that("expect_identical_linter skips 3e cases needing expect_equal", {
55+
expect_lint("expect_equal(x, y, ignore_attr = 'names')", NULL, expect_identical_linter())
56+
})
57+
58+
# this arose where a helper function was wrapping expect_equal() and
59+
# some of the "allowed" arguments were being passed --> false positive
60+
test_that("expect_identical_linter skips calls using ...", {
61+
expect_lint("expect_equal(x, y, ...)", NULL, expect_identical_linter())
62+
})

0 commit comments

Comments
 (0)