Skip to content

Commit

Permalink
New consecutive_mutate_linter (#2305)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelChirico authored Nov 21, 2023
1 parent 0815b2a commit a99e7d4
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 4 deletions.
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'
'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
#' 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

0 comments on commit a99e7d4

Please sign in to comment.