Skip to content

Commit

Permalink
New conjunct_expecation_linter (#948)
Browse files Browse the repository at this point in the history
* New expect_true_false_and_condition_linter

* dedup NEWS

* rename to conjunct_expectation_linter

* NEWS

* decruft

* correct logic error re: expect_false(); NEWS

* NEWS typo

* fix terminology

* extend tests

* <- not =

* add operator precedence tests

* use new xml_nodes_to_lint

Co-authored-by: AshesITR <[email protected]>
  • Loading branch information
MichaelChirico and AshesITR authored Mar 21, 2022
1 parent dc734b7 commit 9e3ddab
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 3 deletions.
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Collate:
'commas_linter.R'
'comment_linters.R'
'comments.R'
'conjunct_expectation_linter.R'
'cyclocomp_linter.R'
'declared_functions.R'
'deprecated.R'
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export(clear_cache)
export(closed_curly_linter)
export(commas_linter)
export(commented_code_linter)
export(conjunct_expectation_linter)
export(cyclocomp_linter)
export(default_linters)
export(default_settings)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function calls. (#850, #851, @renkun-ken)
+ `expect_type_linter()` Require usage of `expect_type(x, t)` over `expect_equal(typeof(x), t)` and similar
+ `expect_s3_class_linter()` Require usage of `expect_s3_class(x, k)` over `expect_equal(class(x), k)` and similar
+ `expect_s4_class_linter()` Require usage of `expect_s4_class(x, k)` over `expect_true(methods::is(x, k))`
+ `conjunct_expectation_linter()` Require usage of `expect_true(x); expect_true(y)` over `expect_true(x && y)` and similar
+ `expect_not_linter()` Require usage of `expect_false(x)` over `expect_true(!x)`, and _vice versa_.
+ `expect_true_false_linter()` Require usage of `expect_true(x)` over `expect_equal(x, TRUE)` and similar
+ `expect_named_linter()` Require usage of `expect_named(x, n)` over `expect_equal(names(x), n)` and similar
Expand Down
47 changes: 47 additions & 0 deletions R/conjunct_expectation_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#' Force && conditions in expect_true(), expect_false() to be written separately
#'
#' For readability of test outputs, testing only one thing per call to
#' [testthat::expect_true()] is preferable, i.e.,
#' `expect_true(A); expect_true(B)` is better than `expect_true(A && B)`, and
#' `expect_false(A); expect_false(B)` is better than `expect_false(A || B)`.
#'
#' @evalRd rd_tags("expect_true_false_and_condition_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
conjunct_expectation_linter <- function() {
Linter(function(source_file) {
# need the full file to also catch usages at the top level
if (length(source_file$full_parsed_content) == 0L) {
return(list())
}

xml <- source_file$full_xml_parsed_content

xpath <- "//expr[
(
expr[SYMBOL_FUNCTION_CALL[text() = 'expect_true']]
and expr[2][AND2]
) or (
expr[SYMBOL_FUNCTION_CALL[text() = 'expect_false']]
and expr[2][OR2]
)
]"

bad_expr <- xml2::xml_find_all(xml, xpath)

return(lapply(
bad_expr,
xml_nodes_to_lint,
source_file,
lint_message = function(expr) {
matched_fun <- xml2::xml_text(xml2::xml_find_first(expr, "expr/SYMBOL_FUNCTION_CALL"))
op <- if (matched_fun == "expect_true") "&&" else "||"
message <-
sprintf("Instead of %1$s(A %2$s B), write multiple expectations like %1$s(A) and %1$s(B)", matched_fun, op)
paste(message, "The latter will produce better error messages in the case of failure.")
},
type = "warning",
global = TRUE
))
})
}
1 change: 1 addition & 0 deletions inst/lintr/linters.csv
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ backport_linter,robustness configurable package_development
closed_curly_linter,style readability default configurable
commas_linter,style readability default
commented_code_linter,style readability best_practices default
conjunct_expectation_linter,package_development best_practices readability
cyclocomp_linter,style readability best_practices default configurable
duplicate_argument_linter,correctness common_mistakes configurable
equals_na_linter,robustness correctness common_mistakes default
Expand Down
1 change: 1 addition & 0 deletions man/best_practices_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions man/conjunct_expectation_linter.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions man/linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/package_development_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/readability_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 59 additions & 0 deletions tests/testthat/test-conjunct_expectation_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
test_that("conjunct_expectation_linter skips allowed usages of expect_true", {
expect_lint("expect_true(x)", NULL, conjunct_expectation_linter())
expect_lint("testthat::expect_true(x, y, z)", NULL, conjunct_expectation_linter())

# more complicated expression
expect_lint("expect_true(x || (y && z))", NULL, conjunct_expectation_linter())
# the same by operator precedence, though not obvious a priori
expect_lint("expect_true(x || y && z)", NULL, conjunct_expectation_linter())
expect_lint("expect_true(x && y || z)", NULL, conjunct_expectation_linter())
})

test_that("conjunct_expectation_linter skips allowed usages of expect_true", {
expect_lint("expect_false(x)", NULL, conjunct_expectation_linter())
expect_lint("testthat::expect_false(x, y, z)", NULL, conjunct_expectation_linter())

# more complicated expression
# (NB: xx && yy || zz and xx || yy && zz both parse with || first)
expect_lint("expect_false(x && (y || z))", NULL, conjunct_expectation_linter())
})

test_that("conjunct_expectation_linter blocks && conditions with expect_true()", {
expect_lint(
"expect_true(x && y)",
rex::rex("Instead of expect_true(A && B), write multiple expectations"),
conjunct_expectation_linter()
)

expect_lint(
"expect_true(x && y && z)",
rex::rex("Instead of expect_true(A && B), write multiple expectations"),
conjunct_expectation_linter()
)
})

test_that("conjunct_expectation_linter blocks || conditions with expect_false()", {
expect_lint(
"expect_false(x || y)",
rex::rex("Instead of expect_false(A || B), write multiple expectations"),
conjunct_expectation_linter()
)

expect_lint(
"expect_false(x || y || z)",
rex::rex("Instead of expect_false(A || B), write multiple expectations"),
conjunct_expectation_linter()
)

# these lint because `||` is always outer by operator precedence
expect_lint(
"expect_false(x || y && z)",
rex::rex("Instead of expect_false(A || B), write multiple expectations"),
conjunct_expectation_linter()
)
expect_lint(
"expect_false(x && y || z)",
rex::rex("Instead of expect_false(A || B), write multiple expectations"),
conjunct_expectation_linter()
)
})

0 comments on commit 9e3ddab

Please sign in to comment.