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

Unreachable loops #2141

Merged
merged 27 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5872b25
Lint unreachable code in loops
MEO265 Sep 10, 2023
aeca511
Add tests
MEO265 Sep 11, 2023
82a22ba
Update doc
MEO265 Sep 11, 2023
db3869f
Update lint warning message
MEO265 Sep 11, 2023
7483afb
Lint next and break
MEO265 Sep 11, 2023
a816863
Update doc
MEO265 Sep 11, 2023
70f05c5
Update man
MEO265 Sep 11, 2023
7e408d5
Add test for next and break
MEO265 Sep 11, 2023
3c490c3
Ignore nolint comments for next and break
MEO265 Sep 11, 2023
2d482e6
Tests for nolint comments
MEO265 Sep 11, 2023
c577c50
Indentation fix
MEO265 Sep 11, 2023
838d9f9
Merge branch 'main' into unreachable_loops
MEO265 Sep 11, 2023
ea3820b
Consider for and else
MEO265 Sep 12, 2023
c22d7f0
Update NEWS
MEO265 Sep 12, 2023
2d1a316
Merge branch 'main' into unreachable_loops
MEO265 Sep 12, 2023
f825ea9
Fix linebreak
MEO265 Sep 12, 2023
134dcaa
Fix NEWS
MEO265 Sep 12, 2023
cbd1352
Additional tests
MEO265 Sep 12, 2023
ad448da
Merge branch 'main' of https://github.com/MEO265/lintr into unreachab…
MEO265 Sep 12, 2023
9cf4f24
Merge branch 'main' into unreachable_loops
MichaelChirico Sep 13, 2023
68d349e
Merge remote-tracking branch 'origin/main' into unreachable_loops
MichaelChirico Oct 9, 2023
792904c
fix merge on NEWS
MichaelChirico Oct 9, 2023
ce0cfa9
fix lint message (merge)
MichaelChirico Oct 9, 2023
4640abb
simplify xpaths
MichaelChirico Oct 9, 2023
23d3dc7
extract to helper
MichaelChirico Oct 9, 2023
78952cf
switch order
MichaelChirico Oct 9, 2023
f645ac3
stale TODO
MichaelChirico Oct 9, 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
4 changes: 3 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@
* `seq_linter()` recommends `rev()` in the lint message for lints like `nrow(x):1` (#1542, @MichaelChirico).
* `function_argument_linter()` detects usage of `missing()` for the linted argument (#1546, @MichaelChirico). The simplest fix for `function_argument_linter()` lints is typically to set that argument to `NULL` by default, in which case it's usually preferable to update function logic checking `missing()` to check `is.null()` instead.
* `commas_linter()` gains an option `allow_trailing` (default `FALSE`) to allow trailing commas while indexing. (#2104, @MEO265)
* `unreachable_code_linter()` checks for code inside `if (FALSE)` and other conditional loops with deterministically false conditions (#1428, @ME0265).
* `unreachable_code_linter()`
+ checks for code inside `if (FALSE)` and other conditional loops with deterministically false conditions (#1428, @ME0265).
+ checks for unreachable code inside `if`, `else`, `for`, `while`, and `repeat` blocks, including combinations with `break` and `next` statements. (#2105, @ME0265).
* `implicit_assignment_linter()` gains an argument `allow_lazy` (default `FALSE`) that allows optionally skipping lazy assignments like `A && (B <- foo(A))` (#2016, @MichaelChirico).
* `unused_import_linter()` gains an argument `interpret_glue` (default `TRUE`) paralleling that in `object_usage_linter()` to toggle whether `glue::glue()` expressions should be inspected for exported object usage (#2042, @MichaelChirico).
* `default_undesirable_functions` is updated to also include `Sys.unsetenv()` and `structure()` (#2192 and #2228, @IndrajeetPatil and @MichaelChirico).
Expand Down
60 changes: 46 additions & 14 deletions R/unreachable_code_linter.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#' Block unreachable code and comments following return statements
#'
#' Code after a top-level [return()] or [stop()]
#' Code after e.g. a [return()] or [stop()]
#' or in deterministically false conditional loops like `if (FALSE)` 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
Expand Down Expand Up @@ -55,18 +55,38 @@
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
unreachable_code_linter <- function() {
expr_after_control <- "
(//REPEAT | //ELSE | //FOR)/following-sibling::expr[1]
| (//IF | //WHILE)/following-sibling::expr[2]
"
# NB: use not(OP-DOLLAR) to prevent matching process$stop(), #1051
xpath_return_stop <- "
(//FUNCTION | //OP-LAMBDA)
/following-sibling::expr
/expr[expr[1][not(OP-DOLLAR or OP-AT) and SYMBOL_FUNCTION_CALL[text() = 'return' or text() = 'stop']]]
xpath_return_stop <- glue("
(
{expr_after_control}
| (//FUNCTION | //OP-LAMBDA)/following-sibling::expr
)
/expr[expr[1][
not(OP-DOLLAR or OP-AT)
and SYMBOL_FUNCTION_CALL[text() = 'return' or text() = 'stop']
]]
/following-sibling::*[
not(self::OP-RIGHT-BRACE or self::OP-SEMICOLON)
and (not(self::COMMENT) or @line2 > preceding-sibling::*[1]/@line2)
][1]
"
")
xpath_next_break <- glue("
({expr_after_control})
/expr[NEXT or BREAK]
/following-sibling::*[
not(self::OP-RIGHT-BRACE or self::OP-SEMICOLON)
and (not(self::COMMENT) or @line2 > preceding-sibling::*[1]/@line2)
][1]
")

xpath_if_while <- "
(//WHILE | //IF)[following-sibling::expr[1]/NUM_CONST[text() = 'FALSE']]/following-sibling::expr[2]
(//WHILE | //IF)
/following-sibling::expr[1][NUM_CONST[text() = 'FALSE']]
/following-sibling::expr[1]
"

xpath_else <- "
Expand All @@ -88,6 +108,13 @@ unreachable_code_linter <- function() {
expr[vapply(expr, xml2::xml_length, integer(1L)) != 0L]
}

# exclude comments that start with a nolint directive
drop_nolint_end_comment <- function(expr) {
is_nolint_end_comment <- xml2::xml_name(expr) == "COMMENT" &
re_matches(xml_text(expr), settings$exclude_end)
expr[!is_nolint_end_comment]
}

Linter(function(source_expression) {
if (!is_lint_level(source_expression, "expression")) {
return(list())
Expand All @@ -97,14 +124,19 @@ unreachable_code_linter <- function() {

expr_return_stop <- xml_find_all(xml, xpath_return_stop)

# exclude comments that start with a nolint directive
is_nolint_end_comment <- xml2::xml_name(expr_return_stop) == "COMMENT" &
re_matches(xml_text(expr_return_stop), settings$exclude_end)

lints_return_stop <- xml_nodes_to_lints(
expr_return_stop[!is_nolint_end_comment],
drop_nolint_end_comment(expr_return_stop),
source_expression = source_expression,
lint_message = "Code and comments coming after a return() or stop() should be removed.",
type = "warning"
)

expr_next_break <- xml_find_all(xml, xpath_next_break)

lints_next_break <- xml_nodes_to_lints(
drop_nolint_end_comment(expr_next_break),
source_expression = source_expression,
lint_message = "Code and comments coming after a top-level return() or stop() should be removed.",
lint_message = "Code and comments coming after a `next` or `break` should be removed.",
type = "warning"
)

Expand All @@ -126,6 +158,6 @@ unreachable_code_linter <- function() {
type = "warning"
)

c(lints_return_stop, lints_if_while, lints_else)
c(lints_return_stop, lints_next_break, lints_if_while, lints_else)
})
}
2 changes: 1 addition & 1 deletion man/unreachable_code_linter.Rd

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

Loading