Skip to content

Commit 51b2ae5

Browse files
New for_loop_index_linter (#1629)
* New for_loop_index_linter * add examples
1 parent a6254c2 commit 51b2ae5

11 files changed

+133
-3
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ Collate:
9494
'extract.R'
9595
'extraction_operator_linter.R'
9696
'fixed_regex_linter.R'
97+
'for_loop_index_linter.R'
9798
'function_argument_linter.R'
9899
'function_left_parentheses_linter.R'
99100
'function_return_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export(expect_true_false_linter)
5757
export(expect_type_linter)
5858
export(extraction_operator_linter)
5959
export(fixed_regex_linter)
60+
export(for_loop_index_linter)
6061
export(function_argument_linter)
6162
export(function_left_parentheses_linter)
6263
export(function_return_linter)

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
* `boolean_arithmetic_linter()` for identifying places where logical aggregations are more appropriate, e.g.
4545
`length(which(x == y)) == 0` is the same as `!any(x == y)` or even `all(x != y)` (@MichaelChirico)
4646

47+
* `for_loop_index_linter()` to prevent overwriting local variables in a `for` loop declared like `for (x in x) { ... }` (@MichaelChirico)
48+
4749
## Notes
4850

4951
* `lint()` continues to support Rmarkdown documents. For users of custom .Rmd engines, e.g.

R/for_loop_index_linter.R

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#' Block usage of for loops directly overwriting the indexing variable
2+
#'
3+
#' `for (x in x)` is a poor choice of indexing variable. This overwrites
4+
#' `x` in the calling scope and is confusing to read.
5+
#'
6+
#' @examples
7+
#' # will produce lints
8+
#' lint(
9+
#' text = "for (x in x) { TRUE }",
10+
#' linters = for_loop_index_linter()
11+
#' )
12+
#'
13+
#' lint(
14+
#' text = "for (x in foo(x, y)) { TRUE }",
15+
#' linters = for_loop_index_linter()
16+
#' )
17+
#'
18+
#' # okay
19+
#' lint(
20+
#' text = "for (xi in x) { TRUE }",
21+
#' linters = for_loop_index_linter()
22+
#' )
23+
#'
24+
#' lint(
25+
#' text = "for (col in DF$col) { TRUE }",
26+
#' linters = for_loop_index_linter()
27+
#' )
28+
#'
29+
#' @evalRd rd_tags("for_loop_index_linter")
30+
#' @seealso [linters] for a complete list of linters available in lintr.
31+
#' @export
32+
for_loop_index_linter <- function() {
33+
xpath <- "
34+
//forcond
35+
/SYMBOL[text() =
36+
following-sibling::expr
37+
//SYMBOL[not(
38+
preceding-sibling::OP-DOLLAR
39+
or parent::expr[preceding-sibling::OP-LEFT-BRACKET]
40+
)]
41+
/text()
42+
]
43+
"
44+
45+
Linter(function(source_expression) {
46+
if (!is_lint_level(source_expression, "expression")) {
47+
return(list())
48+
}
49+
50+
xml <- source_expression$xml_parsed_content
51+
52+
bad_expr <- xml2::xml_find_all(xml, xpath)
53+
54+
xml_nodes_to_lints(
55+
bad_expr,
56+
source_expression = source_expression,
57+
lint_message = "Don't re-use any sequence symbols as the index symbol in a for loop.",
58+
type = "warning"
59+
)
60+
})
61+
}

inst/lintr/linters.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ expect_true_false_linter,package_development best_practices readability
2828
expect_type_linter,package_development best_practices
2929
extraction_operator_linter,style best_practices
3030
fixed_regex_linter,best_practices readability efficiency
31+
for_loop_index_linter,best_practices readability robustness
3132
function_argument_linter,style consistency best_practices
3233
function_left_parentheses_linter,style readability default
3334
function_return_linter,readability 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/for_loop_index_linter.Rd

Lines changed: 42 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: 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/robustness_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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
test_that("for_loop_index_linter skips allowed usages", {
2+
expect_lint("for (xi in x) {}", NULL, for_loop_index_linter())
3+
4+
# this is OK, so not every symbol is problematic
5+
expect_lint("for (col in DF$col) {}", NULL, for_loop_index_linter())
6+
expect_lint("for (col in DT[, col]) {}", NULL, for_loop_index_linter())
7+
})
8+
9+
test_that("for_loop_index_linter blocks simple disallowed usages", {
10+
linter <- for_loop_index_linter()
11+
lint_msg <- "Don't re-use any sequence symbols as the index symbol in a for loop"
12+
13+
expect_lint("for (x in x) {}", lint_msg, linter)
14+
# these also overwrite a variable in calling scope
15+
expect_lint("for (x in foo(x)) {}", lint_msg, linter)
16+
# arbitrary nesting
17+
expect_lint("for (x in foo(bar(y, baz(2, x)))) {}", lint_msg, linter)
18+
})

0 commit comments

Comments
 (0)