Skip to content
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 consecutive_mutate_linter #2305

Merged
merged 15 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ Collate:
'condition_message_linter.R'
'conjunct_test_linter.R'
'consecutive_assertion_linter.R'
'consecutive_mutate_linter.R'
IndrajeetPatil marked this conversation as resolved.
Show resolved Hide resolved
'cyclocomp_linter.R'
'declared_functions.R'
'deprecated.R'
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export(comparison_negation_linter)
export(condition_message_linter)
export(conjunct_test_linter)
export(consecutive_assertion_linter)
export(consecutive_mutate_linter)
export(consecutive_stopifnot_linter)
export(cyclocomp_linter)
export(default_linters)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* `which_grepl_linter()` for discouraging `which(grepl(ptn, x))` in favor of directly using `grep(ptn, x)` (part of #884, @MichaelChirico).
* `list_comparison_linter()` for discouraging comparisons on the output of `lapply()`, e.g. `lapply(x, sum) > 10` (part of #884, @MichaelChirico).
* `print_linter()` for discouraging usage of `print()` on string literals like `print("Reached here")` or `print(paste("Found", nrow(DF), "rows."))` (#1894, @MichaelChirico).
* `consecutive_mutate_linter()` for encouraging consecutive calls to `dplyr::mutate()` to be combined (part of #884, @MichaelChirico).
* `if_switch_linter()` for encouraging `switch()` over repeated `if`/`else` tests (part of #884, @MichaelChirico).
* `nested_pipe_linter()` for discouraging pipes within pipes, e.g. `df1 %>% inner_join(df2 %>% select(a, b))` (part of #884, @MichaelChirico).
* `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).
Expand Down
101 changes: 101 additions & 0 deletions R/consecutive_mutate_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#' Require consecutive calls to mutate() to be combined when possible
#'
#' `dplyr::mutate()` accepts any number of columns, so sequences like
#' `DF %>% dplyr::mutate(..1) %>% dplyr::mutate(..2)` are redundant --
#' they can always be expressed with a single call to `dplyr::mutate()`.
#'
#' An exception is for some SQL back-ends, where the translation logic may not be
#' as sophisticated as that in the default `dplyr`, for example in
#' `DF %>% mutate(a = a + 1) %>% mutate(b = a - 2)`.
#'
#' @param invalid_backends Character vector of packages providing dplyr backends
#' which may not be compatible with combining `mutate()` calls in all cases.
#' Defaults to `"dbplyr"` since not all SQL backends can handle re-using
#' a variable defined in the same `mutate()` expression.
#'
#' @examples
#' # will produce lints
#' lint(
#' text = "x %>% mutate(a = 1) %>% mutate(b = 2)",
#' linters = consecutive_mutate_linter()
#' )
#'
#' # okay
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
#' lint(
#' text = "x %>% mutate(a = 1, b = 2)",
#' linters = consecutive_mutate_linter()
#' )
#'
#' code <- "library(dbplyr)\nx %>% mutate(a = 1) %>% mutate(a = a + 1)"
#' writeLines(code)
#' lint(
#' text = code,
#' linters = consecutive_mutate_linter()
#' )
#'
#' @evalRd rd_tags("consecutive_mutate_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
consecutive_mutate_linter <- function(invalid_backends = "dbplyr") {
attach_pkg_xpath <- glue("
//SYMBOL_FUNCTION_CALL[text() = 'library' or text() = 'require']
/parent::expr
/following-sibling::expr
/*[self::SYMBOL or self::STR_CONST]
")

namespace_xpath <- glue("
//SYMBOL_PACKAGE[{ xp_text_in_table(invalid_backends) }]
|
//COMMENT[
contains(text(), '@import')
and (
{xp_or(sprintf(\"contains(text(), '%s')\", invalid_backends))}
)
]
")

# match on the expr, not the SYMBOL_FUNCTION_CALL, to ensure
# namespace-qualified calls only match if the namespaces do.
# expr[2] needed in expr[1][expr[2]] to skip matches on pipelines
# starting like mutate(DF, ...) %>% foo() %>% mutate().
# similarly, expr[1][expr[call='mutate']] covers pipelines
# starting like mutate(DF, ...) %>% mutate(...)
mutate_cond <- xp_and(
"expr/SYMBOL_FUNCTION_CALL[text() = 'mutate']",
"not(SYMBOL_SUB[text() = '.keep' or text() = '.by'])"
)
xpath <- glue("
(//PIPE | //SPECIAL[{ xp_text_in_table(magrittr_pipes) }])
/preceding-sibling::expr[expr[2][{ mutate_cond }] or ({ mutate_cond })]
/following-sibling::expr[{ mutate_cond }]
")

Linter(function(source_expression) {
# need the full file to also catch usages at the top level
if (!is_lint_level(source_expression, "file")) {
return(list())
}

xml <- source_expression$full_xml_parsed_content

attach_str <- get_r_string(xml_find_all(xml, attach_pkg_xpath))
if (any(invalid_backends %in% attach_str)) {
return(list())
}

namespace_expr <- xml_find_first(xml, namespace_xpath)
if (!is.na(namespace_expr)) {
return(list())
}

bad_expr <- xml_find_all(xml, xpath)

xml_nodes_to_lints(
bad_expr,
source_expression = source_expression,
lint_message = "Unify consecutive calls to mutate().",
type = "warning"
)
})
}
1 change: 1 addition & 0 deletions inst/lintr/linters.csv
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ comparison_negation_linter,readability consistency
condition_message_linter,best_practices consistency
conjunct_test_linter,package_development best_practices readability configurable pkg_testthat
consecutive_assertion_linter,style readability consistency
consecutive_mutate_linter,consistency readability configurable efficiency
consecutive_stopifnot_linter,style readability consistency deprecated
cyclocomp_linter,style readability best_practices default configurable
duplicate_argument_linter,correctness common_mistakes configurable
Expand Down
1 change: 1 addition & 0 deletions man/configurable_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions man/consecutive_mutate_linter.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/consistency_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/efficiency_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions man/linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/readability_linters.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading