Skip to content

Commit a3234f4

Browse files
Merge branch 'main' into consecutive_suppression
2 parents 78156b9 + ff1dc21 commit a3234f4

25 files changed

+848
-8
lines changed

DESCRIPTION

+4
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,14 @@ Collate:
139139
'namespace_linter.R'
140140
'nested_ifelse_linter.R'
141141
'nonportable_path_linter.R'
142+
'nrow_subset_linter.R'
142143
'numeric_leading_zero_linter.R'
143144
'nzchar_linter.R'
144145
'object_length_linter.R'
145146
'object_name_linter.R'
147+
'object_overwrite_linter.R'
146148
'object_usage_linter.R'
149+
'one_call_pipe_linter.R'
147150
'outer_negation_linter.R'
148151
'package_hooks_linter.R'
149152
'paren_body_linter.R'
@@ -158,6 +161,7 @@ Collate:
158161
'redundant_equals_linter.R'
159162
'redundant_ifelse_linter.R'
160163
'regex_subset_linter.R'
164+
'rep_len_linter.R'
161165
'repeat_linter.R'
162166
'routine_registration_linter.R'
163167
'sample_int_linter.R'

NAMESPACE

+4
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,14 @@ export(namespace_linter)
101101
export(nested_ifelse_linter)
102102
export(no_tab_linter)
103103
export(nonportable_path_linter)
104+
export(nrow_subset_linter)
104105
export(numeric_leading_zero_linter)
105106
export(nzchar_linter)
106107
export(object_length_linter)
107108
export(object_name_linter)
109+
export(object_overwrite_linter)
108110
export(object_usage_linter)
111+
export(one_call_pipe_linter)
109112
export(open_curly_linter)
110113
export(outer_negation_linter)
111114
export(package_hooks_linter)
@@ -121,6 +124,7 @@ export(quotes_linter)
121124
export(redundant_equals_linter)
122125
export(redundant_ifelse_linter)
123126
export(regex_subset_linter)
127+
export(rep_len_linter)
124128
export(repeat_linter)
125129
export(routine_registration_linter)
126130
export(sample_int_linter)

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
* `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).
3030
* `nzchar_linter()` for encouraging `nzchar()` to test for empty strings, e.g. `nchar(x) > 0` can be `nzchar(x)` (part of #884, @MichaelChirico).
3131
* `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.
32+
* `rep_len_linter()` for encouraging use of `rep_len()` directly instead of `rep(x, length.out = n)` (part of #884, @MichaelChirico).
3233
* `which_grepl_linter()` for discouraging `which(grepl(ptn, x))` in favor of directly using `grep(ptn, x)` (part of #884, @MichaelChirico).
3334
* `list_comparison_linter()` for discouraging comparisons on the output of `lapply()`, e.g. `lapply(x, sum) > 10` (part of #884, @MichaelChirico).
3435
* `print_linter()` for discouraging usage of `print()` on string literals like `print("Reached here")` or `print(paste("Found", nrow(DF), "rows."))` (#1894, @MichaelChirico).
36+
* `nrow_subset_linter()` for discouraging usage like `nrow(subset(x, conditions))` in favor of something like `with(x, sum(conditions))` which doesn't require a full subset of `x` (part of #884, @MichaelChirico).
3537
* `pipe_return_linter()` for discouraging usage of `return()` inside a {magrittr} pipeline (part of #884, @MichaelChirico).
3638

3739
### Lint accuracy fixes: removing false positives

R/nrow_subset_linter.R

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#' Block usage of `nrow(subset(x, .))`
2+
#'
3+
#' Using `nrow(subset(x, condition))` to count the instances where `condition`
4+
#' applies inefficiently requires doing a full subset of `x` just to
5+
#' count the number of rows in the resulting subset.
6+
#' There are a number of equivalent expressions that don't require the full
7+
#' subset, e.g. `with(x, sum(condition))` (or, more generically,
8+
#' `with(x, sum(condition, na.rm = TRUE))`).
9+
#'
10+
#' @examples
11+
#' # will produce lints
12+
#' lint(
13+
#' text = "nrow(subset(x, is_treatment))",
14+
#' linters = nrow_subset_linter()
15+
#' )
16+
#'
17+
#' # okay
18+
#' lint(
19+
#' text = "with(x, sum(is_treatment, na.rm = TRUE))",
20+
#' linters = nrow_subset_linter()
21+
#' )
22+
#'
23+
#' @evalRd rd_tags("nrow_subset_linter")
24+
#' @seealso [linters] for a complete list of linters available in lintr.
25+
#' @export
26+
nrow_subset_linter <- make_linter_from_xpath(
27+
xpath = "
28+
//SYMBOL_FUNCTION_CALL[text() = 'subset']
29+
/parent::expr
30+
/parent::expr
31+
/parent::expr[expr/SYMBOL_FUNCTION_CALL[text() = 'nrow']]
32+
",
33+
lint_message = paste(
34+
"Use arithmetic to count the number of rows satisfying a condition,",
35+
"rather than fully subsetting the data.frame and counting the resulting rows.",
36+
"For example, replace nrow(subset(x, is_treatment))",
37+
"with sum(x$is_treatment). NB: use na.rm = TRUE if `is_treatment` has",
38+
"missing values."
39+
)
40+
)

R/object_overwrite_linter.R

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#' Block assigning any variables whose name clashes with a `base` R function
2+
#'
3+
#' Re-using existing names creates a risk of subtle error best avoided.
4+
#' Avoiding this practice also encourages using better, more descriptive names.
5+
#'
6+
#' @param packages Character vector of packages to search for names that should
7+
#' be avoided. Defaults to the most common default packages: base, stats,
8+
#' utils, tools, methods, graphics, and grDevices.
9+
#' @param allow_names Character vector of object names to ignore, i.e., which
10+
#' are allowed to collide with exports from `packages`.
11+
#'
12+
#' @examples
13+
#' # will produce lints
14+
#' code <- "function(x) {\n data <- x\n data\n}"
15+
#' writeLines(code)
16+
#' lint(
17+
#' text = code,
18+
#' linters = object_overwrite_linter()
19+
#' )
20+
#'
21+
#' code <- "function(x) {\n lint <- 'fun'\n lint\n}"
22+
#' writeLines(code)
23+
#' lint(
24+
#' text = code,
25+
#' linters = object_overwrite_linter(packages = "lintr")
26+
#' )
27+
#'
28+
#' # okay
29+
#' code <- "function(x) {\n data('mtcars')\n}"
30+
#' writeLines(code)
31+
#' lint(
32+
#' text = code,
33+
#' linters = object_overwrite_linter()
34+
#' )
35+
#'
36+
#' code <- "function(x) {\n data <- x\n data\n}"
37+
#' writeLines(code)
38+
#' lint(
39+
#' text = code,
40+
#' linters = object_overwrite_linter(packages = "base")
41+
#' )
42+
#'
43+
#' # names in function signatures are ignored
44+
#' lint(
45+
#' text = "function(data) data <- subset(data, x > 0)",
46+
#' linters = object_overwrite_linter()
47+
#' )
48+
#'
49+
#' @evalRd rd_tags("object_overwrite_linter")
50+
#' @seealso
51+
#' - [linters] for a complete list of linters available in lintr.
52+
#' - <https://style.tidyverse.org/syntax.html#object-names>
53+
#' @export
54+
object_overwrite_linter <- function(
55+
packages = c("base", "stats", "utils", "tools", "methods", "graphics", "grDevices"),
56+
allow_names = character()) {
57+
for (package in packages) {
58+
if (!requireNamespace(package, quietly = TRUE)) {
59+
stop("Package '", package, "' is not available.")
60+
}
61+
}
62+
pkg_exports <- lapply(
63+
packages,
64+
# .__C__ etc.: drop 150+ "virtual" names since they are very unlikely to appear anyway
65+
function(pkg) setdiff(grep("^[.]__[A-Z]__", getNamespaceExports(pkg), value = TRUE, invert = TRUE), allow_names)
66+
)
67+
pkg_exports <- data.frame(
68+
package = rep(packages, lengths(pkg_exports)),
69+
name = unlist(pkg_exports),
70+
stringsAsFactors = FALSE
71+
)
72+
73+
# test that the symbol doesn't match an argument name in the function
74+
# NB: data.table := has parse token LEFT_ASSIGN as well
75+
xpath <- glue("
76+
//SYMBOL[
77+
not(text() = ancestor::expr/preceding-sibling::SYMBOL_FORMALS/text())
78+
and ({ xp_text_in_table(pkg_exports$name) })
79+
]/
80+
parent::expr[
81+
count(*) = 1
82+
and (
83+
following-sibling::LEFT_ASSIGN[text() != ':=']
84+
or following-sibling::EQ_ASSIGN
85+
or preceding-sibling::RIGHT_ASSIGN
86+
)
87+
and ancestor::*[
88+
(self::expr or self::expr_or_assign_or_help or self::equal_assign)
89+
and (preceding-sibling::FUNCTION or preceding-sibling::OP-LAMBDA)
90+
]
91+
]
92+
")
93+
94+
Linter(function(source_expression) {
95+
if (!is_lint_level(source_expression, "expression")) {
96+
return(list())
97+
}
98+
99+
xml <- source_expression$xml_parsed_content
100+
101+
bad_expr <- xml_find_all(xml, xpath)
102+
bad_symbol <- xml_text(xml_find_first(bad_expr, "SYMBOL"))
103+
source_pkg <- pkg_exports$package[match(bad_symbol, pkg_exports$name)]
104+
lint_message <-
105+
sprintf("'%s' is an exported object from package '%s'. Avoid re-using such symbols.", bad_symbol, source_pkg)
106+
107+
xml_nodes_to_lints(
108+
bad_expr,
109+
source_expression = source_expression,
110+
lint_message = lint_message,
111+
type = "warning"
112+
)
113+
})
114+
}

R/one_call_pipe_linter.R

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#' Block single-call magrittr pipes
2+
#'
3+
#' Prefer using a plain call instead of a pipe with only one call,
4+
#' i.e. `1:10 %>% sum()` should instead be `sum(1:10)`. Note that
5+
#' calls in the first `%>%` argument count. `rowSums(x) %>% max()` is OK
6+
#' because there are two total calls (`rowSums()` and `max()`).
7+
#'
8+
#' Note also that un-"called" steps are *not* counted, since they should
9+
#' be calls (see [pipe_call_linter()]).
10+
#'
11+
#' @examples
12+
#' # will produce lints
13+
#' lint(
14+
#' text = "(1:10) %>% sum()",
15+
#' linters = one_call_pipe_linter()
16+
#' )
17+
#'
18+
#' lint(
19+
#' text = "DT %>% .[grp == 'a', sum(v)]",
20+
#' linters = one_call_pipe_linter()
21+
#' )
22+
#'
23+
#' # okay
24+
#' lint(
25+
#' text = "rowSums(x) %>% mean()",
26+
#' linters = one_call_pipe_linter()
27+
#' )
28+
#'
29+
#' lint(
30+
#' text = "DT[src == 'a', .N, by = grp] %>% .[N > 10]",
31+
#' linters = one_call_pipe_linter()
32+
#' )
33+
#'
34+
#' @evalRd rd_tags("one_call_pipe_linter")
35+
#' @seealso
36+
#' - [linters] for a complete list of linters available in lintr.
37+
#' - <https://style.tidyverse.org/pipes.html#short-pipes>
38+
#' @export
39+
one_call_pipe_linter <- function() {
40+
pipes_cond <- xp_text_in_table(magrittr_pipes)
41+
42+
# preceding-sibling::SPECIAL: if there are ever two pipes, don't lint
43+
# OP-LEFT-BRACKET/LBB: accept DT[...] %>% .[...] as a two-call pipe,
44+
# (but not DT %>% .[...])
45+
# parent::expr/SPECIAL: make sure we are at the top of a pipeline
46+
# count(): any call anywhere else in the AST within the pipe expression
47+
xpath <- glue("
48+
(//SPECIAL[{pipes_cond}] | //PIPE)[
49+
not(preceding-sibling::expr[1]/*[self::SPECIAL[{pipes_cond}] or self::PIPE])
50+
and (
51+
not(following-sibling::expr[OP-LEFT-BRACKET or LBB])
52+
or not(preceding-sibling::expr[OP-LEFT-BRACKET or LBB])
53+
)
54+
]
55+
/parent::expr[
56+
not(parent::expr/*[self::SPECIAL[{ pipes_cond }] or self::PIPE])
57+
and count(.//SYMBOL_FUNCTION_CALL) <= 1
58+
]
59+
")
60+
61+
Linter(function(source_expression) {
62+
if (!is_lint_level(source_expression, "expression")) {
63+
return(list())
64+
}
65+
66+
xml <- source_expression$xml_parsed_content
67+
68+
bad_expr <- xml_find_all(xml, xpath)
69+
pipe <- xml_find_chr(bad_expr, "string(SPECIAL | PIPE)")
70+
71+
xml_nodes_to_lints(
72+
bad_expr,
73+
source_expression = source_expression,
74+
lint_message = paste0("Expressions with only a single call shouldn't use pipe ", pipe, "."),
75+
type = "warning"
76+
)
77+
})
78+
}

R/rep_len_linter.R

+41
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

+4
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,14 @@ namespace_linter,correctness robustness configurable executing
5858
nested_ifelse_linter,efficiency readability
5959
no_tab_linter,style consistency deprecated
6060
nonportable_path_linter,robustness best_practices configurable
61+
nrow_subset_linter,efficiency consistency best_practices
6162
numeric_leading_zero_linter,style consistency readability
6263
nzchar_linter,efficiency best_practices consistency
6364
object_length_linter,style readability default configurable executing
6465
object_name_linter,style consistency default configurable executing
66+
object_overwrite_linter,best_practices robustness readability configurable executing
6567
object_usage_linter,style readability correctness default executing configurable
68+
one_call_pipe_linter,style readability
6669
open_curly_linter,defunct
6770
outer_negation_linter,readability efficiency best_practices
6871
package_hooks_linter,style correctness package_development
@@ -78,6 +81,7 @@ quotes_linter,style consistency readability default configurable
7881
redundant_equals_linter,best_practices readability efficiency common_mistakes
7982
redundant_ifelse_linter,best_practices efficiency consistency configurable
8083
regex_subset_linter,best_practices efficiency regex
84+
rep_len_linter,readability consistency best_practices
8185
repeat_linter,style readability
8286
routine_registration_linter,best_practices efficiency robustness
8387
sample_int_linter,efficiency readability robustness

man/best_practices_linters.Rd

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

man/configurable_linters.Rd

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

man/consistency_linters.Rd

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

0 commit comments

Comments
 (0)