Skip to content

Commit 8a4c25a

Browse files
authored
Check continuous scale limits (#6114)
* re-import standalone rlang files * add length checker * add `allow_na` option to `check_object()` * simplify coord limit check * use `check_length()` more often * add `check_continuous_limits()` * add test * fix snapshots * relax coord requirements * TIL: is.vector(<Date>) is FALSE, whereas is_vector(<Date>) is TRUE * restore previous error * update snapshots
1 parent bf75f30 commit 8a4c25a

22 files changed

+163
-64
lines changed

R/bin.R

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ bin_breaks <- function(breaks, closed = c("right", "left")) {
5454

5555
bin_breaks_width <- function(x_range, width = NULL, center = NULL,
5656
boundary = NULL, closed = c("right", "left")) {
57-
if (length(x_range) != 2) {
58-
cli::cli_abort("{.arg x_range} must have two elements.")
59-
}
57+
check_length(x_range, 2L)
6058

6159
# binwidth seems to be the argument name supplied to width. (stat-bin and stat-bindot)
6260
check_number_decimal(width, min = 0, allow_infinite = FALSE, arg = "binwidth")
@@ -106,9 +104,7 @@ bin_breaks_width <- function(x_range, width = NULL, center = NULL,
106104

107105
bin_breaks_bins <- function(x_range, bins = 30, center = NULL,
108106
boundary = NULL, closed = c("right", "left")) {
109-
if (length(x_range) != 2) {
110-
cli::cli_abort("{.arg x_range} must have two elements.")
111-
}
107+
check_length(x_range, 2L)
112108

113109
check_number_whole(bins, min = 1)
114110
if (zero_range(x_range)) {

R/coord-.R

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,6 @@ check_coord_limits <- function(
281281
if (is.null(limits)) {
282282
return(invisible(NULL))
283283
}
284-
if (!obj_is_vector(limits) || length(limits) != 2) {
285-
what <- "{.obj_type_friendly {limits}}"
286-
if (is.vector(limits)) {
287-
what <- paste0(what, " of length {length(limits)}")
288-
}
289-
cli::cli_abort(
290-
paste0("{.arg {arg}} must be a vector of length 2, not ", what, "."),
291-
call = call
292-
)
293-
}
284+
check_object(limits, is_vector, "a vector", arg = arg, call = call)
285+
check_length(limits, 2L, arg = arg, call = call)
294286
}

R/import-standalone-obj-type.R

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
# Standalone file: do not edit by hand
2-
# Source: <https://github.com/r-lib/rlang/blob/main/R/standalone-obj-type.R>
2+
# Source: https://github.com/r-lib/rlang/blob/HEAD/R/standalone-obj-type.R
3+
# Generated by: usethis::use_standalone("r-lib/rlang", "obj-type")
34
# ----------------------------------------------------------------------
45
#
56
# ---
67
# repo: r-lib/rlang
78
# file: standalone-obj-type.R
8-
# last-updated: 2022-10-04
9+
# last-updated: 2024-02-14
910
# license: https://unlicense.org
1011
# imports: rlang (>= 1.1.0)
1112
# ---
1213
#
1314
# ## Changelog
1415
#
16+
# 2024-02-14:
17+
# - `obj_type_friendly()` now works for S7 objects.
18+
#
19+
# 2023-05-01:
20+
# - `obj_type_friendly()` now only displays the first class of S3 objects.
21+
#
22+
# 2023-03-30:
23+
# - `stop_input_type()` now handles `I()` input literally in `arg`.
24+
#
1525
# 2022-10-04:
1626
# - `obj_type_friendly(value = TRUE)` now shows numeric scalars
1727
# literally.
@@ -65,7 +75,7 @@ obj_type_friendly <- function(x, value = TRUE) {
6575
if (inherits(x, "quosure")) {
6676
type <- "quosure"
6777
} else {
68-
type <- paste(class(x), collapse = "/")
78+
type <- class(x)[[1L]]
6979
}
7080
return(sprintf("a <%s> object", type))
7181
}
@@ -261,19 +271,19 @@ vec_type_friendly <- function(x, length = FALSE) {
261271
#' Return OO type
262272
#' @param x Any R object.
263273
#' @return One of `"bare"` (for non-OO objects), `"S3"`, `"S4"`,
264-
#' `"R6"`, or `"R7"`.
274+
#' `"R6"`, or `"S7"`.
265275
#' @noRd
266276
obj_type_oo <- function(x) {
267277
if (!is.object(x)) {
268278
return("bare")
269279
}
270280

271-
class <- inherits(x, c("R6", "R7_object"), which = TRUE)
281+
class <- inherits(x, c("R6", "S7_object"), which = TRUE)
272282

273283
if (class[[1]]) {
274284
"R6"
275285
} else if (class[[2]]) {
276-
"R7"
286+
"S7"
277287
} else if (isS4(x)) {
278288
"S4"
279289
} else {
@@ -315,10 +325,15 @@ stop_input_type <- function(x,
315325
if (length(what)) {
316326
what <- oxford_comma(what)
317327
}
328+
if (inherits(arg, "AsIs")) {
329+
format_arg <- identity
330+
} else {
331+
format_arg <- cli$format_arg
332+
}
318333

319334
message <- sprintf(
320335
"%s must be %s, not %s.",
321-
cli$format_arg(arg),
336+
format_arg(arg),
322337
what,
323338
obj_type_friendly(x, value = show_value)
324339
)

R/import-standalone-types-check.R

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Standalone file: do not edit by hand
2-
# Source: <https://github.com/r-lib/rlang/blob/main/R/standalone-types-check.R>
2+
# Source: https://github.com/r-lib/rlang/blob/HEAD/R/standalone-types-check.R
3+
# Generated by: usethis::use_standalone("r-lib/rlang", "types-check")
34
# ----------------------------------------------------------------------
45
#
56
# ---
@@ -13,6 +14,9 @@
1314
#
1415
# ## Changelog
1516
#
17+
# 2024-08-15:
18+
# - `check_character()` gains an `allow_na` argument (@martaalcalde, #1724)
19+
#
1620
# 2023-03-13:
1721
# - Improved error messages of number checkers (@teunbrand)
1822
# - Added `allow_infinite` argument to `check_number_whole()` (@mgirlich).
@@ -461,15 +465,28 @@ check_formula <- function(x,
461465

462466
# Vectors -----------------------------------------------------------------
463467

468+
# TODO: Figure out what to do with logical `NA` and `allow_na = TRUE`
469+
464470
check_character <- function(x,
465471
...,
472+
allow_na = TRUE,
466473
allow_null = FALSE,
467474
arg = caller_arg(x),
468475
call = caller_env()) {
476+
469477
if (!missing(x)) {
470478
if (is_character(x)) {
479+
if (!allow_na && any(is.na(x))) {
480+
abort(
481+
sprintf("`%s` can't contain NA values.", arg),
482+
arg = arg,
483+
call = call
484+
)
485+
}
486+
471487
return(invisible(NULL))
472488
}
489+
473490
if (allow_null && is_null(x)) {
474491
return(invisible(NULL))
475492
}
@@ -479,7 +496,6 @@ check_character <- function(x,
479496
x,
480497
"a character vector",
481498
...,
482-
allow_na = FALSE,
483499
allow_null = allow_null,
484500
arg = arg,
485501
call = call

R/limits.R

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,7 @@ ylim <- function(...) {
113113
limits <- function(lims, var, call = caller_env()) UseMethod("limits")
114114
#' @export
115115
limits.numeric <- function(lims, var, call = caller_env()) {
116-
if (length(lims) != 2) {
117-
cli::cli_abort("{.arg {var}} must be a two-element vector.", call = call)
118-
}
116+
check_length(lims, 2L, arg = var, call = call)
119117
if (!anyNA(lims) && lims[1] > lims[2]) {
120118
trans <- "reverse"
121119
} else {
@@ -143,23 +141,17 @@ limits.factor <- function(lims, var, call = caller_env()) {
143141
}
144142
#' @export
145143
limits.Date <- function(lims, var, call = caller_env()) {
146-
if (length(lims) != 2) {
147-
cli::cli_abort("{.arg {var}} must be a two-element vector.", call = call)
148-
}
144+
check_length(lims, 2L, arg = var, call = call)
149145
make_scale("date", var, limits = lims, call = call)
150146
}
151147
#' @export
152148
limits.POSIXct <- function(lims, var, call = caller_env()) {
153-
if (length(lims) != 2) {
154-
cli::cli_abort("{.arg {var}} must be a two-element vector.", call = call)
155-
}
149+
check_length(lims, 2L, arg = var, call = call)
156150
make_scale("datetime", var, limits = lims, call = call)
157151
}
158152
#' @export
159153
limits.POSIXlt <- function(lims, var, call = caller_env()) {
160-
if (length(lims) != 2) {
161-
cli::cli_abort("{.arg {var}} must be a two-element vector.", call = call)
162-
}
154+
check_length(lims, 2L, arg = var, call = call)
163155
make_scale("datetime", var, limits = as.POSIXct(lims), call = call)
164156
}
165157

R/plot-build.R

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -358,13 +358,10 @@ table_add_tag <- function(table, label, theme) {
358358
),
359359
call = expr(theme()))
360360
}
361-
if (length(position) != 2) {
362-
cli::cli_abort(paste0(
363-
"A {.cls numeric} {.arg plot.tag.position} ",
364-
"theme setting must have length 2."
365-
),
366-
call = expr(theme()))
367-
}
361+
check_length(
362+
position, 2L, call = expr(theme()),
363+
arg = I("A {.cls numeric} {.arg plot.tag.position}")
364+
)
368365
top <- left <- right <- bottom <- FALSE
369366
} else {
370367
# Break position into top/left/right/bottom

R/scale-.R

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,14 @@ continuous_scale <- function(aesthetics, scale_name = deprecated(), palette, nam
128128
}
129129

130130
transform <- as.transform(transform)
131+
limits <- allow_lambda(limits)
132+
131133
if (!is.null(limits) && !is.function(limits)) {
132134
limits <- transform$transform(limits)
133135
}
136+
check_continuous_limits(limits, call = call)
134137

135138
# Convert formula to function if appropriate
136-
limits <- allow_lambda(limits)
137139
breaks <- allow_lambda(breaks)
138140
labels <- allow_lambda(labels)
139141
rescaler <- allow_lambda(rescaler)
@@ -1400,6 +1402,16 @@ check_transformation <- function(x, transformed, name, arg = NULL, call = NULL)
14001402
cli::cli_warn(msg, call = call)
14011403
}
14021404

1405+
check_continuous_limits <- function(limits, ...,
1406+
arg = caller_arg(limits),
1407+
call = caller_env()) {
1408+
if (is.null(limits) || is.function(limits)) {
1409+
return(invisible())
1410+
}
1411+
check_numeric(limits, arg = arg, call = call, allow_na = TRUE)
1412+
check_length(limits, 2L, arg = arg, call = call)
1413+
}
1414+
14031415
trans_support_nbreaks <- function(trans) {
14041416
"n" %in% names(formals(trans$breaks))
14051417
}

R/utilities-checks.R

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ check_object <- function(x,
77
check_fun,
88
what,
99
...,
10+
allow_na = FALSE,
1011
allow_null = FALSE,
1112
arg = caller_arg(x),
1213
call = caller_env()) {
@@ -18,6 +19,9 @@ check_object <- function(x,
1819
if (allow_null && is_null(x)) {
1920
return(invisible(NULL))
2021
}
22+
if (allow_na && all(is.na(x))) {
23+
return(invisible(NULL))
24+
}
2125
}
2226

2327
stop_input_type(
@@ -69,6 +73,60 @@ check_inherits <- function(x,
6973
)
7074
}
7175

76+
check_length <- function(x, length = integer(), ..., min = 0, max = Inf,
77+
arg = caller_arg(x), call = caller_env()) {
78+
if (missing(x)) {
79+
stop_input_type(x, "a vector", arg = arg, call = call)
80+
}
81+
82+
n <- length(x)
83+
if (n %in% length) {
84+
return(invisible(NULL))
85+
}
86+
fmt <- if (inherits(arg, "AsIs")) identity else function(x) sprintf("`%s`", x)
87+
if (length(length) > 0) {
88+
type <- paste0("a vector of length ", oxford_comma(length))
89+
if (length(length) == 1) {
90+
type <- switch(
91+
sprintf("%d", length),
92+
"0" = "an empty vector",
93+
"1" = "a scalar of length 1",
94+
type
95+
)
96+
}
97+
msg <- sprintf(
98+
"%s must be %s, not length %d.",
99+
fmt(arg), type, n
100+
)
101+
cli::cli_abort(msg, call = call, arg = arg)
102+
}
103+
104+
range <- pmax(range(min, max, na.rm = TRUE), 0)
105+
if (n >= min & n <= max) {
106+
return(invisible(NULL))
107+
}
108+
if (identical(range[1], range[2])) {
109+
check_length(x, range[1], arg = arg, call = call)
110+
return(invisible(NULL))
111+
}
112+
113+
type <- if (range[2] == 1) "scalar" else "vector"
114+
115+
what <- paste0("a length between ", range[1], " and ", range[2])
116+
if (identical(range[2], Inf)) {
117+
what <- paste0("at least length ", range[1])
118+
}
119+
if (identical(range[1], 0)) {
120+
what <- paste0("at most length ", range[2])
121+
}
122+
123+
msg <- sprintf(
124+
"`%s` must be a %s with %s, not length %d.",
125+
fmt(arg), type, what, n
126+
)
127+
cli::cli_abort(msg, call = call, arg = arg)
128+
}
129+
72130
#' Check graphics device capabilities
73131
#'
74132
#' This function makes an attempt to estimate whether the graphics device is

tests/testthat/_snaps/coord-.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@
2424
check_coord_limits(xlim(1, 2))
2525
Condition
2626
Error:
27-
! `xlim(1, 2)` must be a vector of length 2, not a <ScaleContinuousPosition> object.
27+
! `xlim(1, 2)` must be a vector, not a <ScaleContinuousPosition> object.
2828

2929
---
3030

3131
Code
3232
check_coord_limits(1:3)
3333
Condition
3434
Error:
35-
! `1:3` must be a vector of length 2, not an integer vector of length 3.
35+
! `1:3` must be a vector of length 2, not length 3.
3636

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# cartesian coords throws error when limits are badly specified
22

3-
`xlim` must be a vector of length 2, not a <ScaleContinuousPosition> object.
3+
`xlim` must be a vector, not a <ScaleContinuousPosition> object.
44

55
---
66

7-
`ylim` must be a vector of length 2, not an integer vector of length 3.
7+
`ylim` must be a vector of length 2, not length 3.
88

tests/testthat/_snaps/coord-flip.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# flip coords throws error when limits are badly specified
22

3-
`xlim` must be a vector of length 2, not a <ScaleContinuousPosition> object.
3+
`xlim` must be a vector, not a <ScaleContinuousPosition> object.
44

55
---
66

7-
`ylim` must be a vector of length 2, not an integer vector of length 3.
7+
`ylim` must be a vector of length 2, not length 3.
88

tests/testthat/_snaps/coord-map.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# coord map throws error when limits are badly specified
22

3-
`xlim` must be a vector of length 2, not a <ScaleContinuousPosition> object.
3+
`xlim` must be a vector, not a <ScaleContinuousPosition> object.
44

55
---
66

7-
`ylim` must be a vector of length 2, not an integer vector of length 3.
7+
`ylim` must be a vector of length 2, not length 3.
88

99
# coord_map throws informative warning about guides
1010

0 commit comments

Comments
 (0)