Skip to content

Commit c0e76e9

Browse files
committed
fix: handle empty module namespace in session destroy machinery
callModule(server, character(0)) reaches makeScope(character(0)). #4372's new destroy-callback code branched on if (!nzchar(namespace)) to detect the root scope, but nzchar(character(0)) is logical(0), so the guard errored with "argument is of length zero". A length-0 namespace is documented NS() behavior meaning "no namespace" (root) and worked before #4372 (makeScope had no nzchar guard). Treat a length-0 namespace as root only in the destroy-key logic, leaving the namespace itself untouched so NS(character(0)) keeps returning ids unchanged (normalizing to "" would instead prefix ids with a stray separator).
1 parent cceb4e4 commit c0e76e9

3 files changed

Lines changed: 29 additions & 4 deletions

File tree

R/mock-session.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,8 @@ MockShinySession <- R6Class(
763763
# @param ns The namespace key.
764764
# @return A Callbacks object.
765765
getOrCreateDestroyCallbacks = function(ns) {
766-
if (!nzchar(ns)) ns <- "..root"
766+
# length-0 namespace is the root scope; nzchar(character(0)) is logical(0)
767+
if (length(ns) == 0L || !nzchar(ns)) ns <- "..root"
767768
if (!private$destroyCallbacksByNs$containsKey(ns)) {
768769
private$destroyCallbacksByNs$set(ns, Callbacks$new())
769770
}
@@ -775,7 +776,7 @@ MockShinySession <- R6Class(
775776
# @param nsPrefix The namespace prefix to match.
776777
invokeDestroyCallbacks = function(nsPrefix = "") {
777778
allNs <- private$destroyCallbacksByNs$keys()
778-
isRoot <- !nzchar(nsPrefix)
779+
isRoot <- length(nsPrefix) == 0L || !nzchar(nsPrefix)
779780

780781
if (!isRoot) {
781782
nsPrefixWithSep <- paste0(nsPrefix, ns.sep)

R/shiny.R

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,8 @@ ShinySession <- R6Class(
812812
destroyNsRoot = "..root",
813813

814814
destroyNsKey = function(ns) {
815-
if (!nzchar(ns)) private$destroyNsRoot else ns
815+
# length-0 namespace is the root scope; nzchar(character(0)) is logical(0)
816+
if (length(ns) == 0L || !nzchar(ns)) private$destroyNsRoot else ns
816817
},
817818

818819
getOrCreateDestroyCallbacks = function(ns) {
@@ -824,7 +825,7 @@ ShinySession <- R6Class(
824825
},
825826
invokeDestroyCallbacks = function(nsPrefix = "") {
826827
allNs <- private$destroyCallbacksByNs$keys()
827-
isRoot <- !nzchar(nsPrefix)
828+
isRoot <- length(nsPrefix) == 0L || !nzchar(nsPrefix)
828829

829830
if (!isRoot) {
830831
nsPrefixWithSep <- paste0(nsPrefix, ns.sep)

tests/testthat/test-destroy.R

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,29 @@ test_that("makeScope rejects reserved namespace '..root'", {
904904
expect_error(session$makeScope("..root"), "reserved")
905905
})
906906

907+
test_that("makeScope() accepts an empty (character(0)) namespace", {
908+
session <- MockShinySession$new()
909+
expect_no_error(scope <- session$makeScope(character(0)))
910+
911+
called <- FALSE
912+
scope$onDestroy(function() called <<- TRUE)
913+
session$close()
914+
expect_true(called)
915+
})
916+
917+
test_that("empty namespace scope leaves ids un-prefixed (NS length-0 semantics)", {
918+
session <- MockShinySession$new()
919+
scope <- session$makeScope(character(0))
920+
expect_identical(scope$ns("x"), "x")
921+
})
922+
923+
test_that("callModule() with an empty namespace id does not error", {
924+
session <- MockShinySession$new()
925+
expect_no_error(
926+
callModule(function(input, output, session) NULL, character(0), session = session)
927+
)
928+
})
929+
907930
test_that("createMockDomain supports onDestroy and destroy", {
908931
domain <- createMockDomain()
909932

0 commit comments

Comments
 (0)