diff --git a/NAMESPACE b/NAMESPACE index 602936a26b..5a6f6156cf 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -136,6 +136,10 @@ export(caller_fn) export(caller_frame) export(calling) export(catch_cnd) +export(check_dots_empty) +export(check_dots_empty0) +export(check_dots_unnamed) +export(check_dots_used) export(check_installed) export(child_env) export(chr) diff --git a/NEWS.md b/NEWS.md index c891f9ce0f..39db6751c1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,13 @@ # rlang (development version) +* `check_dots_empty()`, `check_dots_unused()`, and + `check_dots_unnamed()` have been moved from ellipsis to rlang. The + ellipsis package is deprecated and will eventually be archived. + + We have added `check_dots_empty0()`. It has a different UI but is + almost as efficient as checking for `missing(...)`. Use this in very + low level functions where a couple microseconds make a difference. + * `s3_register()` no longer fails when generic does not exist. This prevents failures when users don't have all the last versions of packages (#1112). diff --git a/R/call.R b/R/call.R index 771738c62c..a183114591 100644 --- a/R/call.R +++ b/R/call.R @@ -665,7 +665,7 @@ call_match <- function(call = NULL, defaults = FALSE, dots_env = NULL, dots_expand = TRUE) { - check_dots_empty(...) + check_dots_empty0(...) if (is_null(call)) { call <- sys.call(sys.parent()) diff --git a/R/cnd-entrace.R b/R/cnd-entrace.R index 81909f6512..5966ea0252 100644 --- a/R/cnd-entrace.R +++ b/R/cnd-entrace.R @@ -35,7 +35,7 @@ #' } #' @export entrace <- function(cnd, ..., top = NULL, bottom = NULL) { - check_dots_empty(...) + check_dots_empty0(...) if (!missing(cnd) && is_trace(cnd$trace)) { return() @@ -58,7 +58,7 @@ entrace <- function(cnd, ..., top = NULL, bottom = NULL) { #' @rdname entrace #' @export cnd_entrace <- function(cnd, ..., top = NULL, bottom = NULL) { - check_dots_empty(...) + check_dots_empty0(...) if (!is_null(cnd$trace)) { return(cnd) diff --git a/R/cnd-signal.R b/R/cnd-signal.R index ccafc623f6..20a19e6a0f 100644 --- a/R/cnd-signal.R +++ b/R/cnd-signal.R @@ -24,7 +24,7 @@ #' #' @param cnd A condition object (see [cnd()]). If `NULL`, #' `cnd_signal()` returns without signalling a condition. -#' @param ... These dots are for extensions and must be empty. +#' @inheritParams dots-empty #' @seealso [abort()], [warn()] and [inform()] for creating and #' signalling structured R conditions. See [with_handlers()] for #' establishing condition handlers. diff --git a/R/dots-ellipsis.R b/R/dots-ellipsis.R new file mode 100644 index 0000000000..3c3180f3d0 --- /dev/null +++ b/R/dots-ellipsis.R @@ -0,0 +1,179 @@ +#' Helper for consistent documentation of empty dots +#' +#' Use `` @inheritParams rlang::`dots-empty` `` in your package +#' to consistently document an unused `...` argument. +#' +#' @param ... These dots are for future extensions and must be empty. +#' @name dots-empty +#' @keywords internal +NULL + +#' Helper for consistent documentation of used dots +#' +#' Use `` @inheritParams rlang::`dots-used` `` in your package +#' to consistently document an unused `...` argument. +#' +#' @param ... Arguments passed to methods. +#' @name dots-used +#' @keywords internal +NULL + +#' Check that all dots have been used +#' +#' Automatically sets exit handler to run when function terminates, checking +#' that all elements of `...` have been evaluated. If you use [on.exit()] +#' elsewhere in your function, make sure to use `add = TRUE` so that you +#' don't override the handler set up by `check_dots_used()`. +#' +#' @param action The action to take when the dots have not been used. One of +#' [rlang::abort()], [rlang::warn()], [rlang::inform()] or [rlang::signal()]. +#' @param env Environment in which to look for `...` and to set up handler. +#' @export +#' @examples +#' f <- function(...) { +#' check_dots_used() +#' g(...) +#' } +#' +#' g <- function(x, y, ...) { +#' x + y +#' } +#' f(x = 1, y = 2) +#' +#' try(f(x = 1, y = 2, z = 3)) +#' try(f(x = 1, y = 2, 3, 4, 5)) +check_dots_used <- function(env = caller_env(), action = abort) { + eval_bare(exit_handler(action), env) + invisible() +} + +check_dots <- function(env = caller_env(), action) { + if (.Call(ffi_ellipsis_dots_used, env)) { + return(invisible()) + } + + proms <- ellipsis_dots(env) + used <- map_lgl(proms, promise_forced) + + unused <- names(proms)[!used] + action_dots( + action = action, + message = paste0(length(unused), " arguments in `...` were not used."), + dot_names = unused, + class = "rlib_error_dots_unused", + ) +} + +exit_handler <- function(action) { + expr( + on.exit((!!check_dots)(environment(), !!action), add = TRUE) + ) +} + +#' Check that all dots are unnamed +#' +#' In functions like `paste()`, named arguments in `...` are often a +#' sign of misspelled argument names. +#' +#' @inheritParams check_dots_used +#' @param env Environment in which to look for `...`. +#' @export +#' @examples +#' f <- function(..., foofy = 8) { +#' check_dots_unnamed() +#' c(...) +#' } +#' +#' f(1, 2, 3, foofy = 4) +#' try(f(1, 2, 3, foof = 4)) +check_dots_unnamed <- function(env = caller_env(), action = abort) { + proms <- ellipsis_dots(env, auto_name = FALSE) + if (length(proms) == 0) { + return() + } + + unnamed <- is.na(names(proms)) + if (all(unnamed)) { + return(invisible()) + } + + named <- names(proms)[!unnamed] + action_dots( + action = action, + message = paste0(length(named), " arguments in `...` had unexpected names."), + dot_names = named, + class = "rlib_error_dots_named", + ) +} + + +#' Check that dots are empty +#' +#' Sometimes you just want to use `...` to force your users to fully name +#' the details arguments. This function fails if `...` is not empty. +#' +#' @inheritParams check_dots_used +#' @param env Environment in which to look for `...`. +#' @export +#' @examples +#' f <- function(x, ..., foofy = 8) { +#' check_dots_empty() +#' x + foofy +#' } +#' +#' # This fails because `foofy` can't be matched positionally +#' try(f(1, 4)) +#' +#' # This fails because `foofy` can't be matched partially by name +#' try(f(1, foof = 4)) +#' +#' # Thanks to `...`, it must be matched exactly +#' f(1, foofy = 4) +check_dots_empty <- function(env = caller_env(), action = abort) { + dots <- ellipsis_dots(env) + if (length(dots) == 0) { + return() + } + + action_dots( + action = action, + message = "`...` is not empty.", + dot_names = names(dots), + note = "These dots only exist to allow future extensions and should be empty.", + class = "rlib_error_dots_nonempty" + ) +} +#' Check that dots are empty (low level variant) +#' +#' `check_dots_empty0()` is a more efficient version of +#' [check_dots_empty()] with a slightly different interface. Instead +#' of inspecting the current environment for dots, it directly takes +#' `...`. It is only meant for very low level functions where a +#' couple microseconds make a difference. +#' +#' @param ... Dots which should be empty. +#' @keywords internal +#' @export +check_dots_empty0 <- function(...) { + if (nargs()) { + check_dots_empty() + } +} + +action_dots <- function(action, message, dot_names, note = NULL, class = NULL, ...) { + message <- format_bullets(c( + message, + i = note, + x = "We detected these problematic arguments:", + set_names(chr_quoted(dot_names), "*"), + i = "Did you misspecify an argument?" + )) + action(message, class = c(class, "rlib_error_dots"), ...) +} + +promise_forced <- function(x) { + .Call(ffi_ellipsis_promise_forced, x) +} +ellipsis_dots <- function(env = caller_env(), auto_name = TRUE) { + .Call(ffi_ellipsis_dots, env, auto_name) +} diff --git a/R/dots.R b/R/dots.R index 78f4a89836..a4b2a60281 100644 --- a/R/dots.R +++ b/R/dots.R @@ -472,12 +472,6 @@ homonym_enum <- function(nm, dups, nms) { chr_enumerate(as.character(which(dups)), final = "and") } -check_dots_empty <- function(...) { - if (nargs()) { - abort("These `...` must be empty") - } -} - # This helper is used when splicing S3 or S4 objects found # in `!!!`. It is similar to `as.list()`, but the names of diff --git a/R/env-special.R b/R/env-special.R index c7daa8b189..13c16da869 100644 --- a/R/env-special.R +++ b/R/env-special.R @@ -307,7 +307,7 @@ is_namespace <- function(x) { #' is_installed(c("base", "ggplot5")) #' is_installed(c("base", "ggplot5"), version = c(NA, "5.1.0")) is_installed <- function(pkg, ..., version = NULL) { - check_dots_empty(...) + check_dots_empty0(...) if (!all(map_lgl(pkg, function(x) is_true(requireNamespace(x, quietly = TRUE))))) { return(FALSE) @@ -330,7 +330,7 @@ check_installed <- function(pkg, reason = NULL, ..., version = NULL) { - check_dots_empty(...) + check_dots_empty0(...) if (!is_character(pkg)) { abort("`pkg` must be a package name or a vector of package names.") diff --git a/R/expr.R b/R/expr.R index 787f95f193..3302f8371a 100644 --- a/R/expr.R +++ b/R/expr.R @@ -385,7 +385,7 @@ expr_print <- function(x, ...) { expr_deparse <- function(x, ..., width = peek_option("width")) { - check_dots_empty(...) + check_dots_empty0(...) deparser <- new_quo_deparser(width = width) quo_deparse(x, deparser) } diff --git a/R/lifecycle-retired.R b/R/lifecycle-retired.R index 607c750a6f..a797547c00 100644 --- a/R/lifecycle-retired.R +++ b/R/lifecycle-retired.R @@ -1418,7 +1418,7 @@ call_standardise <- function(call, ..., defaults = FALSE, dots_env = empty_env()) { - check_dots_empty(...) + check_dots_empty() expr <- get_expr(call) if (!is_call(expr)) { diff --git a/R/trace.R b/R/trace.R index 61a328173b..1cde9f11f6 100644 --- a/R/trace.R +++ b/R/trace.R @@ -909,7 +909,7 @@ trace_root <- function() { #' @author IƱaki Ucar (ORCID: 0000-0001-6403-5550) #' @noRd trace_pkgs <- function(pkgs, max_level = Inf, ..., regexp = NULL) { - check_dots_empty(...) + check_dots_empty() # Avoids namespace loading issues lapply(pkgs, requireNamespace, quietly = TRUE) diff --git a/_pkgdown.yml b/_pkgdown.yml index 8418106eef..072ffdb29d 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -57,6 +57,15 @@ reference: - nse-defuse - as_label - as_name + - title: Dots + desc: > + Utils to work with `...`. + contents: + - dots-empty + - dots-used + - check_dots_used + - check_dots_unnamed + - check_dots_empty - title: Tidy dots desc: > Collect arguments contained in `...` with `!!!` and diff --git a/man/check_dots_empty.Rd b/man/check_dots_empty.Rd new file mode 100644 index 0000000000..024f4719a2 --- /dev/null +++ b/man/check_dots_empty.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dots-ellipsis.R +\name{check_dots_empty} +\alias{check_dots_empty} +\title{Check that dots are empty} +\usage{ +check_dots_empty(env = caller_env(), action = abort) +} +\arguments{ +\item{env}{Environment in which to look for \code{...}.} + +\item{action}{The action to take when the dots have not been used. One of +\code{\link[=abort]{abort()}}, \code{\link[=warn]{warn()}}, \code{\link[=inform]{inform()}} or \code{\link[=signal]{signal()}}.} +} +\description{ +Sometimes you just want to use \code{...} to force your users to fully name +the details arguments. This function fails if \code{...} is not empty. +} +\examples{ +f <- function(x, ..., foofy = 8) { + check_dots_empty() + x + foofy +} + +# This fails because `foofy` can't be matched positionally +try(f(1, 4)) + +# This fails because `foofy` can't be matched partially by name +try(f(1, foof = 4)) + +# Thanks to `...`, it must be matched exactly +f(1, foofy = 4) +} diff --git a/man/check_dots_empty0.Rd b/man/check_dots_empty0.Rd new file mode 100644 index 0000000000..8fd5177c56 --- /dev/null +++ b/man/check_dots_empty0.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dots-ellipsis.R +\name{check_dots_empty0} +\alias{check_dots_empty0} +\title{Check that dots are empty (low level variant)} +\usage{ +check_dots_empty0(...) +} +\arguments{ +\item{...}{Dots which should be empty.} +} +\description{ +\code{check_dots_empty0()} is a more efficient version of +\code{\link[=check_dots_empty]{check_dots_empty()}} with a slightly different interface. Instead +of inspecting the current environment for dots, it directly takes +\code{...}. It is only meant for very low level functions where a +couple microseconds make a difference. +} +\keyword{internal} diff --git a/man/check_dots_unnamed.Rd b/man/check_dots_unnamed.Rd new file mode 100644 index 0000000000..191427142b --- /dev/null +++ b/man/check_dots_unnamed.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dots-ellipsis.R +\name{check_dots_unnamed} +\alias{check_dots_unnamed} +\title{Check that all dots are unnamed} +\usage{ +check_dots_unnamed(env = caller_env(), action = abort) +} +\arguments{ +\item{env}{Environment in which to look for \code{...}.} + +\item{action}{The action to take when the dots have not been used. One of +\code{\link[=abort]{abort()}}, \code{\link[=warn]{warn()}}, \code{\link[=inform]{inform()}} or \code{\link[=signal]{signal()}}.} +} +\description{ +In functions like \code{paste()}, named arguments in \code{...} are often a +sign of misspelled argument names. +} +\examples{ +f <- function(..., foofy = 8) { + check_dots_unnamed() + c(...) +} + +f(1, 2, 3, foofy = 4) +try(f(1, 2, 3, foof = 4)) +} diff --git a/man/check_dots_used.Rd b/man/check_dots_used.Rd new file mode 100644 index 0000000000..13308acf70 --- /dev/null +++ b/man/check_dots_used.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dots-ellipsis.R +\name{check_dots_used} +\alias{check_dots_used} +\title{Check that all dots have been used} +\usage{ +check_dots_used(env = caller_env(), action = abort) +} +\arguments{ +\item{env}{Environment in which to look for \code{...} and to set up handler.} + +\item{action}{The action to take when the dots have not been used. One of +\code{\link[=abort]{abort()}}, \code{\link[=warn]{warn()}}, \code{\link[=inform]{inform()}} or \code{\link[=signal]{signal()}}.} +} +\description{ +Automatically sets exit handler to run when function terminates, checking +that all elements of \code{...} have been evaluated. If you use \code{\link[=on.exit]{on.exit()}} +elsewhere in your function, make sure to use \code{add = TRUE} so that you +don't override the handler set up by \code{check_dots_used()}. +} +\examples{ +f <- function(...) { + check_dots_used() + g(...) +} + +g <- function(x, y, ...) { + x + y +} +f(x = 1, y = 2) + +try(f(x = 1, y = 2, z = 3)) +try(f(x = 1, y = 2, 3, 4, 5)) +} diff --git a/man/cnd_signal.Rd b/man/cnd_signal.Rd index f3ec6d8b34..0a6558b249 100644 --- a/man/cnd_signal.Rd +++ b/man/cnd_signal.Rd @@ -10,7 +10,7 @@ cnd_signal(cnd, ...) \item{cnd}{A condition object (see \code{\link[=cnd]{cnd()}}). If \code{NULL}, \code{cnd_signal()} returns without signalling a condition.} -\item{...}{These dots are for extensions and must be empty.} +\item{...}{These dots are for future extensions and must be empty.} } \description{ The type of signal depends on the class of the condition: diff --git a/man/dots-empty.Rd b/man/dots-empty.Rd new file mode 100644 index 0000000000..4181ede688 --- /dev/null +++ b/man/dots-empty.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dots-ellipsis.R +\name{dots-empty} +\alias{dots-empty} +\title{Helper for consistent documentation of empty dots} +\arguments{ +\item{...}{These dots are for future extensions and must be empty.} +} +\description{ +Use \verb{@inheritParams rlang::`dots-empty`} in your package +to consistently document an unused \code{...} argument. +} +\keyword{internal} diff --git a/man/dots-used.Rd b/man/dots-used.Rd new file mode 100644 index 0000000000..150607bb46 --- /dev/null +++ b/man/dots-used.Rd @@ -0,0 +1,13 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/dots-ellipsis.R +\name{dots-used} +\alias{dots-used} +\title{Helper for consistent documentation of used dots} +\arguments{ +\item{...}{Arguments passed to methods.} +} +\description{ +Use \verb{@inheritParams rlang::`dots-used`} in your package +to consistently document an unused \code{...} argument. +} +\keyword{internal} diff --git a/src/Makevars b/src/Makevars index 62cb991057..16564db067 100644 --- a/src/Makevars +++ b/src/Makevars @@ -42,6 +42,7 @@ internal-files = \ internal/attr.c \ internal/call.c \ internal/dots.c \ + internal/dots-ellipsis.c \ internal/env.c \ internal/env-binding.c \ internal/eval.c \ diff --git a/src/internal/dots-ellipsis.c b/src/internal/dots-ellipsis.c new file mode 100644 index 0000000000..d29df6e262 --- /dev/null +++ b/src/internal/dots-ellipsis.c @@ -0,0 +1,92 @@ +#include "rlang.h" + +static +r_obj* ffi_ellipsis_find_dots(r_obj* env) { + if (r_typeof(env) != R_TYPE_environment) { + r_abort("`env` is a not an environment."); + } + + r_obj* dots = KEEP(r_env_find(env, r_syms.dots)); + if (dots == r_syms.unbound) { + r_abort("No `...` found."); + } + + FREE(1); + return dots; +} + +r_obj* ffi_ellipsis_dots(r_obj* env, r_obj* auto_name) { + r_obj* dots = ffi_ellipsis_find_dots(env); + + // Empty dots + if (dots == r_syms.missing) { + return r_globals.empty_list; + } + + KEEP(dots); + + bool c_auto_name = r_as_bool(auto_name); + + int n = r_length(dots); + r_obj* out = KEEP(r_alloc_list(n)); + + r_obj* names = r_alloc_character(n); + r_attrib_poke(out, r_syms.names, names); + + for (int i = 0; i < n; ++i) { + r_list_poke(out, i, r_node_car(dots)); + + r_obj* name = r_node_tag(dots); + if (r_typeof(name) == R_TYPE_symbol) { + r_chr_poke(names, i, r_sym_string(name)); + } else { + if (c_auto_name) { + char buffer[20]; + snprintf(buffer, 20, "..%i", i + 1); + r_chr_poke(names, i, r_str(buffer)); + } else { + r_chr_poke(names, i, r_globals.na_str); + } + } + + dots = r_node_cdr(dots); + } + + FREE(2); + return out; +} + +static +bool ellipsis_promise_forced(r_obj* x) { + if (r_typeof(x) != R_TYPE_promise) { + return true; + } else { + return PRVALUE(x) != r_syms.unbound; + } +} +r_obj* ffi_ellipsis_promise_forced(r_obj* x) { + return r_lgl(ellipsis_promise_forced(x)); +} + +r_obj* ffi_ellipsis_dots_used(r_obj* env) { + r_obj* dots = KEEP(ffi_ellipsis_find_dots(env)); + + if (dots == r_syms.missing) { + FREE(1); + return r_true; + } + + while (dots != r_null) { + r_obj* elt = r_node_car(dots); + + if (!ellipsis_promise_forced(elt)) { + FREE(1); + return r_false; + } + + dots = r_node_cdr(dots); + } + + FREE(1); + return r_true; +} diff --git a/src/internal/init.c b/src/internal/init.c index 58fd1702cd..9891d6579d 100644 --- a/src/internal/init.c +++ b/src/internal/init.c @@ -41,6 +41,9 @@ static const R_CallMethodDef r_callables[] = { {"ffi_dots_list", (DL_FUNC) &ffi_dots_list, 7}, {"ffi_dots_pairlist", (DL_FUNC) &ffi_dots_pairlist, 7}, {"ffi_duplicate", (DL_FUNC) &ffi_duplicate, 2}, + {"ffi_ellipsis_dots", (DL_FUNC) &ffi_ellipsis_dots, 2}, + {"ffi_ellipsis_dots_used", (DL_FUNC) &ffi_ellipsis_dots_used, 1}, + {"ffi_ellipsis_promise_forced", (DL_FUNC) &ffi_ellipsis_promise_forced, 1}, {"ffi_enexpr", (DL_FUNC) &ffi_enexpr, 2}, {"ffi_enquo", (DL_FUNC) &ffi_enquo, 2}, {"ffi_ensym", (DL_FUNC) &ffi_ensym, 2}, diff --git a/src/internal/internal.c b/src/internal/internal.c index 4f045eb453..c8a668295e 100644 --- a/src/internal/internal.c +++ b/src/internal/internal.c @@ -6,6 +6,7 @@ #include "attr.c" #include "call.c" #include "dots.c" +#include "dots-ellipsis.c" #include "env.c" #include "env-binding.c" #include "eval.c" diff --git a/tests/testthat/_snaps/dots-ellipsis.md b/tests/testthat/_snaps/dots-ellipsis.md new file mode 100644 index 0000000000..5a517b84b1 --- /dev/null +++ b/tests/testthat/_snaps/dots-ellipsis.md @@ -0,0 +1,38 @@ +# error if dots not used by another function + + Code + expect_error(f(x = 10, c = 3), class = "rlib_error_dots_unused") + +# error if dots named + + Code + (expect_error(f(1, 2, 3, xy = 4, x = 5), class = "rlib_error_dots_named")) + Output + + 2 arguments in `...` had unexpected names. + x We detected these problematic arguments: + * `xy` + * `x` + i Did you misspecify an argument? + +# error if if dots not empty + + Code + (expect_error(f(xy = 4), class = "rlib_error_dots_nonempty")) + Output + + `...` is not empty. + i These dots only exist to allow future extensions and should be empty. + x We detected these problematic arguments: + * `xy` + i Did you misspecify an argument? + Code + (expect_error(f0(xy = 4), class = "rlib_error_dots_nonempty")) + Output + + `...` is not empty. + i These dots only exist to allow future extensions and should be empty. + x We detected these problematic arguments: + * `xy` + i Did you misspecify an argument? + diff --git a/tests/testthat/test-dots-ellipsis.R b/tests/testthat/test-dots-ellipsis.R new file mode 100644 index 0000000000..ef41316c2d --- /dev/null +++ b/tests/testthat/test-dots-ellipsis.R @@ -0,0 +1,88 @@ +test_that("error if dots not used", { + f <- function(x, y, ...) { + check_dots_used() + x + y + } + + expect_error(f(1, 2), NA) + expect_error(f(1, 2, 3), class = "rlib_error_dots_unused") +}) + +test_that("error if dots not used by another function", { + g <- function(a = 1, b = 1, ...) { + a + b + } + f <- function(x = 1, ...) { + check_dots_used() + x * g(...) + } + + expect_error(f(x = 10, a = 1), NA) + + expect_snapshot({ + expect_error(f(x = 10, c = 3), class = "rlib_error_dots_unused") + }) +}) + +test_that("error if dots named", { + f <- function(..., xyz = 1) { + check_dots_unnamed() + } + + expect_error(f(xyz = 1), NA) + expect_error(f(1, 2, 3), NA) + expect_error(f(1, 2, 3, xyz = 4), NA) + expect_error(f(1, 2, 3, xy = 4), class = "rlib_error_dots_named") + + expect_snapshot({ + (expect_error(f(1, 2, 3, xy = 4, x = 5), class = "rlib_error_dots_named")) + }) +}) + +test_that("error if if dots not empty", { + f <- function(..., xyz = 1) { + check_dots_empty() + } + f0 <- function(..., xyz = 1) { + check_dots_empty0(...) + } + + expect_error(f(xyz = 1), NA) + expect_error(f0(xyz = 1), NA) + + expect_snapshot({ + (expect_error(f(xy = 4), class = "rlib_error_dots_nonempty")) + (expect_error(f0(xy = 4), class = "rlib_error_dots_nonempty")) + }) +}) + +test_that("can control the action", { + f <- function(action, check, ..., xyz = 1) { + check(action = action) + } + + expect_error(f(abort, check_dots_used, xy = 4), class = "rlib_error_dots_unused") + expect_warning(f(warn, check_dots_used, xy = 4), class = "rlib_error_dots_unused") + expect_message(f(inform, check_dots_used, xy = 4), class = "rlib_error_dots_unused") + + expect_error(f(abort, check_dots_unnamed, xy = 4), class = "rlib_error_dots_named") + expect_warning(f(warn, check_dots_unnamed, xy = 4), class = "rlib_error_dots_named") + expect_message(f(inform, check_dots_unnamed, xy = 4), class = "rlib_error_dots_named") + + expect_error(f(abort, check_dots_empty, xy = 4), class = "rlib_error_dots_nonempty") + expect_warning(f(warn, check_dots_empty, xy = 4), class = "rlib_error_dots_nonempty") + expect_message(f(inform, check_dots_empty, xy = 4), class = "rlib_error_dots_nonempty") +}) + +test_that("warn if unused dots", { + safe_median <- function(x, ...) { + check_dots_used() + UseMethod("safe_median") + } + safe_median.numeric <- function(x, ..., na.rm = TRUE) { + stats::median(x, na.rm = na.rm) + } + expect_error(safe_median(1:10), NA) + expect_error(safe_median(1:10, na.rm = TRUE), NA) + expect_error(safe_median(1:10, y = 1), class = "rlib_error_dots_unused") +})