Skip to main content

srcgraph_core/
graphml.rs

1use std::collections::HashMap;
2
3use petgraph::graph::NodeIndex;
4use quick_xml::events::Event;
5use quick_xml::Reader;
6use thiserror::Error;
7
8use crate::owned::{OwnedClassNode, OwnedGraph};
9use crate::EdgeType;
10
11#[derive(Debug, Error)]
12pub enum Error {
13    #[error("XML: {0}")]
14    Xml(#[from] quick_xml::Error),
15    #[error("JSON for key '{key}': {source}")]
16    Json {
17        key: String,
18        #[source]
19        source: serde_json::Error,
20    },
21    #[error("missing required node attribute '{0}'")]
22    MissingAttr(String),
23    #[error("invalid integer for '{key}': {source}")]
24    ParseInt {
25        key: String,
26        #[source]
27        source: std::num::ParseIntError,
28    },
29    #[error("unknown edge type '{0}'")]
30    UnknownEdgeType(String),
31    #[error("UTF-8: {0}")]
32    Utf8(#[from] std::string::FromUtf8Error),
33}
34
35// ── reader ────────────────────────────────────────────────────────────────────
36
37fn attr_str(e: &quick_xml::events::BytesStart<'_>, name: &[u8]) -> Option<String> {
38    e.attributes()
39        .filter_map(|a| a.ok())
40        .find(|a| a.key.local_name().as_ref() == name)
41        .and_then(|a| String::from_utf8(a.value.into_owned()).ok())
42}
43
44fn parse_edge_type(s: &str) -> Result<EdgeType, Error> {
45    match s {
46        "MethodCall" => Ok(EdgeType::MethodCall),
47        "FieldRef" => Ok(EdgeType::FieldRef),
48        "Inheritance" => Ok(EdgeType::Inheritance),
49        other => Err(Error::UnknownEdgeType(other.to_owned())),
50    }
51}
52
53/// Read a GraphML file produced by the visiting-tool extractor into an [`OwnedGraph`].
54pub fn read_graphml(xml: &str) -> Result<OwnedGraph, Error> {
55    let mut reader = Reader::from_str(xml);
56
57    let mut graph = OwnedGraph::new();
58    let mut node_index: HashMap<String, NodeIndex> = HashMap::new();
59
60    // Flat parsing state — no recursion needed for this schema.
61    let mut in_node = false;
62    let mut in_edge = false;
63    let mut in_data = false;
64    let mut current_node_id = String::new();
65    let mut current_edge_source = String::new();
66    let mut current_edge_target = String::new();
67    let mut current_data: HashMap<String, String> = HashMap::new();
68    let mut current_data_key = String::new();
69    // Deferred edges so the graph is correct even if edges precede nodes.
70    let mut pending_edges: Vec<(String, String, String)> = Vec::new();
71
72    loop {
73        match reader.read_event()? {
74            Event::Start(ref e) => match e.name().local_name().as_ref() {
75                b"node" => {
76                    in_node = true;
77                    current_node_id = attr_str(e, b"id").unwrap_or_default();
78                    current_data.clear();
79                }
80                b"edge" => {
81                    in_edge = true;
82                    current_edge_source = attr_str(e, b"source").unwrap_or_default();
83                    current_edge_target = attr_str(e, b"target").unwrap_or_default();
84                    current_data.clear();
85                }
86                b"data" => {
87                    in_data = true;
88                    current_data_key = attr_str(e, b"key").unwrap_or_default();
89                }
90                _ => {}
91            },
92            Event::End(ref e) => match e.name().local_name().as_ref() {
93                b"node" => {
94                    let node = OwnedClassNode::from_data(&current_node_id, &current_data)?;
95                    let idx = graph.add_node(node);
96                    node_index.insert(current_node_id.clone(), idx);
97                    in_node = false;
98                }
99                b"edge" => {
100                    let type_str = current_data
101                        .get("type")
102                        .cloned()
103                        .unwrap_or_else(|| "MethodCall".to_owned());
104                    pending_edges.push((
105                        current_edge_source.clone(),
106                        current_edge_target.clone(),
107                        type_str,
108                    ));
109                    in_edge = false;
110                }
111                b"data" => {
112                    in_data = false;
113                }
114                _ => {}
115            },
116            Event::Text(ref e) => {
117                if in_data && (in_node || in_edge) {
118                    if let Ok(text) = e.unescape() {
119                        current_data.insert(current_data_key.clone(), text.into_owned());
120                    }
121                }
122            }
123            // CDATA sections (used by write_graphml for JSON blobs)
124            Event::CData(ref e) => {
125                if in_data && (in_node || in_edge) {
126                    if let Ok(s) = std::str::from_utf8(e.as_ref()) {
127                        current_data.insert(current_data_key.clone(), s.to_owned());
128                    }
129                }
130            }
131            Event::Eof => break,
132            _ => {}
133        }
134    }
135
136    for (src, tgt, type_str) in pending_edges {
137        let edge_type = parse_edge_type(&type_str)?;
138        if let (Some(&s), Some(&t)) = (node_index.get(&src), node_index.get(&tgt)) {
139            graph.add_edge(s, t, edge_type);
140        }
141    }
142
143    Ok(graph)
144}
145
146// ── writer ────────────────────────────────────────────────────────────────────
147
148/// Serialize an [`OwnedGraph`] to GraphML, matching the visiting-tool extractor's schema.
149pub fn write_graphml(graph: &OwnedGraph) -> Result<String, Error> {
150    use quick_xml::events::{BytesCData, BytesDecl, BytesEnd, BytesStart, BytesText, Event};
151    use quick_xml::Writer;
152
153    let mut w = Writer::new(Vec::new());
154
155    w.write_event(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)))?;
156    w.write_event(Event::Text(BytesText::new("\n")))?;
157
158    let mut graphml = BytesStart::new("graphml");
159    graphml.push_attribute(("xmlns", "http://graphml.graphdrawing.org/xmlns"));
160    w.write_event(Event::Start(graphml))?;
161    w.write_event(Event::Text(BytesText::new("\n")))?;
162
163    // Key declarations (node attributes)
164    let node_keys: &[(&str, &str)] = &[
165        ("name", "string"),
166        ("namespace", "string"),
167        ("lineCount", "int"),
168        ("methodCount", "int"),
169        ("methodConnectivity", "string"),
170        ("halstead_eta1", "int"),
171        ("halstead_eta2", "int"),
172        ("halstead_N1", "int"),
173        ("halstead_N2", "int"),
174        ("stateTransitions", "string"),
175        ("invariants", "string"),
176        ("tenantBranches", "string"),
177        ("errorMessages", "string"),
178        ("deadCode", "string"),
179        ("methodFingerprints", "string"),
180        ("magicNumbers", "string"),
181        ("callSequences", "string"),
182        ("cyclomaticComplexity", "string"),
183        ("methodTokens", "string"),
184        ("pathConditions", "string"),
185    ];
186    for (id, attr_type) in node_keys {
187        let mut k = BytesStart::new("key");
188        k.push_attribute(("id", *id));
189        k.push_attribute(("for", "node"));
190        k.push_attribute(("attr.name", *id));
191        k.push_attribute(("attr.type", *attr_type));
192        w.write_event(Event::Empty(k))?;
193        w.write_event(Event::Text(BytesText::new("\n")))?;
194    }
195    {
196        let mut k = BytesStart::new("key");
197        k.push_attribute(("id", "type"));
198        k.push_attribute(("for", "edge"));
199        k.push_attribute(("attr.name", "type"));
200        k.push_attribute(("attr.type", "string"));
201        w.write_event(Event::Empty(k))?;
202        w.write_event(Event::Text(BytesText::new("\n")))?;
203    }
204
205    let mut graph_elem = BytesStart::new("graph");
206    graph_elem.push_attribute(("id", "G"));
207    graph_elem.push_attribute(("edgedefault", "directed"));
208    w.write_event(Event::Start(graph_elem))?;
209    w.write_event(Event::Text(BytesText::new("\n")))?;
210
211    // Nodes
212    for idx in graph.node_indices() {
213        let n = &graph[idx];
214        let mut node_elem = BytesStart::new("node");
215        node_elem.push_attribute(("id", n.id.as_str()));
216        w.write_event(Event::Start(node_elem))?;
217        w.write_event(Event::Text(BytesText::new("\n")))?;
218
219        // Helper: write <data key="k">plain_text</data>
220        // Plain values (names, integers) cannot contain XML-special chars.
221        macro_rules! text_data {
222            ($key:expr, $val:expr) => {{
223                let mut d = BytesStart::new("data");
224                d.push_attribute(("key", $key));
225                w.write_event(Event::Start(d))?;
226                w.write_event(Event::Text(BytesText::new($val)))?;
227                w.write_event(Event::End(BytesEnd::new("data")))?;
228                w.write_event(Event::Text(BytesText::new("\n")))?;
229            }};
230        }
231
232        // Helper: write <data key="k"><![CDATA[json]]></data>
233        // JSON blobs may contain <, >, &, " — CDATA avoids escaping.
234        macro_rules! json_data {
235            ($key:expr, $val:expr) => {{
236                let json = serde_json::to_string($val).unwrap_or_default();
237                let mut d = BytesStart::new("data");
238                d.push_attribute(("key", $key));
239                w.write_event(Event::Start(d))?;
240                w.write_event(Event::CData(BytesCData::new(json.as_str())))?;
241                w.write_event(Event::End(BytesEnd::new("data")))?;
242                w.write_event(Event::Text(BytesText::new("\n")))?;
243            }};
244        }
245
246        text_data!("name", n.name.as_str());
247        text_data!("namespace", n.namespace.as_str());
248        text_data!("lineCount", &n.line_count.to_string());
249        text_data!("methodCount", &n.method_count.to_string());
250
251        if n.halstead_eta1 > 0 || n.halstead_eta2 > 0 || n.halstead_n1 > 0 || n.halstead_n2 > 0 {
252            text_data!("halstead_eta1", &n.halstead_eta1.to_string());
253            text_data!("halstead_eta2", &n.halstead_eta2.to_string());
254            text_data!("halstead_N1", &n.halstead_n1.to_string());
255            text_data!("halstead_N2", &n.halstead_n2.to_string());
256        }
257
258        if let Some(mc) = &n.method_connectivity {
259            json_data!("methodConnectivity", mc);
260        }
261        if let Some(v) = &n.method_fingerprints {
262            json_data!("methodFingerprints", v);
263        }
264        if let Some(v) = &n.call_sequences {
265            json_data!("callSequences", v);
266        }
267        if let Some(v) = &n.cyclomatic_complexity {
268            json_data!("cyclomaticComplexity", v);
269        }
270        if let Some(v) = &n.method_tokens {
271            json_data!("methodTokens", v);
272        }
273        if let Some(v) = &n.path_conditions {
274            json_data!("pathConditions", v);
275        }
276        if let Some(v) = &n.invariants {
277            json_data!("invariants", v);
278        }
279        if let Some(v) = &n.error_messages {
280            json_data!("errorMessages", v);
281        }
282        if let Some(v) = &n.magic_numbers {
283            json_data!("magicNumbers", v);
284        }
285        if let Some(v) = &n.dead_code {
286            json_data!("deadCode", v);
287        }
288        if let Some(v) = &n.tenant_branches {
289            json_data!("tenantBranches", v);
290        }
291        if let Some(v) = &n.state_transitions {
292            json_data!("stateTransitions", v);
293        }
294
295        w.write_event(Event::End(BytesEnd::new("node")))?;
296        w.write_event(Event::Text(BytesText::new("\n")))?;
297    }
298
299    // Edges
300    for edge_idx in graph.edge_indices() {
301        let (src_idx, tgt_idx) = graph.edge_endpoints(edge_idx).unwrap();
302        let src_id = &graph[src_idx].id;
303        let tgt_id = &graph[tgt_idx].id;
304        let edge_type = graph.edge_weight(edge_idx).unwrap();
305
306        let mut e = BytesStart::new("edge");
307        e.push_attribute(("source", src_id.as_str()));
308        e.push_attribute(("target", tgt_id.as_str()));
309        w.write_event(Event::Start(e))?;
310        w.write_event(Event::Text(BytesText::new("\n")))?;
311
312        let type_str = match edge_type {
313            EdgeType::MethodCall => "MethodCall",
314            EdgeType::FieldRef => "FieldRef",
315            EdgeType::Inheritance => "Inheritance",
316        };
317        let mut d = BytesStart::new("data");
318        d.push_attribute(("key", "type"));
319        w.write_event(Event::Start(d))?;
320        w.write_event(Event::Text(BytesText::new(type_str)))?;
321        w.write_event(Event::End(BytesEnd::new("data")))?;
322        w.write_event(Event::Text(BytesText::new("\n")))?;
323
324        w.write_event(Event::End(BytesEnd::new("edge")))?;
325        w.write_event(Event::Text(BytesText::new("\n")))?;
326    }
327
328    w.write_event(Event::End(BytesEnd::new("graph")))?;
329    w.write_event(Event::Text(BytesText::new("\n")))?;
330    w.write_event(Event::End(BytesEnd::new("graphml")))?;
331
332    Ok(String::from_utf8(w.into_inner())?)
333}