Skip to content

Commit

Permalink
Merge 743322d into fbadcfc
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelChirico authored Dec 6, 2023
2 parents fbadcfc + 743322d commit ce5a9c7
Show file tree
Hide file tree
Showing 4 changed files with 346 additions and 8 deletions.
8 changes: 6 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@

## Changes to default linters

* New default linter `return_linter()` for the style guide rule that terminal returns should be left implicit (#1100, @MEO265).
* New default linter `return_linter()` for the style guide rule that terminal returns should be left implicit (#1100, #2354, and #2356, @MEO265 and @MichaelChirico).

## New and improved features

* More helpful errors for invalid configs (#2253, @MichaelChirico).
* `library_call_linter()` is extended
+ to encourage all packages to be attached with `library(symbol)`, not `library("symbol", character.only = TRUE)` or "vectorized" approaches looping over package names (part of #884, @MichaelChirico).
+ to discourage many consecutive calls to `suppressMessages()` or `suppressPackageStartupMessages()` (part of #884, @MichaelChirico).
* `return_linter()` also has an argument `return_style` (`"implicit"` by default) which checks that all functions confirm to the specified return style of `"implicit"` or `"explicit"` (part of #884, @MichaelChirico, @AshesITR and @MEO265).
* `return_linter()` also has arguments for fine-tuning which functions get linted:
+ `return_style` (`"implicit"` by default) which checks that all functions confirm to the specified return style of `"implicit"` or `"explicit"` (#2271 and part of #884, @MichaelChirico, @AshesITR and @MEO265).
+ `allow_implicit_else` (default `TRUE`) which, when `FALSE`, checks that all terminal `if` statements are paired with a corresponding `else` statement (part of #884, @MichaelChirico).
+ `return_functions` to customize which functions are equivalent to `return()` as "exit" clauses, e.g. `rlang::abort()` can be considered in addition to the default functions like `stop()` and `q()` from base (#2271 and part of #884, @MichaelChirico and @MEO265).
+ `except` to customize which functions are ignored entirely (i.e., whether they have a return of the specified style is not checked; #2271 and part of #884, @MichaelChirico and @MEO265). Namespace hooks like `.onAttach()` and `.onLoad()` are always ignored.
* `unnecessary_lambda_linter` is extended to encourage vectorized comparisons where possible, e.g. `sapply(x, sum) > 0` instead of `sapply(x, function(x) sum(x) > 0)` (part of #884, @MichaelChirico). Toggle this behavior with argument `allow_comparison`.
* `backport_linter()` is slightly faster by moving expensive computations outside the linting function (#2339, #2348, @AshesITR and @MichaelChirico).
* `Linter()` has a new argument `linter_level` (default `NA`). This is used by `lint()` to more efficiently check for expression levels than the idiom `if (!is_lint_level(...)) { return(list()) }` (#2351, @AshesITR).
Expand Down
58 changes: 52 additions & 6 deletions R/return_linter.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
#' the default, enforeces the Tidyverse guide recommendation to leave terminal
#' returns implicit. `"explicit"` style requires that `return()` always be
#' explicitly supplied.
#' @param allow_implicit_else Logical, default `TRUE`. If `FALSE`, functions with a terminal
#' `if` clause must always have an `else` clause, making the `NULL` alternative explicit
#' if necessary.
#' @param return_functions Character vector of functions that are accepted as terminal calls
#' when `return_style = "explicit"`. These are in addition to exit functions
#' from base that are always allowed: [stop()], [q()], [quit()], [invokeRestart()],
Expand All @@ -32,6 +35,13 @@
#' linters = return_linter(return_style = "explicit")
#' )
#'
#' code <- "function(x) {\n if (x > 0) 2\n}"
#' writeLines(code)
#' lint(
#' text = code,
#' linters = return_linter(allow_implicit_else = FALSE)
#' )
#'
#' # okay
#' code <- "function(x) {\n x + 1\n}"
#' writeLines(code)
Expand All @@ -47,6 +57,12 @@
#' linters = return_linter(return_style = "explicit")
#' )
#'
#' code <- "function(x) {\n if (x > 0) 2 else NULL\n}"
#' writeLines(code)
#' lint(
#' text = code,
#' linters = return_linter(allow_implicit_else = FALSE)
#' )
#'
#' @evalRd rd_tags("return_linter")
#' @seealso
Expand All @@ -55,13 +71,19 @@
#' @export
return_linter <- function(
return_style = c("implicit", "explicit"),
allow_implicit_else = TRUE,
return_functions = NULL,
except = NULL) {
return_style <- match.arg(return_style)

if (!allow_implicit_else || return_style == "explicit") {
except_xpath <- glue("parent::expr[not(
preceding-sibling::expr/SYMBOL[{ xp_text_in_table(union(special_funs, except)) }]
)]")
}

if (return_style == "implicit") {
body_xpath <- "(//FUNCTION | //OP-LAMBDA)/following-sibling::expr[1]"
# nolint next: object_usage. False positive from {codetools} says 'params' isn't used.
params <- list(
implicit = TRUE,
type = "style",
Expand Down Expand Up @@ -89,9 +111,7 @@ return_linter <- function(
return_functions <- union(base_return_functions, return_functions)

body_xpath <- glue("
(//FUNCTION | //OP-LAMBDA)[parent::expr[not(
preceding-sibling::expr[SYMBOL[{ xp_text_in_table(except) }]]
)]]
(//FUNCTION | //OP-LAMBDA)[{ except_xpath }]
/following-sibling::expr[OP-LEFT-BRACE and expr[last()]/@line1 != @line1]
/expr[last()]
")
Expand All @@ -106,15 +126,31 @@ return_linter <- function(
)
}

params$allow_implicit_else <- allow_implicit_else

Linter(linter_level = "expression", function(source_expression) {
xml <- source_expression$xml_parsed_content
if (is.null(xml)) return(list())

body_expr <- xml_find_all(xml, body_xpath)

params$source_expression <- source_expression

if (params$implicit && !params$allow_implicit_else) {
# can't incorporate this into the body_xpath for implicit return style,
# since we still lint explicit returns for except= functions.
allow_implicit_else <- is.na(xml_find_first(body_expr, except_xpath))
} else {
allow_implicit_else <- rep(params$allow_implicit_else, length(body_expr))
}
# nested_return_lints not "vectorized" due to xml_children()
lapply(body_expr, nested_return_lints, params)
Map(
function(expr, allow_implicit_else) {
params$allow_implicit_else <- allow_implicit_else
nested_return_lints(expr, params)
},
body_expr, allow_implicit_else
)
})
}

Expand Down Expand Up @@ -142,7 +178,17 @@ nested_return_lints <- function(expr, params) {
nested_return_lints(child_expr[[tail(expr_idx, 1L)]], params)
} else if (child_node[1L] == "IF") {
expr_idx <- which(child_node %in% c("expr", "equal_assign", "expr_or_assign_or_help"))
lapply(child_expr[expr_idx[-1L]], nested_return_lints, params)
return_lints <- lapply(child_expr[expr_idx[-1L]], nested_return_lints, params)
if (params$allow_implicit_else || length(expr_idx) == 3L) {
return(return_lints)
}
implicit_else_lints <- list(xml_nodes_to_lints(
expr,
source_expression = params$source_expression,
lint_message = "All functions with terminal if statements must have a corresponding terminal else clause",
type = "warning"
))
c(return_lints, implicit_else_lints)
} else {
xml_nodes_to_lints(
xml_find_first(child_expr[[1L]], params$lint_xpath),
Expand Down
18 changes: 18 additions & 0 deletions man/return_linter.Rd

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

Loading

0 comments on commit ce5a9c7

Please sign in to comment.