Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

256 lines
7.3 KiB
Go

package exporter
import (
"encoding/json"
"encoding/xml"
"fmt"
"io"
"github.com/zzet/gortex/internal/graph"
)
// WriteGraphML emits the graph as GraphML XML, the de-facto interchange
// format for graph visualizers (yEd, Gephi, Cytoscape Desktop, networkx).
//
// All Gortex node properties are projected to GraphML <data> attributes.
// Free-form Meta is JSON-encoded into a single `meta_json` attribute so no
// information is lost — viewers that don't care about it ignore it.
func WriteGraphML(w io.Writer, g graph.Store, opts Options) (Stats, error) {
cw := &countingWriter{w: w}
nodes, edges, _ := snapshot(g, opts)
stats := Stats{}
if _, err := io.WriteString(cw, xml.Header); err != nil {
return stats, err
}
if _, err := io.WriteString(cw,
`<graphml xmlns="http://graphml.graphdrawing.org/xmlns" `+
`xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" `+
`xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns `+
`http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">`+"\n",
); err != nil {
return stats, err
}
// <key> declarations. Any data element used inside <node> / <edge> needs
// a key. We declare a fixed set covering the strongly-typed gortex fields
// plus a `meta_json` catch-all per element type.
keys := []graphMLKey{
// Node keys.
{ID: "name", For: "node", AttrName: "name", AttrType: "string"},
{ID: "kind", For: "node", AttrName: "kind", AttrType: "string"},
{ID: "qual_name", For: "node", AttrName: "qual_name", AttrType: "string"},
{ID: "file_path", For: "node", AttrName: "file_path", AttrType: "string"},
{ID: "start_line", For: "node", AttrName: "start_line", AttrType: "int"},
{ID: "end_line", For: "node", AttrName: "end_line", AttrType: "int"},
{ID: "language", For: "node", AttrName: "language", AttrType: "string"},
{ID: "repo_prefix", For: "node", AttrName: "repo_prefix", AttrType: "string"},
{ID: "workspace_id", For: "node", AttrName: "workspace_id", AttrType: "string"},
{ID: "project_id", For: "node", AttrName: "project_id", AttrType: "string"},
{ID: "node_meta", For: "node", AttrName: "meta_json", AttrType: "string"},
// Edge keys.
{ID: "edge_kind", For: "edge", AttrName: "kind", AttrType: "string"},
{ID: "confidence", For: "edge", AttrName: "confidence", AttrType: "double"},
{ID: "confidence_label", For: "edge", AttrName: "confidence_label", AttrType: "string"},
{ID: "origin", For: "edge", AttrName: "origin", AttrType: "string"},
{ID: "edge_file_path", For: "edge", AttrName: "file_path", AttrType: "string"},
{ID: "edge_line", For: "edge", AttrName: "line", AttrType: "int"},
{ID: "cross_repo", For: "edge", AttrName: "cross_repo", AttrType: "boolean"},
{ID: "edge_meta", For: "edge", AttrName: "meta_json", AttrType: "string"},
}
for _, k := range keys {
if _, err := fmt.Fprintf(cw,
` <key id=%q for=%q attr.name=%q attr.type=%q/>`+"\n",
k.ID, k.For, k.AttrName, k.AttrType,
); err != nil {
return stats, err
}
}
if _, err := io.WriteString(cw, ` <graph id="G" edgedefault="directed">`+"\n"); err != nil {
return stats, err
}
for _, n := range nodes {
if err := writeGraphMLNode(cw, n); err != nil {
return stats, err
}
stats.NodesWritten++
}
for i, e := range edges {
if err := writeGraphMLEdge(cw, e, i); err != nil {
return stats, err
}
stats.EdgesWritten++
}
if _, err := io.WriteString(cw, " </graph>\n</graphml>\n"); err != nil {
return stats, err
}
stats.BytesWritten = cw.n
return stats, nil
}
type graphMLKey struct {
ID string
For string
AttrName string
AttrType string
}
func writeGraphMLNode(w io.Writer, n *graph.Node) error {
if _, err := fmt.Fprintf(w, ` <node id=%q>`+"\n", n.ID); err != nil {
return err
}
writeData := func(key, val string) error {
if val == "" {
return nil
}
_, err := fmt.Fprintf(w, ` <data key=%q>%s</data>`+"\n", key, xmlEscape(val))
return err
}
writeIntData := func(key string, val int) error {
if val <= 0 {
return nil
}
_, err := fmt.Fprintf(w, ` <data key=%q>%d</data>`+"\n", key, val)
return err
}
if err := writeData("name", n.Name); err != nil {
return err
}
if err := writeData("kind", string(n.Kind)); err != nil {
return err
}
if err := writeData("qual_name", n.QualName); err != nil {
return err
}
if err := writeData("file_path", n.FilePath); err != nil {
return err
}
if err := writeIntData("start_line", n.StartLine); err != nil {
return err
}
if err := writeIntData("end_line", n.EndLine); err != nil {
return err
}
if err := writeData("language", n.Language); err != nil {
return err
}
if err := writeData("repo_prefix", n.RepoPrefix); err != nil {
return err
}
if err := writeData("workspace_id", n.WorkspaceID); err != nil {
return err
}
if err := writeData("project_id", n.ProjectID); err != nil {
return err
}
if metaJSON := metaToJSON(n.Meta); metaJSON != "" {
if err := writeData("node_meta", metaJSON); err != nil {
return err
}
}
_, err := io.WriteString(w, " </node>\n")
return err
}
func writeGraphMLEdge(w io.Writer, e *graph.Edge, idx int) error {
if _, err := fmt.Fprintf(w,
` <edge id="e%d" source=%q target=%q>`+"\n",
idx, e.From, e.To,
); err != nil {
return err
}
writeData := func(key, val string) error {
if val == "" {
return nil
}
_, err := fmt.Fprintf(w, ` <data key=%q>%s</data>`+"\n", key, xmlEscape(val))
return err
}
if err := writeData("edge_kind", string(e.Kind)); err != nil {
return err
}
if e.Confidence > 0 {
if _, err := fmt.Fprintf(w, ` <data key="confidence">%g</data>`+"\n", e.Confidence); err != nil {
return err
}
}
if err := writeData("confidence_label", e.ConfidenceLabel); err != nil {
return err
}
if err := writeData("origin", e.Origin); err != nil {
return err
}
if err := writeData("edge_file_path", e.FilePath); err != nil {
return err
}
if e.Line > 0 {
if _, err := fmt.Fprintf(w, ` <data key="edge_line">%d</data>`+"\n", e.Line); err != nil {
return err
}
}
if e.CrossRepo {
if _, err := io.WriteString(w, ` <data key="cross_repo">true</data>`+"\n"); err != nil {
return err
}
}
if metaJSON := metaToJSON(e.Meta); metaJSON != "" {
if err := writeData("edge_meta", metaJSON); err != nil {
return err
}
}
_, err := io.WriteString(w, " </edge>\n")
return err
}
// xmlEscape escapes XML reserved characters for use inside <data> bodies.
// Uses encoding/xml's EscapeText behind a small wrapper so this stays
// faithful to the standard library's rules without re-implementing them.
func xmlEscape(s string) string {
var buf escapeBuffer
_ = xml.EscapeText(&buf, []byte(s))
return buf.String()
}
type escapeBuffer struct {
bytes []byte
}
func (e *escapeBuffer) Write(p []byte) (int, error) {
e.bytes = append(e.bytes, p...)
return len(p), nil
}
func (e *escapeBuffer) String() string { return string(e.bytes) }
// metaToJSON serializes Meta to a JSON object string. Returns empty when
// Meta is nil/empty so the caller can omit the data element entirely.
func metaToJSON(meta map[string]any) string {
if len(meta) == 0 {
return ""
}
// Use the flattened, sanitized, sorted view so the output is stable.
pairs := flattenMeta(meta)
if len(pairs) == 0 {
return ""
}
out := make(map[string]any, len(pairs))
for _, p := range pairs {
out[p.Key] = p.Value
}
data, err := json.Marshal(out)
if err != nil {
return ""
}
return string(data)
}