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

Linter for explicit/implicit returns #2271

Merged
merged 47 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
bc9f197
feat: Add `return_linter`
MEO265 Nov 8, 2023
2a54612
feat: Dont lint deterministically returning control statements
MEO265 Nov 9, 2023
293e4a2
test: Accept false negatives
MEO265 Nov 9, 2023
842a006
feat: Do not lint stop
MEO265 Nov 9, 2023
cda5823
feat: Refined lint of `switch`
MEO265 Nov 9, 2023
c091d1f
test: Add line tests
MEO265 Nov 10, 2023
067a33d
doc: Mark as configurable
MEO265 Nov 10, 2023
94955ed
mnt: Add terminal new lines
MEO265 Nov 10, 2023
8a27435
Merge branch 'main' into feature/return_linter
MEO265 Nov 10, 2023
ae5304f
incorporate new test cases, improve lint message, begin work to recon…
MichaelChirico Nov 20, 2023
336e9a5
drop in the other XPath
MichaelChirico Nov 20, 2023
a9c8801
catch OP-LAMBDA, typos, fix lint metadata
MichaelChirico Nov 20, 2023
a4a18e3
remove vestigial, clean up repeated var usage
MichaelChirico Nov 20, 2023
23b0fdb
Merge remote-tracking branch 'origin/main' into feature/return_linter
MichaelChirico Nov 20, 2023
63741c3
progress on rectifying disagreements
MichaelChirico Nov 20, 2023
580b935
more progress, simplifying XPath
MichaelChirico Nov 20, 2023
48eaab0
more test+logic adjustments, now passing tests
MichaelChirico Nov 20, 2023
c6b11be
simplify implicit XPath
MichaelChirico Nov 20, 2023
ee5eed3
test code style
MichaelChirico Nov 20, 2023
0826721
Merge branch 'main' into feature/return_linter
MichaelChirico Nov 20, 2023
bafe717
add simple examples
MichaelChirico Nov 20, 2023
f58d408
set as a default linter
MichaelChirico Nov 20, 2023
514927d
test-defaults
MichaelChirico Nov 20, 2023
b3ac25f
style guide ref in doc
MichaelChirico Nov 20, 2023
f8d958d
finish TODOs
MichaelChirico Nov 21, 2023
de3e9ce
NEWS entry
MichaelChirico Nov 21, 2023
95edca4
Merge branch 'main' into feature/return_linter
MichaelChirico Nov 21, 2023
3810968
Merge branch 'main' into feature/return_linter
MichaelChirico Nov 21, 2023
9bb7d3a
fix merge
MichaelChirico Nov 21, 2023
414257b
feat: Add parameters for exceptions
MEO265 Nov 21, 2023
8da36f4
feat: Add parameter for Runit
MEO265 Nov 21, 2023
5d67b25
feat: Lint `warning`, `message`, and `stopifnot`
MEO265 Nov 21, 2023
0b95fc7
mnt: Add terminal newline to tests
MEO265 Nov 21, 2023
4b09754
doc: Fix doc of `additional_side_effect_func`
MEO265 Nov 21, 2023
ca945bc
Merge branch 'main' into feature/return_linter
MichaelChirico Nov 23, 2023
f7afa54
Merge branch 'feature/return_linter' of github.com:MEO265/lintr into …
MichaelChirico Nov 23, 2023
91820bd
drop runit support for now
MichaelChirico Nov 23, 2023
e1961d5
style
MichaelChirico Nov 23, 2023
5248af9
rename parameter to accept "implicit"/"explicit"
MichaelChirico Nov 23, 2023
8612b70
rename other parameters
MichaelChirico Nov 23, 2023
aff7765
corresponding changes to tests
MichaelChirico Nov 23, 2023
28b51f4
dont link R4.0+ tryInvokeRestart, which is in linked page already anyway
MichaelChirico Nov 23, 2023
cb2d9c4
Merge branch 'main' into feature/return_linter
MEO265 Nov 23, 2023
8a0ea77
Merge branch 'main' into MEO265-feature/return_linter
AshesITR Nov 23, 2023
63eba24
review and fixes
AshesITR Nov 23, 2023
f89c8bc
document()
AshesITR Nov 23, 2023
325205c
Merge branch 'main' into feature/return_linter
AshesITR Nov 23, 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
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Collate:
'regex_subset_linter.R'
'rep_len_linter.R'
'repeat_linter.R'
'return_linter.R'
'routine_registration_linter.R'
'sample_int_linter.R'
'scalar_in_linter.R'
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export(redundant_ifelse_linter)
export(regex_subset_linter)
export(rep_len_linter)
export(repeat_linter)
export(return_linter)
export(routine_registration_linter)
export(sample_int_linter)
export(sarif_output)
Expand Down
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@

* `object_name_linter()` no longer errors when user-supplied `regexes=` have capture groups (#2188, @MichaelChirico).

## Changes to default linters

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

## 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).
* `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
176 changes: 176 additions & 0 deletions R/return_linter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#' Return linter
#'
#' This linter checks functions' [return()] expressions.
#'
#' @param return_style Character string naming the return style. `"implicit"`,
#' the default, enforeces the Tidyverse guide recommendation to leave terminal
#' returns implicit. `"explicit"` style requires that `return()` always be
#' explicitly supplied.
#' @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()],
#' `tryInvokeRestart()`, [UseMethod()], [NextMethod()], [standardGeneric()],
#' [callNextMethod()], [.C()], [.Call()], [.External()], and [.Fortran()].
#' @param except Character vector of functions that are not checked when
#' `return_style = "explicit"`. These are in addition to namespace hook functions
#' that are never checked: `.onLoad()`, `.onUnload()`, `.onAttach()`, `.onDetach()`,
#' `.Last.lib()`, `.First()` and `.Last()`.
#'
#' @examples
#' # will produce lints
#' code <- "function(x) {\n return(x + 1)\n}"
#' writeLines(code)
#' lint(
#' text = code,
#' linters = return_linter()
#' )
#'
#' code <- "function(x) {\n x + 1\n}"
#' writeLines(code)
#' lint(
#' text = code,
#' linters = return_linter(return_style = "explicit")
#' )
#'
#' # okay
#' code <- "function(x) {\n x + 1\n}"
#' writeLines(code)
#' lint(
#' text = code,
#' linters = return_linter()
#' )
#'
#' code <- "function(x) {\n return(x + 1)\n}"
#' writeLines(code)
#' lint(
#' text = code,
#' linters = return_linter(return_style = "explicit")
#' )
#'
#'
#' @evalRd rd_tags("return_linter")
#' @seealso
#' - [linters] for a complete list of linters available in lintr.
#' - <https://style.tidyverse.org/functions.html?q=return#return>
#' @export
return_linter <- function(
MichaelChirico marked this conversation as resolved.
Show resolved Hide resolved
return_style = c("implicit", "explicit"),
return_functions = NULL,
except = NULL) {
return_style <- match.arg(return_style)

if (return_style == "implicit") {
xpath <- "
(//FUNCTION | //OP-LAMBDA)
/following-sibling::expr[1][*[1][self::OP-LEFT-BRACE]]
/expr[last()][
expr[1][
not(OP-DOLLAR or OP-AT)
and SYMBOL_FUNCTION_CALL[text() = '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",
"invokeRestart", "tryInvokeRestart",

# Functions related to S3 methods
"UseMethod", "NextMethod",

# Functions related to S4 methods
"standardGeneric", "callNextMethod",

# Functions related to C interfaces
".C", ".Call", ".External", ".Fortran"
)

return_functions <- union(base_return_functions, return_functions)

control_calls <- c("IF", "FOR", "WHILE", "REPEAT")

# from top, look for a FUNCTION definition that uses { (one-line
# function definitions are excepted), then look for failure to find
# return() on the last() expr of the function definition.
# exempt .onLoad which shows up in the tree like
# <expr><expr><SYMBOL>.onLoad</></><LEFT_ASSIGN></><expr><FUNCTION>...
# simple final expression (no control flow) must be
# <expr><expr> CALL( <expr> ) </expr></expr>
# NB: if this syntax _isn't_ used, the node may not be <expr>, hence
# the use of /*[...] below and self::expr here. position() = 1 is
# needed to guard against a few other cases.
# We also need to make sure that this expression isn't followed by a pipe
# symbol, which would indicate that we need to also check the last
# expression.
# pipe expressions are like
# ...
# <SPECIAL>%&gt;%</SPECIAL>
# <expr><expr><SYMBOL_FUNCTION_CALL>return</SYMBOL_FUNCTION_CALL>
# </expr></expr>
# Unlike the following case, the return should be the last expression in
# the sequence.
# conditional expressions are like
# <expr><IF> ( <expr> ) <expr> [ <ELSE> <expr>] </expr>
# we require _any_ call to return() in either of the latter two <expr>, i.e.,
# we don't apply recursive logic to check every branch, only that the
# 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("
(//FUNCTION | //OP-LAMBDA)[parent::expr[not(
preceding-sibling::expr[SYMBOL[{ xp_text_in_table(except) }]]
)]]
/following-sibling::expr[OP-LEFT-BRACE and expr[last()]/@line1 != @line1]
/expr[last()]
/*[
(
position() = 1
and (
(
{ xp_or(paste0('self::', setdiff(control_calls, 'IF'))) }
) or (
not({ xp_or(paste0('self::', control_calls)) })
and not(
following-sibling::PIPE
or following-sibling::SPECIAL[text() = '%>%']
)
and not(self::expr/SYMBOL_FUNCTION_CALL[
{ xp_text_in_table(return_functions) }
])
)
)
) or (
preceding-sibling::IF
and self::expr
and position() > 4
and not(.//SYMBOL_FUNCTION_CALL[{ xp_text_in_table(return_functions) }])
)
]
")
msg <- "All functions must have an explicit return()."
}

Linter(function(source_expression) {
if (!is_lint_level(source_expression, "expression")) {
return(list())
}

xml <- source_expression$xml_parsed_content

xml_nodes <- xml_find_all(xml, xpath)

xml_nodes_to_lints(
xml_nodes,
source_expression = source_expression,
lint_message = msg,
type = "style"
)
})
}
1 change: 1 addition & 0 deletions R/zzz.R
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ default_linters <- modify_defaults(
paren_body_linter(),
pipe_continuation_linter(),
quotes_linter(),
return_linter(),
semicolon_linter(),
seq_linter(),
spaces_inside_linter(),
Expand Down
1 change: 1 addition & 0 deletions inst/lintr/linters.csv
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ redundant_ifelse_linter,best_practices efficiency consistency configurable
regex_subset_linter,best_practices efficiency regex
rep_len_linter,readability consistency best_practices
repeat_linter,style readability
return_linter,style configurable default
routine_registration_linter,best_practices efficiency robustness
sample_int_linter,efficiency readability robustness
scalar_in_linter,readability consistency best_practices efficiency
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.

3 changes: 2 additions & 1 deletion man/default_linters.Rd

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

7 changes: 4 additions & 3 deletions man/linters.Rd

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

74 changes: 74 additions & 0 deletions man/return_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/style_linters.Rd

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

5 changes: 5 additions & 0 deletions tests/testthat/default_linter_testcode.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
# paren_brace
f = function (x,y = 1){}

# return_linter
g <- function(x) {
return(x + 1)
}

# commented_code
# some <- commented("out code")

Expand Down
Loading
Loading