klmr / box

Write reusable, composable and modular R code
https://klmr.me/box/
MIT License
862 stars 48 forks source link

Extract and visualize dependencies #299

Open DavZim opened 1 year ago

DavZim commented 1 year ago

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:

#' @export
foo <- function(...) {
  box::use(bar[baz])
}
DavZim commented 1 year ago

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.