Skip to content

Commit 81b518f

Browse files
Merge e8f755d into 15a22fe
2 parents 15a22fe + e8f755d commit 81b518f

12 files changed

+271
-3
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Collate:
105105
'function_return_linter.R'
106106
'get_source_expressions.R'
107107
'ids_with_token.R'
108+
'if_not_else_linter.R'
108109
'ifelse_censor_linter.R'
109110
'implicit_assignment_linter.R'
110111
'implicit_integer_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export(function_return_linter)
6969
export(get_r_string)
7070
export(get_source_expressions)
7171
export(ids_with_token)
72+
export(if_not_else_linter)
7273
export(ifelse_censor_linter)
7374
export(implicit_assignment_linter)
7475
export(implicit_integer_linter)

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
* `library_call_linter()` can detect if all library/require calls are not at the top of your script (#2027 and #2043, @nicholas-masel and @MichaelChirico).
3939
* `keyword_quote_linter()` for finding unnecessary or discouraged quoting of symbols in assignment, function arguments, or extraction (part of #884, @MichaelChirico). Quoting is unnecessary when the target is a valid R name, e.g. `c("a" = 1)` can be `c(a = 1)`. The same goes to assignment (`"a" <- 1`) and extraction (`x$"a"`). Where quoting is necessary, the linter encourages doing so with backticks (e.g. `` x$`a b` `` instead of `x$"a b"`).
4040
* `length_levels_linter()` for using the specific function `nlevels()` instead of checking `length(levels(x))` (part of #884, @MichaelChirico).
41+
* `if_not_else_linter()` for encouraging `if` statements to be structured as `if (A) x else y` instead of `if (!A) y else x` (part of #884, @MichaelChirico).
4142

4243
## Changes to defaults
4344

R/if_not_else_linter.R

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#' Block statements like if (!A) x else y
2+
#'
3+
#' `if (!A) x else y` is the same as `if (A) y else x`, but the latter is
4+
#' easier to reason about in the `else` case. The former requires
5+
#' double negation that can be avoided by switching the statement order.
6+
#'
7+
#' This only applies in the simple `if/else` case. Statements like
8+
#' `if (!A) x else if (B) y else z` don't always have a simpler or
9+
#' more readable form.
10+
#'
11+
#' It also applies to [ifelse()] and the package equivalents
12+
#' `dplyr::if_else()` and `data.table::fifelse()`.
13+
#'
14+
#' @param exceptions Character vector of calls to exclude from linting.
15+
#' By default, [is.null()], [is.na()], and [missing()] are excluded
16+
#' given the common idiom `!is.na(x)` as "x is present".
17+
#'
18+
#' @examples
19+
#' # will produce lints
20+
#' lint(
21+
#' text = "if (!A) x else y",
22+
#' linters = if_not_else_linter()
23+
#' )
24+
#'
25+
#' lint(
26+
#' text = "ifelse(!is_treatment, x, y)",
27+
#' linters = if_not_else_linter()
28+
#' )
29+
#'
30+
#' lint(
31+
#' text = "if (!is.null(x)) x else 2",
32+
#' linters = if_not_else_linter(exceptions = character())
33+
#' )
34+
#'
35+
#' # okay
36+
#' lint(
37+
#' text = "if (A) x else y",
38+
#' linters = if_not_else_linter()
39+
#' )
40+
#'
41+
#' lint(
42+
#' text = "ifelse(is_treatment, y, x)",
43+
#' linters = if_not_else_linter()
44+
#' )
45+
#'
46+
#' lint(
47+
#' text = "if (!is.null(x)) x else 2",
48+
#' linters = if_not_else_linter()
49+
#' )
50+
#'
51+
#' @evalRd rd_tags("if_not_else_linter")
52+
#' @seealso [linters] for a complete list of linters available in lintr.
53+
#' @export
54+
if_not_else_linter <- function(exceptions = c("is.null", "is.na", "missing")) {
55+
if_xpath <- glue("
56+
//IF[following-sibling::ELSE[not(following-sibling::expr[IF])]]
57+
/following-sibling::expr[1][
58+
OP-EXCLAMATION
59+
and not(expr[expr[SYMBOL_FUNCTION_CALL[{ xp_text_in_table(exceptions) }]]])
60+
]
61+
")
62+
63+
ifelse_xpath <- glue("
64+
//SYMBOL_FUNCTION_CALL[ {xp_text_in_table(ifelse_funs)} ]
65+
/parent::expr
66+
/parent::expr[expr[
67+
position() = 2
68+
and OP-EXCLAMATION
69+
and not(expr[
70+
OP-EXCLAMATION
71+
or expr/SYMBOL_FUNCTION_CALL[{ xp_text_in_table(exceptions) }]
72+
])
73+
]]
74+
")
75+
76+
Linter(function(source_expression) {
77+
if (!is_lint_level(source_expression, "expression")) {
78+
return(list())
79+
}
80+
81+
xml <- source_expression$xml_parsed_content
82+
83+
if_expr <- xml_find_all(xml, if_xpath)
84+
if_lints <- xml_nodes_to_lints(
85+
if_expr,
86+
source_expression = source_expression,
87+
lint_message = paste(
88+
"In a simple if/else statement,",
89+
"prefer `if (A) x else y` to the less-readable `if (!A) y else x`."
90+
),
91+
type = "warning"
92+
)
93+
94+
ifelse_expr <- xml_find_all(xml, ifelse_xpath)
95+
ifelse_call <- xp_call_name(ifelse_expr)
96+
ifelse_lints <- xml_nodes_to_lints(
97+
ifelse_expr,
98+
source_expression = source_expression,
99+
lint_message = sprintf(
100+
"Prefer `%1$s(A, x, y)` to the less-readable `%1$s(!A, y, x)`.",
101+
ifelse_call
102+
),
103+
type = "warning"
104+
)
105+
106+
c(if_lints, ifelse_lints)
107+
})
108+
}

R/xp_utils.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# like `text() %in% table`, translated to XPath 1.0
44
xp_text_in_table <- function(table) {
5+
if (length(table) == 0L) return("true")
56
# xpath doesn't seem to have a standard way of escaping quotes, so attempt
67
# to use "" whenever the string has ' (not a perfect solution). info on
78
# escaping from https://stackoverflow.com/questions/14822153

inst/lintr/linters.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ for_loop_index_linter,best_practices readability robustness
3434
function_argument_linter,style consistency best_practices
3535
function_left_parentheses_linter,style readability default
3636
function_return_linter,readability best_practices
37+
if_not_else_linter,readability consistency configurable
3738
ifelse_censor_linter,best_practices efficiency
3839
implicit_assignment_linter,style best_practices readability configurable
3940
implicit_integer_linter,style consistency best_practices configurable

man/configurable_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/if_not_else_linter.Rd

Lines changed: 66 additions & 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.

0 commit comments

Comments
 (0)