|
| 1 | +#' Require usage of expect_s3_class() |
| 2 | +#' |
| 3 | +#' [testthat::expect_s3_class()] exists specifically for testing the class |
| 4 | +#' of S3 objects. [testthat::expect_equal()], [testthat::expect_identical()], |
| 5 | +#' and [testthat::expect_true()] can also be used for such tests, |
| 6 | +#' but it is better to use the tailored function instead. |
| 7 | +#' |
| 8 | +#' @evalRd rd_tags("expect_s3_class_linter") |
| 9 | +#' @seealso [linters] for a complete list of linters available in lintr. |
| 10 | +#' @export |
| 11 | +expect_s3_class_linter <- function() { |
| 12 | + Linter(function(source_file) { |
| 13 | + if (length(source_file$parsed_content) == 0L) { |
| 14 | + return(list()) |
| 15 | + } |
| 16 | + |
| 17 | + xml <- source_file$xml_parsed_content |
| 18 | + |
| 19 | + # (1) expect_{equal,identical}(class(x), C) |
| 20 | + # (2) expect_true(is.<class>(x)) and expect_true(inherits(x, C)) |
| 21 | + is_class_call <- xp_text_in_table(c(is_s3_class_calls, "inherits")) # nolint: object_usage_linter. TODO(#942): fix this. |
| 22 | + xpath <- glue::glue("//expr[ |
| 23 | + ( |
| 24 | + SYMBOL_FUNCTION_CALL[text() = 'expect_equal' or text() = 'expect_identical'] |
| 25 | + and following-sibling::expr[ |
| 26 | + expr[SYMBOL_FUNCTION_CALL[text() = 'class']] |
| 27 | + and (position() = 1 or preceding-sibling::expr[STR_CONST]) |
| 28 | + ] |
| 29 | + ) or ( |
| 30 | + SYMBOL_FUNCTION_CALL[text() = 'expect_true'] |
| 31 | + and following-sibling::expr[1][expr[SYMBOL_FUNCTION_CALL[ {is_class_call} ]]] |
| 32 | + ) |
| 33 | + ]") |
| 34 | + |
| 35 | + bad_expr <- xml2::xml_find_all(xml, xpath) |
| 36 | + return(lapply(bad_expr, gen_expect_s3_class_lint, source_file)) |
| 37 | + }) |
| 38 | +} |
| 39 | + |
| 40 | +# NB: there is no easy way to make an exhaustive list of places where an |
| 41 | +# is.<x> call can be replaced by expect_s3_class(); this list was manually |
| 42 | +# populated from the default R packages by inspection. For example, |
| 43 | +# is.matrix(x) cannot be replaced by expect_s3_class(x, "matrix") because |
| 44 | +# it is not actually an S3 class (is.object(x) is not TRUE since there |
| 45 | +# is no class set for a matrix [I am not sure if this changes in R 4]. |
| 46 | +# Further, there are functions named is.<x> that have nothing to do with |
| 47 | +# object type, e.g. is.finite(), is.nan(), or is.R(). |
| 48 | +is_s3_class_calls <- paste0("is.", c( |
| 49 | + # base |
| 50 | + "data.frame", "factor", "numeric_version", |
| 51 | + "ordered", "package_version", "qr", "table", |
| 52 | + # utils grDevices tcltk tcltk grid grid |
| 53 | + "relistable", "raster", "tclObj", "tkwin", "grob", "unit", |
| 54 | + # stats |
| 55 | + "mts", "stepfun", "ts", "tskernel" |
| 56 | +)) |
| 57 | + |
| 58 | +gen_expect_s3_class_lint <- function(expr, source_file) { |
| 59 | + matched_function <- xml2::xml_text(xml2::xml_find_first(expr, "SYMBOL_FUNCTION_CALL")) |
| 60 | + if (matched_function %in% c("expect_equal", "expect_identical")) { |
| 61 | + lint_msg <- sprintf("expect_s3_class(x, k) is better than %s(class(x), k).", matched_function) |
| 62 | + } else { |
| 63 | + lint_msg <- "expect_s3_class(x, k) is better than expect_true(is.<k>(x)) or expect_true(inherits(x, k))." |
| 64 | + } |
| 65 | + lint_msg <- paste(lint_msg, "Note also expect_s4_class() available for testing S4 objects.") |
| 66 | + xml_nodes_to_lint(expr, source_file, lint_msg, type = "warning") |
| 67 | +} |
| 68 | + |
| 69 | +#' Require usage of expect_s4_class(x, k) over expect_true(is(x, k)) |
| 70 | +#' |
| 71 | +#' [testthat::expect_s4_class()] exists specifically for testing the class |
| 72 | +#' of S4 objects. [testthat::expect_true()] can also be used for such tests, |
| 73 | +#' but it is better to use the tailored function instead. |
| 74 | +#' |
| 75 | +#' @evalRd rd_tags("expect_s3_class_linter") |
| 76 | +#' @seealso [linters] for a complete list of linters available in lintr. |
| 77 | +#' @export |
| 78 | +expect_s4_class_linter <- function() { |
| 79 | + Linter(function(source_file) { |
| 80 | + if (length(source_file$parsed_content) == 0L) { |
| 81 | + return(list()) |
| 82 | + } |
| 83 | + |
| 84 | + xml <- source_file$xml_parsed_content |
| 85 | + |
| 86 | + # TODO(michaelchirico): also catch expect_{equal,identical}(methods::is(x), k). |
| 87 | + # there are no hits for this on google3 as of now. |
| 88 | + |
| 89 | + # require 2 expressions because methods::is(x) alone is a valid call, even |
| 90 | + # though the character output wouldn't make any sense for expect_true(). |
| 91 | + xpath <- "//expr[ |
| 92 | + SYMBOL_FUNCTION_CALL[text() = 'expect_true'] |
| 93 | + and following-sibling::expr[1][count(expr) = 3 and expr[SYMBOL_FUNCTION_CALL[text() = 'is']]] |
| 94 | + ]" |
| 95 | + |
| 96 | + bad_expr <- xml2::xml_find_all(xml, xpath) |
| 97 | + return(lapply( |
| 98 | + bad_expr, |
| 99 | + xml_nodes_to_lint, |
| 100 | + source_file = source_file, |
| 101 | + message = paste( |
| 102 | + "expect_s4_class(x, k) is better than expect_true(is(x, k)).", |
| 103 | + "Note also expect_s3_class() available for testing S3 objects." |
| 104 | + ), |
| 105 | + type = "warning" |
| 106 | + )) |
| 107 | + }) |
| 108 | +} |
0 commit comments