Skip to content

Commit e53a2c8

Browse files
New expect_named_linter (#949)
* New expect_named_linter * NEWS * fix lints in lintr
1 parent 2580ab2 commit e53a2c8

12 files changed

+111
-7
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Collate:
6464
'exclude.R'
6565
'expect_length_linter.R'
6666
'expect_lint.R'
67+
'expect_named_linter.R'
6768
'expect_not_linter.R'
6869
'expect_null_linter.R'
6970
'expect_s3_class_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export(equals_na_linter)
3131
export(expect_length_linter)
3232
export(expect_lint)
3333
export(expect_lint_free)
34+
export(expect_named_linter)
3435
export(expect_not_linter)
3536
export(expect_null_linter)
3637
export(expect_s3_class_linter)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ function calls. (#850, #851, @renkun-ken)
9292
+ `expect_s4_class_linter()` Require usage of `expect_s4_class(x, k)` over `expect_true(methods::is(x, k))`
9393
+ `expect_not_linter()` Require usage of `expect_false(x)` over `expect_true(!x)`, and _vice versa_.
9494
+ `expect_true_false_linter()` Require usage of `expect_true(x)` over `expect_equal(x, TRUE)` and similar
95+
+ `expect_named_linter()` Require usage of `expect_named(x, n)` over `expect_equal(names(x), n)` and similar
9596
* `expect_length_linter()` Require usage of `expect_length(x, n)` over `expect_equal(length(x), n)` and similar
9697
* `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)
9798
* `infix_spaces_linter()` gains argument `exclude_operators` to disable lints on selected infix operators. By default, all "low-precedence" operators throw lints; see `?infix_spaces_linter` for an enumeration of these. (#914 @michaelchirico)

R/expect_named_linter.R

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#' Require usage of expect_named(x, n) over expect_equal(names(x), n)
2+
#'
3+
#' [testthat::expect_named()] exists specifically for testing the [names()] of
4+
#' an object. [testthat::expect_equal()] can also be used for such tests,
5+
#' but it is better to use the tailored function instead.
6+
#'
7+
#' @evalRd rd_tags("expect_named_linter")
8+
#' @seealso [linters] for a complete list of linters available in lintr.
9+
#' @export
10+
expect_named_linter <- function() {
11+
Linter(function(source_file) {
12+
if (length(source_file$parsed_content) == 0L) {
13+
return(list())
14+
}
15+
16+
xml <- source_file$xml_parsed_content
17+
18+
xpath <- "//expr[
19+
SYMBOL_FUNCTION_CALL[text() = 'expect_equal' or text() = 'expect_identical']
20+
and following-sibling::expr[
21+
expr[SYMBOL_FUNCTION_CALL[text() = 'names']]
22+
and (position() = 1 or preceding-sibling::expr[STR_CONST])
23+
]
24+
]"
25+
26+
bad_expr <- xml2::xml_find_all(xml, xpath)
27+
return(lapply(bad_expr, gen_expect_named_lint, source_file))
28+
})
29+
}
30+
31+
gen_expect_named_lint <- function(expr, source_file) {
32+
matched_function <- xml2::xml_text(xml2::xml_find_first(expr, "SYMBOL_FUNCTION_CALL"))
33+
lint_msg <- sprintf("expect_named(x, n) is better than %s(names(x), n)", matched_function)
34+
xml_nodes_to_lint(expr, source_file, lint_msg, type = "warning")
35+
}

inst/lintr/linters.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ cyclocomp_linter,style readability best_practices default configurable
99
duplicate_argument_linter,correctness common_mistakes configurable
1010
equals_na_linter,robustness correctness common_mistakes default
1111
expect_length_linter,package_development best_practices readability
12+
expect_named_linter,package_development best_practices readability
1213
expect_not_linter,package_development best_practices readability
1314
expect_null_linter,package_development best_practices
1415
expect_s3_class_linter,package_development best_practices

man/best_practices_linters.Rd

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

man/expect_named_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: 1 addition & 0 deletions
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: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/readability_linters.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
test_that("expect_named_linter doesn't raise false positive lints", {
2+
expect_lint("expect_equal(nrow(x), 4L)", NULL, expect_named_linter())
3+
# NB: also applies to tinytest, but it's sufficient to test testthat
4+
expect_lint("testthat::expect_equal(nrow(x), 4L)", NULL, expect_named_linter())
5+
})
6+
7+
test_that("expect_named_linter skips allowed usages", {
8+
# colnames(), rownames(), and dimnames() tests are not equivalent
9+
expect_lint("expect_equal(colnames(x), 'a')", NULL, expect_named_linter())
10+
expect_lint("expect_equal(rownames(x), 'a')", NULL, expect_named_linter())
11+
expect_lint("expect_equal(dimnames(x), 'a')", NULL, expect_named_linter())
12+
13+
# only check the first argument. yoda tests in the second argument will be
14+
# missed, but there are legitimate uses of names() in argument 2
15+
expect_lint("expect_equal(colnames(x), names(y))", NULL, expect_named_linter())
16+
})
17+
18+
test_that("expect_named_linter blocks simple disallowed usages", {
19+
expect_lint(
20+
"expect_equal(names(x), 'a')",
21+
rex::rex("expect_named(x, n) is better than expect_equal(names(x), n)"),
22+
expect_named_linter()
23+
)
24+
25+
expect_lint(
26+
"testthat::expect_equal(names(DF), names(old))",
27+
rex::rex("expect_named(x, n) is better than expect_equal(names(x), n)"),
28+
expect_named_linter()
29+
)
30+
31+
expect_lint(
32+
"expect_equal('a', names(x))",
33+
rex::rex("expect_named(x, n) is better than expect_equal(names(x), n)"),
34+
expect_named_linter()
35+
)
36+
})
37+
38+
test_that("expect_named_linter blocks expect_identical usage as well", {
39+
expect_lint(
40+
"expect_identical(names(x), 'a')",
41+
rex::rex("expect_named(x, n) is better than expect_identical(names(x), n)"),
42+
expect_named_linter()
43+
)
44+
})

tests/testthat/test-settings.R

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,9 @@ test_that("it errors if the config file does not end in a newline", {
7474
expect_error(read_settings("foo"), "Malformed config file")
7575
})
7676

77-
test_that("with_defaults works as expected", {
78-
# test capturing unnamed args
79-
defaults <- with_defaults(assignment_linter)
77+
test_that("with_defaults works as expected with unnamed args", {
8078
# assignment_linter is in defaults, so output doesn't change
81-
expect_equal(names(defaults), names(with_defaults()))
79+
expect_named(with_defaults(assignment_linter), names(with_defaults()))
8280
})
8381

8482
test_that("rot utility works as intended", {
@@ -106,8 +104,8 @@ test_that("logical_env utility works as intended", {
106104

107105
# fixing #774
108106
test_that("with_defaults doesn't break on very long input", {
109-
expect_equal(
110-
names(with_defaults(
107+
expect_named(
108+
with_defaults(
111109
default = list(),
112110
lintr::undesirable_function_linter(c(
113111
detach = paste(
@@ -120,7 +118,7 @@ test_that("with_defaults doesn't break on very long input", {
120118
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
121119
)
122120
))
123-
)),
121+
),
124122
"lintr::undesirable_function_linter"
125123
)
126124
})

0 commit comments

Comments
 (0)