From 69bd6cd95aa0a40621df209869acb823fa67681b Mon Sep 17 00:00:00 2001 From: Davide Garolini Date: Wed, 1 Jul 2026 11:55:36 +0000 Subject: [PATCH 1/3] check_*() error on empty input when allow_empty = FALSE (#30) Previously, empty but correctly-typed input (e.g. character(0), numeric(0), 0-row/0-col data frames) silently passed check_*() class and type checks even under the default allow_empty = FALSE. The empty-handling guard now errors in that case and only returns invisibly when allow_empty = TRUE. Updated the affected guard sites uniformly, added tests covering empty input for all affected functions, fixed a stale test that asserted the old behavior, and updated the script changelog and last-updated field. --- R/standalone-checks.R | 78 +++++++++++++++++-------- tests/testthat/test-standalone-checks.R | 33 ++++++++++- 2 files changed, 85 insertions(+), 26 deletions(-) diff --git a/R/standalone-checks.R b/R/standalone-checks.R index 3f6fdbe..7e0b33b 100644 --- a/R/standalone-checks.R +++ b/R/standalone-checks.R @@ -1,7 +1,7 @@ # --- # repo: insightsengineering/standalone # file: standalone-checks.R -# last-updated: 2025-05-08 +# last-updated: 2026-07-01 # license: https://unlicense.org # dependencies: standalone-cli_call_env.R # imports: [rlang, cli] @@ -12,6 +12,10 @@ # # ## Changelog # +# 2026-07-01 +# - `check_*()` functions now error on empty input when `allow_empty = FALSE` +# (previously empty input silently passed class/type checks) (#30) +# # 2025-05-08 # - Added `check_identical()` and `check_identical_length()` # 2025-04-27 @@ -57,9 +61,12 @@ check_class <- function(x, class = "check_class", call = get_cli_abort_call(), envir = rlang::current_env()) { - # if empty, skip test - if (isTRUE(allow_empty) && rlang::is_empty(x)) { - return(invisible(x)) + # if empty: return invisibly when allowed, otherwise error + if (rlang::is_empty(x)) { + if (isTRUE(allow_empty)) { + return(invisible(x)) + } + cli::cli_abort(message, class = c(class, "standalone-checks"), call = call, .envir = envir) } if (!inherits(x, cls)) { @@ -221,9 +228,12 @@ check_length <- function(x, class = "check_length", call = get_cli_abort_call(), envir = rlang::current_env()) { - # if empty, skip test - if (isTRUE(allow_empty) && rlang::is_empty(x)) { - return(invisible(x)) + # if empty: return invisibly when allowed, otherwise error + if (rlang::is_empty(x)) { + if (isTRUE(allow_empty)) { + return(invisible(x)) + } + cli::cli_abort(message, class = c(class, "standalone-checks"), call = call, .envir = envir) } # check length @@ -303,9 +313,12 @@ check_range <- function(x, class = "check_range", call = get_cli_abort_call(), envir = rlang::current_env()) { - # if empty, skip test - if (isTRUE(allow_empty) && rlang::is_empty(x)) { - return(invisible(x)) + # if empty: return invisibly when allowed, otherwise error + if (rlang::is_empty(x)) { + if (isTRUE(allow_empty)) { + return(invisible(x)) + } + cli::cli_abort(message, class = c(class, "standalone-checks"), call = call, .envir = envir) } print_error <- FALSE @@ -396,9 +409,12 @@ check_binary <- function(x, class = "check_binary", call = get_cli_abort_call(), envir = rlang::current_env()) { - # if empty, skip test - if (isTRUE(allow_empty) && rlang::is_empty(x)) { - return(invisible(x)) + # if empty: return invisibly when allowed, otherwise error + if (rlang::is_empty(x)) { + if (isTRUE(allow_empty)) { + return(invisible(x)) + } + cli::cli_abort(message, class = c(class, "standalone-checks"), call = call, .envir = envir) } # first check x is either logical or numeric @@ -441,9 +457,12 @@ check_formula_list_selector <- function(x, class = "check_formula_list_selector", call = get_cli_abort_call(), envir = rlang::current_env()) { - # if empty, skip test - if (isTRUE(allow_empty) && rlang::is_empty(x)) { - return(invisible(x)) + # if empty: return invisibly when allowed, otherwise error + if (rlang::is_empty(x)) { + if (isTRUE(allow_empty)) { + return(invisible(x)) + } + cli::cli_abort(message, class = c(class, "standalone-checks"), call = call, .envir = envir) } # first check the general structure; must be a list or formula @@ -481,9 +500,12 @@ check_integerish <- function(x, class = "check_integerish", call = get_cli_abort_call(), envir = rlang::current_env()) { - # if empty, skip test - if (isTRUE(allow_empty) && rlang::is_empty(x)) { - return(invisible(x)) + # if empty: return invisibly when allowed, otherwise error + if (rlang::is_empty(x)) { + if (isTRUE(allow_empty)) { + return(invisible(x)) + } + cli::cli_abort(message, class = c(class, "standalone-checks"), call = call, .envir = envir) } if (!rlang::is_integerish(x)) { @@ -510,9 +532,12 @@ check_scalar_integerish <- function(x, class = "check_integerish", call = get_cli_abort_call(), envir = rlang::current_env()) { - # if empty, skip test - if (isTRUE(allow_empty) && rlang::is_empty(x)) { - return(invisible(x)) + # if empty: return invisibly when allowed, otherwise error + if (rlang::is_empty(x)) { + if (isTRUE(allow_empty)) { + return(invisible(x)) + } + cli::cli_abort(message, class = c(class, "standalone-checks"), call = call, .envir = envir) } if (!rlang::is_scalar_integerish(x)) { @@ -593,9 +618,12 @@ check_numeric <- function(x, class = "check_numeric", call = get_cli_abort_call(), envir = rlang::current_env()) { - # if empty, skip test - if (isTRUE(allow_empty) && rlang::is_empty(x)) { - return(invisible(x)) + # if empty: return invisibly when allowed, otherwise error + if (rlang::is_empty(x)) { + if (isTRUE(allow_empty)) { + return(invisible(x)) + } + cli::cli_abort(message, class = c(class, "standalone-checks"), call = call, .envir = envir) } if (!is.numeric(x)) { diff --git a/tests/testthat/test-standalone-checks.R b/tests/testthat/test-standalone-checks.R index 3f0fd46..80a6abe 100644 --- a/tests/testthat/test-standalone-checks.R +++ b/tests/testthat/test-standalone-checks.R @@ -18,7 +18,8 @@ test_that("check functions work", { expect_snapshot(myfunc(1), error = TRUE) # check_data_frame() - expect_silent(check_data_frame(data.frame())) + expect_silent(check_data_frame(data.frame(a = 1))) + expect_silent(check_data_frame(data.frame(), allow_empty = TRUE)) expect_error( check_data_frame("mystring"), @@ -250,3 +251,33 @@ test_that("check functions work", { expect_silent(check_identical_length(letters, letters)) expect_snapshot(check_identical_length(letters[1], letters), error = TRUE) }) + +test_that("check functions error on empty input when allow_empty = FALSE", { + # empty input must error by default (allow_empty = FALSE) and pass when allowed + expect_error(check_class(character(0), "character")) + expect_silent(check_class(character(0), "character", allow_empty = TRUE)) + + expect_error(check_data_frame(data.frame())) + expect_silent(check_data_frame(data.frame(), allow_empty = TRUE)) + + expect_error(check_logical(logical(0))) + expect_silent(check_logical(logical(0), allow_empty = TRUE)) + + expect_error(check_range(numeric(0), c(0, 1))) + expect_silent(check_range(numeric(0), c(0, 1), allow_empty = TRUE)) + + expect_error(check_binary(integer(0))) + expect_silent(check_binary(integer(0), allow_empty = TRUE)) + + expect_error(check_integerish(integer(0))) + expect_silent(check_integerish(integer(0), allow_empty = TRUE)) + + expect_error(check_numeric(numeric(0))) + expect_silent(check_numeric(numeric(0), allow_empty = TRUE)) + + expect_error(check_formula_list_selector(list())) + expect_silent(check_formula_list_selector(list(), allow_empty = TRUE)) + + # empty string is not empty input and must still pass check_string() + expect_silent(check_string("")) +}) From e58f551ea0092210ccfd8e1dc2210d05bbab618e Mon Sep 17 00:00:00 2001 From: Davide Garolini Date: Wed, 1 Jul 2026 12:09:24 +0000 Subject: [PATCH 2/3] Add NEWS entry for check_*() empty-input behavior change (#30) --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 2aff13c..2873773 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,5 @@ # standalone (development version) * Added standalone function for `cards::round5()` (#28) + +* `check_*()` functions now error on empty input when `allow_empty = FALSE` (previously empty but correctly-typed input silently passed the class/type checks) (#30) From 1828f872994cde44effdaec1a317584699234377 Mon Sep 17 00:00:00 2001 From: Melkiades <11279768+Melkiades@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:26:46 +0000 Subject: [PATCH 3/3] Fix message grammar, condition class, and test coverage for check_*() - fix 'must an' typo in check_integerish/check_scalar_integerish messages - check_scalar_integerish() now uses its own condition class instead of check_integerish, so callers can catch scalar failures specifically - add empty-input regression tests for check_scalar_integerish and check_length - update snapshot for the corrected message text --- R/standalone-checks.R | 12 +++++++----- tests/testthat/_snaps/standalone-checks.md | 4 ++-- tests/testthat/test-standalone-checks.R | 6 ++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/R/standalone-checks.R b/R/standalone-checks.R index 7e0b33b..0d19387 100644 --- a/R/standalone-checks.R +++ b/R/standalone-checks.R @@ -15,6 +15,8 @@ # 2026-07-01 # - `check_*()` functions now error on empty input when `allow_empty = FALSE` # (previously empty input silently passed class/type checks) (#30) +# - `check_scalar_integerish()` failures now use the `check_scalar_integerish` +# condition class (previously `check_integerish`) (#30) # # 2025-05-08 # - Added `check_identical()` and `check_identical_length()` @@ -493,8 +495,8 @@ check_integerish <- function(x, message = ifelse( allow_empty, - "The {.arg {arg_name}} argument must an integer vector or empty.", - "The {.arg {arg_name}} argument must an integer vector." + "The {.arg {arg_name}} argument must be an integer vector or empty.", + "The {.arg {arg_name}} argument must be an integer vector." ), arg_name = rlang::caller_arg(x), class = "check_integerish", @@ -525,11 +527,11 @@ check_scalar_integerish <- function(x, message = ifelse( allow_empty, - "The {.arg {arg_name}} argument must an scalar integer or empty.", - "The {.arg {arg_name}} argument must an scalar integer." + "The {.arg {arg_name}} argument must be a scalar integer or empty.", + "The {.arg {arg_name}} argument must be a scalar integer." ), arg_name = rlang::caller_arg(x), - class = "check_integerish", + class = "check_scalar_integerish", call = get_cli_abort_call(), envir = rlang::current_env()) { # if empty: return invisibly when allowed, otherwise error diff --git a/tests/testthat/_snaps/standalone-checks.md b/tests/testthat/_snaps/standalone-checks.md index f9b24d1..3893753 100644 --- a/tests/testthat/_snaps/standalone-checks.md +++ b/tests/testthat/_snaps/standalone-checks.md @@ -109,7 +109,7 @@ myfunc(pi) Condition Error in `myfunc()`: - ! The `x` argument must an integer vector. + ! The `x` argument must be an integer vector. --- @@ -117,7 +117,7 @@ myfunc(pi) Condition Error in `myfunc()`: - ! The `x` argument must an scalar integer. + ! The `x` argument must be a scalar integer. --- diff --git a/tests/testthat/test-standalone-checks.R b/tests/testthat/test-standalone-checks.R index 80a6abe..e5ec198 100644 --- a/tests/testthat/test-standalone-checks.R +++ b/tests/testthat/test-standalone-checks.R @@ -272,6 +272,12 @@ test_that("check functions error on empty input when allow_empty = FALSE", { expect_error(check_integerish(integer(0))) expect_silent(check_integerish(integer(0), allow_empty = TRUE)) + expect_error(check_scalar_integerish(integer(0))) + expect_silent(check_scalar_integerish(integer(0), allow_empty = TRUE)) + + expect_error(check_length(character(0), length = 1)) + expect_silent(check_length(character(0), length = 1, allow_empty = TRUE)) + expect_error(check_numeric(numeric(0))) expect_silent(check_numeric(numeric(0), allow_empty = TRUE))