box
box copied to clipboard
Extract and visualize dependencies
Please describe your feature request
Great package Konrad! I love it and find it very useful for complicated projects.
One thing that I was missing every now and then was a way to extract and maybe visualize dependencies of a file/function.
Do you think something like this is worth adding to box
?
I'd be happy to create a proper draft PR (with less dependencies, cleaner code, tests, etc).
Quick Example
A quick-and-dirty function I threw together looks like this: get_box_dependencies
which either takes a file or a text and reports its dependencies:
library(stringr)
# extracts the box dependencies from a file or a text string
get_box_dependencies <- function(file = NULL, text = NULL) {
stopifnot(
"Either file or text must be provided but not both" = xor(is.null(file),
is.null(text)))
if (!is.null(file)) text <- paste(readLines(file), collapse = "\n")
# remove commented code
x <- text |> str_replace_all("#[^\\n]+", "")
# extract box dependencies
box_txt <- str_extract_all(x, "(?<=box::use\\()[^\\)]*") |>
unlist() |>
paste(collapse = "\n") |>
str_replace_all("\\n", " ")
if (nchar(box_txt) == 0) {
return(tibble::tibble(
type = character(0),
dependency = character(0),
exports = list(),
exports_agg = character(0)
))
}
# [a-zA-Z0-9\\.\\/]+ Name regex for the packages/files
# (\\[[^\\]]+\\])? Maybe followed by text in []
deps <- str_extract_all(box_txt, "[a-zA-Z0-9_\\.\\/]+(\\[[^\\]]+\\])?")[[1]]
is_file <- str_detect(deps, "\\/")
reps_res <- deps |> str_replace_all("\\[[^\\]]+\\]", "")
export_names <- deps |>
str_extract_all("(?<=\\[)[^\\]]+(?=\\])") |>
sapply(str_split, pattern = ", *")
tibble::tibble(
type = ifelse(is_file, "file", "package"),
dependency = ifelse(!endsWith(reps_res, ".R") & is_file,
paste0(reps_res, ".R"), reps_res),
exports = export_names,
exports_agg = sapply(export_names, \(x) paste(unlist(x), collapse = " "))
)
}
An example of this function is this
test_string <- "
box::use(
pkg1[fun1, fun2,
fun3, fun4],
pkg2[...],
pkg3
# pkg4[fun5] not used
)
box::use(
./file1 ,
./file2[ffun1, ffun2,
ffun3],
folder/file3[...]
)
"
get_box_dependencies(text = test_string)
#> # A tibble: 6 × 4
#> type dependency exports exports_agg
#> <chr> <chr> <list> <chr>
#> 1 package pkg1 <list [1]> "fun1 fun2 fun3 fun4"
#> 2 package pkg2 <list [1]> "..."
#> 3 package pkg3 <list [0]> ""
#> 4 file ./file1.R <list [0]> ""
#> 5 file ./file2.R <list [1]> "ffun1 ffun2 ffun3"
#> 6 file folder/file3.R <list [1]> "..."
A larger (but non-reproducible) example output is this code which lists all dependencies and its connections in a directory app/
files <- list.files("app", pattern = "\\.R$", full.names = TRUE, recursive = TRUE)
> res <- purrr::map_dfr(files, get_box_dependencies, .id = "file") |>
+ dplyr::mutate(file = files[as.numeric(file)])
> res
# A tibble: 116 × 5
file type dependency exports exports_agg
<chr> <chr> <chr> <list> <chr>
1 app/logic/misc.R package shiny <chr [1]> div
2 app/logic/misc.R package shinyWidgets <chr [1]> downloadBttn
3 app/logic/misc.R package data.table <chr [1]> ...
4 app/logic/misc.R package dplyr <chr [1]> group_by
5 app/main.R package shiny <list [1]> div h1 NS tagList moduleServer sliderInput i…
Caveats to the solution above:
- this does not work when
box
is used within a function. Eg the followingbar
dependency is picked up but is not reported to belong tofoo
#' @export
foo <- function(...) {
box::use(bar[baz])
}
- At the moment the function depends on
stringr
andtibble
(to a lesser degree) and lots of regex. This can be brought down if needed.
One advantage of having this, is that it would also enable box to warn the user if a function is imported using box::use()
but not actually used.