Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export(assert_flag)
export(assert_function)
export(assert_function_expects)
export(assert_function_expects_n_arguments)
export(assert_function_variadic)
export(assert_greater_than)
export(assert_greater_than_or_equal_to)
export(assert_identical)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

* Added `assert_function_expects()` for checking required argument names in functions

* Added `assert_function_variadic()` for checking whether functions declare `...`

* Added `assert_all_strings_contain()` and `assert_string_contains()` for checking character inputs against regular expressions

* Added `assert_packages_installed()`
Expand Down
36 changes: 36 additions & 0 deletions R/assert_functions.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ function_expects_n_arguments_advanced <- function(x, n, dots = c("throw_error","
return(TRUE)
}

function_variadic_advanced <- function(x){
if(!is.function(x))
return("{.strong '{arg_name}'} must be a function, not a {class(arg_value)}")

if(!func_supports_variable_arguments(x))
return("{.strong '{arg_name}'} must accept variable arguments via {.strong ...}")
Comment on lines +27 to +32

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat primitives with NULL args() as variadic

assert_function_variadic() relies on func_supports_variable_arguments(), which in turn inspects names(formals(args(func))). For several base primitives (e.g., subsetting [/[[), args() returns NULL even though the function accepts ..., so this check returns FALSE and the new assertion incorrectly fails for genuinely variadic primitives. This means callers trying to assert variadic behavior on such primitives will get a false negative. Consider a fallback that treats args(func) == NULL differently (or a different detection strategy) so primitives with undocumented signatures aren’t misclassified.

Useful? React with 👍 / 👎.


return(TRUE)
}


# Expect function has arguments
# @param
Expand Down Expand Up @@ -70,6 +80,32 @@ function_expects_advanced <- function(x, required){
#' @export
assert_function_expects_n_arguments <- assert_create(func = function_expects_n_arguments_advanced)

#' Assert function is variadic
#'
#' Assert that a function signature declares the `...` argument (variadic
#' arguments).
#'
#' @param x a function to check includes `...` in its signature
#' @inheritParams common_roxygen_params
#'
#' @return invisible(TRUE) if function `x` declares `...`, otherwise aborts with
#' the error message specified by `msg`
#'
#' @examples
#' my_fun <- function(x, ...) x
#' assert_function_variadic(my_fun)
#'
#' try({
#' my_fun_no_dots <- function(x) x
#' assert_function_variadic(my_fun_no_dots)
#' })
#'
#' @export
assert_function_variadic <- assert_create_chain(
assert_function,
assert_create(func = function_variadic_advanced)
)

#' Assert function expects specific parameter names
#'
#' Assert that a function signature includes required set of parameter names in its
Expand Down
1 change: 1 addition & 0 deletions man/assert_function_expects.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions man/assert_function_variadic.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 53 additions & 1 deletion tests/testthat/test-assert_functions.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fn_2_args_with_dots <- function(a, b, ...) {}
fn_with_required_args <- function(x, y, z = 1, ...) {}
fn_with_defaults <- function(x = 1, y = 2) {}
fn_with_no_required <- function(...) {}
fn_no_dots <- function(x, y) x + y

# Unit tests for `function_expects_n_arguments_advanced`
test_that("function_expects_n_arguments_advanced behaves correctly for exact argument count", {
Expand All @@ -25,7 +26,7 @@ test_that("function_expects_n_arguments_advanced behaves correctly for exact arg
test_that("function_expects_n_arguments_advanced handles dots behavior correctly", {
# Test with `...` and dots="throw_error"
expect_match(function_expects_n_arguments_advanced(fn_1_arg_with_dots, 1, dots = "throw_error"),
"must not contain ... arguments", fixed = TRUE)
"must not contain ... arguments", fixed = TRUE) # dots throw error by default

# Test with `...` and dots="count_as_0"
expect_true(function_expects_n_arguments_advanced(fn_1_arg_with_dots, 1, dots = "count_as_0"))
Expand All @@ -44,6 +45,8 @@ test_that("function_expects_n_arguments_advanced returns correct error for non-f
# Input is not a function
expect_match(function_expects_n_arguments_advanced(42, 1), "must be a function, not a", fixed = TRUE)
expect_match(function_expects_n_arguments_advanced("not_a_function", 1), "must be a function, not a", fixed = TRUE)
expect_match(function_expects_n_arguments_advanced(list(), 1), "must be a function, not a", fixed = TRUE)
expect_match(function_expects_n_arguments_advanced(NULL, 1), "must be a function, not a", fixed = TRUE)
})

test_that("function_expects_n_arguments_advanced correctly counts arguments for functions with no arguments", {
Expand Down Expand Up @@ -87,6 +90,18 @@ test_that("function_expects_advanced validates required argument names", {
"must be a function, not a", fixed = TRUE)
})

test_that("function_variadic_advanced validates variable arguments", {
expect_true(function_variadic_advanced(fn_1_arg_with_dots))
expect_true(function_variadic_advanced(fn_2_args_with_dots))

expect_match(function_variadic_advanced(fn_no_dots),
"must accept variable arguments", fixed = TRUE)
expect_match(function_variadic_advanced(1),
"must be a function, not a", fixed = TRUE)
expect_match(function_variadic_advanced(list()),
"must be a function, not a", fixed = TRUE)
})




Expand Down Expand Up @@ -171,5 +186,42 @@ cli::test_that_cli("assert_function_expects() works", config = "plain", {
"Custom error message"
)

# Works with a single required argument
expect_true(assert_function_expects(fn_with_required_args, "x"))

# Reports missing parameters when multiple are absent
expect_error(
assert_function_expects(fn_with_defaults, c("x", "z")),
"missing the following parameter",
fixed = TRUE
)
})

cli::test_that_cli("assert_function_variadic() works", config = "plain", {
my_fun <- function(a, ...) { a }
my_fun_no_dots <- function(a) { a }
my_fun_with_defaults <- function(a = 1, ...) { a }
my_fun_only_dots <- function(...) { }
my_fun_many <- function(a, b = 1, ...) { a + b }

# Accepts a basic variadic signature
expect_true(assert_function_variadic(my_fun))

# Accepts variadic signatures with defaults
expect_true(assert_function_variadic(my_fun_with_defaults))

# Accepts dots-only signatures
expect_true(assert_function_variadic(my_fun_only_dots))

# Accepts multiple arguments with dots
expect_true(assert_function_variadic(my_fun_many))

# Rejects non-variadic signatures
expect_error(assert_function_variadic(my_fun_no_dots), "must accept variable arguments", fixed = TRUE)

# Rejects non-function numeric input
expect_error(assert_function_variadic(123), "'123' must be a function, not a numeric")

# Rejects non-function list input
expect_error(assert_function_variadic(list()), "must be a .*function", fixed = FALSE)
})