Skip to content

Commit 5446a3f

Browse files
committed
Resolve named handlers by name at dispatch, like dest_infer
`register_r2f_handler()` stores the handler as a function object captured at build time. covr rebinds its instrumented copies into the namespace after the package has loaded, so a handler registered as a top-level named function keeps dispatching the copy taken at registration: the instrumented copy never runs and the handler reads as 0% covered however well it is tested. Its callees still read as covered, because their names resolve from the namespace at call time -- which is what makes the pattern recognisable. The file already documents this hazard and works around it for `dest_infer`, by recording `dest_infer_name` and resolving it at call time. Do the same for the handler itself: record `fun_name` at registration and let `get_r2f_handler()` swap in the current namespace binding. The object stays authoritative. Of the registrations in the tree, 27 pass an anonymous function literal, which has no name to resolve; the name is a supplement for the ones that don't. Recording it is deliberately stricter than `dest_infer_name`: the argument must be a symbol *and* name this same function in a namespace, since that is the only environment covr rebinds into. That excludes the local `handler` closure built by `register_unary_intrinsic()`, whose name means something else on the next call, and `r2f_handlers[["<-"]]`, which is not a symbol at all. `dest_infer` is left as it is; it is advisory, whereas the handler is called, so a wrong resolution there would be a bug. No handler in the tree is currently registered by name, so nothing changes today. It is what lets a handler be extracted into a named function without the extraction reading as untested.
1 parent f77a07a commit 5446a3f

4 files changed

Lines changed: 130 additions & 1 deletion

File tree

R/classes.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,9 @@ R2FHandler := new_class(
426426
dest_supported = prop_bool(default = FALSE),
427427
dest_infer = new_property(NULL | class_function),
428428
dest_infer_name = prop_string(default = NULL, allow_null = TRUE),
429+
# Set when the handler was registered as a namespace-level named function,
430+
# so dispatch can re-resolve it by name. See register_r2f_handler().
431+
fun_name = prop_string(default = NULL, allow_null = TRUE),
429432
# When NULL, r2f will resolve the callable by name and use match.call().
430433
# When FALSE, r2f will not attempt match.call().
431434
match_fun = new_property(

R/r2f-aaa-registry.R

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ register_r2f_handler <- function(
1414

1515
handler <- if (inherits(fun, R2FHandler)) fun else R2FHandler(fun)
1616

17+
# Same hazard as `dest_infer` below, for the handler itself: the function
18+
# object is captured here, at build time, while covr rebinds its instrumented
19+
# copies into the namespace after the package has loaded. A handler registered
20+
# as a top-level named function would keep dispatching the copy taken here and
21+
# read as 0% covered however well it is tested. Record the name so
22+
# get_r2f_handler() can re-resolve it; the object stays authoritative, since
23+
# most handlers are anonymous literals with no name to resolve.
24+
handler@fun_name <- registered_fun_name(substitute(fun), fun)
25+
1726
if (!is.null(dest_supported)) {
1827
handler@dest_supported <- isTRUE(dest_supported)
1928
}
@@ -47,3 +56,28 @@ register_r2f_handler <- function(
4756
}
4857
invisible(handler)
4958
}
59+
60+
61+
# The name to re-resolve a registered handler by, or NULL if there isn't one.
62+
#
63+
# `expr` is the unevaluated `fun` argument and `fun` its value. A name is only
64+
# usable if `expr` is a symbol *and* it names this same function in a namespace
65+
# -- the only environment covr rebinds into. That rules out the two non-literal
66+
# registrations that are not namespace-level functions: the local `handler`
67+
# closure built by register_unary_intrinsic() (a symbol, but bound in a call
68+
# frame, where the name would mean something else on a later call) and
69+
# `r2f_handlers[["<-"]]` (not a symbol at all).
70+
registered_fun_name <- function(expr, fun) {
71+
if (!is.symbol(expr)) {
72+
return(NULL)
73+
}
74+
name <- as.character(expr)
75+
env <- environment(fun)
76+
if (!is.environment(env) || !isNamespace(env)) {
77+
return(NULL)
78+
}
79+
if (!identical(get0(name, envir = env, mode = "function"), fun)) {
80+
return(NULL)
81+
}
82+
name
83+
}

R/r2f-aab-core.R

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,8 +380,31 @@ num2fortran <- function(x) {
380380

381381
get_r2f_handler <- function(name) {
382382
stopifnot("All functions called must be named as symbols" = is.symbol(name))
383-
get0(name, r2f_handlers) %||%
383+
handler <- get0(name, r2f_handlers) %||%
384384
stop("Unsupported function: ", name, call. = FALSE)
385+
resolve_handler_fun(handler)
386+
}
387+
388+
389+
# Swap in the handler's current namespace binding, so an instrumented or
390+
# otherwise rebound copy is dispatched instead of the one captured at
391+
# registration. Only handlers registered as namespace-level named functions
392+
# carry a `fun_name`; for every other handler this is a property read and a
393+
# return. See register_r2f_handler() for why the name is recorded.
394+
resolve_handler_fun <- function(handler) {
395+
if (!inherits(handler, R2FHandler)) {
396+
return(handler)
397+
}
398+
name <- handler@fun_name
399+
if (!is_string(name)) {
400+
return(handler)
401+
}
402+
current <- get0(name, envir = environment(handler), mode = "function")
403+
if (is.null(current) || identical(current, S7_data(handler))) {
404+
return(handler)
405+
}
406+
S7_data(handler) <- current
407+
handler
385408
}
386409

387410

tests/testthat/test-r2f-registry.R

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,75 @@ test_that("register_r2f_handler does not set match.fun when TRUE", {
6262
expect_null(result@match_fun)
6363
})
6464

65+
test_that("register_r2f_handler records the name of a namespace-level handler", {
66+
withr::defer(
67+
rm(list = "test_handler_named", envir = quickr:::r2f_handlers),
68+
envir = environment()
69+
)
70+
# Passed as a bare symbol, the way the package's own top-level registrations
71+
# do it -- `quickr:::last` would be a call, with no name to record.
72+
result <- quickr:::register_r2f_handler("test_handler_named", last)
73+
expect_identical(result@fun_name, "last")
74+
})
75+
76+
test_that("register_r2f_handler leaves anonymous and local handlers unnamed", {
77+
local_handler <- function(e, scope, ...) NULL
78+
withr::defer(
79+
rm(
80+
list = c("test_handler_unnamed_anon", "test_handler_unnamed_local"),
81+
envir = quickr:::r2f_handlers
82+
),
83+
envir = environment()
84+
)
85+
anon <- quickr:::register_r2f_handler(
86+
"test_handler_unnamed_anon",
87+
function(e, scope, ...) NULL
88+
)
89+
# A symbol, but bound in a call frame rather than a namespace, so the name
90+
# would mean something else the next time the frame is entered.
91+
local <- quickr:::register_r2f_handler(
92+
"test_handler_unnamed_local",
93+
local_handler
94+
)
95+
expect_null(anon@fun_name)
96+
expect_null(local@fun_name)
97+
})
98+
99+
test_that("dispatch re-resolves a named handler's namespace binding", {
100+
# covr rebinds its instrumented copies into the namespace after the package
101+
# has loaded -- that is, after registration captured the function object.
102+
# Mocking the binding reproduces that sequence exactly.
103+
original <- last
104+
withr::defer(
105+
rm(list = "test_handler_rebound", envir = quickr:::r2f_handlers),
106+
envir = environment()
107+
)
108+
registered <- quickr:::register_r2f_handler("test_handler_rebound", last)
109+
expect_identical(registered@fun_name, "last")
110+
111+
local_mocked_bindings(last = function(x) "rebound")
112+
resolved <- quickr:::get_r2f_handler(quote(test_handler_rebound))
113+
expect_identical(resolved("ignored"), "rebound")
114+
115+
# Resolving hands back a copy; the registry still holds what was registered.
116+
expect_identical(
117+
S7::S7_data(quickr:::r2f_handlers[["test_handler_rebound"]]),
118+
original
119+
)
120+
})
121+
122+
test_that("dispatch leaves unnamed handlers alone", {
123+
handler <- function(e, scope, ...) "anonymous"
124+
withr::defer(
125+
rm(list = "test_handler_untouched", envir = quickr:::r2f_handlers),
126+
envir = environment()
127+
)
128+
quickr:::register_r2f_handler("test_handler_untouched", handler)
129+
resolved <- quickr:::get_r2f_handler(quote(test_handler_untouched))
130+
expect_null(resolved@fun_name)
131+
expect_identical(S7::S7_data(resolved), handler)
132+
})
133+
65134
test_that("register_r2f_handler registers multiple names", {
66135
handler <- function(e, scope, ...) NULL
67136
withr::defer(

0 commit comments

Comments
 (0)