Skip to content

Commit 700607e

Browse files
New coalesce_linter for encouraging %||% (#2767)
* initial work check-in * complete first pass of linter * delint * Redefine our %||%-->%|||% * land on full 'if' expression, add metadata tests * don't need %|||% in non-package file * Need to specify linter * fix metadata & add more tests * oops, wrong name * still wrong name * some more tracing to possibly help diagnose in the future * ws fixes Co-authored-by: AshesITR <[email protected]> * another Co-authored-by: AshesITR <[email protected]> * use unset= to avoid %|||% * remove unused rot() * remove dead test * rename * document() * move onload-only helper next to it, inside nocov --------- Co-authored-by: AshesITR <[email protected]>
1 parent 0939d20 commit 700607e

19 files changed

+252
-24
lines changed

.dev/lint_metadata_test.R

+2
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,7 @@ passed <- setdiff(
5050
)
5151

5252
if (length(passed) > 0L) {
53+
# Extra logging to possibly help debug
54+
cat(sprintf("Executed the following test files: %s\n", toString(failed)))
5355
stop("Please add tests of lint metadata for the following linters: ", toString(passed))
5456
}

DESCRIPTION

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ BugReports: https://github.com/r-lib/lintr/issues
2323
Depends:
2424
R (>= 4.0)
2525
Imports:
26-
backports (>= 1.4.0),
26+
backports (>= 1.5.0),
2727
cli (>= 3.4.0),
2828
codetools,
2929
digest,
@@ -73,6 +73,7 @@ Collate:
7373
'brace_linter.R'
7474
'cache.R'
7575
'class_equals_linter.R'
76+
'coalesce_linter.R'
7677
'commas_linter.R'
7778
'commented_code_linter.R'
7879
'comparison_negation_linter.R'

NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export(brace_linter)
2929
export(checkstyle_output)
3030
export(class_equals_linter)
3131
export(clear_cache)
32+
export(coalesce_linter)
3233
export(commas_linter)
3334
export(commented_code_linter)
3435
export(comparison_negation_linter)

NEWS.md

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
* `object_name_linter()` and `object_length_linter()` apply to objects assigned with `assign()` or generics created with `setGeneric()` (#1665, @MichaelChirico).
2727
* `object_usage_linter()` gains argument `interpret_extensions` to govern which false positive-prone common syntaxes should be checked for used objects (#1472, @MichaelChirico). Currently `"glue"` (renamed from earlier argument `interpret_glue`) and `"rlang"` are supported. The latter newly covers usage of the `.env` pronoun like `.env$key`, where `key` was previously missed as being a used variable.
2828

29+
### New linters
30+
31+
* `coalesce_linter()` encourages the use of the infix operator `x %||% y`, which is equivalent to `if (is.null(x)) y else x` (#2246, @MichaelChirico). While this has long been used in many tidyverse packages (it was added to {ggplot2} in 2008), it became part of every R installation from R 4.4.0.
32+
2933
### Lint accuracy fixes: removing false positives
3034

3135
* `unnecessary_nesting_linter()`:

R/coalesce_linter.R

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#' Encourage usage of the null coalescing operator `%||%`
2+
#'
3+
#' The `x %||% y` is equivalent to
4+
#' `if (is.null(x)) y else x`, but more expressive.
5+
#' It is exported by R since 4.4.0, and equivalents
6+
#' have been available in other tidyverse packages
7+
#' for much longer, e.g. 2008 for ggplot2.
8+
#'
9+
#' @examples
10+
#' # will produce lints
11+
#' lint(
12+
#' text = "if (is.null(x)) y else x",
13+
#' linters = coalesce_linter()
14+
#' )
15+
#'
16+
#' lint(
17+
#' text = "if (!is.null(x)) x else y",
18+
#' linters = coalesce_linter()
19+
#' )
20+
#'
21+
#' lint(
22+
#' text = "if (is.null(x[1])) x[2] else x[1]",
23+
#' linters = coalesce_linter()
24+
#' )
25+
#'
26+
#' # okay
27+
#' lint(
28+
#' text = "x %||% y",
29+
#' linters = coalesce_linter()
30+
#' )
31+
#'
32+
#' lint(
33+
#' text = "x %||% y",
34+
#' linters = coalesce_linter()
35+
#' )
36+
#'
37+
#' lint(
38+
#' text = "x[1] %||% x[2]",
39+
#' linters = coalesce_linter()
40+
#' )
41+
#'
42+
#'
43+
#' @evalRd rd_tags("coalesce_linter")
44+
#' @seealso [linters] for a complete list of linters available in lintr.
45+
#' @export
46+
coalesce_linter <- function() {
47+
braced_expr_cond <- "expr[1][OP-LEFT-BRACE and count(*) = 3]/expr"
48+
xpath <- glue("
49+
parent::expr[
50+
preceding-sibling::IF
51+
and (
52+
expr[2] = following-sibling::ELSE/following-sibling::expr
53+
or expr[2] = following-sibling::ELSE/following-sibling::{braced_expr_cond}
54+
or expr[2][LEFT_ASSIGN]/expr[1] = following-sibling::ELSE/following-sibling::expr
55+
or expr[2][LEFT_ASSIGN]/expr[1] = following-sibling::ELSE/following-sibling::{braced_expr_cond}
56+
)
57+
]
58+
/parent::expr
59+
|
60+
parent::expr[
61+
preceding-sibling::OP-EXCLAMATION
62+
and parent::expr/preceding-sibling::IF
63+
and (
64+
expr[2] = parent::expr/following-sibling::expr[1]
65+
or expr[2] = parent::expr/following-sibling::{braced_expr_cond}
66+
or expr[2][LEFT_ASSIGN]/expr[1] = parent::expr/following-sibling::expr[1]
67+
or expr[2][LEFT_ASSIGN]/expr[1] = parent::expr/following-sibling::{braced_expr_cond}
68+
)
69+
]
70+
/parent::expr
71+
/parent::expr
72+
")
73+
74+
Linter(linter_level = "expression", function(source_expression) {
75+
null_calls <- source_expression$xml_find_function_calls("is.null")
76+
bad_expr <- xml_find_all(null_calls, xpath)
77+
is_negation <- !is.na(xml_find_first(bad_expr, "expr/OP-EXCLAMATION"))
78+
observed <- ifelse(is_negation, "if (!is.null(x)) x else y", "if (is.null(x)) y else x")
79+
80+
xml_nodes_to_lints(
81+
bad_expr,
82+
source_expression = source_expression,
83+
lint_message = paste0("Use x %||% y instead of ", observed, "."),
84+
type = "warning"
85+
)
86+
})
87+
}

R/get_source_expressions.R

+1-5
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,7 @@ get_source_expressions <- function(filename, lines = NULL) {
6767
old_lang <- set_lang("en")
6868
on.exit(reset_lang(old_lang))
6969

70-
source_expression$lines <- if (is.null(lines)) {
71-
read_lines(filename)
72-
} else {
73-
lines
74-
}
70+
source_expression$lines <- lines %||% read_lines(filename)
7571

7672
# Only regard explicit attribute terminal_newline=FALSE as FALSE and all other cases (e.g. NULL or TRUE) as TRUE.
7773
terminal_newline <- !isFALSE(attr(source_expression$lines, "terminal_newline", exact = TRUE))

R/methods.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ summary.lints <- function(object, ...) {
177177
tbl <- table(filenames, types)
178178
filenames <- rownames(tbl)
179179
res <- as.data.frame.matrix(tbl, row.names = NULL)
180-
res$filenames <- filenames %||% character()
180+
res$filenames <- filenames %|||% character()
181181
nms <- colnames(res)
182182
res[order(res$filenames), c("filenames", nms[nms != "filenames"])]
183183
}

R/object_usage_linter.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ object_usage_linter <- function(interpret_glue = NULL, interpret_extensions = c(
8181
Linter(linter_level = "file", function(source_expression) {
8282
pkg_name <- pkg_name(find_package(dirname(source_expression$filename)))
8383

84-
declared_globals <- try_silently(globalVariables(package = pkg_name %||% globalenv()))
84+
declared_globals <- try_silently(globalVariables(package = pkg_name %|||% globalenv()))
8585

8686
xml <- source_expression$full_xml_parsed_content
8787

R/settings.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ validate_named_exclusion <- function(exclusions, idx) {
269269
lintr_option <- function(setting, default = NULL) getOption(paste0("lintr.", setting), default)
270270

271271
get_setting <- function(setting, config, defaults) {
272-
lintr_option(setting) %||% config[[setting]] %||% defaults[[setting]]
272+
lintr_option(setting) %|||% config[[setting]] %|||% defaults[[setting]]
273273
}
274274

275275
reset_settings <- function() list2env(default_settings, envir = settings)

R/trailing_blank_lines_linter.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ trailing_blank_lines_linter <- function() {
5454
lints[[length(lints) + 1L]] <- Lint(
5555
filename = source_expression$filename,
5656
line_number = length(source_expression$file_lines),
57-
column_number = (nchar(last_line) %||% 0L) + 1L,
57+
column_number = (nchar(last_line) %|||% 0L) + 1L,
5858
type = "style",
5959
message = "Add a terminal newline.",
6060
line = last_line

R/utils.R

+3-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
`%||%` <- function(x, y) {
1+
# TODO(#2768): possibly just use %||% instead
2+
`%|||%` <- function(x, y) {
23
if (is.null(x) || length(x) == 0L || (is.atomic(x[[1L]]) && is.na(x[[1L]]))) {
34
y
45
} else {
@@ -83,7 +84,7 @@ auto_names <- function(x) {
8384

8485
# The following functions is from dplyr
8586
names2 <- function(x) {
86-
names(x) %||% rep("", length(x))
87+
names(x) %|||% rep("", length(x))
8788
}
8889

8990
get_content <- function(lines, info) {
@@ -103,14 +104,6 @@ get_content <- function(lines, info) {
103104
paste(lines, collapse = "\n")
104105
}
105106

106-
logical_env <- function(x) {
107-
res <- as.logical(Sys.getenv(x))
108-
if (is.na(res)) {
109-
return(NULL)
110-
}
111-
res
112-
}
113-
114107
try_silently <- function(expr) {
115108
suppressWarnings(
116109
suppressMessages(

R/zzz.R

+12-1
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,14 @@ default_settings <- NULL
290290
settings <- new.env(parent = emptyenv())
291291

292292
# nocov start
293+
logical_env <- function(x, unset = "") {
294+
res <- as.logical(Sys.getenv(x, unset = unset))
295+
if (is.na(res)) {
296+
return(NULL)
297+
}
298+
res
299+
}
300+
293301
.onLoad <- function(libname, pkgname) {
294302
op <- options()
295303
op_lintr <- list(
@@ -301,6 +309,9 @@ settings <- new.env(parent = emptyenv())
301309
# R>=4.1.0: ...names
302310
backports::import(pkgname, "...names")
303311

312+
# R>=4.4.0: %||%
313+
backports::import(pkgname, "%||%")
314+
304315
utils::assignInMyNamespace("default_settings", list(
305316
linters = default_linters,
306317
encoding = "UTF-8",
@@ -319,7 +330,7 @@ settings <- new.env(parent = emptyenv())
319330
exclude_linter_sep = rex(any_spaces, ",", any_spaces),
320331
exclusions = list(),
321332
cache_directory = R_user_dir("lintr", "cache"),
322-
error_on_lint = logical_env("LINTR_ERROR_ON_LINT") %||% FALSE
333+
error_on_lint = logical_env("LINTR_ERROR_ON_LINT", unset = FALSE)
323334
))
324335

325336
reset_settings()

inst/lintr/linters.csv

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ backport_linter,robustness configurable package_development
77
boolean_arithmetic_linter,efficiency best_practices readability
88
brace_linter,style readability default configurable
99
class_equals_linter,best_practices robustness consistency
10+
coalesce_linter,readability consistency best_practices
1011
commas_linter,style readability default configurable
1112
commented_code_linter,style readability best_practices default
1213
comparison_negation_linter,readability consistency

man/best_practices_linters.Rd

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/coalesce_linter.Rd

+56
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/consistency_linters.Rd

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/linters.Rd

+4-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/readability_linters.Rd

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)