-
Notifications
You must be signed in to change notification settings - Fork 186
expect_type_linter #924
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_type_linter #924
Changes from all commits
b47d18a
f51273d
f3623eb
a93bb18
449c59d
55039ef
d303fca
1927647
eb0a0f4
8d3a625
a3ba403
90678d4
176b9a5
059d3e3
b2b7070
f072c4c
b4aafb3
ccf7933
454ebbc
99b5139
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,62 @@ | ||
#' Require usage of expect_type(x, type) over expect_equal(typeof(x), type) | ||
#' | ||
#' [testthat::expect_type()] exists specifically for testing the storage type | ||
#' of 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_type_linter") | ||
#' @seealso [linters] for a complete list of linters available in lintr. | ||
#' @export | ||
expect_type_linter <- function() { | ||
Linter(function(source_file) { | ||
if (length(source_file$parsed_content) == 0L) { | ||
return(list()) | ||
} | ||
|
||
xml <- source_file$xml_parsed_content | ||
|
||
base_type_tests <- xp_text_in_table(paste0("is.", base_types)) # nolint: object_usage_linter. TODO(#942): fix this. | ||
xpath <- glue::glue("//expr[ | ||
( | ||
SYMBOL_FUNCTION_CALL[text() = 'expect_equal' or text() = 'expect_identical'] | ||
and following-sibling::expr[ | ||
expr[SYMBOL_FUNCTION_CALL[text() = 'typeof']] | ||
and (position() = 1 or preceding-sibling::expr[STR_CONST]) | ||
] | ||
) or ( | ||
SYMBOL_FUNCTION_CALL[text() = 'expect_true'] | ||
and following-sibling::expr[1][expr[SYMBOL_FUNCTION_CALL[ {base_type_tests} ]]] | ||
) | ||
]") | ||
|
||
bad_expr <- xml2::xml_find_all(xml, xpath) | ||
return(lapply(bad_expr, gen_expect_type_lint, source_file)) | ||
}) | ||
} | ||
|
||
gen_expect_type_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_type(x, t) is better than %s(typeof(x), t)", matched_function) | ||
} else { | ||
lint_msg <- "expect_type(x, t) is better than expect_true(is.<t>(x))" | ||
} | ||
xml_nodes_to_lint(expr, source_file, lint_msg, type = "warning") | ||
} | ||
|
||
|
||
# NB: the full list of values that can arise from `typeof(x)` is available | ||
# in ?typeof (or, slightly more robustly, in the R source: src/main/util.c. | ||
# Not all of them are available in is.<type> form, e.g. 'any' or | ||
# 'special'. 'builtin' and 'closure' are special cases, corresponding to | ||
# is.primitive and is.function (essentially). | ||
base_types <- c( | ||
"raw", "logical", "integer", "double", "complex", "character", "list", | ||
"numeric", "function", "primitive", "environment", "pairlist", "promise", | ||
# Per ?is.language, it's the same as is.call || is.name || is.expression. | ||
# so by blocking it, we're forcing more precise tests of one of | ||
# those directly ("language", "symbol", and "expression", resp.) | ||
# NB: is.name and is.symbol are identical. | ||
"language", "call", "name", "symbol", "expression" | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,45 @@ | ||
linter,tags | ||
absolute_path_linter,robustness best_practices configurable | ||
assignment_linter,style consistency default | ||
assignment_spaces_linter,style readability | ||
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 | ||
cyclocomp_linter,style readability best_practices default configurable | ||
duplicate_argument_linter,correctness common_mistakes configurable | ||
equals_na_linter,robustness correctness common_mistakes default | ||
expect_null_linter,package_development best_practices | ||
expect_type_linter,package_development best_practices | ||
extraction_operator_linter,style best_practices | ||
function_left_parentheses_linter,style readability default | ||
implicit_integer_linter,style consistency best_practices | ||
infix_spaces_linter,style readability default | ||
line_length_linter,style readability default configurable | ||
missing_argument_linter,correctness common_mistakes configurable | ||
missing_package_linter,robustness common_mistakes | ||
namespace_linter,correctness robustness configurable | ||
no_tab_linter,style consistency default | ||
nonportable_path_linter,robustness best_practices configurable | ||
object_length_linter,style readability default configurable | ||
object_name_linter,style consistency default configurable | ||
object_usage_linter,style readability correctness default | ||
open_curly_linter,style readability default configurable | ||
package_hooks_linter,style correctness package_development | ||
paren_body_linter,style readability default | ||
paren_brace_linter,style readability default | ||
pipe_call_linter,style readability | ||
pipe_continuation_linter,style readability default | ||
semicolon_terminator_linter,style readability default configurable | ||
seq_linter,robustness efficiency consistency best_practices default | ||
single_quotes_linter,style consistency readability default | ||
spaces_inside_linter,style readability default | ||
spaces_left_parentheses_linter,style readability default | ||
sprintf_linter,correctness common_mistakes | ||
T_and_F_symbol_linter,style readability robustness consistency best_practices default | ||
todo_comment_linter,style configurable | ||
trailing_blank_lines_linter,style default | ||
trailing_whitespace_linter,style default | ||
undesirable_function_linter,style efficiency configurable robustness best_practices | ||
undesirable_operator_linter,style efficiency configurable robustness best_practices | ||
unneeded_concatenation_linter,style readability efficiency | ||
assignment_spaces_linter,style readability | ||
MichaelChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
duplicate_argument_linter,correctness common_mistakes configurable | ||
missing_argument_linter,correctness common_mistakes configurable | ||
missing_package_linter,robustness common_mistakes | ||
namespace_linter,correctness robustness configurable | ||
paren_body_linter,style readability default | ||
pipe_call_linter,style readability | ||
sprintf_linter,correctness common_mistakes |
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,62 @@ | ||
test_that("expect_type_linter skips allowed usages", { | ||
# expect_type doesn't have an inverted version | ||
expect_lint("expect_true(!is.numeric(x))", NULL, expect_type_linter()) | ||
# NB: also applies to tinytest, but it's sufficient to test testthat | ||
expect_lint("testthat::expect_true(!is.numeric(x))", NULL, expect_type_linter()) | ||
|
||
# other is.<x> calls are not suitable for expect_type in particular | ||
expect_lint("expect_true(is.data.frame(x))", NULL, expect_type_linter()) | ||
|
||
# expect_type(x, ...) cannot be cleanly used here: | ||
expect_lint("expect_true(typeof(x) %in% c('builtin', 'closure'))", NULL, expect_type_linter()) | ||
}) | ||
|
||
test_that("expect_type_linter blocks simple disallowed usages", { | ||
expect_lint( | ||
"expect_equal(typeof(x), 'double')", | ||
rex::rex("expect_type(x, t) is better than expect_equal(typeof(x), t)"), | ||
expect_type_linter() | ||
) | ||
|
||
# expect_identical is treated the same as expect_equal | ||
expect_lint( | ||
"testthat::expect_identical(typeof(x), 'language')", | ||
rex::rex("expect_type(x, t) is better than expect_identical(typeof(x), t)"), | ||
expect_type_linter() | ||
) | ||
|
||
# different equivalent usage | ||
expect_lint( | ||
"expect_true(is.complex(foo(x)))", | ||
rex::rex("expect_type(x, t) is better than expect_true(is.<t>(x))"), | ||
expect_type_linter() | ||
) | ||
|
||
# yoda test with clear expect_type replacement | ||
expect_lint( | ||
"expect_equal('integer', typeof(x))", | ||
rex::rex("expect_type(x, t) is better than expect_equal(typeof(x), t)"), | ||
expect_type_linter() | ||
) | ||
}) | ||
|
||
|
||
skip_if_not_installed("patrick") | ||
local({ | ||
# test for lint errors appropriately raised for all is.<type> calls | ||
is_types <- c( | ||
"raw", "logical", "integer", "double", "complex", "character", "list", | ||
"numeric", "function", "primitive", "environment", "pairlist", "promise", | ||
"language", "call", "name", "symbol", "expression" | ||
) | ||
patrick::with_parameters_test_that( | ||
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. We use |
||
"expect_type_linter catches expect_true(is.<base type>)", | ||
expect_lint( | ||
sprintf("expect_true(is.%s(x))", is_type), | ||
rex::rex("expect_type(x, t) is better than expect_true(is.<t>(x))"), | ||
expect_type_linter() | ||
), | ||
.test_name = is_types, | ||
is_type = is_types | ||
) | ||
}) |
Uh oh!
There was an error while loading. Please reload this page.