Skip to content

Commit ccad66a

Browse files
Fix false positive in if_switch_linter() for empty strings (#3018)
* Fix false positive in `if_switch_linter()` for empty strings Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com> * Fix false positive in `if_switch_linter()` for empty strings Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com> * Fix false positive in `if_switch_linter()` for empty strings Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com> * Fix false positive in `if_switch_linter()` for empty strings Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com> * more raw string tests involving non-empty strings * rename helpers * include negative cases in multi-lint test --------- Signed-off-by: Emmanuel Ferdman <emmanuelferdman@gmail.com> Co-authored-by: Michael Chirico <chiricom@google.com>
1 parent f185cf1 commit ccad66a

File tree

3 files changed

+82
-16
lines changed

3 files changed

+82
-16
lines changed

NEWS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
* Excluding `cyclocomp_linter()` in `available_linters()` or `linters_with_tags()`, which requires the weak dependency {cyclocomp}, no longer emits a warning (#2909, @MichaelChirico).
1212
* `repeat_linter()` no longer errors when `while` is in a column to the right of `}` (#2828, @MichaelChirico).
13-
* `undesirable_operator_linter(call_is_undesirable = FALSE)` now correctly skips prefix notation like `` `:::`(pkg, fun) `` (#2999, @emmanuel-ferdman).
1413

1514
## New and improved features
1615

@@ -52,6 +51,8 @@
5251

5352
### Lint accuracy fixes: removing false positives
5453

54+
* `if_switch_linter()` no longer produces a false positive when comparing to empty strings (`""`, `''`, or raw strings like `R"()"`), which cannot be used as `switch()` case names (#2835, @emmanuel-ferdman).
55+
* `undesirable_operator_linter(call_is_undesirable = FALSE)` now correctly skips prefix notation like `` `:::`(pkg, fun) `` (#2999, @emmanuel-ferdman).
5556
* `unnecessary_nesting_linter()` treats `=` assignment the same as `<-` for several pieces of logic (#2245 and #2829, @MichaelChirico).
5657
* `vector_logic_linter()` ignores scalar operators (`&&`/`||`) inside anonymous functions within `filter()`/`subset()` (#2935, @emmanuel-ferdman).
5758

R/if_switch_linter.R

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
#' @seealso [linters] for a complete list of linters available in lintr.
160160
#' @export
161161
if_switch_linter <- function(max_branch_lines = 0L, max_branch_expressions = 0L) {
162-
equal_str_cond <- "expr[1][EQ and expr/STR_CONST]"
162+
equal_str_cond <- "expr[1][EQ and expr/STR_CONST[string-length(text()) > 2]]"
163163

164164
if (max_branch_lines > 0L || max_branch_expressions > 0L) {
165165
complexity_cond <- xp_or(c(
@@ -188,9 +188,6 @@ if_switch_linter <- function(max_branch_lines = 0L, max_branch_expressions = 0L)
188188
switch_xpath <- NULL
189189
}
190190

191-
# NB: IF AND {...} AND ELSE/... implies >= 3 equality conditions are present
192-
# .//expr/IF/...: the expr in `==` that's _not_ the STR_CONST
193-
# not(preceding::IF): prevent nested matches which might be incorrect globally
194191
if_xpath <- glue("
195192
//IF
196193
/parent::expr[
@@ -205,23 +202,24 @@ if_switch_linter <- function(max_branch_lines = 0L, max_branch_expressions = 0L)
205202
]
206203
")
207204

208-
# not(. != .): don't match if there are _any_ expr which _don't_ match the top expr
209-
equality_test_cond <- glue("self::*[
210-
.//expr/IF/following-sibling::{equal_str_cond}/expr[not(STR_CONST)]
211-
!= expr[1][EQ]/expr[not(STR_CONST)]
212-
]")
213-
214205
Linter(linter_level = "expression", function(source_expression) {
215206
xml <- source_expression$xml_parsed_content
216207

217208
bad_expr <- xml_find_all(xml, if_xpath)
218-
expr_all_equal <- is.na(xml_find_first(
219-
strip_comments_from_subtree(bad_expr),
220-
equality_test_cond
221-
))
209+
210+
bad_expr_clean <- strip_comments_from_subtree(bad_expr)
211+
expr_all_equal <- vapply(bad_expr_clean, if_else_chain_expr_is_unique, logical(1L))
212+
bad_expr <- bad_expr[expr_all_equal]
213+
214+
# Exclude empty strings, which can't be used as switch() case names
215+
nonempty <- vapply(bad_expr, function(expr) {
216+
str_nodes <- if_else_chain_strings(expr)
217+
all(vapply(str_nodes, function(n) nzchar(get_r_string(n)), logical(1L)))
218+
}, logical(1L))
219+
bad_expr <- bad_expr[nonempty]
222220

223221
lints <- xml_nodes_to_lints(
224-
bad_expr[expr_all_equal],
222+
bad_expr,
225223
source_expression = source_expression,
226224
lint_message = paste(
227225
"Prefer switch() statements over repeated if/else equality tests,",
@@ -246,3 +244,35 @@ if_switch_linter <- function(max_branch_lines = 0L, max_branch_expressions = 0L)
246244
lints
247245
})
248246
}
247+
248+
# Extract STR_CONST nodes from equality conditions in an if/else if chain
249+
if_else_chain_strings <- function(expr) {
250+
str_nodes <- list()
251+
first <- xml_find_first(expr, "IF/following-sibling::expr[1][EQ]/expr/STR_CONST")
252+
if (!is.na(first)) str_nodes <- c(str_nodes, list(first))
253+
current <- expr
254+
repeat {
255+
else_if <- xml_find_first(current, "ELSE/following-sibling::expr[IF]")
256+
if (is.na(else_if)) break
257+
current <- else_if
258+
str_node <- xml_find_first(current, "IF/following-sibling::expr[1][EQ]/expr/STR_CONST")
259+
if (!is.na(str_node)) str_nodes <- c(str_nodes, list(str_node))
260+
}
261+
str_nodes
262+
}
263+
264+
# Check that equality conditions in an if/else if chain use the same expression
265+
if_else_chain_expr_is_unique <- function(expr) {
266+
expr_nodes <- character()
267+
first <- xml_find_first(expr, "IF/following-sibling::expr[1][EQ]/expr[not(STR_CONST)]")
268+
if (!is.na(first)) expr_nodes <- c(expr_nodes, xml_text(first))
269+
current <- expr
270+
repeat {
271+
else_if <- xml_find_first(current, "ELSE/following-sibling::expr[IF]")
272+
if (is.na(else_if)) break
273+
current <- else_if
274+
expr_node <- xml_find_first(current, "IF/following-sibling::expr[1][EQ]/expr[not(STR_CONST)]")
275+
if (!is.na(expr_node)) expr_nodes <- c(expr_nodes, xml_text(expr_node))
276+
}
277+
length(unique(expr_nodes)) == 1L
278+
}

tests/testthat/test-if_switch_linter.R

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,27 @@ test_that("if_switch_linter skips allowed usages", {
1717
# simple cases with two conditions might be more natural
1818
# without switch(); require at least three branches to trigger a lint
1919
expect_no_lint("if (x == 'a') 1 else if (x == 'b') 2", linter)
20+
# not thrown by raw strings
21+
expect_no_lint("if (x == 'a') 1 else if (x == R'(b)') 2", linter)
2022
# still no third if() clause
2123
expect_no_lint("if (x == 'a') 1 else if (x == 'b') 2 else 3", linter)
24+
# not thrown by raw strings
25+
expect_no_lint("if (x == 'a') 1 else if (x == R'(b)') 2 else 3", linter)
26+
27+
# empty string comparisons can't use switch()
28+
expect_no_lint("if (x == '') 1 else if (x == 'a') 2 else if (x == 'b') 3", linter)
29+
expect_no_lint('if (x == "") 1 else if (x == "a") 2 else if (x == "b") 3', linter)
30+
expect_no_lint("if (x == 'a') 1 else if (x == '') 2 else if (x == 'b') 3", linter)
31+
expect_no_lint("if (x == 'a') 1 else if (x == 'b') 2 else if (x == '') 3", linter)
32+
expect_no_lint("if (x == 'a') 1 else if (x == 'b') 2 else if (x == 'c') 3 else if (x == '') 4", linter)
33+
})
34+
35+
test_that("if_switch_linter handles raw empty strings", {
36+
linter <- if_switch_linter()
37+
38+
# raw empty strings can't use switch() either
39+
expect_no_lint('if (x == R"--()--") 1 else if (x == "a") 2 else if (x == "b") 3', linter)
40+
expect_no_lint('if (x == R"()") 1 else if (x == R"{}") 2 else if (x == R"[]") 3', linter)
2241
})
2342

2443
test_that("if_switch_linter blocks simple disallowed usages", {
@@ -27,6 +46,8 @@ test_that("if_switch_linter blocks simple disallowed usages", {
2746

2847
# anything with >= 2 equality statements is deemed switch()-worthy
2948
expect_lint("if (x == 'a') 1 else if (x == 'b') 2 else if (x == 'c') 3", lint_msg, linter)
49+
# regardless of raw strings
50+
expect_lint("if (x == 'a') 1 else if (x == r'(b)') 2 else if (x == R'--[c]--') 3", lint_msg, linter)
3051
# expressions are also OK
3152
expect_lint("if (foo(x) == 'a') 1 else if (foo(x) == 'b') 2 else if (foo(x) == 'c') 3", lint_msg, linter)
3253
# including when comments are present
@@ -77,6 +98,20 @@ test_that("multiple lints have right metadata", {
7798
} else if (y == 'C') {
7899
do_C()
79100
}
101+
if (z1 == 'a') {
102+
do_a()
103+
} else if (z2 == 'b') {
104+
do_b()
105+
} else if (z3 == 'c') {
106+
do_c()
107+
}
108+
if (a == '1') {
109+
do_1()
110+
} else if (a == '2') {
111+
do_2()
112+
} else if (a == '') {
113+
do_blank()
114+
}
80115
}"),
81116
list(
82117
list(lint_msg, line_number = 2L),

0 commit comments

Comments
 (0)