-
Notifications
You must be signed in to change notification settings - Fork 186
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
assignment_linter() gains operator= argument to allow, e.g. '=' assignments #2711
base: main
Are you sure you want to change the base?
Changes from all commits
c03a1be
b29efda
e2af317
6308bff
1ffd72b
c25bfde
4f39f6e
69841ea
73e3cac
cff4f72
aa05ac7
9792001
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 |
---|---|---|
@@ -1,12 +1,15 @@ | ||
#' Assignment linter | ||
#' | ||
#' Check that `<-` is always used for assignment. | ||
#' Check that the specified operator is used for assignment. | ||
#' | ||
#' @param allow_cascading_assign Logical, default `TRUE`. | ||
#' @param operator Character vector of valid assignment operators. Defaults to allowing `<-` and `<<-`; other valid | ||
#' options are `=`, `->`, `->>`, `%<>%`; use `"any"` to denote "allow all operators", in which case this linter only | ||
#' considers `allow_trailing` for generating lints. | ||
#' @param allow_cascading_assign (Deprecated) Logical, default `TRUE`. | ||
#' If `FALSE`, [`<<-`][base::assignOps] and `->>` are not allowed. | ||
#' @param allow_right_assign Logical, default `FALSE`. If `TRUE`, `->` and `->>` are allowed. | ||
#' @param allow_right_assign (Deprecated) Logical, default `FALSE`. If `TRUE`, `->` and `->>` are allowed. | ||
#' @param allow_trailing Logical, default `TRUE`. If `FALSE` then assignments aren't allowed at end of lines. | ||
#' @param allow_pipe_assign Logical, default `FALSE`. If `TRUE`, magrittr's `%<>%` assignment is allowed. | ||
#' @param allow_pipe_assign (Deprecated) Logical, default `FALSE`. If `TRUE`, magrittr's `%<>%` assignment is allowed. | ||
#' | ||
#' @examples | ||
#' # will produce lints | ||
|
@@ -27,6 +30,11 @@ | |
#' linters = assignment_linter() | ||
#' ) | ||
#' | ||
#' lint( | ||
#' text = "x <- 1", | ||
#' linters = assignment_linter(operator = "=") | ||
#' ) | ||
#' | ||
#' # okay | ||
#' lint( | ||
#' text = "x <- mean(x)", | ||
|
@@ -64,63 +72,113 @@ | |
#' linters = assignment_linter(allow_pipe_assign = TRUE) | ||
#' ) | ||
#' | ||
#' lint( | ||
#' text = "x = 1", | ||
#' linters = assignment_linter(operator = "=") | ||
#' ) | ||
#' | ||
#' @evalRd rd_tags("assignment_linter") | ||
#' @seealso | ||
#' - [linters] for a complete list of linters available in lintr. | ||
#' - <https://style.tidyverse.org/syntax.html#assignment-1> | ||
#' - <https://style.tidyverse.org/pipes.html#assignment-2> | ||
#' @export | ||
assignment_linter <- function(allow_cascading_assign = TRUE, | ||
assignment_linter <- function(operator = c("<-", "<<-"), | ||
allow_cascading_assign = TRUE, | ||
allow_right_assign = FALSE, | ||
allow_trailing = TRUE, | ||
allow_pipe_assign = FALSE) { | ||
trailing_assign_xpath <- paste( | ||
if (!missing(allow_cascading_assign)) { | ||
lintr_deprecated("allow_cascading_assign", '"<<-" and/or "->>" in operator', version = "3.2.0", type = "Argument") | ||
operator <- drop_or_add(operator, "<<-", allow_cascading_assign) | ||
} | ||
if (!missing(allow_right_assign)) { | ||
lintr_deprecated("allow_right_assign", '"->" in operator', version = "3.2.0", type = "Argument") | ||
operator <- drop_or_add(operator, c("->", if (allow_cascading_assign) "->>"), allow_right_assign) | ||
} | ||
if (!missing(allow_pipe_assign)) { | ||
lintr_deprecated("allow_pipe_assign", '"%<>%" in operator', version = "3.2.0", type = "Argument") | ||
operator <- drop_or_add(operator, "%<>%", allow_pipe_assign) | ||
} | ||
all_operators <- c("<-", "=", "->", "<<-", "->>", "%<>%") | ||
if ("any" %in% operator) { | ||
operator <- all_operators | ||
} else { | ||
operator <- match.arg(operator, all_operators, several.ok = TRUE) | ||
} | ||
trailing_assign_xpath <- paste0( | ||
collapse = " | ", | ||
c( | ||
paste0("//LEFT_ASSIGN", if (allow_cascading_assign) "" else "[text() = '<-']"), | ||
if (allow_right_assign) paste0("//RIGHT_ASSIGN", if (allow_cascading_assign) "" else "[text() = '->']"), | ||
"//LEFT_ASSIGN", | ||
"//RIGHT_ASSIGN", | ||
"//EQ_ASSIGN", | ||
"//EQ_SUB", | ||
"//EQ_FORMALS", | ||
if (!allow_pipe_assign) "//SPECIAL[text() = '%<>%']" | ||
"//SPECIAL[text() = '%<>%']" | ||
), | ||
"[@line1 < following-sibling::expr[1]/@line1]" | ||
) | ||
|
||
xpath <- paste(collapse = " | ", c( | ||
# always block = (NB: the parser differentiates EQ_ASSIGN, EQ_SUB, and EQ_FORMALS) | ||
"//EQ_ASSIGN", | ||
op_xpath_parts <- c( | ||
if (!"=" %in% operator) "//EQ_ASSIGN", | ||
# -> and ->> are both 'RIGHT_ASSIGN' | ||
if (!allow_right_assign) "//RIGHT_ASSIGN" else if (!allow_cascading_assign) "//RIGHT_ASSIGN[text() = '->>']", | ||
glue("//RIGHT_ASSIGN[{ xp_text_in_table(setdiff(c('->', '->>'), operator)) }]"), | ||
# <-, :=, and <<- are all 'LEFT_ASSIGN'; check the text if blocking <<-. | ||
# NB: := is not linted because of (1) its common usage in rlang/data.table and | ||
# (2) it's extremely uncommon as a normal assignment operator | ||
if (!allow_cascading_assign) "//LEFT_ASSIGN[text() = '<<-']", | ||
if (!allow_trailing) trailing_assign_xpath, | ||
if (!allow_pipe_assign) "//SPECIAL[text() = '%<>%']" | ||
)) | ||
glue("//LEFT_ASSIGN[{ xp_text_in_table(setdiff(c('<-', '<<-'), operator)) }]"), | ||
if (!"%<>%" %in% operator) "//SPECIAL[text() = '%<>%']" | ||
) | ||
if (!is.null(op_xpath_parts)) { | ||
# NB: Also used, essentially, in implicit_assignment_linter. Keep in sync. | ||
implicit_assignment_xpath <- " | ||
[not(parent::expr[ | ||
preceding-sibling::*[2][self::IF or self::WHILE] | ||
or parent::forcond | ||
or parent::expr/*[1][self::OP-LEFT-PAREN] | ||
])] | ||
" | ||
op_xpath <- paste0(op_xpath_parts, implicit_assignment_xpath, collapse = " | ") | ||
} else { | ||
op_xpath <- NULL | ||
} | ||
|
||
Linter(linter_level = "expression", function(source_expression) { | ||
xml <- source_expression$xml_parsed_content | ||
|
||
bad_expr <- xml_find_all(xml, xpath) | ||
if (length(bad_expr) == 0L) { | ||
return(list()) | ||
} | ||
lints <- NULL | ||
if (!is.null(op_xpath)) { | ||
op_expr <- xml_find_all(xml, op_xpath) | ||
|
||
operator <- xml_text(bad_expr) | ||
lint_message_fmt <- rep("Use <-, not %s, for assignment.", length(operator)) | ||
lint_message_fmt[operator %in% c("<<-", "->>")] <- | ||
"Replace %s by assigning to a specific environment (with assign() or <-) to avoid hard-to-predict behavior." | ||
lint_message_fmt[operator == "%<>%"] <- | ||
"Avoid the assignment pipe %s; prefer using <- and %%>%% separately." | ||
op_text <- xml_text(op_expr) | ||
op_lint_message_fmt <- rep( | ||
sprintf( | ||
"Use %s for assignment, not %%s.", | ||
if (length(operator) > 1L) paste("one of", toString(operator)) else operator | ||
), | ||
length(op_text) | ||
) | ||
op_lint_message_fmt[op_text %in% c("<<-", "->>")] <- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This path will produce a confusing message for |
||
"Replace %s by assigning to a specific environment (with assign() or <-) to avoid hard-to-predict behavior." | ||
op_lint_message_fmt[op_text == "%<>%"] <- | ||
"Avoid the assignment pipe %s; prefer using <- and %%>%% separately." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be inconsistent advice if |
||
|
||
op_lint_message <- sprintf(op_lint_message_fmt, op_text) | ||
lints <- xml_nodes_to_lints(op_expr, source_expression, op_lint_message, type = "style") | ||
} | ||
|
||
if (!allow_trailing) { | ||
bad_trailing_expr <- xml_find_all(xml, trailing_assign_xpath) | ||
trailing_assignments <- xml2::xml_attrs(bad_expr) %in% xml2::xml_attrs(bad_trailing_expr) | ||
lint_message_fmt[trailing_assignments] <- "Assignment %s should not be trailing at the end of a line." | ||
trailing_assign_expr <- xml_find_all(xml, trailing_assign_xpath) | ||
trailing_assign_text <- xml_text(trailing_assign_expr) | ||
trailing_assign_msg_fmt <- "Assignment %s should not be trailing at the end of a line." | ||
trailing_assign_msg <- sprintf(trailing_assign_msg_fmt, trailing_assign_text) | ||
lints <- c(lints, | ||
xml_nodes_to_lints(trailing_assign_expr, source_expression, trailing_assign_msg, type = "style") | ||
) | ||
} | ||
|
||
lint_message <- sprintf(lint_message_fmt, operator) | ||
xml_nodes_to_lints(bad_expr, source_expression, lint_message, type = "style") | ||
lints | ||
}) | ||
} | ||
|
||
drop_or_add <- function(x, y, add) (if (add) union else setdiff)(x, y) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Positional changes could affect downstream dependencies if the linter is configured using positional arguments. We should at least check for
is.logical(operator)
to stop with a helpful error message.