Skip to content

Commit 134ceb0

Browse files
authored
Merge 7882955 into edf8df2
2 parents edf8df2 + 7882955 commit 134ceb0

13 files changed

+268
-3
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Collate:
7878
'comment_linters.R'
7979
'comments.R'
8080
'comparison_negation_linter.R'
81+
'condition_call_linter.R'
8182
'condition_message_linter.R'
8283
'conjunct_test_linter.R'
8384
'consecutive_assertion_linter.R'

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export(closed_curly_linter)
3737
export(commas_linter)
3838
export(commented_code_linter)
3939
export(comparison_negation_linter)
40+
export(condition_call_linter)
4041
export(condition_message_linter)
4142
export(conjunct_test_linter)
4243
export(consecutive_assertion_linter)

NEWS.md

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

3535
### New linters
3636

37+
* `condition_call_linter()` for ensuring consistent use of `call.` in `warning()` and `stop()`. The default `call. = FALSE` follows the tidyverse guidance of not displaying the call (#2226, @Bisaloo)
3738
* `sample_int_linter()` for encouraging `sample.int(n, ...)` over equivalents like `sample(1:n, ...)` (part of #884, @MichaelChirico).
3839
* `stopifnot_all_linter()` discourages tests with `all()` like `stopifnot(all(x > 0))`; `stopifnot()` runs `all()` itself, and uses a better error message (part of #884, @MichaelChirico).
3940
* `comparison_negation_linter()` for discouraging negated comparisons when a direct negation is preferable, e.g. `!(x == y)` could be `x != y` (part of #884, @MichaelChirico).

R/condition_call_linter.R

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#' Recommend usage of `call. = FALSE` in conditions
2+
#'
3+
#' This linter, with the default `display_call = FALSE`, enforces the
4+
#' recommendation of the tidyverse design guide regarding displaying error
5+
#' calls.
6+
#'
7+
#' @param display_call Logical specifying expected behaviour regarding `call.`
8+
#' argument in conditions.
9+
#' - `NA` forces providing `call.=` but ignores its value (this can be used in
10+
#' cases where you expect a mix of `call. = FALSE` and `call. = TRUE`)
11+
#' - lints `call. = FALSE`
12+
#' - forces `call. = FALSE` (lints `call. = TRUE` or missing `call.=` value)
13+
#'
14+
#'
15+
#' @examples
16+
#' # will produce lints
17+
#' lint(
18+
#' text = "stop('test')",
19+
#' linters = condition_call_linter()
20+
#' )
21+
#'
22+
#' lint(
23+
#' text = "stop('test', call. = TRUE)",
24+
#' linters = condition_call_linter()
25+
#' )
26+
#'
27+
#' lint(
28+
#' text = "stop('test', call. = FALSE)",
29+
#' linters = condition_call_linter(display_call = TRUE)
30+
#' )
31+
#'
32+
#' lint(
33+
#' text = "stop('this is a', 'test', call. = FALSE)",
34+
#' linters = condition_call_linter(display_call = TRUE)
35+
#' )
36+
#'
37+
#' # okay
38+
#' lint(
39+
#' text = "stop('test', call. = FALSE)",
40+
#' linters = condition_call_linter()
41+
#' )
42+
#'
43+
#' lint(
44+
#' text = "stop('this is a', 'test', call. = FALSE)",
45+
#' linters = condition_call_linter()
46+
#' )
47+
#'
48+
#' lint(
49+
#' text = "stop('test', call. = TRUE)",
50+
#' linters = condition_call_linter(display_call = TRUE)
51+
#' )
52+
#'
53+
#' @evalRd rd_tags("condition_call_linter")
54+
#' @seealso
55+
#' - [linters] for a complete list of linters available in lintr.
56+
#' - <https://design.tidyverse.org/err-call.html>>
57+
#' @export
58+
condition_call_linter <- function(display_call = FALSE) {
59+
call_xpath <- glue::glue("
60+
following-sibling::SYMBOL_SUB[text() = 'call.']
61+
/following-sibling::expr[1]
62+
/NUM_CONST[text() = '{!display_call}']
63+
")
64+
no_call_xpath <- "
65+
parent::expr[
66+
count(SYMBOL_SUB[text() = 'call.']) = 0
67+
]
68+
"
69+
70+
if (is.na(display_call)) {
71+
frag <- no_call_xpath
72+
} else if (display_call) {
73+
frag <- call_xpath
74+
} else {
75+
# call. = TRUE can be expressed in two way:
76+
# - either explicitly with call. = TRUE
77+
# - or by implicitly relying on the default
78+
frag <- xp_or(call_xpath, no_call_xpath)
79+
}
80+
81+
xpath <- glue::glue("
82+
//SYMBOL_FUNCTION_CALL[text() = 'stop' or text() = 'warning']
83+
/parent::expr[{frag}]
84+
/parent::expr
85+
")
86+
87+
Linter(linter_level = "expression", function(source_expression) {
88+
89+
xml <- source_expression$xml_parsed_content
90+
if (is.null(xml)) return(list())
91+
92+
bad_expr <- xml_find_all(xml, xpath)
93+
94+
if (is.na(display_call)) {
95+
msg <- glue::glue(
96+
"Provide an explicit value for call. in {xp_call_name(bad_expr)}()."
97+
)
98+
} else if (display_call) {
99+
msg <- glue::glue(
100+
"Use {xp_call_name(bad_expr)}(.) to display call in error message."
101+
)
102+
} else {
103+
msg <- glue::glue(
104+
"Use {xp_call_name(bad_expr)}(., call. = FALSE)",
105+
" to not display call in error message."
106+
)
107+
}
108+
109+
xml_nodes_to_lints(
110+
bad_expr,
111+
source_expression = source_expression,
112+
lint_message = msg,
113+
type = "warning"
114+
)
115+
})
116+
}

R/linter_tag_docs.R

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,13 @@ NULL
119119
#' @evalRd rd_linters("regex")
120120
#' @seealso [linters] for a complete list of linters available in lintr.
121121
NULL
122+
123+
#' Tidyverse design linters
124+
#' @name tidy_design_linters
125+
#' @description
126+
#' Linters based on guidelines described in the 'Tidy design principles' book.
127+
#' @evalRd rd_linters("tidy_design")
128+
#' @seealso
129+
#' - [linters] for a complete list of linters available in lintr.
130+
#' - <https://design.tidyverse.org/>
131+
NULL

inst/lintr/linters.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ closed_curly_linter,defunct
1111
commas_linter,style readability default configurable
1212
commented_code_linter,style readability best_practices default
1313
comparison_negation_linter,readability consistency
14+
condition_call_linter,style tidy_design best_practices configurable
1415
condition_message_linter,best_practices consistency
1516
conjunct_test_linter,package_development best_practices readability configurable pkg_testthat
1617
consecutive_assertion_linter,style readability consistency

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/condition_call_linter.Rd

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

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/linters.Rd

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

man/style_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/tidy_design_linters.Rd

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
test_that("condition_call_linter skips allowed usages", {
2+
linter <- condition_call_linter()
3+
4+
expect_lint("stop('test', call. = FALSE)", NULL, linter)
5+
6+
# works even with multiple arguments
7+
expect_lint("stop('this is a', 'test', call. = FALSE)", NULL, linter)
8+
9+
linter <- condition_call_linter(display_call = TRUE)
10+
11+
expect_lint("stop('test', call. = TRUE)", NULL, linter)
12+
13+
linter <- condition_call_linter(display_call = NA)
14+
15+
expect_lint("stop('test', call. = TRUE)", NULL, linter)
16+
expect_lint("stop('test', call. = FALSE)", NULL, linter)
17+
})
18+
19+
patrick::with_parameters_test_that(
20+
"condition_call_linter blocks disallowed usages",
21+
{
22+
linter <- condition_call_linter()
23+
lint_message <- rex::rex(call_name, anything, "to not display call")
24+
25+
expect_lint(paste0(call_name, "('test')"), lint_message, linter)
26+
expect_lint(paste0(call_name, "('test', call. = TRUE)"), lint_message, linter)
27+
28+
linter <- condition_call_linter(display_call = TRUE)
29+
lint_message <- rex::rex(call_name, anything, "to display call")
30+
31+
expect_lint(paste0(call_name, "('test', call. = FALSE)"), lint_message, linter)
32+
33+
linter <- condition_call_linter(display_call = NA)
34+
lint_message <- rex::rex("explicit value", anything, call_name)
35+
36+
expect_lint(paste0(call_name, "('test')"), lint_message, linter)
37+
},
38+
call_name = c("stop", "warning")
39+
)

0 commit comments

Comments
 (0)