-
Notifications
You must be signed in to change notification settings - Fork 188
New linter for complex conditional expressions #2676
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
base: main
Are you sure you want to change the base?
Changes from all commits
0417757
031329f
bd616ad
f402853
1acf920
575db33
78b0130
0269a5f
c6929fd
49acffd
66c8102
746eba2
bea5900
fed5efa
249db6e
16896a0
f6a5b61
a685c99
532b9c2
421c1db
c04e2f6
3bb5f63
8de0c31
9cbbe4a
fa1c525
6e44e86
b7857c5
9cc2df7
2e9cf1b
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,111 @@ | ||
#' Complex Conditional Expressions Linter | ||
#' | ||
#' Detects complex conditional expressions and suggests extracting | ||
#' them into Boolean functions or variables for improved readability and reusability. | ||
#' | ||
#' For example, if you have a conditional expression with more than two logical operands, | ||
#' | ||
#' ``` | ||
#' if (looks_like_a_duck(x) && | ||
#' swims_like_a_duck(x) && | ||
#' quacks_like_a_duck(x)) { | ||
#' ... | ||
#' } | ||
#' ```` | ||
#' | ||
#' to improve its readability and reusability, you can extract the conditional expression. | ||
#' | ||
#' You can either extract it into a Boolean function: | ||
#' | ||
#' ``` | ||
#' is_duck <- function(x) { | ||
#' looks_like_a_duck(x) && | ||
#' swims_like_a_duck(x) && | ||
#' quacks_like_a_duck(x) | ||
#' } | ||
#' | ||
#' if (is_duck(x)) { | ||
#' ... | ||
#' } | ||
#' ``` | ||
#' | ||
#' or into a Boolean variable: | ||
#' | ||
#' ``` | ||
#' is_duck <- looks_like_a_duck(x) && | ||
#' swims_like_a_duck(x) && | ||
#' quacks_like_a_duck(x) | ||
#' | ||
#' if (is_duck) { | ||
#' ... | ||
#' } | ||
#' ``` | ||
#' | ||
#' In addition to improving code readability, extracting complex conditional expressions | ||
#' has the added benefit of introducing a reusable abstraction. | ||
#' | ||
#' @param threshold Integer. The maximum number of logical operators (`&&` or `||`) | ||
#' allowed in a conditional expression. The default is `2L`, meaning any conditional expression | ||
#' with more than two logical operators will be flagged. | ||
#' | ||
#' @examples | ||
#' # will produce lints | ||
#' code <- "if (a && b && c) { do_something() }" | ||
#' writeLines(code) | ||
#' lint( | ||
#' text = code, | ||
#' linters = complex_conditional_linter() | ||
#' ) | ||
#' | ||
#' # okay | ||
#' code <- "if (ready_to_do_something) { do_something() }" | ||
#' writeLines(code) | ||
#' lint( | ||
#' text = code, | ||
#' linters = complex_conditional_linter() | ||
#' ) | ||
#' | ||
#' code <- "if (a && b && c) { do_something() }" | ||
#' writeLines(code) | ||
#' lint( | ||
#' text = code, | ||
#' linters = complex_conditional_linter(threshold = 2L) | ||
#' ) | ||
#' | ||
#' @evalRd rd_tags("complex_conditional_linter") | ||
#' @seealso [linters] for a complete list of linters available in lintr. | ||
#' @export | ||
complex_conditional_linter <- function(threshold = 2L) { | ||
stopifnot(is.numeric(threshold), length(threshold) == 1L, threshold >= 1L) | ||
threshold <- as.integer(threshold) | ||
|
||
xpath <- glue::glue("//expr[ | ||
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. Let's avoid |
||
parent::expr[IF or WHILE] | ||
and | ||
preceding-sibling::*[1][self::OP-LEFT-PAREN] | ||
and | ||
following-sibling::*[1][self::OP-RIGHT-PAREN] | ||
and | ||
count(descendant-or-self::*[AND2 or OR2]) > {threshold} | ||
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. It's a bit strange to me to consider these of different complexity: if (any(x | y, na.rm = TRUE)) { ... }
if (any(x, na.rm = na.rm || y)) { ... } One "fix" would be to focus on the operators strictly related to the condition at hand, i.e. only those "outmost" enough in the expressions to directly affect the if (x || y)
# ^ yes
if (x || (y && z))
# ^ yes ^ yes
if (foo(x && y))
# ^ no
if (foo(x && y) && z)
# ^ no ^ yes Just generally, I am still hung up on whether we have a sufficiently good+objective way to measure "complexity". |
||
]") | ||
|
||
|
||
Linter(linter_level = "expression", function(source_expression) { | ||
xml <- source_expression$xml_parsed_content | ||
|
||
nodes <- xml2::xml_find_all(xml, xpath) | ||
|
||
lints <- xml_nodes_to_lints( | ||
nodes, | ||
source_expression = source_expression, | ||
lint_message = paste0( | ||
"Complex conditional with more than ", | ||
threshold, | ||
" logical operator(s). Consider extracting into a boolean function or variable for readability and reusability." | ||
), | ||
type = "warning" | ||
) | ||
|
||
lints | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
Version: 1.0 | ||
ProjectId: ab4c2695-5b43-4cc6-8ea0-0daf83afd8a3 | ||
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. spurious? |
||
|
||
RestoreWorkspace: No | ||
SaveWorkspace: No | ||
|
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.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.
the NEWS merge appears broken