Skip to content

Commit 07e54f4

Browse files
committed
fix(filter.epi_archive): avoid other lazy eval traps
1 parent 80d787b commit 07e54f4

File tree

2 files changed

+49
-16
lines changed

2 files changed

+49
-16
lines changed

R/methods-epi_archive.R

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,30 +1063,46 @@ filter.epi_archive <- function(.data, ..., .by = NULL, .format_aware = FALSE) {
10631063
# They are expected to be active bindings, so directly
10641064
# assigning has issues; `rm` first.
10651065
rm(list = forbidden_colnames, envir = e)
1066-
delayedAssign("version", cli::cli_abort(c(
1067-
"Using `version` in `filter.epi_archive` may produce unexpected results.",
1068-
">" = "See if `epix_as_of` or `epix_slide` would work instead.",
1069-
">" = "If not, see `?filter.epi_archive` details for how to proceed."
1070-
), class = "epiprocess__filter_archive__used_version"), assign.env = e)
1066+
eval_env <- new.env(parent = asNamespace("epiprocess")) # see (2) below
1067+
delayedAssign(
1068+
"version",
1069+
cli_abort(c(
1070+
"Using `version` in `filter.epi_archive` may produce unexpected results.",
1071+
">" = "See if `epix_as_of` or `epix_slide` would work instead.",
1072+
">" = "If not, see `?filter.epi_archive` details for how to proceed."
1073+
), class = "epiprocess__filter_archive__used_version"),
1074+
eval.env = eval_env,
1075+
assign.env = e
1076+
)
10711077
for (measurement_colname in measurement_colnames) {
1072-
# Record current `measurement_colname` and set up delayed
1073-
# binding for error in a child environment, so that `for` loop
1074-
# updating its value and `rm` cleanup don't mess things up:
1075-
local({
1076-
local_measurement_colname <- measurement_colname
1077-
delayedAssign(measurement_colname, cli::cli_abort(c(
1078+
# Record current `measurement_colname` and set up execution for
1079+
# the promise for the error in its own dedicated environment, so
1080+
# that (1) `for` loop updating its value and `rm` cleanup don't
1081+
# mess things up. We can also (2) prevent changes to data mask
1082+
# ancestry (to involve user's quosure env rather than our
1083+
# quosure env) or contents (from edge case of user binding
1084+
# functions inside the mask) from potentially interfering by
1085+
# setting the promise's execution environment to skip over the
1086+
# data mask.
1087+
eval_env <- new.env(parent = asNamespace("epiprocess"))
1088+
eval_env[["local_measurement_colname"]] <- measurement_colname
1089+
delayedAssign(
1090+
measurement_colname,
1091+
cli_abort(c(
10781092
"Using `{format_varname(local_measurement_colname)}`
10791093
in `filter.epi_archive` may produce unexpected results.",
10801094
">" = "See `?filter.epi_archive` details for how to proceed."
1081-
), class = "epiprocess__filter_archive__used_measurement"), assign.env = e)
1082-
})
1095+
), class = "epiprocess__filter_archive__used_measurement"),
1096+
eval.env = eval_env,
1097+
assign.env = e
1098+
)
10831099
}
10841100
break
10851101
}
10861102
e <- parent.env(e)
10871103
}
1088-
# Don't mask similarly-named user objects:
1089-
rm(list = c("e", "measurement_colname"))
1104+
# Don't mask similarly-named user objects in ancestor envs:
1105+
rm(list = c("e", "measurement_colname", "eval_env"))
10901106
TRUE
10911107
},
10921108
...,

tests/testthat/test-methods-epi_archive.R

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,10 @@ test_that("filter.epi_archive works as expected", {
222222
ea2 %>% filter(time_value >= as.Date("2020-06-02"), cases >= 2L),
223223
class = "epiprocess__filter_archive__used_measurement"
224224
)
225+
# Check for `for` + `delayedAssign` mishap in `expect_snapshot` (we should say
226+
# something about `cases` (the relevant colname), not `deaths` (the last
227+
# measurement colname)):
225228
ea2p <- ea2_data %>%
226-
# to check for `for` + `delayedAssign` mishap in expect_snapshot
227229
mutate(deaths = 0) %>%
228230
as_epi_archive()
229231
expect_error(
@@ -234,6 +236,21 @@ test_that("filter.epi_archive works as expected", {
234236
ea2p %>% filter(cases >= median(cases), .by = geo_value),
235237
error = TRUE, cnd_class = TRUE
236238
)
239+
# Check that we are insulated from other lazy eval traps:
240+
expected <- rlang::catch_cnd(ea2p %>% filter(cases >= median(cases), .by = geo_value))
241+
expect_class(expected$parent, "epiprocess__filter_archive__used_measurement")
242+
with(list(cli_abort = function(...) stop("now, pretend user didn't have cli attached")), {
243+
expect_equal(rlang::catch_cnd(ea2p %>% filter(cases >= median(cases), .by = geo_value))$parent$message,
244+
expected$parent$message)
245+
})
246+
expect_equal(
247+
rlang::catch_cnd(ea2p %>% filter({
248+
c <- function(...) stop("and that they overwrote `c` to try to debug their own code")
249+
cases >= median(cases)
250+
}, .by = geo_value))$parent$message,
251+
expected$parent$message
252+
)
253+
237254

238255
# Escape hatch:
239256
expect_equal(

0 commit comments

Comments
 (0)