Skip to content

Commit 463cd86

Browse files
expect_s3_class_linter and expect_s4_class_linter (#943)
* expect_s3_class_linter and expect_s4_class_linter * missed s4 in inst db * extension for yoda tests * fix issues identified by linter * fix test * add a test vs. a vector of classes * nolint
1 parent 88b2bb9 commit 463cd86

12 files changed

+268
-3
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Collate:
6666
'expect_lint.R'
6767
'expect_not_linter.R'
6868
'expect_null_linter.R'
69+
'expect_s3_class_linter.R'
6970
'expect_type_linter.R'
7071
'extract.R'
7172
'extraction_operator_linter.R'

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export(expect_lint)
3333
export(expect_lint_free)
3434
export(expect_not_linter)
3535
export(expect_null_linter)
36+
export(expect_s3_class_linter)
37+
export(expect_s4_class_linter)
3638
export(expect_type_linter)
3739
export(extraction_operator_linter)
3840
export(function_left_parentheses_linter)

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ function calls. (#850, #851, @renkun-ken)
8989
* `lintr` is adopting a new set of linters provided as part of Google's extension to the tidyverse style guide (#884, @michaelchirico)
9090
+ `expect_null_linter()` Require usage of `expect_null(x)` over `expect_equal(x, NULL)` and similar
9191
+ `expect_type_linter()` Require usage of `expect_type(x, t)` over `expect_equal(typeof(x), t)` and similar
92+
+ `expect_s3_class_linter()` Require usage of `expect_s3_class(x, k)` over `expect_equal(class(x), k)` and similar
93+
+ `expect_s4_class_linter()` Require usage of `expect_s4_class(x, k)` over `expect_true(methods::is(x, k))`
9294
+ `expect_not_linter()` Require usage of `expect_false(x)` over `expect_true(!x)`, and _vice versa_.
9395
* `assignment_linter()` now lints right assignment (`->` and `->>`) and gains two arguments. `allow_cascading_assign` (`TRUE` by default) toggles whether to lint `<<-` and `->>`; `allow_right_assign` toggles whether to lint `->` and `->>` (#915, @michaelchirico)
9496

R/expect_s3_class_linter.R

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
}

inst/lintr/linters.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ duplicate_argument_linter,correctness common_mistakes configurable
1111
equals_na_linter,robustness correctness common_mistakes default
1212
expect_not_linter,package_development best_practices readability
1313
expect_null_linter,package_development best_practices
14+
expect_s3_class_linter,package_development best_practices
15+
expect_s4_class_linter,package_development best_practices
1416
expect_type_linter,package_development best_practices
1517
extraction_operator_linter,style best_practices
1618
function_left_parentheses_linter,style readability default

man/best_practices_linters.Rd

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/expect_s3_class_linter.Rd

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/expect_s4_class_linter.Rd

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/linters.Rd

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/package_development_linters.Rd

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)