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

New parameters for if_switch_linter() to optionally skip lints on "complex" if/else usage #2413

Merged
merged 23 commits into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
* `print_linter()` for discouraging usage of `print()` on string literals like `print("Reached here")` or `print(paste("Found", nrow(DF), "rows."))` (#1894, @MichaelChirico).
* `unnecessary_nesting_linter()` for discouraging overly-nested code where an early return or eliminated sub-expression (inside '{') is preferable (#2317 and part of #884, @MichaelChirico).
* `consecutive_mutate_linter()` for encouraging consecutive calls to `dplyr::mutate()` to be combined (part of #884, @MichaelChirico).
* `if_switch_linter()` for encouraging `switch()` over repeated `if`/`else` tests (part of #884, @MichaelChirico).
* `if_switch_linter()` for encouraging `switch()` over repeated `if`/`else` tests (#2322 and part of #884, @MichaelChirico).
* `nested_pipe_linter()` for discouraging pipes within pipes, e.g. `df1 %>% inner_join(df2 %>% select(a, b))` (part of #884, @MichaelChirico).
* `nrow_subset_linter()` for discouraging usage like `nrow(subset(x, conditions))` in favor of something like `with(x, sum(conditions))` which doesn't require a full subset of `x` (#2313, #2314 and part of #884, @MichaelChirico).
* `pipe_return_linter()` for discouraging usage of `return()` inside a {magrittr} pipeline (part of #884, @MichaelChirico).
Expand Down
176 changes: 171 additions & 5 deletions R/if_switch_linter.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,77 @@
#' approach is roughly linear in the number of conditions that need to
#' be evaluated, here up to 3 times).
#'
#' @param max_branch_lines,max_branch_expressions Integer, default 0 indicates "no maximum".
#' If set any `if`/`else if`/.../`else` chain where any branch occupies more than
#' this number of lines (resp. expressions) will not be linted. The conjugate
#' applies to `switch()` statements -- if these parameters are set, any `switch()`
#' statement with any overly-complicated branches will be linted. See examples.
#'
#' @examples
#' # will produce lints
#' lint(
#' text = "if (x == 'a') 1 else if (x == 'b') 2 else 3",
#' linters = if_switch_linter()
#' )
#'
#' code <- paste(
#' "if (x == 'a') {",
#' " 1",
#' "} else if (x == 'b') {",
#' " 2",
#' "} else if (x == 'c') {",
#' " y <- x",
#' " z <- sqrt(match(y, letters))",
#' " z",
#' "}",
#' sep = "\n"
#' )
#' writeLines(code)
#' lint(
#' text = code,
#' linters = if_switch_linter()
#' )
#'
#' code <- paste(
#' "if (x == 'a') {",
#' " 1",
#' "} else if (x == 'b') {",
#' " 2",
#' "} else if (x == 'c') {",
#' " y <- x",
#' " z <- sqrt(",
#' " match(y, letters)",
#' " )",
#' " z",
#' "}",
#' sep = "\n"
#' )
#' writeLines(code)
#' lint(
#' text = code,
#' linters = if_switch_linter()
#' )
#'
#' code <- paste(
#' "switch(x,",
#' " a = {",
#' " 1",
#' " 2",
#' " 3",
#' " },",
#' " b = {",
#' " 1",
#' " 2",
#' " }",
#' ")",
#' sep = "\n"
#' )
#' writeLines(code)
#' lint(
#' text = code,
#' linters = if_switch_linter(max_branch_lines = 2L)
#' )
#'
#' # okay
#' lint(
#' text = "switch(x, a = 1, b = 2, 3)",
Expand All @@ -33,18 +97,105 @@
#' linters = if_switch_linter()
#' )
#'
#' code <- paste(
#' "if (x == 'a') {",
#' " 1",
#' "} else if (x == 'b') {",
#' " 2",
#' "} else if (x == 'c') {",
#' " y <- x",
#' " z <- sqrt(match(y, letters))",
#' " z",
#' "}",
#' sep = "\n"
#' )
#' writeLines(code)
#' lint(
#' text = code,
#' linters = if_switch_linter(max_branch_lines = 2L)
#' )
#'
#' code <- paste(
#' "if (x == 'a') {",
#' " 1",
#' "} else if (x == 'b') {",
#' " 2",
#' "} else if (x == 'c') {",
#' " y <- x",
#' " z <- sqrt(",
#' " match(y, letters)",
#' " )",
#' " z",
#' "}",
#' sep = "\n"
#' )
#' writeLines(code)
#' lint(
#' text = code,
#' linters = if_switch_linter(max_branch_expressions = 2L)
#' )
#'
#' code <- paste(
#' "switch(x,",
#' " a = {",
#' " 1",
#' " 2",
#' " 3",
#' " },",
#' " b = {",
#' " 1",
#' " 2",
#' " }",
#' ")",
#' sep = "\n"
#' )
#' writeLines(code)
#' lint(
#' text = code,
#' linters = if_switch_linter(max_branch_lines = 3L)
#' )
#'
#' @evalRd rd_tags("if_switch_linter")
#' @seealso [linters] for a complete list of linters available in lintr.
#' @export
if_switch_linter <- function() {
equal_str_cond <- "expr[1][EQ and expr[STR_CONST]]"
if_switch_linter <- function(max_branch_lines = 0L, max_branch_expressions = 0L) {
equal_str_cond <- "expr[1][EQ and expr/STR_CONST]"

if (max_branch_lines > 0L || max_branch_expressions > 0L) {
complexity_cond <- xp_or(c(
if (max_branch_lines > 0L) paste("OP-RIGHT-BRACE/@line2 - OP-LEFT-BRACE/@line1 > 1 +", max_branch_lines),
if (max_branch_expressions > 0L) paste("count(expr) >", max_branch_expressions)
))
branch_expr_cond <- xp_and(c(
xp_or(
# if (x) { <this expr> } ...
xp_and("preceding-sibling::IF", "position() = 2"),
# if (x) { ... } else { <this expr> }
xp_and("preceding-sibling::ELSE", "not(IF)")
),
complexity_cond
))
max_lines_cond <- glue(".//expr[{branch_expr_cond}]")

switch_xpath <- glue("
parent::expr
/parent::expr[expr[
position() > 2
and {complexity_cond}
]]
")
} else {
max_lines_cond <- "false"

switch_xpath <- NULL
}

# NB: IF AND {...} AND ELSE/... implies >= 3 equality conditions are present
# .//expr/IF/...: the expr in `==` that's _not_ the STR_CONST
# not(preceding::IF): prevent nested matches which might be incorrect globally
# not(. != .): don't match if there are _any_ expr which _don't_ match the top
# expr
xpath <- glue("
if_xpath <- glue("
//IF
/parent::expr[
not(preceding-sibling::IF)
Expand All @@ -58,15 +209,16 @@ if_switch_linter <- function() {
.//expr/IF/following-sibling::{equal_str_cond}/expr[not(STR_CONST)]
!= expr[1][EQ]/expr[not(STR_CONST)]
)
and not({ max_lines_cond })
]
")

Linter(linter_level = "expression", function(source_expression) {
xml <- source_expression$xml_parsed_content

bad_expr <- xml_find_all(xml, xpath)
bad_expr <- xml_find_all(xml, if_xpath)

xml_nodes_to_lints(
lints <- xml_nodes_to_lints(
bad_expr,
source_expression = source_expression,
lint_message = paste(
Expand All @@ -76,5 +228,19 @@ if_switch_linter <- function() {
),
type = "warning"
)

if (!is.null(switch_xpath)) {
xml_calls <- source_expression$xml_find_function_calls("switch")
switch_expr <- xml_find_all(xml_calls, switch_xpath)

lints <- c(lints, xml_nodes_to_lints(
switch_expr,
source_expression = source_expression,
lint_message = "Prefer repeated if/else statements over overly-complicated switch() statements.",
type = "warning"
))
}

lints
})
}
2 changes: 1 addition & 1 deletion inst/lintr/linters.csv
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function_argument_linter,style consistency best_practices
function_left_parentheses_linter,style readability default
function_return_linter,readability best_practices
if_not_else_linter,readability consistency configurable
if_switch_linter,best_practices readability consistency efficiency
if_switch_linter,best_practices readability consistency efficiency configurable
ifelse_censor_linter,best_practices efficiency
implicit_assignment_linter,style best_practices readability configurable
implicit_integer_linter,style consistency best_practices configurable
Expand Down
1 change: 1 addition & 0 deletions man/configurable_linters.Rd

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

Loading
Loading