Skip to content

Unreachable loops #2141

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

Merged
merged 27 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from 19 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
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
* `unreachable_code_linter()`
+ finds unreachable code even in the presence of a comment or semicolon after `return()` or `stop()` (#2127, @MEO265).
+ 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()` finds assignments in call arguments besides the first one (#2136, @MichaelChirico).
* `inner_combine_linter()` no longer throws on length-1 calls to `c()` like `c(exp(2))` or `c(log(3))` (#2017, @MichaelChirico). Such usage is discouraged by `unnecessary_concatenation_linter()`, but `inner_combine_linter()` _per se_ does not apply.
* `condition_message_linter()` ignores usages of extracted calls like `env$stop(paste(a, b))` (#1455, @MichaelChirico).
Expand Down
30 changes: 25 additions & 5 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 @@ -57,14 +57,22 @@
unreachable_code_linter <- function() {
# NB: use not(OP-DOLLAR) to prevent matching process$stop(), #1051
xpath_return_stop <- "
//FUNCTION
/following-sibling::expr
((//FUNCTION | //REPEAT | //ELSE | //FOR)/following-sibling::expr | (//IF | //WHILE)/following-sibling::expr[2])
/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 <- "
((//REPEAT | //ELSE | //FOR)/following-sibling::expr | (//IF | //WHILE)/following-sibling::expr[2])
/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]
"
Expand Down Expand Up @@ -104,7 +112,19 @@ unreachable_code_linter <- function() {
lints_return_stop <- xml_nodes_to_lints(
expr_return_stop[!is_nolint_end_comment],
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 return() or stop() should be removed.",
type = "warning"
)

expr_next_break <- xml_find_all(xml, xpath_next_break)

is_nolint_end_comment <- xml2::xml_name(expr_next_break) == "COMMENT" &
re_matches(xml_text(expr_next_break), settings$exclude_end)

lints_next_break <- xml_nodes_to_lints(
expr_next_break[!is_nolint_end_comment],
source_expression = source_expression,
lint_message = "Code and comments coming after a `next` or `break` should be removed.",
type = "warning"
)

Expand All @@ -126,6 +146,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.

220 changes: 212 additions & 8 deletions tests/testthat/test-unreachable_code_linter.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,196 @@ test_that("unreachable_code_linter works in simple function", {
expect_lint(lines, NULL, unreachable_code_linter())
})

test_that("unreachable_code_linter works in sub expressions", {
linter <- unreachable_code_linter()
msg <- rex::rex("Code and comments coming after a return() or stop()")

lines <- trim_some("
Copy link
Collaborator

@MichaelChirico MichaelChirico Sep 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

long tests like this are good for robustness, but best to start with very succinct tests of only one aspect of the lint logic.

it means more lines of code which can be tedious, but is much preferable for test readability & ease of maintenance

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it may also help to either annotate the test with a comment on what all is being tested (it looks like "various types of if/else cases"), or to split into different test_that() and record that in the test's name

foo <- function(bar) {
if (bar) {
return(bar)
# Test comment
while (bar) {
return(bar)
5 + 3
repeat {
return(bar)
# Test comment
}
}
} else if (bla) {
# test
return(5)
# Test 2
} else {
return(bar)
# Test comment
for(i in 1:3) {
return(bar)
5 + 4
}
}
return(bar)
5 + 1
}
")

expect_lint(
lines,
list(
list(line_number = 4L, message = msg),
list(line_number = 7L, message = msg),
list(line_number = 10L, message = msg),
list(line_number = 16L, message = msg),
list(line_number = 19L, message = msg),
list(line_number = 22L, message = msg),
list(line_number = 26L, message = msg)
),
linter
)

lines <- trim_some("
foo <- function(bar) {
if (bar) {
return(bar) # Test comment
}
while (bar) {
return(bar) # 5 + 3
}
repeat {
return(bar) # Test comment
}

}
")

expect_lint(lines, NULL, linter)

lines <- trim_some("
foo <- function(bar) {
if (bar) {
return(bar); x <- 2
} else {
return(bar); x <- 3
}
while (bar) {
return(bar); 5 + 3
}
repeat {
return(bar); test()
}
for(i in 1:3) {
return(bar); 5 + 4
}
}
")

expect_lint(
lines,
list(
list(line_number = 3L, message = msg),
list(line_number = 5L, message = msg),
list(line_number = 8L, message = msg),
list(line_number = 11L, message = msg),
list(line_number = 14L, message = msg)
),
linter
)
})

test_that("unreachable_code_linter works with next and break in sub expressions", {
linter <- unreachable_code_linter()
msg <- rex::rex("Code and comments coming after a `next` or `break`")

lines <- trim_some("
foo <- function(bar) {
if (bar) {
next
# Test comment
while (bar) {
break
5 + 3
repeat {
next
# Test comment
}
}
} else {
next
# test
for(i in 1:3) {
break
5 + 4
}
}
}
")

expect_lint(
lines,
list(
list(line_number = 4L, message = msg),
list(line_number = 7L, message = msg),
list(line_number = 10L, message = msg),
list(line_number = 15L, message = msg),
list(line_number = 18L, message = msg)
),
linter
)

lines <- trim_some("
foo <- function(bar) {
if (bar) {
break # Test comment
} else {
next # Test comment
}
while (bar) {
next # 5 + 3
}
repeat {
next # Test comment
}
for(i in 1:3) {
break # 5 + 4
}
}
")

expect_lint(lines, NULL, linter)

lines <- trim_some("
foo <- function(bar) {
if (bar) {
next; x <- 2
} else {
break; x <- 3
}
while (bar) {
break; 5 + 3
}
repeat {
next; test()
}
for(i in 1:3) {
break; 5 + 4
}
}
")

expect_lint(
lines,
list(
list(line_number = 3L, message = msg),
list(line_number = 5L, message = msg),
list(line_number = 8L, message = msg),
list(line_number = 11L, message = msg),
list(line_number = 14L, message = msg)
),
linter
)
})

test_that("unreachable_code_linter ignores expressions that aren't functions", {
expect_lint("x + 1", NULL, unreachable_code_linter())
})
Expand Down Expand Up @@ -57,7 +247,7 @@ test_that("unreachable_code_linter identifies simple unreachable code", {
lines,
list(
line_number = 3L,
message = rex::rex("Code and comments coming after a top-level return() or stop()")
message = rex::rex("Code and comments coming after a return() or stop()")
),
unreachable_code_linter()
)
Expand All @@ -73,13 +263,13 @@ test_that("unreachable_code_linter finds unreachable comments", {
")
expect_lint(
lines,
rex::rex("Code and comments coming after a top-level return() or stop()"),
rex::rex("Code and comments coming after a return() or stop()"),
unreachable_code_linter()
)
})

test_that("unreachable_code_linter finds expressions in the same line", {
msg <- rex::rex("Code and comments coming after a top-level return() or stop()")
msg <- rex::rex("Code and comments coming after a return() or stop()")
linter <- unreachable_code_linter()

lines <- trim_some("
Expand Down Expand Up @@ -107,7 +297,7 @@ test_that("unreachable_code_linter finds expressions in the same line", {
})

test_that("unreachable_code_linter finds expressions and comments after comment in return line", {
msg <- rex::rex("Code and comments coming after a top-level return() or stop()")
msg <- rex::rex("Code and comments coming after a return() or stop()")
linter <- unreachable_code_linter()

lines <- trim_some("
Expand Down Expand Up @@ -136,7 +326,7 @@ test_that("unreachable_code_linter finds a double return", {
")
expect_lint(
lines,
rex::rex("Code and comments coming after a top-level return() or stop()"),
rex::rex("Code and comments coming after a return() or stop()"),
unreachable_code_linter()
)
})
Expand All @@ -151,7 +341,7 @@ test_that("unreachable_code_linter finds code after stop()", {
")
expect_lint(
lines,
rex::rex("Code and comments coming after a top-level return() or stop()"),
rex::rex("Code and comments coming after a return() or stop()"),
unreachable_code_linter()
)
})
Expand Down Expand Up @@ -195,6 +385,20 @@ test_that("unreachable_code_linter ignores terminal nolint end comments", {
NULL,
list(unreachable_code_linter(), one_linter = assignment_linter())
)

expect_lint(
trim_some("
foo <- function() {
do_something
# nolint start: one_linter.
a = 42
next
# nolint end
}
"),
NULL,
unreachable_code_linter()
)
})

test_that("unreachable_code_linter identifies unreachable code in conditional loops", {
Expand Down Expand Up @@ -349,7 +553,7 @@ test_that("unreachable_code_linter identifies unreachable code in mixed conditio
),
list(
line_number = 13L,
message = rex::rex("Code and comments coming after a top-level return() or stop()")
message = rex::rex("Code and comments coming after a return() or stop()")
)
),
linter
Expand Down Expand Up @@ -389,7 +593,7 @@ test_that("unreachable_code_linter identifies unreachable code in mixed conditio
# ")
# expect_lint(
# unreachable_inside_switch_lines,
# rex::rex("Code and comments coming after a top-level return() or stop()"),
# rex::rex("Code and comments coming after a return() or stop()"),
# unreachable_code_linter()
# )
# })
Expand Down