Skip to content

Commit 01b3d15

Browse files
Merge f1634ef into 0aefa5f
2 parents 0aefa5f + f1634ef commit 01b3d15

11 files changed

+143
-3
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ Collate:
158158
'redundant_equals_linter.R'
159159
'redundant_ifelse_linter.R'
160160
'regex_subset_linter.R'
161+
'rep_len_linter.R'
161162
'repeat_linter.R'
162163
'routine_registration_linter.R'
163164
'sample_int_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export(quotes_linter)
121121
export(redundant_equals_linter)
122122
export(redundant_ifelse_linter)
123123
export(regex_subset_linter)
124+
export(rep_len_linter)
124125
export(repeat_linter)
125126
export(routine_registration_linter)
126127
export(sample_int_linter)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* `comparison_negation_linter()` for discouraging negated comparisons when a direct negation is preferable, e.g. `!(x == y)` could be `x != y` (part of #884, @MichaelChirico).
2828
* `nzchar_linter()` for encouraging `nzchar()` to test for empty strings, e.g. `nchar(x) > 0` can be `nzchar(x)` (part of #884, @MichaelChirico).
2929
* `terminal_close_linter()` for discouraging using `close()` to end functions (part of #884, @MichaelChirico). Such usages are not robust to errors, where `close()` will not be run as intended. Put `close()` in an `on.exit()` hook, or use {withr} to manage connections with proper cleanup.
30+
* `rep_len_linter()` for encouraging use of `rep_len()` directly instead of `rep(x, length.out = n)` (part of #884, @MichaelChirico).
3031
* `which_grepl_linter()` for discouraging `which(grepl(ptn, x))` in favor of directly using `grep(ptn, x)` (part of #884, @MichaelChirico).
3132
* `list_comparison_linter()` for discouraging comparisons on the output of `lapply()`, e.g. `lapply(x, sum) > 10` (part of #884, @MichaelChirico).
3233
* `print_linter()` for discouraging usage of `print()` on string literals like `print("Reached here")` or `print(paste("Found", nrow(DF), "rows."))` (#1894, @MichaelChirico).

R/rep_len_linter.R

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#' Require usage of rep_len(x, n) over rep(x, length.out = n)
2+
#'
3+
#' `rep(x, length.out = n)` calls `rep_len(x, n)` "under the hood". The latter
4+
#' is thus more direct and equally readable.
5+
#'
6+
#' @examples
7+
#' # will produce lints
8+
#' lint(
9+
#' text = "rep(1:3, length.out = 10)",
10+
#' linters = rep_len_linter()
11+
#' )
12+
#'
13+
#' # okay
14+
#' lint(
15+
#' text = "rep_len(1:3, 10)",
16+
#' linters = rep_len_linter()
17+
#' )
18+
#'
19+
#' lint(
20+
#' text = "rep(1:3, each = 2L, length.out = 10L)",
21+
#' linters = rep_len_linter()
22+
#' )
23+
#'
24+
#' @evalRd rd_tags("rep_len_linter")
25+
#' @seealso [linters] for a complete list of linters available in lintr.
26+
#' @export
27+
rep_len_linter <- make_linter_from_xpath(
28+
# count(expr) is for cases using positional matching; see ?rep.
29+
xpath = "
30+
//SYMBOL_FUNCTION_CALL[text() = 'rep']
31+
/parent::expr
32+
/parent::expr[
33+
(
34+
SYMBOL_SUB[text() = 'length.out']
35+
or (not(SYMBOL_SUB) and count(expr) = 4)
36+
)
37+
and not(SYMBOL_SUB[text() = 'each'] or count(expr) = 5)
38+
]
39+
",
40+
lint_message = "Use rep_len(x, n) instead of rep(x, length.out = n)."
41+
)

inst/lintr/linters.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ quotes_linter,style consistency readability default configurable
7878
redundant_equals_linter,best_practices readability efficiency common_mistakes
7979
redundant_ifelse_linter,best_practices efficiency consistency configurable
8080
regex_subset_linter,best_practices efficiency regex
81+
rep_len_linter,readability consistency best_practices
8182
repeat_linter,style readability
8283
routine_registration_linter,best_practices efficiency robustness
8384
sample_int_linter,efficiency readability robustness

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/consistency_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/linters.Rd

Lines changed: 4 additions & 3 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.

man/rep_len_linter.Rd

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

tests/testthat/test-rep_len_linter.R

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
test_that("rep_len_linter skips allowed usages", {
2+
linter <- rep_len_linter()
3+
4+
# only catch length.out usages
5+
expect_lint("rep(x, y)", NULL, linter)
6+
expect_lint("rep(1:10, 2)", NULL, linter)
7+
expect_lint("rep(1:10, 10:1)", NULL, linter)
8+
9+
# usage of each is not compatible with rep_len; see ?rep.
10+
expect_lint("rep(x, each = 4, length.out = 50)", NULL, linter)
11+
# each is implicitly the 4th positional argument. a very strange usage
12+
# (because length.out is ignored), but doesn't hurt to catch it
13+
expect_lint("rep(a, b, length.out = c, d)", NULL, linter)
14+
# ditto for implicit length.out=
15+
expect_lint("rep(a, b, c, d)", NULL, linter)
16+
})
17+
18+
test_that("rep_len_linter blocks simple disallowed usages", {
19+
linter <- rep_len_linter()
20+
lint_msg <- rex::rex("Use rep_len(x, n) instead of rep(x, length.out = n).")
21+
22+
# only catch length.out usages
23+
expect_lint("rep(x, length.out = 4L)", lint_msg, linter)
24+
25+
# implicit times= argument; length.out has priority over times= (see ?rep),
26+
# so we still lint since it's as if times= is not supplied.
27+
# (notice here that the base behavior is odd -- one might expect output like
28+
# head(rep(1:10, 10:1), 50), but instead we get rep(1:10, length.out = 50))
29+
expect_lint("rep(1:10, 10:1, length.out = 50)", lint_msg, linter)
30+
31+
# ditto for explicit times= argument
32+
expect_lint("rep(1:10, times = 10:1, length.out = 50)", lint_msg, linter)
33+
34+
# implicit usage in third argument
35+
expect_lint("rep(1:10, 10:1, 50)", lint_msg, linter)
36+
})
37+
38+
test_that("vectorized lints work", {
39+
lint_msg <- rex::rex("Use rep_len(x, n) instead of rep(x, length.out = n).")
40+
41+
expect_lint(
42+
trim_some("{
43+
rep(x, y)
44+
rep(1:10, length.out = 50)
45+
rep(x, each = 4, length.out = 50)
46+
rep(x, length.out = 50)
47+
}"),
48+
list(
49+
list(lint_msg, line_number = 3L),
50+
list(lint_msg, line_number = 5L)
51+
),
52+
rep_len_linter()
53+
)
54+
})

0 commit comments

Comments
 (0)