-
Notifications
You must be signed in to change notification settings - Fork 187
New unreachable_code_linter #1003
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
Changes from all commits
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 |
---|---|---|
@@ -0,0 +1,46 @@ | ||
#' Block unreachable code and comments following return statements | ||
#' | ||
#' Code after a top-level [return()] or [stop()] can't be reached; typically | ||
#' this is vestigial code left after refactoring or sandboxing code, which is | ||
#' fine for exploration, but shouldn't ultimately be checked in. Comments | ||
#' meant for posterity should be placed *before* the final `return()`. | ||
#' | ||
#' @evalRd rd_tags("unreachable_code_linter") | ||
#' @seealso [linters] for a complete list of linters available in lintr. | ||
#' @export | ||
unreachable_code_linter <- function() { | ||
Linter(function(source_file) { | ||
if (length(source_file$xml_parsed_content) == 0L) { | ||
return(list()) | ||
} | ||
|
||
xml <- source_file$xml_parsed_content | ||
|
||
# NB: | ||
# - * returns all children, including the terminal }, so the position | ||
# is not last(), but last()-1. If there's no }, this linter doesn't apply. | ||
# this is also why we need /* and not /expr -- position() must include all nodes | ||
# - land on the culprit expression | ||
xpath <- " | ||
//FUNCTION | ||
/following-sibling::expr | ||
/*[ | ||
self::expr | ||
and expr[SYMBOL_FUNCTION_CALL[text() = 'return' or text() = 'stop']] | ||
and (position() != last() - 1 or not(following-sibling::OP-RIGHT-BRACE)) | ||
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. Curious: Does this cope properly with return in switch? 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. Indeed it doesn't -- just added an example test (commented out) to make sure I captured what you had in mind. I think |
||
and @line2 < following-sibling::*[1]/@line2 | ||
] | ||
/following-sibling::*[1] | ||
" | ||
|
||
bad_expr <- xml2::xml_find_all(xml, xpath) | ||
|
||
return(lapply( | ||
bad_expr, | ||
xml_nodes_to_lint, | ||
source_file = source_file, | ||
lint_message = "Code and comments coming after a top-level return() or stop() should be removed.", | ||
type = "warning" | ||
)) | ||
}) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
test_that("unreachable_code_linter works in simple function", { | ||
lines <- trim_some(" | ||
foo <- function(bar) { | ||
return(bar) | ||
} | ||
") | ||
expect_lint(lines, NULL, unreachable_code_linter()) | ||
}) | ||
|
||
test_that("unreachable_code_linter ignores expressions that aren't functions", { | ||
expect_lint("x + 1", NULL, unreachable_code_linter()) | ||
}) | ||
|
||
test_that("unreachable_code_linter ignores anonymous/inline functions", { | ||
expect_lint("lapply(rnorm(10), function(x) x + 1)", NULL, unreachable_code_linter()) | ||
}) | ||
|
||
test_that("unreachable_code_linter passes on multi-line functions", { | ||
lines <- trim_some(" | ||
oo <- function(x) { | ||
y <- x + 1 | ||
return(y) | ||
} | ||
") | ||
expect_lint(lines, NULL, unreachable_code_linter()) | ||
}) | ||
|
||
test_that("unreachable_code_linter ignores comments on the same expression", { | ||
lines <- trim_some(" | ||
foo <- function(x) { | ||
return( | ||
y^2 | ||
) # y^3 | ||
} | ||
") | ||
expect_lint(lines, NULL, unreachable_code_linter()) | ||
}) | ||
|
||
test_that("unreachable_code_linter ignores comments on the same line", { | ||
lines <- trim_some(" | ||
foo <- function(x) { | ||
return(y^2) # y^3 | ||
} | ||
") | ||
expect_lint(lines, NULL, unreachable_code_linter()) | ||
}) | ||
|
||
test_that("unreachable_code_linter identifies simple unreachable code", { | ||
lines <- trim_some(" | ||
foo <- function(bar) { | ||
return(bar) | ||
x + 3 | ||
} | ||
") | ||
# testing the correct expression is linted (the first culprit line) | ||
expect_lint( | ||
lines, | ||
list( | ||
line_number = 3L, | ||
message = rex::rex("Code and comments coming after a top-level return() or stop()") | ||
), | ||
unreachable_code_linter() | ||
) | ||
}) | ||
|
||
test_that("unreachable_code_linter finds unreachable comments", { | ||
lines <- trim_some(" | ||
foo <- function(x) { | ||
y <- x + 1 | ||
return(y^2) | ||
# y^3 | ||
} | ||
") | ||
expect_lint( | ||
lines, | ||
rex::rex("Code and comments coming after a top-level return() or stop()"), | ||
unreachable_code_linter() | ||
) | ||
}) | ||
|
||
test_that("unreachable_code_linter finds a double return", { | ||
lines <- trim_some(" | ||
foo <- function(x) { | ||
return(y^2) | ||
return(y^3) | ||
} | ||
") | ||
expect_lint( | ||
lines, | ||
rex::rex("Code and comments coming after a top-level return() or stop()"), | ||
unreachable_code_linter() | ||
) | ||
}) | ||
|
||
test_that("unreachable_code_linter finds code after stop()", { | ||
lines <- trim_some(" | ||
foo <- function(x) { | ||
y <- x + 1 | ||
stop(y^2) | ||
# y^3 | ||
} | ||
") | ||
expect_lint( | ||
lines, | ||
rex::rex("Code and comments coming after a top-level return() or stop()"), | ||
unreachable_code_linter() | ||
) | ||
}) | ||
|
||
# TODO(michaelchirico): extend to work on switch() statements | ||
# test_that("unreachable_code_linter interacts with switch() as expected", { | ||
# unreachable_inside_switch_lines <- trim_some(" | ||
# foo <- function(x) { | ||
# switch(x, | ||
# a = { | ||
# return(x) | ||
# x + 1 | ||
# }, | ||
# b = { | ||
# return(x + 1) | ||
# } | ||
# ) | ||
# } | ||
# ") | ||
# expect_lint( | ||
# unreachable_inside_switch_lines, | ||
# rex::rex("Code and comments coming after a top-level return() or stop()"), | ||
# unreachable_code_linter() | ||
# ) | ||
# }) | ||
|
||
# TODO(michaelchirico): the logic could be extended to terminal if statements | ||
# or control flows (for/while). There shouldn't really be such a thing as | ||
# a terminal for/while (owing to ExplicitReturnLinter forcing these to | ||
# be followed by return(invisible()) or similar), but could be included to | ||
# catch comments for completeness / robustness as a standalone function. | ||
# Terminal if statements are a bit messy, but would have some payoff. | ||
# TODO(michaelchirico): similarly, return(x); x+1 should also lint, even though | ||
# the styler won't allow this in our current setup. | ||
# TODO(michaelchirico): again similarly, this could also apply to cases without | ||
# explicit returns (where it can only apply to comments) |
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.
This is equivalent to using
/expr
instead of/*
Uh oh!
There was an error while loading. Please reload this page.
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.
not quite, because
position()
needs to include all nodes, not justexpr
. I'll add a comment.