Skip to content

Commit 175065c

Browse files
Add a third fuzzer for @/$ equivalency (#2821)
1 parent 30d1e8e commit 175065c

17 files changed

+275
-165
lines changed

.dev/maybe_fuzz_content.R

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ maybe_fuzz_content <- function(file, lines) {
99
file.copy(file, new_file, copy.mode = FALSE)
1010
}
1111

12-
apply_fuzzers(new_file)
12+
apply_fuzzers(new_file, list(function_lambda_fuzzer, pipe_fuzzer, dollar_at_fuzzer))
1313

1414
new_file
1515
}
@@ -54,16 +54,21 @@ pipe_fuzzer <- simple_swap_fuzzer(
5454
replacements = c("%>%", "|>")
5555
)
5656

57+
dollar_at_fuzzer <- simple_swap_fuzzer(
58+
\(pd) pd$token %in% c("'$'", "'@'"),
59+
replacements = c("$", "@")
60+
)
61+
5762
# we could also consider just passing any test where no fuzzing takes place,
5863
# i.e. letting the other GHA handle whether unfuzzed tests pass as expected.
59-
apply_fuzzers <- function(f) {
64+
apply_fuzzers <- function(f, fuzzers) {
6065
pd <- error_or_parse_data(f)
6166
if (inherits(pd, "error")) {
6267
return(invisible())
6368
}
6469

6570
unedited <- lines <- readLines(f)
66-
for (fuzzer in list(function_lambda_fuzzer, pipe_fuzzer)) {
71+
for (fuzzer in fuzzers) {
6772
updated_lines <- fuzzer(pd, lines)
6873
if (is.null(updated_lines) || identical(unedited, updated_lines)) next # skip some I/O if we can
6974
writeLines(updated_lines, f)

NEWS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
* `brace_linter()` requires `test_that()`'s `code=` argument to have curly braces (#2292, @MichaelChirico).
4646
* `fixed_regex_linter()` recognizes usage of the new (R 4.5.0) `grepv()` wrapper of `grep()`; `regex_subset_linter()` also recommends `grepv()` alternatives (#2855, @MichaelChirico).
4747
* `object_usage_linter()` lints missing packages that may cause false positives (#2872, @AshesITR)
48+
* New argument `include_s4_slots` for the `xml_find_function_calls()` entry in the `get_source_expressions()` to govern whether calls of the form `s4Obj@fun()` are included in the result (#2820, @MichaelChirico).
4849

4950
### New linters
5051

@@ -76,6 +77,11 @@ files in Windows (#2882, @Bisaloo).
7677
+ `library_call_linter()`
7778
+ `terminal_close_linter()`
7879
+ `unnecessary_lambda_linter()`
80+
* More consistency on handling `@` extractions (#2820, @MichaelChirico).
81+
+ `function_left_parentheses_linter()`
82+
+ `indentation_linter()`
83+
+ `library_call_linter()`
84+
+ `missing_argument_linter()`
7985

8086
## Notes
8187

R/function_left_parentheses_linter.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ function_left_parentheses_linter <- function() { # nolint: object_length.
4747
# because it allows the xpath to be the same for both FUNCTION and SYMBOL_FUNCTION_CALL.
4848
# Further, write 4 separate XPaths because the 'range_end_xpath' differs for these two nodes.
4949
bad_line_fun_xpath <- "(//FUNCTION | //OP-LAMBDA)[@line1 != following-sibling::OP-LEFT-PAREN/@line1]"
50-
bad_line_call_xpath <- "//SYMBOL_FUNCTION_CALL[@line1 != parent::expr/following-sibling::OP-LEFT-PAREN/@line1]"
50+
bad_line_call_xpath <-
51+
"(//SYMBOL_FUNCTION_CALL | //SLOT)[@line1 != parent::expr/following-sibling::OP-LEFT-PAREN/@line1]"
5152
bad_col_fun_xpath <- "(//FUNCTION | //OP-LAMBDA)[
5253
@line1 = following-sibling::OP-LEFT-PAREN/@line1
5354
and @col2 != following-sibling::OP-LEFT-PAREN/@col1 - 1

R/indentation_linter.R

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,21 @@ indentation_linter <- function(indent = 2L, hanging_indent_style = c("tidy", "al
161161
glue("self::{paren_tokens_left}/following-sibling::{paren_tokens_right}/preceding-sibling::*[1]/@line2"),
162162
glue("
163163
self::*[{xp_and(paste0('not(self::', paren_tokens_left, ')'))}]
164-
/following-sibling::SYMBOL_FUNCTION_CALL
164+
/following-sibling::*[
165+
self::SYMBOL_FUNCTION_CALL
166+
or self::SLOT[parent::expr/following-sibling::OP-LEFT-PAREN]
167+
]
165168
/parent::expr
166169
/following-sibling::expr[1]
167170
/@line2
168171
"),
169172
glue("
170173
self::*[
171174
{xp_and(paste0('not(self::', paren_tokens_left, ')'))}
172-
and not(following-sibling::SYMBOL_FUNCTION_CALL)
175+
and not(following-sibling::*[
176+
self::SYMBOL_FUNCTION_CALL
177+
or self::SLOT[parent::expr/following-sibling::OP-LEFT-PAREN]
178+
])
173179
]
174180
/following-sibling::*[not(self::COMMENT)][1]
175181
/@line2
@@ -237,20 +243,19 @@ indentation_linter <- function(indent = 2L, hanging_indent_style = c("tidy", "al
237243
is_hanging <- logical(length(indent_levels))
238244

239245
indent_changes <- xml_find_all(xml, xp_indent_changes)
240-
for (change in indent_changes) {
241-
change_type <- find_indent_type(change)
242-
change_begin <- as.integer(xml_attr(change, "line1")) + 1L
243-
change_end <- xml_find_num(change, xp_block_ends)
244-
if (isTRUE(change_begin <= change_end)) {
245-
to_indent <- seq(from = change_begin, to = change_end)
246-
expected_indent_levels[to_indent] <- find_new_indent(
247-
current_indent = expected_indent_levels[to_indent],
248-
change_type = change_type,
249-
indent = indent,
250-
hanging_indent = as.integer(xml_attr(change, "col2"))
251-
)
252-
is_hanging[to_indent] <- change_type == "hanging"
253-
}
246+
change_types <- vapply(indent_changes, find_indent_type, character(1L))
247+
change_begins <- as.integer(xml_attr(indent_changes, "line1")) + 1L
248+
change_ends <- xml_find_num(indent_changes, xp_block_ends)
249+
col2s <- as.integer(xml_attr(indent_changes, "col2"))
250+
for (ii in which(change_begins <= change_ends)) {
251+
to_indent <- seq(from = change_begins[ii], to = change_ends[ii])
252+
expected_indent_levels[to_indent] <- find_new_indent(
253+
current_indent = expected_indent_levels[to_indent],
254+
change_type = change_types[ii],
255+
indent = indent,
256+
hanging_indent = col2s[ii]
257+
)
258+
is_hanging[to_indent] <- change_types[ii] == "hanging"
254259
}
255260

256261
in_str_const <- logical(length(indent_levels))

R/library_call_linter.R

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ library_call_linter <- function(allow_preamble = TRUE) {
9797
upfront_call_xpath <- glue("
9898
//SYMBOL_FUNCTION_CALL[{ attach_call_cond }][last()]
9999
/preceding::expr
100-
/SYMBOL_FUNCTION_CALL[{ unsuppressed_call_cond }][last()]
100+
/*[
101+
(self::SYMBOL_FUNCTION_CALL or self::SLOT[parent::expr/following-sibling::OP-LEFT-PAREN])
102+
and ({ unsuppressed_call_cond })
103+
][last()]
101104
/following::expr[SYMBOL_FUNCTION_CALL[{ attach_call_cond }]]
102105
/parent::expr
103106
")

R/missing_argument_linter.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ missing_argument_linter <- function(except = c("alist", "quote", "switch"), allo
5050
")
5151

5252
Linter(linter_level = "file", function(source_expression) {
53-
xml_targets <- source_expression$xml_find_function_calls(NULL, keep_names = TRUE)
53+
xml_targets <- source_expression$xml_find_function_calls(NULL, keep_names = TRUE, include_s4_slots = TRUE)
5454
xml_targets <- xml_targets[!names(xml_targets) %in% except]
5555

5656
missing_args <- xml_find_all(xml_targets, xpath)

R/source_utils.R

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,26 @@ build_xml_find_function_calls <- function(xml) {
1313
function_call_cache <- xml_find_all(xml, "//SYMBOL_FUNCTION_CALL/parent::expr")
1414
names(function_call_cache) <- get_r_string(function_call_cache, "SYMBOL_FUNCTION_CALL")
1515

16-
function(function_names, keep_names = FALSE) {
16+
s4_slot_cache <- xml_find_all(xml, "//SLOT/parent::expr[following-sibling::OP-LEFT-PAREN]")
17+
names(s4_slot_cache) <- get_r_string(s4_slot_cache, "SLOT")
18+
19+
function(function_names, keep_names = FALSE, include_s4_slots = FALSE) {
1720
if (is.null(function_names)) {
18-
res <- function_call_cache
21+
if (include_s4_slots) {
22+
res <- combine_nodesets(function_call_cache, s4_slot_cache)
23+
} else {
24+
res <- function_call_cache
25+
}
1926
} else {
20-
res <- function_call_cache[names(function_call_cache) %in% function_names]
27+
include_function_idx <- names(function_call_cache) %in% function_names
28+
if (include_s4_slots) {
29+
res <- combine_nodesets(
30+
function_call_cache[include_function_idx],
31+
s4_slot_cache[names(s4_slot_cache) %in% function_names]
32+
)
33+
} else {
34+
res <- function_call_cache[include_function_idx]
35+
}
2136
}
2237
if (keep_names) res else unname(res)
2338
}

tests/testthat/test-any_duplicated_linter.R

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# nofuzz start
12
test_that("any_duplicated_linter skips allowed usages", {
23
linter <- any_duplicated_linter()
34

@@ -101,3 +102,4 @@ test_that("any_duplicated_linter catches expression with two types of lint", {
101102
linter
102103
)
103104
})
105+
# nofuzz end

tests/testthat/test-function_left_parentheses_linter.R

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
test_that("function_left_parentheses_linter skips allowed usages", {
22
linter <- function_left_parentheses_linter()
33

4-
expect_lint("blah", NULL, linter)
5-
expect_lint("print(blah)", NULL, linter)
6-
expect_lint('"print"(blah)', NULL, linter)
7-
expect_lint("base::print(blah)", NULL, linter)
8-
expect_lint('base::"print"(blah)', NULL, linter)
9-
expect_lint("base::print(blah, fun(1))", NULL, linter)
10-
expect_lint("blah <- function(blah) { }", NULL, linter)
11-
expect_lint("(1 + 1)", NULL, linter)
12-
expect_lint("( (1 + 1) )", NULL, linter)
13-
expect_lint("if (blah) { }", NULL, linter)
14-
expect_lint("for (i in j) { }", NULL, linter)
15-
expect_lint("1 * (1 + 1)", NULL, linter)
16-
expect_lint("!(1 == 1)", NULL, linter)
17-
expect_lint("(2 - 1):(3 - 1)", NULL, linter)
18-
expect_lint("c(1, 2, 3)[(2 - 1)]", NULL, linter)
19-
expect_lint("list(1, 2, 3)[[(2 - 1)]]", NULL, linter)
20-
expect_lint("range(10)[(2 - 1):(10 - 1)]", NULL, linter)
21-
expect_lint("function(){function(){}}()()", NULL, linter)
22-
expect_lint("c(function(){})[1]()", NULL, linter)
23-
expect_lint("function(x) (mean(x) + 3)", NULL, linter)
24-
expect_lint("\"blah (1)\"", NULL, linter)
4+
expect_no_lint("blah", linter)
5+
expect_no_lint("print(blah)", linter)
6+
expect_no_lint('"print"(blah)', linter)
7+
expect_no_lint("base::print(blah)", linter)
8+
expect_no_lint('base::"print"(blah)', linter)
9+
expect_no_lint("base::print(blah, fun(1))", linter)
10+
expect_no_lint("blah <- function(blah) { }", linter)
11+
expect_no_lint("(1 + 1)", linter)
12+
expect_no_lint("( (1 + 1) )", linter)
13+
expect_no_lint("if (blah) { }", linter)
14+
expect_no_lint("for (i in j) { }", linter)
15+
expect_no_lint("1 * (1 + 1)", linter)
16+
expect_no_lint("!(1 == 1)", linter)
17+
expect_no_lint("(2 - 1):(3 - 1)", linter)
18+
expect_no_lint("c(1, 2, 3)[(2 - 1)]", linter)
19+
expect_no_lint("list(1, 2, 3)[[(2 - 1)]]", linter)
20+
expect_no_lint("range(10)[(2 - 1):(10 - 1)]", linter)
21+
expect_no_lint("function(){function(){}}()()", linter)
22+
expect_no_lint("c(function(){})[1]()", linter)
23+
expect_no_lint("function(x) (mean(x) + 3)", linter)
24+
expect_no_lint('"blah (1)"', linter)
2525
})
2626

2727
test_that("function_left_parentheses_linter blocks disallowed usages", {
@@ -168,7 +168,7 @@ test_that("it doesn't produce invalid lints", {
168168
test_that("newline in character string doesn't trigger false positive (#1963)", {
169169
linter <- function_left_parentheses_linter()
170170

171-
expect_lint('foo("\n")$bar()', NULL, linter)
171+
expect_no_lint('foo("\n")$bar()', linter)
172172
# also corrected the lint metadata for similar cases
173173
expect_lint(
174174
trim_some('
@@ -182,14 +182,27 @@ test_that("newline in character string doesn't trigger false positive (#1963)",
182182
list(line_number = 3L, column_number = 6L),
183183
linter
184184
)
185+
186+
expect_lint(
187+
trim_some('
188+
(
189+
foo("
190+
")@bar
191+
()
192+
)
193+
'),
194+
# attach to 'b' in '@bar'
195+
list(line_number = 3L, column_number = 6L),
196+
linter
197+
)
185198
})
186199

187200
test_that("shorthand functions are handled", {
188201
skip_if_not_r_version("4.1.0")
189202
linter <- function_left_parentheses_linter()
190203
fun_lint_msg <- rex::rex("Remove spaces before the left parenthesis in a function definition.")
191204

192-
expect_lint("blah <- \\(blah) { }", NULL, linter)
193-
expect_lint("\\(){\\(){}}()()", NULL, linter)
205+
expect_no_lint("blah <- \\(blah) { }", linter)
206+
expect_no_lint("\\(){\\(){}}()()", linter)
194207
expect_lint("test <- \\ (x) { }", fun_lint_msg, linter)
195208
})

tests/testthat/test-get_source_expressions.R

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,14 @@ test_that("returned data structure is complete", {
255255
})
256256

257257
test_that("xml_find_function_calls works as intended", {
258-
lines <- c("foo()", "bar()", "foo()", "{ foo(); foo(); bar() }")
258+
lines <- c(
259+
"foo()",
260+
"bar()",
261+
"foo()",
262+
"s4Obj@baz()",
263+
"{ foo(); foo(); bar(); s4Obj@baz() }",
264+
NULL
265+
)
259266
temp_file <- withr::local_tempfile(lines = lines)
260267

261268
exprs <- get_source_expressions(temp_file)
@@ -270,30 +277,40 @@ test_that("xml_find_function_calls works as intended", {
270277
expect_length(exprs$expressions[[2L]]$xml_find_function_calls("foo"), 0L)
271278
expect_length(exprs$expressions[[2L]]$xml_find_function_calls("bar"), 1L)
272279

273-
expect_length(exprs$expressions[[4L]]$xml_find_function_calls("foo"), 2L)
274-
expect_length(exprs$expressions[[4L]]$xml_find_function_calls("bar"), 1L)
275-
expect_length(exprs$expressions[[4L]]$xml_find_function_calls(c("foo", "bar")), 3L)
280+
expect_length(exprs$expressions[[5L]]$xml_find_function_calls("foo"), 2L)
281+
expect_length(exprs$expressions[[5L]]$xml_find_function_calls("bar"), 1L)
282+
expect_length(exprs$expressions[[5L]]$xml_find_function_calls(c("foo", "bar")), 3L)
276283

277284
# file-level source expression contains all function calls
278-
expect_length(exprs$expressions[[5L]]$xml_find_function_calls("foo"), 4L)
279-
expect_length(exprs$expressions[[5L]]$xml_find_function_calls("bar"), 2L)
280-
expect_length(exprs$expressions[[5L]]$xml_find_function_calls(c("foo", "bar")), 6L)
285+
expect_length(exprs$expressions[[6L]]$xml_find_function_calls("foo"), 4L)
286+
expect_length(exprs$expressions[[6L]]$xml_find_function_calls("bar"), 2L)
287+
expect_length(exprs$expressions[[6L]]$xml_find_function_calls(c("foo", "bar")), 6L)
281288

282289
# Also check order is retained:
283290
expect_identical(
284-
exprs$expressions[[5L]]$xml_find_function_calls(c("foo", "bar")),
285-
xml_find_all(exprs$expressions[[5L]]$full_xml_parsed_content, "//SYMBOL_FUNCTION_CALL/parent::expr")
291+
exprs$expressions[[6L]]$xml_find_function_calls(c("foo", "bar")),
292+
xml_find_all(exprs$expressions[[6L]]$full_xml_parsed_content, "//SYMBOL_FUNCTION_CALL/parent::expr")
286293
)
287294

288295
# Check naming and full cache
289296
expect_identical(
290-
exprs$expressions[[5L]]$xml_find_function_calls(NULL),
291-
exprs$expressions[[5L]]$xml_find_function_calls(c("foo", "bar"))
297+
exprs$expressions[[6L]]$xml_find_function_calls(NULL),
298+
exprs$expressions[[6L]]$xml_find_function_calls(c("foo", "bar"))
292299
)
293300
expect_named(
294-
exprs$expressions[[4L]]$xml_find_function_calls(c("foo", "bar"), keep_names = TRUE),
301+
exprs$expressions[[5L]]$xml_find_function_calls(c("foo", "bar"), keep_names = TRUE),
295302
c("foo", "foo", "bar")
296303
)
304+
305+
# include_s4_slots
306+
expect_identical(
307+
exprs$expressions[[6L]]$xml_find_function_calls(NULL, include_s4_slots = TRUE),
308+
exprs$expressions[[6L]]$xml_find_function_calls(c("foo", "bar", "baz"), include_s4_slots = TRUE)
309+
)
310+
expect_named(
311+
exprs$expressions[[5L]]$xml_find_function_calls(NULL, keep_names = TRUE, include_s4_slots = TRUE),
312+
c("foo", "foo", "bar", "baz")
313+
)
297314
})
298315

299316
test_that("#1262: xml_parsed_content gets returned as missing even if there's no parsed_content", {

0 commit comments

Comments
 (0)