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
15 changes: 15 additions & 0 deletions .github/workflows/custom/after-install/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,18 @@ runs:
run: |
echo -e 'CPPFLAGS = -I/opt/homebrew/include\nLDFLAGS = -L/opt/homebrew/lib' | tee ~/.R/Makevars
shell: bash

# Regenerate the in-place ARG_HANDLE blocks from tools/migrations.R and fail
# if they have drifted. Runs from the source checkout (R is available after
# install; tools/ is excluded from the built package) so a contributor who
# forgot to regenerate gets a hard failure. Local dev refreshes them
# automatically via tests/testthat/helper-migrations.R.
- name: Regenerate argument-migration blocks and check for drift
run: |
Rscript tools/generate-migrations.R
if ! git diff --exit-code -- R; then
echo "::error::Generated ARG_HANDLE blocks are out of sync with tools/migrations.R."
echo "Run 'Rscript tools/generate-migrations.R' and commit the result."
Comment thread
schochastics marked this conversation as resolved.
exit 1
fi
shell: bash
114 changes: 114 additions & 0 deletions R/migrate-args.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Runtime helper behind the generated `# ... ARG_HANDLE` blocks (see
# tools/migrations.R, tools/generate-migrations.R). Hand-written and tested
# directly -- the generated blocks only carry the per-function configuration and
# call this. Kept a plain function (not an inline closure) so it is easy to step
# through in a debugger.
#
# Pure: it inspects `dots` against the supplied maps and returns the recovered
# values plus the deprecation message parts, or NULL when there is nothing to
# recover. It never touches an environment and never emits the deprecation
# itself -- the caller assigns the values into its own frame and calls
# `lifecycle::deprecate_soft()` inline, so the warning is attributed correctly
# without any caller-env plumbing.

#' @noRd
migrate_recover_args <- function(
dots,
current,
recover_new,
recover_old,
match_names,
match_to,
defaults,
head_args,
fn_name,
call = rlang::caller_env()
) {
if (length(dots) == 0L) {
return(NULL)
}

dot_names <- rlang::names2(dots)
values <- list()
rebound_old <- character()
rebound_new <- character()
pos <- 0L
for (k in seq_along(dots)) {
nm <- dot_names[[k]]
if (nzchar(nm)) {
# Named (possibly abbreviated): partial-match the recoverable names.
j <- charmatch(nm, match_names)
if (is.na(j)) {
cli::cli_abort(
c(
"Unexpected argument passed to {.fn {fn_name}}: {.arg {nm}}.",
i = "Arguments after {.arg ...} must be spelled out in full."
),
call = call
)
}
if (j == 0L) {
cli::cli_abort(
"Argument {.arg {nm}} matches multiple arguments of {.fn {fn_name}}.",
call = call
)
}
new_name <- match_to[[j]]
old_label <- match_names[[j]]
} else {
# Unnamed: recover by position into the next old slot past the head.
pos <- pos + 1L
if (pos > length(recover_new)) {
cli::cli_abort(
"Too many arguments passed to {.fn {fn_name}}.",
call = call
)
}
new_name <- recover_new[[pos]]
old_label <- recover_old[[pos]]
}

duplicated <- new_name %in% rebound_new
has_default <- new_name %in% names(defaults)
reassigned <- has_default &&
!identical(current[[new_name]], defaults[[new_name]])
if (duplicated || reassigned) {
cli::cli_abort(
c(
"Argument {.arg {new_name}} of {.fn {fn_name}} was supplied more than once.",
i = "Pass it exactly once, by its new name {.arg {new_name}}."
),
call = call
)
}

values[[new_name]] <- dots[[k]]
rebound_old <- c(rebound_old, old_label)
rebound_new <- c(rebound_new, new_name)
}

detected <- paste0(
fn_name,
"(",
paste(c(head_args, rebound_old), collapse = ", "),
")"
)
requested <- paste0(
fn_name,
"(",
paste(c(head_args, paste0(rebound_new, " = ")), collapse = ", "),
")"
)
list(
values = values,
what = paste0(
"Calling `",
fn_name,
"()` with positional or abbreviated arguments"
),
details = c(
i = paste0("Detected call: ", detected),
i = paste0("Use instead: ", requested)
)
)
}
48 changes: 48 additions & 0 deletions R/migration-fixture.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Test fixture for the in-place argument-migration generator (tools/migrations.R,
# tools/generate-migrations.R). `migration_fixture()` carries a generated
# ARG_HANDLE block that recovers a legacy call to its pre-3.0.0 signature
# f(graph, n, weight, type, directed) -- now
# f(graph, n, ..., weights, type, directed) with weight renamed to weights. The
# names are chosen so the tests can exercise renames, unique abbreviations and an
# ambiguous one. It exists only to exercise the generator end-to-end; see
# tests/testthat/test-migration-fixture.R.

#' @noRd
migration_fixture <- function(
graph,
n,
...,
weights = NULL,
type = "out",
directed = FALSE
) {
# BEGIN GENERATED ARG_HANDLE: migration_fixture, do not edit, see tools/generate-migrations.R
if (...length() > 0L) {
.arg_handle <- migrate_recover_args(
list(...),
current = list(weights = weights, type = type, directed = directed),
recover_new = c("weights", "type", "directed"),
recover_old = c("weight", "type", "directed"),
match_names = c("weight", "weights", "type", "directed"),
match_to = c("weights", "weights", "type", "directed"),
defaults = list(weights = NULL, type = "out", directed = FALSE),
head_args = c("graph", "n"),
fn_name = "migration_fixture"
)
list2env(.arg_handle$values, environment())
lifecycle::deprecate_soft(
"3.0.0",
what = I(.arg_handle$what),
details = .arg_handle$details
)
}
# END GENERATED ARG_HANDLE

list(
graph = graph,
n = n,
weights = weights,
type = type,
directed = directed
)
}
94 changes: 94 additions & 0 deletions tests/testthat/_snaps/migration-fixture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# recovery deprecation messages

Code
x <- migration_fixture("g", 5, 1:3, "in", TRUE)
Condition
Warning:
Calling `migration_fixture()` with positional or abbreviated arguments was deprecated in igraph 3.0.0.
i Detected call: migration_fixture(graph, n, weight, type, directed)
i Use instead: migration_fixture(graph, n, weights = , type = , directed = )

---

Code
x <- migration_fixture("g", 5, 1:3)
Condition
Warning:
Calling `migration_fixture()` with positional or abbreviated arguments was deprecated in igraph 3.0.0.
i Detected call: migration_fixture(graph, n, weight)
i Use instead: migration_fixture(graph, n, weights = )

---

Code
x <- migration_fixture("g", 5, weight = 1:3)
Condition
Warning:
Calling `migration_fixture()` with positional or abbreviated arguments was deprecated in igraph 3.0.0.
i Detected call: migration_fixture(graph, n, weight)
i Use instead: migration_fixture(graph, n, weights = )

---

Code
x <- migration_fixture("g", 5, ty = "in")
Condition
Warning:
Calling `migration_fixture()` with positional or abbreviated arguments was deprecated in igraph 3.0.0.
i Detected call: migration_fixture(graph, n, type)
i Use instead: migration_fixture(graph, n, type = )

---

Code
x <- migration_fixture("g", 5, dir = TRUE)
Condition
Warning:
Calling `migration_fixture()` with positional or abbreviated arguments was deprecated in igraph 3.0.0.
i Detected call: migration_fixture(graph, n, directed)
i Use instead: migration_fixture(graph, n, directed = )

---

Code
x <- migration_fixture("g", 5, 1:3, dir = TRUE)
Condition
Warning:
Calling `migration_fixture()` with positional or abbreviated arguments was deprecated in igraph 3.0.0.
i Detected call: migration_fixture(graph, n, weight, directed)
i Use instead: migration_fixture(graph, n, weights = , directed = )

# error message snapshots

Code
migration_fixture("g", 5, weig = 1)
Condition
Error in `migration_fixture()`:
! Argument `weig` matches multiple arguments of `migration_fixture()`.

---

Code
migration_fixture("g", 5, foo = 1)
Condition
Error in `migration_fixture()`:
! Unexpected argument passed to `migration_fixture()`: `foo`.
i Arguments after `...` must be spelled out in full.

---

Code
migration_fixture("g", 5, 1:3, weights = 9)
Condition
Error in `migration_fixture()`:
! Argument `weights` of `migration_fixture()` was supplied more than once.
i Pass it exactly once, by its new name `weights`.

---

Code
migration_fixture("g", 5, 1, 2, 3, 4)
Condition
Error in `migration_fixture()`:
! Too many arguments passed to `migration_fixture()`.

21 changes: 21 additions & 0 deletions tests/testthat/helper-migrations.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Keep the in-place ARG_HANDLE blocks in sync with the registry during
# development: regenerate them before the tests run so editing tools/migrations.R
# and re-running the tests refreshes the spliced code.
#
# tools/ is excluded from the built package (.Rbuildignore), so this is a no-op
# under `R CMD check` of the tarball; it only fires from a source checkout
# (devtools::test(), covr). The generator only rewrites a file when its block
# actually changes, so a clean tree stays clean. CI additionally guards against
# committed drift (see .github/workflows/custom/before-install).
local({
registry <- testthat::test_path("..", "..", "tools", "migrations.R")
generator <- testthat::test_path("..", "..", "tools", "generate-migrations.R")
if (!file.exists(registry) || !file.exists(generator)) {
return(invisible())
}
src_dir <- testthat::test_path("..", "..", "R")

gen_env <- new.env()
sys.source(generator, envir = gen_env)
suppressMessages(gen_env$generate_migrations(registry, src_dir))
})
Loading
Loading