Skip to content

Commit 5b99e6e

Browse files
Support pattern-based exclusion in return_linter() (#2433)
1 parent c912988 commit 5b99e6e

File tree

4 files changed

+103
-14
lines changed

4 files changed

+103
-14
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
+ `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).
3535
+ `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).
3636
+ `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.
37+
+ `except_regex`, the same purpose as `except=`, but filters functions by pattern. This is motivated by {RUnit}, where test suites are based on unit test functions matched by pattern, e.g. `^Test`, and where explicit return may be awkward (#2335, @MichaelChirico).
3738
* `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`.
3839
* `backport_linter()` is slightly faster by moving expensive computations outside the linting function (#2339, #2348, @AshesITR and @MichaelChirico).
3940
* `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).

R/return_linter.R

+31-11
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
#' from base that are always allowed: [stop()], [q()], [quit()], [invokeRestart()],
1515
#' `tryInvokeRestart()`, [UseMethod()], [NextMethod()], [standardGeneric()],
1616
#' [callNextMethod()], [.C()], [.Call()], [.External()], and [.Fortran()].
17-
#' @param except Character vector of functions that are not checked when
17+
#' @param except,except_regex Character vector of functions that are not checked when
1818
#' `return_style = "explicit"`. These are in addition to namespace hook functions
1919
#' that are never checked: `.onLoad()`, `.onUnload()`, `.onAttach()`, `.onDetach()`,
20-
#' `.Last.lib()`, `.First()` and `.Last()`.
20+
#' `.Last.lib()`, `.First()` and `.Last()`. `except` matches function names exactly,
21+
#' while `except_regex` does exclusion by pattern matching with [rex::re_matches()].
2122
#'
2223
#' @examples
2324
#' # will produce lints
@@ -73,16 +74,25 @@ return_linter <- function(
7374
return_style = c("implicit", "explicit"),
7475
allow_implicit_else = TRUE,
7576
return_functions = NULL,
76-
except = NULL) {
77+
except = NULL,
78+
except_regex = NULL) {
7779
return_style <- match.arg(return_style)
7880

79-
if (!allow_implicit_else || return_style == "explicit") {
80-
except_xpath <- glue("parent::expr[not(
81-
preceding-sibling::expr/SYMBOL[{ xp_text_in_table(union(special_funs, except)) }]
82-
)]")
81+
check_except <- !allow_implicit_else || return_style == "explicit"
82+
# We defer building the XPath strings in this case since we can't build the
83+
# pattern-based "except" logic directly into the XPath (because of v1.0)
84+
defer_except <- check_except && !is.null(except_regex)
85+
86+
if (check_except) {
87+
except_xpath_fmt <- "parent::expr[not(
88+
preceding-sibling::expr/SYMBOL[{ xp_text_in_table(except) }]
89+
)]"
90+
except <- union(special_funs, except)
91+
if (!defer_except) except_xpath <- glue(except_xpath_fmt, except = except)
8392
}
8493

8594
if (return_style == "implicit") {
95+
# nolint next: object_usage. False positive.
8696
body_xpath <- "(//FUNCTION | //OP-LAMBDA)/following-sibling::expr[1]"
8797
params <- list(
8898
implicit = TRUE,
@@ -91,8 +101,6 @@ return_linter <- function(
91101
lint_message = "Use implicit return behavior; explicit return() is not needed."
92102
)
93103
} else {
94-
except <- union(special_funs, except)
95-
96104
base_return_functions <- c(
97105
# Normal calls
98106
"return", "stop", "q", "quit",
@@ -110,11 +118,17 @@ return_linter <- function(
110118

111119
return_functions <- union(base_return_functions, return_functions)
112120

113-
body_xpath <- glue("
121+
body_xpath_fmt <- "
114122
(//FUNCTION | //OP-LAMBDA)[{ except_xpath }]
115123
/following-sibling::expr[OP-LEFT-BRACE and expr[last()]/@line1 != @line1]
116124
/expr[last()]
117-
")
125+
"
126+
if (defer_except) {
127+
function_name_xpath <- "(//FUNCTION | //OP-LAMBDA)/parent::expr/preceding-sibling::expr/SYMBOL"
128+
} else {
129+
body_xpath <- glue(body_xpath_fmt, except_xpath = except_xpath)
130+
}
131+
118132
params <- list(
119133
implicit = FALSE,
120134
type = "warning",
@@ -130,6 +144,12 @@ return_linter <- function(
130144

131145
Linter(linter_level = "expression", function(source_expression) {
132146
xml <- source_expression$xml_parsed_content
147+
if (defer_except) {
148+
assigned_functions <- xml_text(xml_find_all(xml, function_name_xpath))
149+
except <- union(except, assigned_functions[re_matches(assigned_functions, except_regex)])
150+
except_xpath <- glue(except_xpath_fmt, except = except)
151+
body_xpath <- glue(body_xpath_fmt, except_xpath = except_xpath)
152+
}
133153

134154
body_expr <- xml_find_all(xml, body_xpath)
135155

man/return_linter.Rd

+5-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-return_linter.R

+66
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,72 @@ test_that("except= argument works", {
698698
)
699699
})
700700

701+
test_that("except_regex= argument works", {
702+
linter <- return_linter(return_style = "explicit", except_regex = "^Test")
703+
704+
expect_lint(
705+
trim_some("
706+
TestSummary <- function() {
707+
context <- foo(72643424)
708+
expected <- data.frame(a = 2)
709+
checkEquals(expected, bar(context))
710+
}
711+
"),
712+
NULL,
713+
linter
714+
)
715+
716+
expect_lint(
717+
trim_some("
718+
TestMyPackage <- function() {
719+
checkMyCustomComparator(x, y)
720+
}
721+
"),
722+
NULL,
723+
linter
724+
)
725+
726+
expect_lint(
727+
trim_some("
728+
TestOuter <- function() {
729+
actual <- lapply(
730+
input,
731+
function(x) {
732+
no_return()
733+
}
734+
)
735+
TestInner <- function() {
736+
no_return()
737+
}
738+
checkEquals(TestInner(), actual)
739+
}
740+
"),
741+
list(rex::rex("All functions must have an explicit return()."), line_number = 5L),
742+
linter
743+
)
744+
})
745+
746+
test_that("except= and except_regex= combination works", {
747+
expect_lint(
748+
trim_some("
749+
foo <- function() {
750+
no_return()
751+
}
752+
bar <- function() {
753+
no_return()
754+
}
755+
abaz <- function() {
756+
no_return()
757+
}
758+
bbaz <- function() {
759+
no_return()
760+
}
761+
"),
762+
NULL,
763+
return_linter(return_style = "explicit", except = c("foo", "bar"), except_regex = "baz$")
764+
)
765+
})
766+
701767
test_that("return_linter skips brace-wrapped inline functions", {
702768
expect_lint("function(x) { sum(x) }", NULL, return_linter(return_style = "explicit"))
703769
})

0 commit comments

Comments
 (0)