-
Notifications
You must be signed in to change notification settings - Fork 187
New yoda_test_linter #957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New yoda_test_linter #957
Changes from all commits
c794550
e53cc4e
fa79cb8
8b95ee7
d829db3
048fe93
154a0f1
0854372
6ce7618
543959d
2ce73f2
7280c09
c22db1c
848e113
af5b6b6
014d9bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
bad.R | ||
script.R | ||
*~ | ||
\#*\# | ||
|
||
*.Rcheck | ||
lintr_*.tar.gz | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
#' Block obvious "yoda tests" | ||
#' | ||
#' Yoda tests use `(expected, actual)` instead of the more common `(actual, expected)`. | ||
#' This is not always possible to detect statically; this linter focuses on | ||
#' the simple case of testing an expression against a literal value, e.g. | ||
#' `(1L, foo(x))` should be `(foo(x), 1L)`. | ||
#' | ||
#' @evalRd rd_tags("yoda_test_linter") | ||
#' @seealso | ||
#' [linters] for a complete list of linters available in lintr. | ||
#' <https://en.wikipedia.org/wiki/Yoda_conditions> | ||
#' @export | ||
yoda_test_linter <- function() { | ||
Linter(function(source_file) { | ||
if (length(source_file$parsed_content) == 0L) { | ||
return(list()) | ||
} | ||
|
||
xml <- source_file$xml_parsed_content | ||
|
||
# catch the following types of literal in the first argument: | ||
# (1) numeric literal (e.g. TRUE, 1L, 1.0, NA) [NUM_CONST] | ||
# (2) string literal (e.g. 'str' or "str") [STR_CONST] | ||
# (3) arithmetic literal (e.g. 1+1 or 0+1i) [OP-PLUS or OP-MINUS...] | ||
# TODO(#963): fully generalize this & re-use elsewhere | ||
xpath <- "//expr[ | ||
expr[SYMBOL_FUNCTION_CALL[text() = 'expect_equal' or text() = 'expect_identical' or text() = 'expect_setequal']] | ||
and expr[2][NUM_CONST or STR_CONST or ((OP-PLUS or OP-MINUS) and count(expr[NUM_CONST]) = 2)] | ||
]" | ||
|
||
bad_expr <- xml2::xml_find_all(xml, xpath) | ||
|
||
return(lapply( | ||
bad_expr, | ||
xml_nodes_to_lint, | ||
source_file = source_file, | ||
lint_message = paste( | ||
"Tests should compare objects in the order 'actual', 'expected', not the reverse.", | ||
"For example, do expect_identical(foo(x), 2L) instead of expect_identical(2L, foo(x))." | ||
), | ||
type = "warning" | ||
)) | ||
}) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
test_that("yoda_test_linter skips allowed usages", { | ||
expect_lint("expect_equal(x, 2)", NULL, yoda_test_linter()) | ||
# namespace qualification doesn't matter | ||
expect_lint("testthat::expect_identical(x, 'a')", NULL, yoda_test_linter()) | ||
# two variables can't be distinguished which is expected/actual (without | ||
# playing quixotic games trying to parse that out from variable names) | ||
expect_lint("expect_equal(x, y)", NULL, yoda_test_linter()) | ||
}) | ||
|
||
test_that("yoda_test_linter blocks simple disallowed usages", { | ||
expect_lint( | ||
"expect_equal(2, x)", | ||
rex::rex("Tests should compare objects in the order 'actual', 'expected'"), | ||
yoda_test_linter() | ||
) | ||
expect_lint( | ||
"testthat::expect_identical('a', x)", | ||
rex::rex("Tests should compare objects in the order 'actual', 'expected'"), | ||
yoda_test_linter() | ||
) | ||
expect_lint( | ||
"expect_setequal(2, x)", | ||
rex::rex("Tests should compare objects in the order 'actual', 'expected'"), | ||
yoda_test_linter() | ||
) | ||
# complex literals are slightly odd | ||
expect_lint( | ||
"expect_equal(2 + 1i, x)", | ||
rex::rex("Tests should compare objects in the order 'actual', 'expected'"), | ||
yoda_test_linter() | ||
) | ||
}) | ||
|
||
# TODO(michaelchirico): Should this be extended to RUnit tests? It seems yes, | ||
# but the argument names in RUnit (inherited from base all.equal()) are a bit | ||
# confusing, e.g. `checkEqual(target=, current=)`. From the name, one might | ||
# reasonably conclude 'expected' comes first, and 'actual' comes second. | ||
# TODO(michaelchirico): What sorts of combinations of literals can be included? | ||
# e.g. expect_equal(c(1, 2), x) is a yoda test; is expect_equal(c(x, 1), y)? | ||
# clearly it's not true for general f() besides c(). What about other | ||
# constructors of literals? data.frame(), data.table(), tibble(), ...? | ||
Comment on lines
+38
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it's worth to craft up an "extended literal" XPath that contains a few whitelisted functions and use that throughout lintr if we want to match a "constant" expression? First thought would be something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds great, I was thinking something similar. We have something like that in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's mark this as a TODO for now. we have a messy XPath that works for a limited number of cases, I'd like to generalize it if possible. For reference here's the XPath for
|
||
# TODO(michaelchirico): The logic could also be extended to "tests" inside regular | ||
# code, not just test suites, e.g. `if (2 == x)`, `while(3 <= x)`, | ||
# `stopifnot('a' == foo(y))`. |
Uh oh!
There was an error while loading. Please reload this page.