feat: sync 12 new skills into catalog (total 31 skills), update manifest and README
This commit is contained in:
144
skills/drawio-skill/scripts/goimports.py
Normal file
144
skills/drawio-skill/scripts/goimports.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract a Go module's package-import graph as autolayout graph JSON.
|
||||
|
||||
The Go counterpart to pyimports.py / jsimports.py. Reads the module path from
|
||||
go.mod, walks the module, treats each directory of .go files as one package
|
||||
(node = its import path), and records the intra-module package imports. Stdlib
|
||||
and third-party imports are ignored. Transitive reduction is on by default so
|
||||
the diagram stays readable.
|
||||
|
||||
python3 goimports.py ./mymodule -o graph.json
|
||||
python3 autolayout.py graph.json -o diagram.drawio
|
||||
|
||||
Parsing is regex-based over `import` statements (single and block form),
|
||||
which is enough for a structural package graph. *_test.go files and vendor/
|
||||
are skipped.
|
||||
|
||||
Usage: python3 goimports.py <module_dir> [-o graph.json] [--direction TB|LR]
|
||||
[--group] [--no-reduce]
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
MODULE = re.compile(r"^module\s+(\S+)", re.MULTILINE)
|
||||
BLOCK = re.compile(r"import\s*\((.*?)\)", re.DOTALL)
|
||||
SINGLE = re.compile(r'import\s+(?:[\w.]+\s+|_\s+)?"([^"]+)"')
|
||||
QUOTED = re.compile(r'"([^"]+)"')
|
||||
|
||||
|
||||
def module_path(root):
|
||||
"""Read the `module` path from go.mod at the module root, or None."""
|
||||
gomod = os.path.join(root, "go.mod")
|
||||
if not os.path.exists(gomod):
|
||||
return None
|
||||
with open(gomod, encoding="utf-8", errors="ignore") as f:
|
||||
m = MODULE.search(f.read())
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def discover(root, modpath):
|
||||
"""Map package import path -> list of .go files (one entry per directory)."""
|
||||
root = os.path.abspath(root)
|
||||
pkgs = {}
|
||||
for dirpath, dirs, files in os.walk(root):
|
||||
dirs[:] = [d for d in dirs
|
||||
if d not in ("vendor", "testdata") and not d.startswith(".")]
|
||||
gofiles = [os.path.join(dirpath, f) for f in files
|
||||
if f.endswith(".go") and not f.endswith("_test.go")]
|
||||
if not gofiles:
|
||||
continue
|
||||
rel = os.path.relpath(dirpath, root).replace(os.sep, "/")
|
||||
ip = modpath if rel == "." else f"{modpath}/{rel}"
|
||||
pkgs[ip] = gofiles
|
||||
return pkgs
|
||||
|
||||
|
||||
def imports_of(files, modpath, pkgs):
|
||||
"""Intra-module package import paths referenced by a package's files."""
|
||||
found = set()
|
||||
for path in files:
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="ignore") as f:
|
||||
src = f.read()
|
||||
except OSError:
|
||||
continue
|
||||
specs = []
|
||||
for block in BLOCK.findall(src):
|
||||
specs += QUOTED.findall(block)
|
||||
specs += SINGLE.findall(src)
|
||||
for spec in specs:
|
||||
if (spec == modpath or spec.startswith(modpath + "/")) and spec in pkgs:
|
||||
found.add(spec)
|
||||
return found
|
||||
|
||||
|
||||
def transitive_reduce(nodes, edges):
|
||||
"""Drop edges implied by a longer path, via Graphviz `tred`."""
|
||||
idx = {n: i for i, n in enumerate(nodes)}
|
||||
dot = "digraph{" + "".join(f"{idx[s]}->{idx[t]};" for s, t in edges) + "}"
|
||||
try:
|
||||
out = subprocess.run(["tred"], input=dot, capture_output=True,
|
||||
text=True, check=True).stdout
|
||||
except (FileNotFoundError, subprocess.CalledProcessError) as exc:
|
||||
sys.stderr.write(f"warning: tred unavailable, keeping all edges ({exc})\n")
|
||||
return edges
|
||||
rev = {i: n for n, i in idx.items()}
|
||||
return [(rev[int(a)], rev[int(b)]) for a, b in re.findall(r"(\d+)\s*->\s*(\d+)", out)]
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Go import graph -> autolayout graph JSON.")
|
||||
ap.add_argument("module", help="module directory (contains go.mod)")
|
||||
ap.add_argument("-o", "--output", help="output JSON path (default: stdout)")
|
||||
ap.add_argument("--direction", default="TB", choices=["TB", "LR"])
|
||||
ap.add_argument("--group", action="store_true",
|
||||
help="group nodes into containers by top-level package dir")
|
||||
ap.add_argument("--no-reduce", action="store_true",
|
||||
help="keep every edge (skip transitive reduction)")
|
||||
args = ap.parse_args()
|
||||
|
||||
modpath = module_path(args.module)
|
||||
if not modpath:
|
||||
sys.exit(f"error: no go.mod with a module path found in {args.module}")
|
||||
pkgs = discover(args.module, modpath)
|
||||
if not pkgs:
|
||||
sys.exit(f"error: no Go packages found under {args.module}")
|
||||
edges = sorted({(ip, t) for ip, files in pkgs.items()
|
||||
for t in imports_of(files, modpath, pkgs) if t != ip})
|
||||
raw = len(edges)
|
||||
if not args.no_reduce:
|
||||
edges = transitive_reduce(list(pkgs), edges)
|
||||
# Drop the module prefix from labels for readability; ids stay full.
|
||||
strip = modpath + "/"
|
||||
label = lambda ip: ip[len(strip):] if ip.startswith(strip) else os.path.basename(ip)
|
||||
|
||||
def node(ip):
|
||||
d = {"id": ip, "label": label(ip)}
|
||||
if args.group:
|
||||
rest = label(ip).split("/")
|
||||
if len(rest) > 1: # nested under a sub-package
|
||||
d["group"] = "/".join(rest[:-1]) # full sub-package path -> nested boxes
|
||||
return d
|
||||
|
||||
graph = {
|
||||
"direction": args.direction,
|
||||
"nodes": [node(ip) for ip in pkgs],
|
||||
"edges": [{"source": s, "target": t} for s, t in edges],
|
||||
}
|
||||
text = json.dumps(graph, indent=2)
|
||||
if args.output:
|
||||
with open(args.output, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
sys.stderr.write(f"wrote {args.output}\n")
|
||||
else:
|
||||
sys.stdout.write(text)
|
||||
note = "" if args.no_reduce else f" (reduced from {raw})"
|
||||
sys.stderr.write(f"{len(pkgs)} packages, {len(edges)} edges{note}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user