imputesrcref

imputesrcref imputes transparent srcref metadata for injected brace calls ({) in R function ASTs.

srcref primer

In R, a srcref is source-location metadata (line/column ranges) attached to parsed objects when source retention is enabled (for example with options(keep.source = TRUE)). This package solves a finer-grained mapping problem for control-flow expressions written without braces (for example if (x) f() else g()) and for unbraced function-call arguments. It adds transparent wrappers and imputes srcrefs from parse data so individual components can be mapped to source precisely. It uses the parser token tables getParseData for it.

What it does

Given code like:

if (x && y) f() else g()

impute_srcrefs() rewrites target positions to:

if ({x} && {y}) {f()} else {g()}

and:

g({x+1}, {f({y+1})})

and assigns srcrefs to injected { calls using parse-data-derived source spans.

Exported API

Installed packages: srcref retention

impute_package_srcrefs() requires each function to carry a srcref attribute. Source installs retain this by default. Binary installs often do too, but if srcref is missing the function will be skipped (failed[i] == "no srcref"). To force retention explicitly, install from source with:

install.packages("<package>", INSTALL_opts = "--with-keep.source")

Cached parse data (--with-keep.parse.data) is no longer required — impute_srcrefs() re-parses from the function's source lines when no parse data is attached.

For functions that truly lack srcref (e.g. some generated closures), options(imputesrcref.allow_deparse_fallback = TRUE) will impute against a deparsed copy of the function instead, at the cost of source line numbers shifting to the deparsed layout.

Usage

Patch functions

options(keep.source = TRUE)
f <- eval(parse(text = "function(x, y) if (x && y) f() else g()", keep.source = TRUE)[[1]])
g <- impute_srcrefs(f)
g

Source a file and patch all loaded functions

env <- new.env(parent = baseenv())
res <- source_impute_srcrefs("path/to/file.R", envir = env)
res$functions

Patch an installed package

res <- impute_package_srcrefs("stringr", include_internal = TRUE)
res$patched_count
head(res$failed[!is.na(res$failed)])

Returned fields are:

Blacklist API

Generic call wrapping excludes SPECIALSXP primitive names by default (from builtins()). Add your own exclusions with:

set_impute_blacklist(c("str_c", "paste0"))

Inspect active values:

head(get_impute_blacklist())
get_impute_blacklist(include_default = FALSE)

Functions without srcref metadata

impute_srcrefs() requires function srcref metadata by default. For functions created without srcrefs, opt into deparse-based fallback explicitly:

options(imputesrcref.allow_deparse_fallback = TRUE)

Tests

Snapshot tests compare generated output against committed .out golden files. They include regression coverage for:

Rscript -e "testthat::test_dir('tests/testthat', load_package = 'source')"

Refresh snapshots:

UPDATE_SNAPSHOTS=1 Rscript -e "testthat::test_dir('tests/testthat', load_package = 'source')"

By default, mismatches fail. With UPDATE_SNAPSHOTS=1, the snapshot file is rewritten.

Package corpus tests

tests/testthat/test-package-srcref-imputation.R runs whole-package srcref imputation over a corpus of popular packages (data.table, dplyr, fs, ggplot2, glue, jsonlite, stringr, zoo). For each it asserts that no function fails for an unexpected reason and that a sample of patched functions is idempotent and srcref-text-consistent (the line-accuracy guarantee).

These tests are slow and require the corpus packages to be installed with srcref retention, so they are gated behind FULL_TEST=1 and skip gracefully when a package is absent or was installed without srcref.

The Makefile provides targets for this. The corpus is installed into a separate library (CORPUS_LIB, default ~/Rlib_test) so your main library is left untouched:

make corpus-install   # install the 8 corpus packages with srcref (run once)
make test-full        # run the full suite including the corpus tests

Use a different library with make test-full CORPUS_LIB=/path/to/lib. Or run it directly:

FULL_TEST=1 Rscript -e \
  ".libPaths(c('~/Rlib_test', .libPaths())); testthat::test_file('tests/testthat/test-package-srcref-imputation.R')"

Implementation

The package's internals are implemented in C and invoked from thin R wrappers via .Call. Building from source requires a C toolchain (Rtools on Windows, the standard R CMD INSTALL toolchain on macOS / Linux). No external libraries are needed.

Acknowledgements

This package was inspired in part by covr's parse-data handling approach.