Skip to content

Commit 191e3b7

Browse files
also skip slot extractions akin to dollar (#2039)
* also skip slot extractions akin to dollar * extend to the full linter suite, add tests * simpler XPath with parent:: axis * revert edit for assignment, add regression tests * new test for T on RHS of @ * new regression test * news PR
1 parent bb7669a commit 191e3b7

15 files changed

+82
-42
lines changed

NEWS.md

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
## New and improved features
99

1010
* `library_call_linter()` can detect if all library calls are not at the top of your script (#2027, @nicholas-masel).
11+
* Several linters avoiding false positives in `$` extractions get the same exceptions for `@` extractions, e.g. `S4@T` will no longer throw a `T_and_F_symbol_linter()` hit (#2039, @MichaelChirico).
12+
+ `T_and_F_symbol_linter()`
13+
+ `for_loop_index_linter()`
14+
+ `literal_coercion_linter()`
15+
+ `object_name_linter()`
16+
+ `undesirable_function_linter()`
17+
+ `unreachable_code_linter()`
18+
+ `yoda_test_linter()`
1119

1220
## Changes to defaults
1321

R/T_and_F_symbol_linter.R

+12-26
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,15 @@
3131
#' - <https://style.tidyverse.org/syntax.html#logical-vectors>
3232
#' @export
3333
T_and_F_symbol_linter <- function() { # nolint: object_name.
34-
xpath <- paste0(
35-
"//SYMBOL[",
36-
" (text() = 'T' or text() = 'F')", # T or F symbol
37-
" and not(preceding-sibling::OP-DOLLAR)", # not part of a $-subset expression
38-
" and not(parent::expr[",
39-
" following-sibling::LEFT_ASSIGN", # not target of left assignment
40-
" or preceding-sibling::RIGHT_ASSIGN", # not target of right assignment
41-
" or following-sibling::EQ_ASSIGN", # not target of equals assignment
42-
" ])",
43-
"]"
44-
)
34+
symbol_xpath <- "//SYMBOL[
35+
(text() = 'T' or text() = 'F')
36+
and not(parent::expr[OP-DOLLAR or OP-AT])
37+
]"
38+
assignment_xpath <-
39+
"parent::expr[following-sibling::LEFT_ASSIGN or preceding-sibling::RIGHT_ASSIGN or following-sibling::EQ_ASSIGN]"
4540

46-
xpath_assignment <- paste0(
47-
"//SYMBOL[",
48-
" (text() = 'T' or text() = 'F')", # T or F symbol
49-
" and not(preceding-sibling::OP-DOLLAR)", # not part of a $-subset expression
50-
" and parent::expr[", # , but ...
51-
" following-sibling::LEFT_ASSIGN", # target of left assignment
52-
" or preceding-sibling::RIGHT_ASSIGN", # target of right assignment
53-
" or following-sibling::EQ_ASSIGN", # target of equals assignment
54-
" ]",
55-
"]"
56-
)
41+
usage_xpath <- sprintf("%s[not(%s)]", symbol_xpath, assignment_xpath)
42+
assignment_xpath <- sprintf("%s[%s]", symbol_xpath, assignment_xpath)
5743

5844
replacement_map <- c(T = "TRUE", F = "FALSE")
5945

@@ -62,8 +48,8 @@ T_and_F_symbol_linter <- function() { # nolint: object_name.
6248
return(list())
6349
}
6450

65-
bad_exprs <- xml2::xml_find_all(source_expression$xml_parsed_content, xpath)
66-
bad_assigns <- xml2::xml_find_all(source_expression$xml_parsed_content, xpath_assignment)
51+
bad_usage <- xml2::xml_find_all(source_expression$xml_parsed_content, usage_xpath)
52+
bad_assignment <- xml2::xml_find_all(source_expression$xml_parsed_content, assignment_xpath)
6753

6854
make_lints <- function(expr, fmt) {
6955
symbol <- xml2::xml_text(expr)
@@ -79,8 +65,8 @@ T_and_F_symbol_linter <- function() { # nolint: object_name.
7965
}
8066

8167
c(
82-
make_lints(bad_exprs, "Use %s instead of the symbol %s."),
83-
make_lints(bad_assigns, "Don't use %2$s as a variable name, as it can break code relying on %2$s being %1$s.")
68+
make_lints(bad_usage, "Use %s instead of the symbol %s."),
69+
make_lints(bad_assignment, "Don't use %2$s as a variable name, as it can break code relying on %2$s being %1$s.")
8470
)
8571
})
8672
}

R/for_loop_index_linter.R

+1-4
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,7 @@ for_loop_index_linter <- function() {
3434
//forcond
3535
/SYMBOL[text() =
3636
following-sibling::expr
37-
//SYMBOL[not(
38-
preceding-sibling::OP-DOLLAR
39-
or parent::expr[preceding-sibling::OP-LEFT-BRACKET]
40-
)]
37+
//SYMBOL[not(parent::expr[OP-DOLLAR or OP-AT or preceding-sibling::OP-LEFT-BRACKET])]
4138
/text()
4239
]
4340
"

R/literal_coercion_linter.R

+2-2
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ literal_coercion_linter <- function() {
5454

5555
# notes for clarification:
5656
# - as.integer(1e6) is arguably easier to read than 1000000L
57-
# - in x$"abc", the "abc" STR_CONST is at the top level, so exclude OP-DOLLAR
57+
# - in x$"abc", the "abc" STR_CONST is at the top level, so exclude OP-DOLLAR (ditto OP-AT)
5858
# - need condition against STR_CONST w/ EQ_SUB to skip quoted keyword arguments (see tests)
5959
# - for {rlang} coercers, both `int(1)` and `int(1, )` need to be linted
6060
not_extraction_or_scientific <- "
61-
not(OP-DOLLAR)
61+
not(OP-DOLLAR or OP-AT)
6262
and (
6363
NUM_CONST[not(contains(translate(text(), 'E', 'e'), 'e'))]
6464
or STR_CONST[not(following-sibling::*[1][self::EQ_SUB])]

R/object_name_linter.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ object_name_xpath <- local({
1010
# is not possible for strings, though we do still have to
1111
# be aware of cases like 'a$"b" <- 1'.
1212
xp_assignment_target_fmt <- paste0(
13-
"not(preceding-sibling::OP-DOLLAR)",
13+
"not(parent::expr[OP-DOLLAR or OP-AT])",
1414
"and %1$s::expr[",
1515
" following-sibling::LEFT_ASSIGN",
1616
" or preceding-sibling::RIGHT_ASSIGN",

R/undesirable_function_linter.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ undesirable_function_linter <- function(fun = default_undesirable_functions,
6969
xp_text_in_table(c("library", "require")),
7070
"]])"
7171
),
72-
"not(preceding-sibling::OP-DOLLAR)"
72+
"not(parent::expr[OP-DOLLAR or OP-AT])"
7373
)
7474

7575
if (symbol_is_undesirable) {

R/unreachable_code_linter.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ unreachable_code_linter <- function() {
3737
/following-sibling::expr
3838
/*[
3939
self::expr
40-
and expr[1][not(OP-DOLLAR) and SYMBOL_FUNCTION_CALL[text() = 'return' or text() = 'stop']]
40+
and expr[1][not(OP-DOLLAR or OP-AT) and SYMBOL_FUNCTION_CALL[text() = 'return' or text() = 'stop']]
4141
and (position() != last() - 1 or not(following-sibling::OP-RIGHT-BRACE))
4242
and @line2 < following-sibling::*[1]/@line2
4343
]

R/yoda_test_linter.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ yoda_test_linter <- function() {
4242
# TODO(#963): fully generalize this & re-use elsewhere
4343
const_condition <- "
4444
NUM_CONST
45-
or (STR_CONST and not(OP-DOLLAR))
45+
or (STR_CONST and not(OP-DOLLAR or OP-AT))
4646
or ((OP-PLUS or OP-MINUS) and count(expr[NUM_CONST]) = 2)
4747
"
4848
xpath <- glue::glue("

tests/testthat/test-T_and_F_symbol_linter.R

+8-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ test_that("T_and_F_symbol_linter blocks disallowed usages", {
2626
linter
2727
)
2828

29+
expect_lint("DF$bool <- T", msg_true, linter)
30+
expect_lint("S4@bool <- T", msg_true, linter)
31+
expect_lint("sum(x, na.rm = T)", msg_true, linter)
32+
2933
# Regression test for #657
3034
expect_lint(
3135
trim_some("
@@ -35,15 +39,16 @@ test_that("T_and_F_symbol_linter blocks disallowed usages", {
3539
)
3640
3741
x$F <- 42L
42+
y@T <- 84L
3843
3944
T <- \"foo\"
4045
F = \"foo2\"
4146
\"foo3\" -> T
4247
"),
4348
list(
44-
list(message = msg_variable_true),
45-
list(message = msg_variable_false),
46-
list(message = msg_variable_true)
49+
list(message = msg_variable_true, line_number = 9L),
50+
list(message = msg_variable_false, line_number = 10L),
51+
list(message = msg_variable_true, line_number = 11L)
4752
),
4853
linter
4954
)

tests/testthat/test-for_loop_index_linter.R

+15
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,22 @@ test_that("for_loop_index_linter skips allowed usages", {
55

66
# this is OK, so not every symbol is problematic
77
expect_lint("for (col in DF$col) {}", NULL, linter)
8+
expect_lint("for (col in S4@col) {}", NULL, linter)
89
expect_lint("for (col in DT[, col]) {}", NULL, linter)
10+
11+
# make sure symbol check is scoped
12+
expect_lint(
13+
trim_some("
14+
{
15+
for (i in 1:10) {
16+
42L
17+
}
18+
i <- 7L
19+
}
20+
"),
21+
NULL,
22+
linter
23+
)
924
})
1025

1126
test_that("for_loop_index_linter blocks simple disallowed usages", {

tests/testthat/test-literal_coercion_linter.R

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ test_that("literal_coercion_linter skips allowed usages", {
33

44
# naive xpath includes the "_f0" here as a literal
55
expect_lint('as.numeric(x$"_f0")', NULL, linter)
6+
expect_lint('as.numeric(x@"_f0")', NULL, linter)
67
# only examine the first method for as.<type> methods
78
expect_lint("as.character(as.Date(x), '%Y%m%d')", NULL, linter)
89

tests/testthat/test-object_name_linter.R

+18-3
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,29 @@ test_that("linter accepts vector of styles", {
105105
test_that("dollar subsetting only lints the first expression", {
106106
# Regression test for #582
107107
linter <- object_name_linter()
108-
lint_msg <- "Variable and function name style should match snake_case or symbols."
108+
lint_msg <- rex::rex("Variable and function name style should match snake_case or symbols.")
109109

110110
expect_lint("my_var$MY_COL <- 42L", NULL, linter)
111111
expect_lint("MY_VAR$MY_COL <- 42L", lint_msg, linter)
112-
expect_lint("my_var$MY_SUB$MY_COL <- 42L", NULL, linter)
113-
expect_lint("MY_VAR$MY_SUB$MY_COL <- 42L", lint_msg, linter)
112+
expect_lint("my_var@MY_SUB <- 42L", NULL, linter)
113+
expect_lint("MY_VAR@MY_SUB <- 42L", lint_msg, linter)
114114
})
115115

116+
patrick::with_parameters_test_that(
117+
"nested extraction only lints on the first symbol",
118+
expect_lint(
119+
sprintf("%s%sMY_SUB%sMY_COL <- 42L", if (should_lint) "MY_VAR" else "my_var", op1, op2),
120+
if (should_lint) rex::rex("Variable and function name style should match snake_case or symbols."),
121+
object_name_linter()
122+
),
123+
.cases = within(
124+
expand.grid(should_lint = c(TRUE, FALSE), op1 = c("$", "@"), op2 = c("$", "@"), stringsAsFactors = FALSE),
125+
{
126+
.test_name <- sprintf("(should lint? %s, op1=%s, op2=%s)", should_lint, op1, op2)
127+
}
128+
)
129+
)
130+
116131
test_that("assignment targets of compound lhs are correctly identified", {
117132
linter <- object_name_linter()
118133
lint_msg <- "Variable and function name style should match snake_case or symbols."

tests/testthat/test-undesirable_function_linter.R

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ test_that("linter returns correct linting", {
2222
)
2323
# regression test for #1050
2424
expect_lint("df$return <- 1", NULL, linter)
25+
expect_lint("df@return <- 1", NULL, linter)
2526
})
2627

2728
test_that("it's possible to NOT lint symbols", {

tests/testthat/test-unreachable_code_linter.R

+11
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,17 @@ test_that("unreachable_code_linter ignores code after foo$stop(), which might be
119119
NULL,
120120
unreachable_code_linter()
121121
)
122+
expect_lint(
123+
trim_some("
124+
foo <- function(x) {
125+
bar <- get_process()
126+
bar@stop()
127+
TRUE
128+
}
129+
"),
130+
NULL,
131+
unreachable_code_linter()
132+
)
122133
})
123134

124135
test_that("unreachable_code_linter ignores terminal nolint end comments", {

tests/testthat/test-yoda_test_linter.R

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ test_that("yoda_test_linter blocks simple disallowed usages", {
3434
test_that("yoda_test_linter ignores strings in $ expressions", {
3535
# the "key" here shows up at the same level of the parse tree as plain "key" normally would
3636
expect_lint('expect_equal(x$"key", 2)', NULL, yoda_test_linter())
37+
expect_lint('expect_equal(x@"key", 2)', NULL, yoda_test_linter())
3738
})
3839

3940
# if we only inspect the first argument & ignore context, get false positives

0 commit comments

Comments
 (0)