Introduction

This vignette explains the hotpatchR design and why runtime namespace patching is the right tool for legacy hotfix workflows.

The legacy package lockdown problem

A loaded R package lives in a locked namespace. Internal functions are resolved from inside the package bubble, so calling a hidden helper from the global environment does not change the package’s internal behavior.

This is the namespace trap that makes legacy hotfixing difficult: a visible exported function may still invoke a broken internal helper even after you have sourced a fixed version elsewhere.

Why the global workaround fails

The usual workaround is:

  • identify the broken internal function
  • find every package caller that depends on it
  • copy the broken internals into a hotfix script
  • source the script into the global environment
  • run tests and hope the package resolves the new bindings

That workflow is brittle because a bug in one hidden helper can force you to patch many callers, even when only one implementation needs to change.

hotpatchR philosophy

Instead of pulling functions out into the global environment, hotpatchR performs surgical edits inside the package namespace. That means:

  • fix only the broken internal function
  • exported callers automatically resolve to the updated implementation
  • the package remains loaded and otherwise unchanged

Basic workflow

Before using the package, install it from CRAN or GitHub:

install.packages("hotpatchR")
# or, for the development version:
# remotes::install_github("munoztd0/hotpatchR")

The package includes a real example of this pattern with an exported parent function and an internal child helper.

library(hotpatchR)

baseline <- dummy_parent_func("test")
print(baseline)
#> [1] "Parent output -> I am the BROKEN child. Input: test"
#> "Parent output -> I am the BROKEN child. Input: test"

inject_patch(
  pkg = "hotpatchR",
  patch_list = list(
    dummy_child_func = function(x) {
      paste("I am the FIXED child! Input:", x)
    }
  )
)

patched_result <- dummy_parent_func("test")
print(patched_result)
#> [1] "Parent output -> I am the FIXED child! Input: test"
#> "Parent output -> I am the FIXED child! Input: test"

How inject_patch works

inject_patch()

  1. identifies the target namespace or environment
  2. unlocks the binding for the named object
  3. assigns the replacement function into that environment
  4. re-locks the binding

Because the replacement function can be defined with the package namespace as its parent, it still has access to the package’s internal helpers.

Rolling back a patch

If you need to restore the original binding, undo_patch() reverses the previous change.

undo_patch(pkg = "hotpatchR", names = "dummy_child_func")
restored_result <- dummy_parent_func("test")
print(restored_result)
#> [1] "Parent output -> I am the BROKEN child. Input: test"
#> "Parent output -> I am the BROKEN child. Input: test"

Hotfix scripts

apply_hotfix_file() is a convenience wrapper for scripted hotfix application. A compatible hotfix file should define:

  • pkg (optional, if not passed explicitly)
  • patch_list, a named list of replacement functions

Example hotfix file for this package:

#
pkg <- "hotpatchR"
patch_list <- list(
  dummy_child_func = function(x) {
    paste("I am the FIXED child! Input:", x)
  }
)

Then apply it with:

apply_hotfix_file("dev/hotpatchR_hotfix.R")

Next steps

The current package is focused on the core runtime patching path. Future enhancements may include patch comparison, dependency scanning, and CI-friendly test wrappers that explicitly preserve the patched namespace during test execution.