Skip to content

Commit

Permalink
Split S3 and S4 linters (and tests) into their own files (#1680)
Browse files Browse the repository at this point in the history
* Split S3 and S4 linters (and tests) into their own files

This always made me do double-take if I was working in the right file.

* correct links
  • Loading branch information
IndrajeetPatil authored Oct 11, 2022
1 parent 849c40e commit 719784c
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 91 deletions.
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Collate:
'expect_not_linter.R'
'expect_null_linter.R'
'expect_s3_class_linter.R'
'expect_s4_class_linter.R'
'expect_true_false_linter.R'
'expect_type_linter.R'
'extract.R'
Expand Down
58 changes: 2 additions & 56 deletions R/expect_s3_class_linter.R
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
#' )
#'
#' @evalRd rd_tags("expect_s3_class_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @seealso [linters] for a complete list of linters available in lintr. \cr
#' [expect_s4_class_linter()]
#' @export
expect_s3_class_linter <- function() {
# (1) expect_{equal,identical}(class(x), C)
Expand Down Expand Up @@ -90,58 +91,3 @@ is_s3_class_calls <- paste0("is.", c(
# stats
"mts", "stepfun", "ts", "tskernel"
))

#' Require usage of `expect_s4_class(x, k)` over `expect_true(is(x, k))`
#'
#' [testthat::expect_s4_class()] exists specifically for testing the class
#' of S4 objects. [testthat::expect_true()] can also be used for such tests,
#' but it is better to use the tailored function instead.
#'
#' @examples
#' # will produce lints
#' lint(
#' text = "expect_true(is(x, 'Matrix'))",
#' linters = expect_s4_class_linter()
#' )
#'
#' # okay
#' lint(
#' text = "expect_s4_class(x, 'Matrix')",
#' linters = expect_s4_class_linter()
#' )
#'
#' @evalRd rd_tags("expect_s4_class_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
expect_s4_class_linter <- function() {
# require 2 expressions because methods::is(x) alone is a valid call, even
# though the character output wouldn't make any sense for expect_true().
xpath <- "
//SYMBOL_FUNCTION_CALL[text() = 'expect_true']
/parent::expr
/following-sibling::expr[1][count(expr) = 3 and expr[1][SYMBOL_FUNCTION_CALL[text() = 'is']]]
/parent::expr[not(SYMBOL_SUB[text() = 'info' or text() = 'label'])]
"

Linter(function(source_expression) {
if (!is_lint_level(source_expression, "expression")) {
return(list())
}

xml <- source_expression$xml_parsed_content

# TODO(michaelchirico): also catch expect_{equal,identical}(methods::is(x), k).
# this seems empirically rare, but didn't check many S4-heavy packages.

bad_expr <- xml2::xml_find_all(xml, xpath)
xml_nodes_to_lints(
bad_expr,
source_expression = source_expression,
lint_message = paste(
"expect_s4_class(x, k) is better than expect_true(is(x, k)).",
"Note also expect_s3_class() available for testing S3 objects."
),
type = "warning"
)
})
}
55 changes: 55 additions & 0 deletions R/expect_s4_class_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#' Require usage of `expect_s4_class(x, k)` over `expect_true(is(x, k))`
#'
#' [testthat::expect_s4_class()] exists specifically for testing the class
#' of S4 objects. [testthat::expect_true()] can also be used for such tests,
#' but it is better to use the tailored function instead.
#'
#' @examples
#' # will produce lints
#' lint(
#' text = "expect_true(is(x, 'Matrix'))",
#' linters = expect_s4_class_linter()
#' )
#'
#' # okay
#' lint(
#' text = "expect_s4_class(x, 'Matrix')",
#' linters = expect_s4_class_linter()
#' )
#'
#' @evalRd rd_tags("expect_s4_class_linter")
#' @seealso [linters] for a complete list of linters available in lintr. \cr
#' [expect_s3_class_linter()]
#' @export
expect_s4_class_linter <- function() {
# require 2 expressions because methods::is(x) alone is a valid call, even
# though the character output wouldn't make any sense for expect_true().
xpath <- "
//SYMBOL_FUNCTION_CALL[text() = 'expect_true']
/parent::expr
/following-sibling::expr[1][count(expr) = 3 and expr[1][SYMBOL_FUNCTION_CALL[text() = 'is']]]
/parent::expr[not(SYMBOL_SUB[text() = 'info' or text() = 'label'])]
"

Linter(function(source_expression) {
if (!is_lint_level(source_expression, "expression")) {
return(list())
}

xml <- source_expression$xml_parsed_content

# TODO(michaelchirico): also catch expect_{equal,identical}(methods::is(x), k).
# this seems empirically rare, but didn't check many S4-heavy packages.

bad_expr <- xml2::xml_find_all(xml, xpath)
xml_nodes_to_lints(
bad_expr,
source_expression = source_expression,
lint_message = paste(
"expect_s4_class(x, k) is better than expect_true(is(x, k)).",
"Note also expect_s3_class() available for testing S3 objects."
),
type = "warning"
)
})
}
3 changes: 2 additions & 1 deletion man/expect_s3_class_linter.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions man/expect_s4_class_linter.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 10 additions & 32 deletions tests/testthat/test-expect_s3_class_linter.R
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
test_that("expect_s3_class_linter skips allowed usages", {
linter <- expect_s3_class_linter()

# expect_s3_class doesn't have an inverted version
expect_lint("expect_true(!inherits(x, 'class'))", NULL, expect_s3_class_linter())
expect_lint("expect_true(!inherits(x, 'class'))", NULL, linter)
# NB: also applies to tinytest, but it's sufficient to test testthat
expect_lint("testthat::expect_true(!inherits(x, 'class'))", NULL, expect_s3_class_linter())
expect_lint("testthat::expect_true(!inherits(x, 'class'))", NULL, linter)

# other is.<x> calls are not suitable for expect_s3_class in particular
expect_lint("expect_true(is.na(x))", NULL, expect_s3_class_linter())
expect_lint("expect_true(is.na(x))", NULL, linter)

# case where expect_s3_class() *could* be used but we don't enforce
expect_lint("expect_true(is.data.table(x))", NULL, expect_s3_class_linter())
expect_lint("expect_true(is.data.table(x))", NULL, linter)

# expect_s3_class() doesn't have info= or label= arguments
expect_lint("expect_equal(class(x), k, info = 'x should have class k')", NULL, expect_s3_class_linter())
expect_lint("expect_equal(class(x), k, label = 'x class')", NULL, expect_s3_class_linter())
expect_lint("expect_equal(class(x), k, expected.label = 'target class')", NULL, expect_s3_class_linter())
expect_lint("expect_true(is.data.frame(x), info = 'x should be a data.frame')", NULL, expect_s3_class_linter())
expect_lint("expect_equal(class(x), k, info = 'x should have class k')", NULL, linter)
expect_lint("expect_equal(class(x), k, label = 'x class')", NULL, linter)
expect_lint("expect_equal(class(x), k, expected.label = 'target class')", NULL, linter)
expect_lint("expect_true(is.data.frame(x), info = 'x should be a data.frame')", NULL, linter)
})

test_that("expect_s3_class_linter blocks simple disallowed usages", {
Expand Down Expand Up @@ -66,31 +68,7 @@ test_that("expect_s3_class_linter blocks simple disallowed usages", {
#> )
})

test_that("expect_s4_class_linter skips allowed usages", {
# expect_s4_class doesn't have an inverted version
expect_lint("expect_true(!is(x, 'class'))", NULL, expect_s4_class_linter())
# NB: also applies to tinytest, but it's sufficient to test testthat
expect_lint("testthat::expect_s3_class(!is(x, 'class'))", NULL, expect_s4_class_linter())

# expect_s4_class() doesn't have info= or label= arguments
expect_lint("expect_true(is(x, 'SpatialPoly'), info = 'x should be SpatialPoly')", NULL, expect_s4_class_linter())
expect_lint("expect_true(is(x, 'SpatialPoly'), label = 'x inheritance')", NULL, expect_s4_class_linter())
})

test_that("expect_s4_class blocks simple disallowed usages", {
expect_lint(
"expect_true(is(x, 'data.frame'))",
rex::rex("expect_s4_class(x, k) is better than expect_true(is(x, k))"),
expect_s4_class_linter()
)

# namespace qualification is irrelevant
expect_lint(
"testthat::expect_true(methods::is(x, 'SpatialPolygonsDataFrame'))",
rex::rex("expect_s4_class(x, k) is better than expect_true(is(x, k))"),
expect_s4_class_linter()
)
})

skip_if_not_installed("patrick")
local({
Expand Down
27 changes: 27 additions & 0 deletions tests/testthat/test-expect_s4_class.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
test_that("expect_s4_class_linter skips allowed usages", {
linter <- expect_s4_class_linter()

# expect_s4_class doesn't have an inverted version
expect_lint("expect_true(!is(x, 'class'))", NULL, linter)
# NB: also applies to tinytest, but it's sufficient to test testthat
expect_lint("testthat::expect_s3_class(!is(x, 'class'))", NULL, linter)

# expect_s4_class() doesn't have info= or label= arguments
expect_lint("expect_true(is(x, 'SpatialPoly'), info = 'x should be SpatialPoly')", NULL, linter)
expect_lint("expect_true(is(x, 'SpatialPoly'), label = 'x inheritance')", NULL, linter)
})

test_that("expect_s4_class blocks simple disallowed usages", {
expect_lint(
"expect_true(is(x, 'data.frame'))",
rex::rex("expect_s4_class(x, k) is better than expect_true(is(x, k))"),
expect_s4_class_linter()
)

# namespace qualification is irrelevant
expect_lint(
"testthat::expect_true(methods::is(x, 'SpatialPolygonsDataFrame'))",
rex::rex("expect_s4_class(x, k) is better than expect_true(is(x, k))"),
expect_s4_class_linter()
)
})

0 comments on commit 719784c

Please sign in to comment.