-
Notifications
You must be signed in to change notification settings - Fork 375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(gnovm): tool for generating dependency graphs #2635
base: master
Are you sure you want to change the base?
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #2635 +/- ##
==========================================
- Coverage 60.11% 60.11% -0.01%
==========================================
Files 560 560
Lines 74918 74918
==========================================
- Hits 45039 45037 -2
- Misses 26504 26505 +1
- Partials 3375 3376 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cc @gfanton for good taste in tools and a first review :)
gnovm/cmd/gno/depgraph.go
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you put the generating source code in gnovm/pkg/depgraph, so we can hope to reuse it?
I'm also thinking how to have this on the CLI; having it as a subcommand of gno
doesn't feel right. I feel like in go this could be go tool xxx
; idk.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea i agree, having it in gno tool
could actually be a good idea. We could also decide to put this kind of tools inside the contribs
folder, at last for now, an move it in the core code when it's mature enough and/or we find out that we use it really often. wdyt ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super cool! (check my comments for requested changes)
As Morgan says, I'm not sure about keeping this in the root of gno
cli. I think we should either move this to contribs
or a tool
subcommand. cc @moul, wdyt?
gnovm/cmd/gno/depgraph.go
Outdated
file.Close() | ||
} else { // useful for testing - makes a separate graph for each found package |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return early here to avoid enclosing the rest of the method in an else
statement.
gnovm/cmd/gno/depgraph.go
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea i agree, having it in gno tool
could actually be a good idea. We could also decide to put this kind of tools inside the contribs
folder, at last for now, an move it in the core code when it's mature enough and/or we find out that we use it really often. wdyt ?
gnovm/cmd/gno/depgraph.go
Outdated
for _, pkg := range allPkgs { | ||
err := buildGraphData(pkg, allPkgs, make(map[string]bool), make(map[string]bool), &graphData) | ||
|
||
if err != nil { | ||
return fmt.Errorf("error in building graph: %w", err) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole purpose of passing the visited map to this method is to keep track of already visited packages. You need to move the declaration of the visited map above the for loop to ensure there will be no duplicates while iterating between packages.
for _, pkg := range allPkgs { | |
err := buildGraphData(pkg, allPkgs, make(map[string]bool), make(map[string]bool), &graphData) | |
if err != nil { | |
return fmt.Errorf("error in building graph: %w", err) | |
} | |
} | |
visited := make(map[string]bool) | |
for _, pkg := range allPkgs { | |
err := buildGraphData(pkg, allPkgs, visited, make(map[string]bool), &graphData) | |
if err != nil { | |
return fmt.Errorf("error in building graph: %w", err) | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh! Yeah, I was thinking about solving that with a different structure, but this is simpler.
gnovm/cmd/gno/depgraph.go
Outdated
func buildGraphData(pkg gnomod.Pkg, allPkgs []gnomod.Pkg, visited map[string]bool, onStack map[string]bool, graphData *string) error { | ||
if onStack[pkg.Name] { | ||
return fmt.Errorf("cycle detected: %s", pkg.Name) | ||
} | ||
if visited[pkg.Name] { | ||
return nil | ||
} | ||
|
||
visited[pkg.Name] = true | ||
onStack[pkg.Name] = true | ||
|
||
for _, req := range pkg.Requires { | ||
found := false | ||
|
||
for _, candidate := range allPkgs { | ||
if candidate.Name != req { | ||
continue | ||
} | ||
if err := buildGraphData(candidate, allPkgs, visited, onStack, graphData); err != nil { | ||
return err | ||
} | ||
found = true | ||
// this check is wildly inefficient - change this to not work with strings directly | ||
if !strings.Contains(*graphData, "\""+pkg.Name+"\" -> \""+req+"\"\n") { | ||
*graphData += "\"" + pkg.Name + "\" -> \"" + req + "\"\n" | ||
} | ||
} | ||
if !found { | ||
return fmt.Errorf("couldn't find dependency %q for package %q", req, pkg.Name) | ||
} | ||
} | ||
|
||
onStack[pkg.Name] = false | ||
|
||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to check if the line already exists because you are already ensuring that you visit the package and requirements only once.
func buildGraphData(pkg gnomod.Pkg, allPkgs []gnomod.Pkg, visited map[string]bool, onStack map[string]bool, graphData *string) error { | |
if onStack[pkg.Name] { | |
return fmt.Errorf("cycle detected: %s", pkg.Name) | |
} | |
if visited[pkg.Name] { | |
return nil | |
} | |
visited[pkg.Name] = true | |
onStack[pkg.Name] = true | |
for _, req := range pkg.Requires { | |
found := false | |
for _, candidate := range allPkgs { | |
if candidate.Name != req { | |
continue | |
} | |
if err := buildGraphData(candidate, allPkgs, visited, onStack, graphData); err != nil { | |
return err | |
} | |
found = true | |
// this check is wildly inefficient - change this to not work with strings directly | |
if !strings.Contains(*graphData, "\""+pkg.Name+"\" -> \""+req+"\"\n") { | |
*graphData += "\"" + pkg.Name + "\" -> \"" + req + "\"\n" | |
} | |
} | |
if !found { | |
return fmt.Errorf("couldn't find dependency %q for package %q", req, pkg.Name) | |
} | |
} | |
onStack[pkg.Name] = false | |
return nil | |
} | |
func buildGraphData(pkg gnomod.Pkg, allPkgs []gnomod.Pkg, visited map[string]bool, onStack map[string]bool, graphData *string) error { | |
if onStack[pkg.Name] { | |
return fmt.Errorf("cycle detected: %s", pkg.Name) | |
} | |
if visited[pkg.Name] { | |
return nil | |
} | |
visited[pkg.Name] = true | |
onStack[pkg.Name] = true | |
for _, req := range pkg.Requires { | |
var candidate gnomod.Pkg | |
var found bool | |
for _, candidate = range allPkgs { | |
if candidate.Name != req { | |
continue | |
} | |
found = true | |
break | |
} | |
if !found { | |
return fmt.Errorf("couldn't find dependency %q for package %q", req, pkg.Name) | |
} | |
if err := buildGraphData(candidate, allPkgs, visited, onStack, graphData); err != nil { | |
return err | |
} | |
*graphData += "\"" + pkg.Name + "\" -> \"" + req + "\"\n" | |
} | |
onStack[pkg.Name] = false | |
return nil | |
} |
contribs/gnodepgraph/depgraph.go
Outdated
nodeData := "" // nodes | ||
graphData := "" // edges |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Using a strings.Builder
or bytes.Buffer
along with fmt.Fprintf
and passing an io.Writer
to buildGraphData
would have been a cleaner approach than directly writing the output to a string directly.
In my opinion, a suitable location for this tool would be in the 'gno mod graph'. It does not appear to import any new dependencies and is closely integrated with gnomod. |
fs.BoolVar(&c.verbose, "v", false, "verbose output when lintning") | ||
fs.StringVar(&c.rootDir, "root-dir", rootDir, "clone location of github.com/gnolang/gno (gno tries to guess it)") | ||
fs.StringVar(&c.output, "o", "depgraph", "output (file if single graph, dir if multiple graphs)") | ||
fs.BoolVar(&c.multipleGraphs, "m", false, "make a separate graph for each package") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this flag, you should always generate a single graph.
rootDir := gnoenv.RootDir() | ||
fs.BoolVar(&c.verbose, "v", false, "verbose output when lintning") | ||
fs.StringVar(&c.rootDir, "root-dir", rootDir, "clone location of github.com/gnolang/gno (gno tries to guess it)") | ||
fs.StringVar(&c.output, "o", "depgraph", "output (file if single graph, dir if multiple graphs)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove this flag, generate to stdout.
// test for big graph | ||
|
||
// test for fail on cyclical dependencies | ||
|
||
// test for fail on missing dependencies | ||
|
||
// test for not duplicating dependencies |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we need more tests.
|
||
func (c *depGraphCfg) RegisterFlags(fs *flag.FlagSet) { | ||
rootDir := gnoenv.RootDir() | ||
fs.BoolVar(&c.verbose, "v", false, "verbose output when lintning") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be nice to have a non-graphviz format by default, could be just lines with:
A -> B
A -> C
C -> D
something we can easily read and grep.
then, the graphviz format would be a flag.
if err != nil { | ||
return fmt.Errorf("couldn't open output file: %w", err) | ||
} | ||
graphFileData := fmt.Sprintf("Digraph G {\nrankdir=\"LR\"\nranksep=20\n%s\n%s\n}", nodeData.String(), graphData.String()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this code is hard to read.
nodeData.WriteString("}\nsubgraph {\nrank=same\n") | ||
for _, pkg := range allPkgs { | ||
if strings.Contains(pkg.Name, "gno.land/r") { | ||
fmt.Fprintf(&nodeData, "\"%s\" [color=\"red\"]\n", pkg.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should use backticks when you generate things with double quotes.
This is related to issue #2478. For now, this is just an implementation of the graph generation, no CI/CD yet. The tool creates one or more
.dot
files that can be used to generate an SVG via graphviz. To use the tool, just do the following:DOT is a graphviz language for describing graphs. It includes layout information as well. For the final step which makes an SVG you need to have graphviz installed.
The graph below will be produced. I tried several different layout schemes provided by graphviz, and I'm pretty sure that the sheer number of packages and connections is probably always going to make a bit of a mess. This particular layout doesn't clearly show what exactly is being referenced, however it DOES clearly show how often packages are being referenced (blue nodes are p/ and red nodes are r/).
If you would like to more clearly see requires for a specific package, the tool can also be invoked with the
-m
flag ("multiple graphs") which produces a hierarchy of directories mimicking the input structure with a.dot
file at the bottom for each package. For now you would need to manually convert those to SVG files. Example usage:The next steps from here could be:
README
so we can easily track examples..dot
and output directly to SVG (this would require graphviz to be installed ofc)Contributors' checklist...
BREAKING CHANGE: xxx
message was included in the description