-
Notifications
You must be signed in to change notification settings - Fork 187
New vector_logic_linter #978
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,59 @@ | ||
#' Enforce usage of scalar logical operators in conditional statements | ||
#' | ||
#' Usage of `&` in conditional statements is error-prone and inefficient. | ||
#' `condition` in `if (condition) expr` must always be length-1, in which | ||
#' case `&&` is to be preferred. Ditto for `|` vs. `||`. | ||
#' | ||
#' This linter covers inputs to `if()` and `while()` conditions and to | ||
#' [testthat::expect_true()] and [testthat::expect_false()]. | ||
#' @evalRd rd_tags("vector_logic_linter") | ||
#' @seealso | ||
#' [linters] for a complete list of linters available in lintr. | ||
#' <https://style.tidyverse.org/syntax.html#if-statements> | ||
#' @export | ||
vector_logic_linter <- function() { | ||
Linter(function(source_file) { | ||
if (length(source_file$xml_parsed_content) == 0L) { | ||
return(list()) | ||
} | ||
|
||
xml <- source_file$xml_parsed_content | ||
|
||
# ensures the expr is in the cond part of `if/while (cond) expr` -- | ||
# if on the XML parse tree is structured like | ||
# <expr> | ||
# <IF> | <expr><SYMBOL_FUNCTION_CALL> | ||
# <OP-LEFT-PAREN> | ||
# <expr> ... </expr> # <- loop condition | ||
# <OP-RIGHT-PAREN> | ||
# <expr> ... </expr> # <- evaluation; includes BRACEs if present | ||
# <ELSE> # (here & below is optional) | ||
# <expr> ... </expr> | ||
# </expr> | ||
# we _don't_ want to match anything on the second expr, hence this | ||
xpath <- "//*[ | ||
(self::AND or self::OR) | ||
and ancestor::expr[ | ||
not(preceding-sibling::OP-RIGHT-PAREN) | ||
and preceding-sibling::*[ | ||
self::IF | ||
or self::WHILE | ||
or self::expr[SYMBOL_FUNCTION_CALL[text() = 'expect_true' or text() = 'expect_false']] | ||
] | ||
] | ||
and not(ancestor::expr[ | ||
preceding-sibling::expr[SYMBOL_FUNCTION_CALL[not(text() = 'expect_true' or text() = 'expect_false')]] | ||
or preceding-sibling::OP-LEFT-BRACKET | ||
]) | ||
]" | ||
bad_expr <- xml2::xml_find_all(xml, xpath) | ||
|
||
return(lapply( | ||
bad_expr, | ||
xml_nodes_to_lint, | ||
source_file = source_file, | ||
lint_message = "Conditional expressions require scalar logical operators (&& and ||)", | ||
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.
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,91 @@ | ||
test_that("vector_logic_linter skips allowed usages", { | ||
expect_lint("if (TRUE) 5 else if (TRUE) 2", NULL, vector_logic_linter()) | ||
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. BTW: Do we have a linter for this kind of unreachable code? 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. I have |
||
|
||
expect_lint("if (TRUE || FALSE) 1; while (TRUE && FALSE) 2", NULL, vector_logic_linter()) | ||
|
||
# function calls and extractions may aggregate to scalars -- only catch | ||
# usages at the highest logical level | ||
expect_lint("if (agg_function(x & y)) 1", NULL, vector_logic_linter()) | ||
expect_lint("if (DT[x | y, cond]) 1", NULL, vector_logic_linter()) | ||
}) | ||
|
||
test_that("vector_logic_linter blocks simple disallowed usages", { | ||
expect_lint( | ||
"if (TRUE & FALSE) 1", | ||
rex::rex("Conditional expressions require scalar logical operators"), | ||
vector_logic_linter() | ||
) | ||
|
||
expect_lint( | ||
"while (TRUE | TRUE) 2", | ||
rex::rex("Conditional expressions require scalar logical operators"), | ||
vector_logic_linter() | ||
) | ||
}) | ||
|
||
test_that("vector_logic_linter detects nested conditions", { | ||
expect_lint( | ||
"if (TRUE & TRUE || FALSE) 4", | ||
rex::rex("Conditional expressions require scalar logical operators"), | ||
vector_logic_linter() | ||
) | ||
|
||
expect_lint( | ||
"if (TRUE && (TRUE | FALSE)) 4", | ||
rex::rex("Conditional expressions require scalar logical operators"), | ||
vector_logic_linter() | ||
) | ||
|
||
# don't match potentially OK usages nested within calls | ||
expect_lint("if (TRUE && any(TRUE | FALSE)) 4", NULL, vector_logic_linter()) | ||
# even if the usage is nested in those calls (b/181915948) | ||
expect_lint("if (TRUE && any(TRUE | FALSE | TRUE)) 4", NULL, vector_logic_linter()) | ||
|
||
# don't match potentially OK usages in the branch itself | ||
lines <- trim_some(" | ||
if (TRUE) { | ||
x | y | ||
} | ||
") | ||
expect_lint(lines, NULL, vector_logic_linter()) | ||
}) | ||
|
||
test_that("vector_logic_linter catches usages in expect_true()/expect_false()", { | ||
expect_lint( | ||
"expect_true(TRUE & FALSE)", | ||
rex::rex("Conditional expressions require scalar logical operators"), | ||
vector_logic_linter() | ||
) | ||
|
||
expect_lint( | ||
"expect_false(TRUE | TRUE)", | ||
rex::rex("Conditional expressions require scalar logical operators"), | ||
vector_logic_linter() | ||
) | ||
|
||
# ditto with namespace qualification | ||
expect_lint( | ||
"testthat::expect_true(TRUE & FALSE)", | ||
rex::rex("Conditional expressions require scalar logical operators"), | ||
vector_logic_linter() | ||
) | ||
|
||
expect_lint( | ||
"testthat::expect_false(TRUE | TRUE)", | ||
rex::rex("Conditional expressions require scalar logical operators"), | ||
vector_logic_linter() | ||
) | ||
|
||
# valid nested usage within aggregator | ||
expect_lint("testthat::expect_false(any(TRUE | TRUE))", NULL, vector_logic_linter()) | ||
}) | ||
|
||
test_that("vector_logic_linter doesn't get mixed up from complex usage", { | ||
lines <- trim_some(" | ||
if (a) { | ||
expect_true(ok) | ||
x <- 2 | ||
a | b | ||
}") | ||
expect_lint(lines, NULL, vector_logic_linter()) | ||
}) |
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.
Could this be caused to trip by
I don't fully understand the XPath, maybe it's worth elaborating what all the parts are for.
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.
Have to admit I have lost some of the subtlety myself since I wrote it >1 year ago :)
the original code has a few more named objects which might help (not really to me at a glance):
I added this example as a test and it still works. Here's the tree of your example:
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.
Since it's very thoroughly tested, it's okay for me. Generally, I find "constructive" XPaths easier to understand, e.g. here select all relevant calls (IF, WHILE, ...), go into their first expr and check for top-level vector logic.
But feel free to keep the working XPath. May also be worth performance checking due to the use of //* which requires traversing all nodes.
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.
I remember it started simpler:
but I rewrote it to handle the much more complicated cases like
if (x && y | z)
, which did pick up a fair amount of new true positives in google.My sense so far is that it's really hard to slow down XPath on R AST-sized XML trees to the point where I notice. OTOH, I do have in mind at some point to run some timing tests of all the linters on a CRAN sample to see if we have any performance issues to worry about. I assume things like
object_usage_linter
that require doing other I/O tasks besides an XPath search will be much slower. Maybe this could be another .dev/ script.