Skip to content

Commit 9bd015d

Browse files
length_test_linter() for length(x == 0) (#2124)
1 parent 02f7db8 commit 9bd015d

10 files changed

+124
-2
lines changed

DESCRIPTION

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Collate:
118118
'is_numeric_linter.R'
119119
'keyword_quote_linter.R'
120120
'length_levels_linter.R'
121+
'length_test_linter.R'
121122
'lengths_linter.R'
122123
'library_call_linter.R'
123124
'line_length_linter.R'

NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export(is_lint_level)
8080
export(is_numeric_linter)
8181
export(keyword_quote_linter)
8282
export(length_levels_linter)
83+
export(length_test_linter)
8384
export(lengths_linter)
8485
export(library_call_linter)
8586
export(line_length_linter)

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
* `scalar_in_linter()` for discouraging `%in%` when the right-hand side is a scalar, e.g. `x %in% 1` (part of #884, @MichaelChirico).
5454
* `if_not_else_linter()` for encouraging `if` statements to be structured as `if (A) x else y` instead of `if (!A) y else x` (part of #884, @MichaelChirico).
5555
* `repeat_linter()` for encouraging `repeat` for infinite loops instead of `while (TRUE)` (#2106, @MEO265).
56+
* `length_test_linter()` detects the common mistake `length(x == 0)` which is meant to be `length(x) == 0` (#1991, @MichaelChirico).
5657

5758
## Changes to defaults
5859

R/length_test_linter.R

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#' Check for a common mistake where length is applied in the wrong place
2+
#'
3+
#' Usage like `length(x == 0)` is a mistake. If you intended to check `x` is empty,
4+
#' use `length(x) == 0`. Other mistakes are possible, but running `length()` on the
5+
#' outcome of a logical comparison is never the best choice.
6+
#'
7+
#' @examples
8+
#' # will produce lints
9+
#' lint(
10+
#' text = "length(x == 0)",
11+
#' linters = length_test_linter()
12+
#' )
13+
#'
14+
#' # okay
15+
#' lint(
16+
#' text = "length(x) > 0",
17+
#' linters = length_test_linter()
18+
#' )
19+
#' @evalRd rd_tags("class_equals_linter")
20+
#' @seealso [linters] for a complete list of linters available in lintr.
21+
#' @export
22+
length_test_linter <- function() {
23+
xpath <- glue::glue("
24+
//SYMBOL_FUNCTION_CALL[text() = 'length']
25+
/parent::expr
26+
/following-sibling::expr[{ xp_or(infix_metadata$xml_tag[infix_metadata$comparator]) }]
27+
/parent::expr
28+
")
29+
30+
Linter(function(source_expression) {
31+
if (!is_lint_level(source_expression, "expression")) {
32+
return(list())
33+
}
34+
35+
xml <- source_expression$xml_parsed_content
36+
37+
bad_expr <- xml_find_all(xml, xpath)
38+
expr_parts <- vapply(lapply(bad_expr, xml_find_all, "expr[2]/*"), xml_text, character(3L))
39+
lint_message <- sprintf(
40+
"Checking the length of a logical vector is likely a mistake. Did you mean `length(%s) %s %s`?",
41+
expr_parts[1L, ], expr_parts[2L, ], expr_parts[3L, ]
42+
)
43+
44+
xml_nodes_to_lints(
45+
bad_expr,
46+
source_expression = source_expression,
47+
lint_message = lint_message,
48+
type = "warning"
49+
)
50+
})
51+
}

inst/lintr/linters.csv

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ inner_combine_linter,efficiency consistency readability
4444
is_numeric_linter,readability best_practices consistency
4545
keyword_quote_linter,readability consistency style
4646
length_levels_linter,readability best_practices consistency
47+
length_test_linter,common_mistakes efficiency
4748
lengths_linter,efficiency readability best_practices
4849
library_call_linter,style best_practices readability
4950
line_length_linter,style readability default configurable

man/common_mistakes_linters.Rd

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/efficiency_linters.Rd

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/length_test_linter.Rd

+32
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/linters.Rd

+3-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
test_that("skips allowed usages", {
2+
linter <- length_test_linter()
3+
4+
expect_lint("length(x) > 0", NULL, linter)
5+
expect_lint("length(DF[key == val, cols])", NULL, linter)
6+
})
7+
8+
test_that("blocks simple disallowed usages", {
9+
linter <- length_test_linter()
10+
lint_msg_stub <- rex::rex("Checking the length of a logical vector is likely a mistake. Did you mean ")
11+
12+
expect_lint("length(x == 0)", rex::rex(lint_msg_stub, "`length(x) == 0`?"), linter)
13+
expect_lint("length(x == y)", rex::rex(lint_msg_stub, "`length(x) == y`?"), linter)
14+
expect_lint("length(x + y == 2)", rex::rex(lint_msg_stub, "`length(x+y) == 2`?"), linter)
15+
})
16+
17+
local({
18+
ops <- c(lt = "<", lte = "<=", gt = ">", gte = ">=", eq = "==", neq = "!=")
19+
linter <- length_test_linter()
20+
lint_msg_stub <- rex::rex("Checking the length of a logical vector is likely a mistake. Did you mean ")
21+
22+
patrick::with_parameters_test_that(
23+
"all logical operators detected",
24+
expect_lint(
25+
paste("length(x", op, "y)"),
26+
rex::rex("`length(x) ", op, " y`?"),
27+
linter
28+
),
29+
op = ops,
30+
.test_name = names(ops)
31+
)
32+
})

0 commit comments

Comments
 (0)