-
Notifications
You must be signed in to change notification settings - Fork 187
expect_null_linter #887
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
expect_null_linter #887
Changes from all commits
43778a6
2b6c53b
4da6666
10f87e5
1bf7d38
7ae8df4
842c15b
4696d5b
5bc7b4b
b007d37
e324084
bf8f726
18810c0
29a9d4c
719e3c7
76f1e69
6f81ae2
c03b606
4f90533
5fce64c
b736aab
0df3979
c15b51d
1dbdbe8
d102c7e
a01e708
c04d24d
8712d9f
86496ab
797e26b
1c5a97d
fb2e6ac
06a6291
a32b298
1a270de
6b6df30
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 |
---|---|---|
@@ -0,0 +1,49 @@ | ||
#' expect_null Linter | ||
#' | ||
#' Require usage of `expect_null(x)` over `expect_equal(x, NULL)` and similar | ||
#' usages. | ||
#' | ||
#' [testthat::expect_null()] exists specifically for testing for `NULL` objects. | ||
#' [testthat::expect_equal()], [testthat::expect_identical()], and | ||
#' [testthat::expect_true()] can also be used for such tests, | ||
#' but it is better to use the tailored function instead. | ||
#' | ||
#' @evalRd rd_tags("expect_null_linter") | ||
#' @seealso [linters] for a complete list of linters available in lintr. | ||
#' @export | ||
expect_null_linter <- function() { | ||
AshesITR marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Linter(function(source_file) { | ||
if (length(source_file$parsed_content) == 0L) { | ||
return(list()) | ||
} | ||
|
||
xml <- source_file$xml_parsed_content | ||
|
||
# two cases two match: | ||
# (1) expect_{equal,identical}(x, NULL) (or NULL, x) | ||
# (2) expect_true(is.null(x)) | ||
xpath <- "//expr[ | ||
( | ||
SYMBOL_FUNCTION_CALL[text() = 'expect_equal' or text() = 'expect_identical'] | ||
and following-sibling::expr[position() <= 2 and NULL_CONST] | ||
) or ( | ||
SYMBOL_FUNCTION_CALL[text() = 'expect_true'] | ||
and following-sibling::expr[1][expr[SYMBOL_FUNCTION_CALL[text() = 'is.null']]] | ||
) | ||
]" | ||
|
||
bad_expr <- xml2::xml_find_all(xml, xpath) | ||
|
||
lapply(bad_expr, gen_expect_null_lint, source_file) | ||
}) | ||
} | ||
|
||
gen_expect_null_lint <- function(expr, source_file) { | ||
matched_function <- xml2::xml_text(xml2::xml_find_first(expr, "SYMBOL_FUNCTION_CALL")) | ||
if (matched_function %in% c("expect_equal", "expect_identical")) { | ||
lint_msg <- sprintf("expect_null(x) is better than %s(x, NULL)", matched_function) | ||
} else { | ||
lint_msg <- "expect_null(x) is better than expect_true(is.null(x))" | ||
} | ||
xml_nodes_to_lint(expr, source_file, lint_msg, type = "warning") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# utils for working with xpaths | ||
|
||
# like `text() %in% table`, translated to XPath 1.0 | ||
xp_text_in_table <- function(table) paste("text() = ", quote_wrap(table, "'"), collapse = " or ") | ||
|
||
# convert an XML match into a Lint | ||
xml_nodes_to_lint <- function(xml, source_file, message, | ||
type = c("style", "warning", "error"), | ||
offset = 0L, | ||
global = FALSE) { | ||
type <- match.arg(type, c("style", "warning", "error")) | ||
line1 <- xml2::xml_attr(xml, "line1")[1] | ||
col1 <- as.integer(xml2::xml_attr(xml, "col1")) + offset | ||
|
||
line_elt <- if (global) "file_lines" else "lines" | ||
|
||
if (xml2::xml_attr(xml, "line2") == line1) { | ||
col2 <- as.integer(xml2::xml_attr(xml, "col2")) + offset | ||
} else { | ||
col2 <- unname(nchar(source_file[[line_elt]][line1])) | ||
} | ||
return(Lint( | ||
filename = source_file$filename, | ||
line_number = as.integer(line1), | ||
column_number = as.integer(col1), | ||
type = type, | ||
message = message, | ||
line = source_file[[line_elt]][line1], | ||
ranges = list(c(col1 - offset, col2)) | ||
)) | ||
} | ||
|
||
#' Safer wrapper for paste(..., sep = " and ") | ||
#' | ||
#' The intent is to use this for readability when combining XPath conditions so | ||
#' the `...` elements should be in that language, but this is not enforced. | ||
#' | ||
#' Inputs are also wrapped in `()` so as not to risk mixing operator precedence. | ||
#' | ||
#' @param ... Series of conditions | ||
#' @noRd | ||
xp_and <- function(...) sprintf("(%s)", paste(..., sep = ") and (")) | ||
|
||
#' Safer wrapper for paste(..., sep = " or ") | ||
#' | ||
#' The intent is to use this for readability when combining XPath conditions so | ||
#' the `...` elements should be in that language, but this is not enforced. | ||
#' | ||
#' Inputs are also wrapped in `()` so as not to risk mixing operator precedence. | ||
#' | ||
#' @param ... Series of conditions | ||
#' @noRd | ||
xp_or <- function(...) sprintf("(%s)", paste(..., sep = ") or (")) |
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,42 @@ | ||
test_that("expect_null_linter skips allowed usages", { | ||
# NB: this usage should be expect_false(), but this linter should | ||
# nevertheless apply (e.g. for maintainers not using expect_not_linter) | ||
expect_lint("expect_true(!is.null(x))", NULL, expect_null_linter()) | ||
# NB: also applies to tinytest, but it's sufficient to test testthat | ||
expect_lint("testthat::expect_true(!is.null(x))", NULL, expect_null_linter()) | ||
|
||
# length-0 could be NULL, but could be integer() or list(), so let it pass | ||
expect_lint("expect_length(x, 0L)", NULL, expect_null_linter()) | ||
|
||
# no false positive for is.null() at the wrong positional argument | ||
expect_lint("expect_true(x, is.null(y))", NULL, expect_null_linter()) | ||
}) | ||
|
||
test_that("expect_null_linter blocks simple disallowed usages", { | ||
expect_lint( | ||
"expect_equal(x, NULL)", | ||
MichaelChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
rex::rex("expect_null(x) is better than expect_equal(x, NULL)"), | ||
expect_null_linter() | ||
) | ||
|
||
# expect_identical is treated the same as expect_equal | ||
expect_lint( | ||
"testthat::expect_identical(x, NULL)", | ||
rex::rex("expect_null(x) is better than expect_identical(x, NULL)"), | ||
expect_null_linter() | ||
) | ||
|
||
# reverse order lints the same | ||
expect_lint( | ||
"expect_equal(NULL, x)", | ||
rex::rex("expect_null(x) is better than expect_equal(x, NULL)"), | ||
expect_null_linter() | ||
) | ||
|
||
# different equivalent usage | ||
expect_lint( | ||
"expect_true(is.null(foo(x)))", | ||
rex::rex("expect_null(x) is better than expect_true(is.null(x))"), | ||
expect_null_linter() | ||
) | ||
}) |
Uh oh!
There was an error while loading. Please reload this page.