Skip to content

Commit 361a808

Browse files
New redundant_ifelse_linter (#1000)
* New redundant_ifelse_litner * linting 1/0 is optional (on by default) * roxygenize * document parameter Co-authored-by: AshesITR <alexander.rosenstock@web.de>
1 parent 3a62b86 commit 361a808

11 files changed

+205
-3
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Collate:
114114
'path_linters.R'
115115
'pipe_call_linter.R'
116116
'pipe_continuation_linter.R'
117+
'redundant_ifelse_linter.R'
117118
'regex_subset_linter.R'
118119
'semicolon_terminator_linter.R'
119120
'seq_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export(paren_brace_linter)
7878
export(paste_sep_linter)
7979
export(pipe_call_linter)
8080
export(pipe_continuation_linter)
81+
export(redundant_ifelse_linter)
8182
export(regex_subset_linter)
8283
export(semicolon_terminator_linter)
8384
export(seq_linter)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ function calls. (#850, #851, @renkun-ken)
116116
* `literal_coercion_linter()` Require using correctly-typed literals instead of direct coercion, e.g. `1L` instead of `as.numeric(1)`
117117
* `paste_sep_linter()` Require usage of `paste0()` over `paste(sep = "")`
118118
* `nested_ifelse_linter()` Prevent nested calls to `ifelse()` like `ifelse(A, x, ifelse(B, y, z))`, and similar
119+
* `redundant_ifelse_linter()` Prevent usage like `ifelse(A & B, TRUE, FALSE)` or `ifelse(C, 0, 1)` (the latter is `as.numeric(!C)`)
119120
* `else_same_line_linter()` Require `else` to come on the same line as the preceding `}`, if present
120121
* `unreachable_code_linter()` Prevent code after `return()` and `stop()` statements that will never be reached
121122
* `regex_subset_linter()` Require usage of `grep(ptn, x, value = TRUE)` over `x[grep(ptn, x)]` and similar

R/redundant_ifelse_linter.R

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#' Prevent ifelse() from being used to produce TRUE/FALSE or 1/0
2+
#'
3+
#' Expressions like `ifelse(x, TRUE, FALSE)` and `ifelse(x, FALSE, TRUE)` are
4+
#' redundant; just `x` or `!x` suffice in R code where logical vectors are a
5+
#' core data structure. `ifelse(x, 1, 0)` is also `as.numeric(x)`, but even
6+
#' this should only be needed rarely.
7+
#'
8+
#' @evalRd rd_tags("redundant_ifelse_linter")
9+
#' @param allow10 Logical, default `FALSE`. If `TRUE`, usage like
10+
#' `ifelse(x, 1, 0)` is allowed, i.e., only usage like
11+
#' `ifelse(x, TRUE, FALSE)` is linted.
12+
#' @seealso [linters] for a complete list of linters available in lintr.
13+
#' @export
14+
redundant_ifelse_linter <- function(allow10 = FALSE) {
15+
Linter(function(source_file) {
16+
if (length(source_file$xml_parsed_content) == 0L) {
17+
return(list())
18+
}
19+
20+
xml <- source_file$xml_parsed_content
21+
22+
tf_xpath <- glue::glue("//expr[
23+
expr[SYMBOL_FUNCTION_CALL[ {xp_text_in_table(ifelse_funs)} ]]
24+
and expr[NUM_CONST[text() = 'TRUE']]
25+
and expr[NUM_CONST[text() = 'FALSE']]
26+
]")
27+
tf_expr <- xml2::xml_find_all(xml, tf_xpath)
28+
tf_lints <- lapply(
29+
tf_expr,
30+
xml_nodes_to_lint,
31+
source_file = source_file,
32+
lint_message = function(expr) {
33+
matched_call <- xml2::xml_text(xml2::xml_find_first(expr, "expr/SYMBOL_FUNCTION_CALL"))
34+
# [1] call; [2] logical condiditon
35+
first_arg <- xml2::xml_text(xml2::xml_find_first(expr, "expr[3]/NUM_CONST"))
36+
second_arg <- xml2::xml_text(xml2::xml_find_first(expr, "expr[4]/NUM_CONST"))
37+
sprintf(
38+
"Just use the logical condition (or its negation) directly instead of calling %s(x, %s, %s)",
39+
matched_call, first_arg, second_arg
40+
)
41+
},
42+
type = "warning"
43+
)
44+
45+
if (allow10) {
46+
num_lints <- NULL
47+
} else {
48+
num_xpath <- glue::glue("//expr[
49+
expr[SYMBOL_FUNCTION_CALL[ {xp_text_in_table(ifelse_funs)} ]]
50+
and expr[NUM_CONST[text() = '1' or text() = '1L']]
51+
and expr[NUM_CONST[text() = '0' or text() = '0L']]
52+
]")
53+
num_expr <- xml2::xml_find_all(xml, num_xpath)
54+
num_lints <- lapply(
55+
num_expr,
56+
xml_nodes_to_lint,
57+
source_file = source_file,
58+
lint_message = function(expr) {
59+
matched_call <- xml2::xml_text(xml2::xml_find_first(expr, "expr/SYMBOL_FUNCTION_CALL"))
60+
# [1] call; [2] logical condiditon
61+
first_arg <- xml2::xml_text(xml2::xml_find_first(expr, "expr[3]/NUM_CONST"))
62+
second_arg <- xml2::xml_text(xml2::xml_find_first(expr, "expr[4]/NUM_CONST"))
63+
replacement <- if (any(c(first_arg, second_arg) %in% c("0", "1"))) "as.numeric" else "as.integer"
64+
message <- sprintf(
65+
"Prefer %s(x) to %s(x, %s, %s) if really needed,",
66+
replacement, matched_call, first_arg, second_arg
67+
)
68+
paste(message, "but do note that R will usually convert logical vectors to 0/1 on the fly when needed.")
69+
},
70+
type = "warning"
71+
)
72+
}
73+
74+
return(c(tf_lints, num_lints))
75+
})
76+
}

inst/lintr/linters.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ paren_brace_linter,style readability default
5151
paste_sep_linter,best_practices consistency
5252
pipe_call_linter,style readability
5353
pipe_continuation_linter,style readability default
54+
redundant_ifelse_linter,best_practices efficiency consistency
5455
regex_subset_linter,best_practices efficiency
5556
semicolon_terminator_linter,style readability default configurable
5657
seq_linter,robustness efficiency consistency best_practices default

man/best_practices_linters.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/consistency_linters.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/efficiency_linters.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/linters.Rd

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/redundant_ifelse_linter.Rd

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
test_that("redundant_ifelse_linter skips allowed usages", {
2+
expect_lint("ifelse(x > 5, 0, 2)", NULL, redundant_ifelse_linter())
3+
expect_lint("ifelse(x > 5, TRUE, NA)", NULL, redundant_ifelse_linter())
4+
expect_lint("ifelse(x > 5, FALSE, NA)", NULL, redundant_ifelse_linter())
5+
expect_lint("ifelse(x > 5, TRUE, TRUE)", NULL, redundant_ifelse_linter())
6+
7+
expect_lint("ifelse(x > 5, 0L, 2L)", NULL, redundant_ifelse_linter())
8+
expect_lint("ifelse(x > 5, 0L, 10L)", NULL, redundant_ifelse_linter())
9+
})
10+
11+
test_that("redundant_ifelse_linter blocks simple disallowed usages", {
12+
expect_lint(
13+
"ifelse(x > 5, TRUE, FALSE)",
14+
rex::rex("Just use the logical condition (or its negation) directly"),
15+
redundant_ifelse_linter()
16+
)
17+
expect_lint(
18+
"ifelse(x > 5, FALSE, TRUE)",
19+
rex::rex("Just use the logical condition (or its negation) directly"),
20+
redundant_ifelse_linter()
21+
)
22+
23+
# other ifelse equivalents from common packages
24+
expect_lint(
25+
"if_else(x > 5, TRUE, FALSE)",
26+
rex::rex("Just use the logical condition (or its negation) directly"),
27+
redundant_ifelse_linter()
28+
)
29+
expect_lint(
30+
"fifelse(x > 5, FALSE, TRUE)",
31+
rex::rex("Just use the logical condition (or its negation) directly"),
32+
redundant_ifelse_linter()
33+
)
34+
})
35+
36+
test_that("redundant_ifelse_linter blocks usages equivalent to as.numeric, optionally", {
37+
expect_lint(
38+
"ifelse(x > 5, 1L, 0L)",
39+
rex::rex("Prefer as.integer(x) to ifelse(x, 1L, 0L)"),
40+
redundant_ifelse_linter()
41+
)
42+
expect_lint(
43+
"ifelse(x > 5, 0L, 1L)",
44+
rex::rex("Prefer as.integer(x) to ifelse(x, 0L, 1L)"),
45+
redundant_ifelse_linter()
46+
)
47+
48+
expect_lint(
49+
"ifelse(x > 5, 1, 0)",
50+
rex::rex("Prefer as.numeric(x) to ifelse(x, 1, 0)"),
51+
redundant_ifelse_linter()
52+
)
53+
expect_lint(
54+
"ifelse(x > 5, 0, 1)",
55+
rex::rex("Prefer as.numeric(x) to ifelse(x, 0, 1)"),
56+
redundant_ifelse_linter()
57+
)
58+
59+
# data.table/dplyr equivalents
60+
expect_lint(
61+
"dplyr::if_else(x > 5, 1L, 0L)",
62+
rex::rex("Prefer as.integer(x) to if_else(x, 1L, 0L)"),
63+
redundant_ifelse_linter()
64+
)
65+
expect_lint(
66+
"data.table::fifelse(x > 5, 0L, 1L)",
67+
rex::rex("Prefer as.integer(x) to fifelse(x, 0L, 1L)"),
68+
redundant_ifelse_linter()
69+
)
70+
71+
expect_lint(
72+
"if_else(x > 5, 1, 0)",
73+
rex::rex("Prefer as.numeric(x) to if_else(x, 1, 0)"),
74+
redundant_ifelse_linter()
75+
)
76+
expect_lint(
77+
"fifelse(x > 5, 0, 1)",
78+
rex::rex("Prefer as.numeric(x) to fifelse(x, 0, 1)"),
79+
redundant_ifelse_linter()
80+
)
81+
82+
expect_lint("ifelse(x > 5, 1L, 0L)", NULL, redundant_ifelse_linter(allow10 = TRUE))
83+
expect_lint("ifelse(x > 5, 0L, 1L)", NULL, redundant_ifelse_linter(allow10 = TRUE))
84+
85+
expect_lint("ifelse(x > 5, 1, 0)", NULL, redundant_ifelse_linter(allow10 = TRUE))
86+
expect_lint("ifelse(x > 5, 0, 1)", NULL, redundant_ifelse_linter(allow10 = TRUE))
87+
88+
expect_lint("dplyr::if_else(x > 5, 1L, 0L)", NULL, redundant_ifelse_linter(allow10 = TRUE))
89+
expect_lint("data.table::fifelse(x > 5, 0L, 1L)", NULL, redundant_ifelse_linter(allow10 = TRUE))
90+
91+
expect_lint("if_else(x > 5, 1, 0)", NULL, redundant_ifelse_linter(allow10 = TRUE))
92+
expect_lint("fifelse(x > 5, 0, 1)", NULL, redundant_ifelse_linter(allow10 = TRUE))
93+
})

0 commit comments

Comments
 (0)