-
Notifications
You must be signed in to change notification settings - Fork 195
New nested_pipe_linter #2301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New nested_pipe_linter #2301
Changes from 8 commits
9952d0f
030673a
baaa279
fb4376f
f32138f
ce6ef5d
695d23b
6156b8e
55152be
b462119
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| #' Block usage of pipes nested inside other calls | ||
| #' | ||
| #' Nesting pipes harms readability; extract sub-steps to separate variables, | ||
| #' append further pipeline steps, or otherwise refactor such usage away. | ||
| #' | ||
| #' When [try()] or [tryCatch()] are the "outer" call, no lint is thrown, | ||
| #' since the "unnested" version of such usage may not work as intended | ||
| #' due to how evaluation happens in such cases. | ||
| #' | ||
| #' @param allow_inline Logical, default `TRUE`, in which case only "inner" | ||
| #' pipelines which span more than one line are linted. If `FALSE`, even | ||
| #' "inner" pipelines that fit in one line are linted. | ||
| #' | ||
| #' @examples | ||
| #' # will produce lints | ||
| #' code <- "df1 %>%\n inner_join(df2 %>%\n select(a, b)\n )" | ||
| #' writeLines(code) | ||
| #' lint( | ||
| #' text = code, | ||
| #' linters = nested_pipe_linter() | ||
| #' ) | ||
| #' | ||
| #' lint( | ||
| #' text = "df1 %>% inner_join(df2 %>% select(a, b))", | ||
| #' linters = nested_pipe_linter(allow_inline = FALSE) | ||
| #' ) | ||
| #' | ||
| #' # okay | ||
| #' lint( | ||
| #' text = "df1 %>% inner_join(df2 %>% select(a, b))", | ||
| #' linters = nested_pipe_linter() | ||
| #' ) | ||
| #' | ||
| #' lint( | ||
| #' text = "tryCatch(x %>% filter(grp == 'a'), error = identity)", | ||
| #' linters = nested_pipe_linter() | ||
| #' ) | ||
| #' | ||
| #' @evalRd rd_tags("nested_pipe_linter") | ||
| #' @seealso [linters] for a complete list of linters available in lintr. | ||
| #' @export | ||
| nested_pipe_linter <- function(allow_inline = TRUE) { | ||
| multiline_and <- if (allow_inline) "@line1 != @line2 and" else "" | ||
| xpath <- glue(" | ||
| (//PIPE | //SPECIAL[{ xp_text_in_table(magrittr_pipes) }]) | ||
| /parent::expr[{multiline_and} preceding-sibling::expr/SYMBOL_FUNCTION_CALL[ | ||
| not(text() = 'try' or text() = 'tryCatch') | ||
| and ( | ||
| text() != 'switch' | ||
| or parent::expr | ||
| /following-sibling::expr[1] | ||
| /*[self::PIPE or self::SPECIAL[{ xp_text_in_table(magrittr_pipes) }]] | ||
| ) | ||
| ]] | ||
| ") | ||
|
|
||
| Linter(function(source_expression) { | ||
| if (!is_lint_level(source_expression, "expression")) { | ||
| return(list()) | ||
| } | ||
|
|
||
| xml <- source_expression$xml_parsed_content | ||
|
|
||
| bad_expr <- xml_find_all(xml, xpath) | ||
|
|
||
| xml_nodes_to_lints( | ||
| bad_expr, | ||
| source_expression = source_expression, | ||
| lint_message = "Don't nest pipes inside other calls.", | ||
| type = "warning" | ||
| ) | ||
| }) | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| test_that("nested_pipe_linter skips allowed usages", { | ||
| linter <- nested_pipe_linter() | ||
|
|
||
| expect_lint("a %>% b() %>% c()", NULL, linter) | ||
|
|
||
| expect_lint( | ||
| trim_some(" | ||
| foo <- function(x) { | ||
| out <- a %>% b() | ||
| return(out) | ||
| } | ||
| "), | ||
| NULL, | ||
| linter | ||
| ) | ||
|
|
||
| # pipes fitting on one line can be ignored | ||
| expect_lint( | ||
| "bind_rows(a %>% select(b), c %>% select(b))", | ||
| NULL, | ||
| linter | ||
| ) | ||
|
|
||
| # switch outputs are OK | ||
| expect_lint("switch(x, a = x %>% foo())", NULL, linter) | ||
| # final position is an output position | ||
| expect_lint("switch(x, a = x, x %>% foo())", NULL, linter) | ||
|
|
||
| # inline switch inputs are not linted | ||
| expect_lint( | ||
| trim_some(" | ||
| switch( | ||
| x %>% foo(), | ||
| a = x | ||
| ) | ||
| "), | ||
| NULL, | ||
| linter | ||
| ) | ||
|
|
||
| # try/tryCatch must be evaluated inside the call | ||
| expect_lint("try(x %>% foo())", NULL, linter) | ||
| expect_lint("tryCatch(x %>% foo(), error = identity)", NULL, linter) | ||
| }) | ||
|
|
||
| test_that("nested_pipe_linter blocks simple disallowed usages", { | ||
| linter <- nested_pipe_linter() | ||
| linter_inline <- nested_pipe_linter(allow_inline = FALSE) | ||
| lint_msg <- rex::rex("Don't nest pipes inside other calls.") | ||
|
|
||
| expect_lint( | ||
| "bind_rows(a %>% select(b), c %>% select(b))", | ||
| list(lint_msg, lint_msg), | ||
| linter_inline | ||
| ) | ||
|
|
||
| expect_lint( | ||
| trim_some(" | ||
| print( | ||
| a %>% | ||
| filter(b > c) | ||
| ) | ||
| "), | ||
| lint_msg, | ||
| linter | ||
| ) | ||
|
|
||
| # switch inputs are linted | ||
| expect_lint( | ||
| trim_some(" | ||
| switch( | ||
| x %>% | ||
| foo(), | ||
| a = x | ||
| ) | ||
| "), | ||
| lint_msg, | ||
| linter | ||
| ) | ||
|
|
||
| expect_lint( | ||
| trim_some(" | ||
| switch( | ||
| x %>% foo(), | ||
| a = x | ||
| ) | ||
| "), | ||
| lint_msg, | ||
| linter_inline | ||
| ) | ||
| }) | ||
|
|
||
| test_that("Native pipes are handled as well", { | ||
| skip_if_not_r_version("4.1.0") | ||
|
|
||
| linter <- nested_pipe_linter() | ||
| linter_inline <- nested_pipe_linter(allow_inline = FALSE) | ||
| lint_msg <- rex::rex("Don't nest pipes inside other calls.") | ||
|
|
||
| expect_lint( | ||
| "bind_rows(a |> select(b), c |> select(b))", | ||
| NULL, | ||
| linter | ||
| ) | ||
| expect_lint( | ||
| "bind_rows(a |> select(b), c |> select(b))", | ||
| list(lint_msg, lint_msg), | ||
| linter_inline | ||
| ) | ||
|
|
||
| expect_lint( | ||
| trim_some(" | ||
| print( | ||
| a |> | ||
| filter(b > c) | ||
| ) | ||
| "), | ||
| lint_msg, | ||
| linter | ||
| ) | ||
| }) | ||
|
|
||
| test_that("lints vectorize", { | ||
| lint_msg <- rex::rex("Don't nest pipes inside other calls.") | ||
|
|
||
| lines <- trim_some("{ | ||
| bind_rows( | ||
| a %>% select(b), | ||
| c %>% | ||
| select(d), | ||
| e %>% | ||
| select(f) %>% | ||
| filter(g > 0), | ||
| h %>% filter(i < 0) | ||
| ) | ||
| }") | ||
| expect_lint( | ||
| lines, | ||
| list( | ||
| list(lint_msg, line_number = 4L), | ||
| list(lint_msg, line_number = 6L) | ||
| ), | ||
| nested_pipe_linter() | ||
| ) | ||
|
|
||
| expect_lint( | ||
| lines, | ||
| list( | ||
| list(lint_msg, line_number = 3L), | ||
| list(lint_msg, line_number = 4L), | ||
| list(lint_msg, line_number = 6L), | ||
| list(lint_msg, line_number = 9L) | ||
| ), | ||
| nested_pipe_linter(allow_inline = FALSE) | ||
| ) | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.