yonicd / foreman

Unpacking, Interrogating and Subsetting R packages
Other
44 stars 0 forks source link

Caller-callee relationship for .R files in a package #2

Open krlmlr opened 3 years ago

krlmlr commented 3 years ago

In R packages, I try to organize the code in .R files so that if a function in a.R calls a function in b.R, no function from b.R calls back to a.R, also not indirectly. How can foreman help verify or visualize this?

Can we do a graphviz visualization with subgraphs -- each file creates a subgraph, each node is a function?

Can we create an igraph object, search for cycles, remove one edge, search again, ... to summarize? (We could label the edges so that they are named by the caller, to identify which function creates the cycle.)

I haven't tried the package yet, appreciate any pointers. Thanks!

yonicd commented 3 years ago

Do you have an example package with a structure like you want?

krlmlr commented 3 years ago

Maybe https://github.com/krlmlr/fledge/?

yonicd commented 3 years ago

assuming you have cloned fledge on your machine and are in the root of the package directory

library(ggraph)
library(igraph)

x <- foreman::unpack()
x_rel <- foreman::relationship(x)
x_rel_df <- as.data.frame(x_rel)
graph <- igraph::graph_from_data_frame(x_rel_df,directed = TRUE)
igraph::V(graph)$parents <- names(igraph::V(graph))

ggraph(graph) +
  geom_edge_link(
    aes(colour = file),
    arrow = grid::arrow(length = unit(0.05, "inches"))) +
  geom_node_text(aes(label = parents),size = 3) +
  labs(title = 'fledge function map') + 
  ggplot2::theme(legend.position   = 'bottom')

The file directional relationship is indicated in the colours.

This info is also in x_rel_df where can you create any output you want.

image

krlmlr commented 3 years ago

Very nice, thanks a lot!

I'm looking for evil circles like this. Here I'm interested in the relationship between files: ideally the directed graph should be cycle-free. This means that it must have a topological sort order. To detect the cycles, I'm computing the strongly connected components -- those with size > 1 are clusters of nodes that can be reached mutually in the connected graph, there must be a cycle in such a cluster. (Can't use igraph::girth() here, its's only for undirected graphs.)

Maybe a variant of this code could find its place here?

library(ggraph)
library(igraph)
library(tidyverse)

setwd("~/git/R/fledge")

x <- foreman::unpack()
#> Warning: No functions found in
#> /home/kirill/git/R/fledge/R/import.R
x_rel <- foreman::relationship(x)
x_rel_df <- as_tibble(as.data.frame(x_rel))

fun_file <-
  x %>% 
  map(names) %>% 
  enframe("file", "fun") %>% 
  unnest(fun)

child_file <- 
  fun_file %>% 
  rename(child = fun, child_file = file)

x_file_rel_df <- 
  x_rel_df %>% 
  left_join(child_file) %>% 
  count(file, child_file)
#> Joining, by = "child"

graph <- igraph::graph_from_data_frame(x_file_rel_df, directed = TRUE)

scc <- igraph::components(graph, "strong")

circle_comp <- which(scc$csize > 1)[[1]]

circle_files <- names(scc$membership)[scc$membership == circle_comp]

x_file_rel_df %>%
  filter(file %in% circle_files & child_file %in% circle_files) %>% 
  filter(file != child_file)
#> # A tibble: 5 x 3
#>   file                 child_file               n
#>   <chr>                <chr>                <int>
#> 1 api-bump-version.R   api-update-version.R     1
#> 2 api-bump-version.R   bump-version.R           1
#> 3 api-update-version.R update-version.R         1
#> 4 bump-version.R       api-update-version.R     1
#> 5 update-version.R     api-bump-version.R       1

plot(igraph::induced_subgraph(graph, circle_files))

Created on 2020-10-23 by the reprex package (v0.3.0)

krlmlr commented 3 years ago

(FWIW, this is a false positive for fledge -- there's a call of the form desc$bump_version(which) that seems to be detected as a call to fledge::bump_version() . If I remove that call, the cycle vanishes.)

library(ggraph)
library(igraph)
library(tidyverse)

setwd("~/git/R/fledge")

x <- foreman::unpack()
#> Warning: No functions found in
#> /home/kirill/git/R/fledge/R/import.R
x_rel <- foreman::relationship(x)
x_rel_df <- as_tibble(as.data.frame(x_rel))

fun_file <-
  x %>% 
  map(names) %>% 
  enframe("file", "fun") %>% 
  unnest(fun)

child_file <- 
  fun_file %>% 
  rename(child = fun, child_file = file)

x_file_rel_df <- 
  x_rel_df %>% 
  left_join(child_file) %>% 
  select(file, child_file, label = child) %>% 
  filter(file != child_file)
#> Joining, by = "child"

graph <- igraph::graph_from_data_frame(x_file_rel_df, directed = TRUE)

scc <- igraph::components(graph, "strong")

circle_comp <- which(scc$csize > 1)[[1]]

circle_files <- names(scc$membership)[scc$membership == circle_comp]

x_file_rel_df %>%
  filter(file %in% circle_files & child_file %in% circle_files) %>% 
  filter(file != child_file)
#> # A tibble: 5 x 3
#>   file                 child_file           label              
#>   <chr>                <chr>                <chr>              
#> 1 api-bump-version.R   api-update-version.R check_which        
#> 2 api-bump-version.R   bump-version.R       bump_version_impl  
#> 3 api-update-version.R update-version.R     update_version_impl
#> 4 bump-version.R       api-update-version.R update_version     
#> 5 update-version.R     api-bump-version.R   bump_version

plot(igraph::induced_subgraph(graph, circle_files))

Created on 2020-10-23 by the reprex package (v0.3.0)