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 allow_implicit_else argument for return_linter #2321

Merged
merged 51 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
1780e62
New implicit_else_return_linter
MichaelChirico Nov 20, 2023
7bc4bc5
Merge branch 'main' into implicit_else_return
MichaelChirico Nov 20, 2023
80b97d9
Merge branch 'main' into implicit_else_return
MichaelChirico Nov 24, 2023
b203459
initial merge of tests
MichaelChirico Nov 24, 2023
7f50101
Use trim_some() in tests
MichaelChirico Nov 24, 2023
c58cf6c
Initial merge of R sources
MichaelChirico Nov 24, 2023
74df3de
1st "completed" merge
MichaelChirico Nov 24, 2023
ea237e9
Fix undefined, document
MichaelChirico Nov 24, 2023
59ecba5
fix 'except' for implicit returns; tests passing
MichaelChirico Nov 24, 2023
2245582
some tests with return_style="explicit" too
MichaelChirico Nov 24, 2023
f267729
tidy up NEWS
MichaelChirico Nov 24, 2023
c111abd
indent XPath
MichaelChirico Nov 27, 2023
3422602
revert
MichaelChirico Nov 27, 2023
7eeb3aa
Merge branch 'main' into implicit_else_return
MichaelChirico Nov 27, 2023
1f183a2
Merge remote-tracking branch 'origin/implicit_else_return' into impli…
MichaelChirico Nov 27, 2023
19602cc
fix incorrect test, add a regression test
MichaelChirico Nov 27, 2023
49926b7
try a better test description
MichaelChirico Nov 27, 2023
7e5cb6b
partial revert, quick fix caused new false positives
MichaelChirico Nov 27, 2023
46ac49c
land on descendant if needed for better metadata
MichaelChirico Nov 27, 2023
7c72f70
explicitly test for 2 lints
MichaelChirico Nov 27, 2023
463e1ec
fix vectorized case, add test
MichaelChirico Nov 27, 2023
613ed36
remove ' to hide 'commented code'
MichaelChirico Nov 27, 2023
86a580b
handle nested terminal if/else
MichaelChirico Nov 28, 2023
62b9565
Merge branch 'main' into return-recurse
MichaelChirico Nov 28, 2023
69253f3
better comment
MichaelChirico Nov 28, 2023
21553c0
more robust courtesy of r-devel
MichaelChirico Nov 28, 2023
f768750
catch another edge case
MichaelChirico Nov 28, 2023
0a8ef29
narrower line, unused object
MichaelChirico Nov 28, 2023
54f5017
another test as exposed by failure on other test files
MichaelChirico Nov 28, 2023
3cb24eb
another edge case exposed by lint_package()
MichaelChirico Nov 28, 2023
aa07981
try with Recall()
MichaelChirico Nov 28, 2023
b4060c7
correct handling of empty "{" for explicit returns
MichaelChirico Nov 28, 2023
669dfcd
add a "weird" test of nested empty "{"
MichaelChirico Nov 29, 2023
faee3c2
simplify recursive signature with fixed "params"
MichaelChirico Nov 29, 2023
e005b4d
drop need for list() by using lapply()
MichaelChirico Nov 29, 2023
1067599
# nolint
MichaelChirico Nov 29, 2023
b34b1a4
Merge branch 'return-recurse' into implicit_else_return
MichaelChirico Nov 29, 2023
826e9c3
try and fix merge gore
MichaelChirico Nov 29, 2023
d08d0f6
implement recursive implicit else linting
MichaelChirico Nov 29, 2023
f09a408
Add a test of nested if/else+implicit nested else
MichaelChirico Nov 29, 2023
ecc499d
mention parent PR in NEWS
MichaelChirico Nov 29, 2023
68ffc86
another regression test
MichaelChirico Nov 29, 2023
0220b36
also test that except= doesnt apply to return_stlye="implicit"
MichaelChirico Nov 29, 2023
9fdbe14
clarifying comment
MichaelChirico Nov 29, 2023
33e1562
pull adjustment for implict+!allow+except= outside the loop
MichaelChirico Nov 29, 2023
cd0bda5
flatten_lints() not necessary
MichaelChirico Nov 29, 2023
4726d08
flatten_lints() not necessary
MichaelChirico Nov 29, 2023
6e85a30
ensure = assignments are handled correctly
MichaelChirico Nov 29, 2023
73b9aee
Merge branch 'implicit_else_return' of github.com:r-lib/lintr into im…
MichaelChirico Nov 29, 2023
d8e84ac
Merge branch 'main' into implicit_else_return
MichaelChirico Dec 4, 2023
743322d
Merge branch 'main' into implicit_else_return
AshesITR Dec 6, 2023
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
6 changes: 5 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@
* `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:
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
+ `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`.

### New linters
Expand Down
74 changes: 61 additions & 13 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,12 +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") {
# See `?.onAttach`; these functions are all exclusively used for their
# side-effects, so implicit return is generally acceptable
except <- union(special_funs, except)
}

if (return_style == "implicit") {
xpath <- "
return_xpath <- "
(//FUNCTION | //OP-LAMBDA)
/following-sibling::expr[1][*[1][self::OP-LEFT-BRACE]]
/expr[last()][
Expand All @@ -70,13 +93,8 @@ return_linter <- function(
]
]
"
msg <- "Use implicit return behavior; explicit return() is not needed."
return_msg <- "Use implicit return behavior; explicit return() is not needed."
} else {
# See `?.onAttach`; these functions are all exclusively used for their
# side-effects, so implicit return is generally acceptable

except <- union(special_funs, except)

base_return_functions <- c(
# Normal calls
"return", "stop", "q", "quit",
Expand Down Expand Up @@ -123,7 +141,7 @@ return_linter <- function(
# two top level branches have at least two return()s
# because of special 'in' syntax for 'for' loops, the condition is
# tagged differently than for 'if'/'while' conditions (simple PAREN)
xpath <- glue("
return_xpath <- glue("
(//FUNCTION | //OP-LAMBDA)[parent::expr[not(
preceding-sibling::expr[SYMBOL[{ xp_text_in_table(except) }]]
)]]
Expand Down Expand Up @@ -154,7 +172,24 @@ return_linter <- function(
)
]
")
msg <- "All functions must have an explicit return()."
return_msg <- "All functions must have an explicit return()."
}

if (!allow_implicit_else) {
# for inline functions, terminal <expr> is a sibling of <FUNCTION>, otherwise
# it's a descendant of the <expr> following <FUNCTION>
implicit_else_xpath <- glue("
//FUNCTION[not(
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
parent::expr/preceding-sibling::expr/SYMBOL[{ xp_text_in_table(except) }]
)]
/following-sibling::expr[
(position() = last() and IF and not(ELSE))
or expr[position() = last() and IF and not(ELSE)]
]
")

implicit_else_msg <-
"All functions with terminal if statements must have a corresponding terminal else clause"
}

Linter(function(source_expression) {
Expand All @@ -164,13 +199,26 @@ return_linter <- function(

xml <- source_expression$xml_parsed_content

xml_nodes <- xml_find_all(xml, xpath)
return_expr <- xml_find_all(xml, return_xpath)

xml_nodes_to_lints(
xml_nodes,
lints <- xml_nodes_to_lints(
return_expr,
source_expression = source_expression,
lint_message = msg,
lint_message = return_msg,
type = "style"
)

if (!allow_implicit_else) {
implicit_else_expr <- xml_find_all(xml, implicit_else_xpath)

lints <- c(lints, xml_nodes_to_lints(
implicit_else_expr,
source_expression = source_expression,
lint_message = implicit_else_msg,
type = "warning"
))
}

lints
})
}
11 changes: 11 additions & 0 deletions R/shared_constants.R
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,14 @@ purrr_mappers <- c(
"map_raw", "map_lgl", "map_int", "map_dbl", "map_chr", "map_vec",
"map_df", "map_dfr", "map_dfc"
)

# Shared between returns_linter and implicit_else_return_inter
# See `?.onAttach`; these functions are all exclusively used for their
# side-effects, so implicit return is generally acceptable
return_not_needed_funs <- c(
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
# namespace hooks
".onLoad", ".onUnload", ".onAttach", ".onDetach", ".Last.lib",

# from RUnit
".setUp", ".tearDown"
)
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.

114 changes: 114 additions & 0 deletions tests/testthat/test-return_linter.R
Original file line number Diff line number Diff line change
Expand Up @@ -882,3 +882,117 @@ test_that("return_linter lints `message`, `warning` and `stopifnot`", {
linter
)
})

test_that("non-if returns are skipped under allow_implicit_else = FALSE", {
expect_lint(
trim_some("
foo <- function(bar) {
bar
}
"),
NULL,
return_linter(allow_implicit_else = FALSE)
)
})

test_that("if/else don't throw a lint under allow_implicit_else = FALSE", {
expect_lint(
trim_some("
foo <- function(bar) {
if (TRUE) {
return(bar)
} else {
return(NULL)
}
}
"),
NULL,
return_linter(allow_implicit_else = FALSE)
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
)
})

test_that("implicit else outside a function doesn't lint under allow_implicit_else = FALSE", {
expect_lint("if(TRUE) TRUE", NULL, return_linter(allow_implicit_else = FALSE))
})

test_that("allow_implicit_else = FALSE identifies a simple implicit else", {
expect_lint(
trim_some("
foo <- function(bar) {
if (TRUE) {
bar
}
}
"),
rex::rex("All functions with terminal if statements must"),
return_linter(allow_implicit_else = FALSE)
)
})

test_that("allow_implicit_else = FALSE finds implicit else with nested if+else", {
lint_msg <- rex::rex("All functions with terminal if statements must have a corresponding terminal else clause")

expect_lint(
trim_some("
foo <- function() {
if (TRUE) {
if (TRUE) {
FALSE
} else {
TRUE
}
}
}
"),
lint_msg,
return_linter(allow_implicit_else = FALSE)
)

expect_lint(
trim_some("
foo <- function() {
if (TRUE) {
if (TRUE) {
return(FALSE)
} else {
return(TRUE)
}
}
}
"),
lint_msg,
return_linter(return_style = "explicit", allow_implicit_else = FALSE)
)
})

test_that("allow_implicit_else = FALSE works on anonymous/inline functions", {
expect_lint(
"lapply(rnorm(10), function(x) if (TRUE) x + 1)",
rex::rex("All functions with terminal if statements must"),
return_linter(allow_implicit_else = FALSE)
)
})

test_that("allow_implicit_else = FALSE skips side-effect functions like .onLoad", {
expect_lint(
trim_some("
.onAttach <- function(libname, pkgname) {
if (TRUE) foo()
}
"),
NULL,
return_linter(allow_implicit_else = FALSE)
)
})

test_that("allow_implicit_else = FALSE + explicit returns skips side-effect functions like .onLoad", {
expect_lint(
trim_some("
.onAttach <- function(libname, pkgname) {
if (TRUE) return(foo())
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
}
"),
NULL,
return_linter(return_style = "explicit", allow_implicit_else = FALSE)
)
})
Loading