use crate::cfg::types::{BlockId, CFGInfo};
fn escape_mermaid_label(s: &str) -> String {
let mut result = String::with_capacity(s.len() * 2);
for c in s.chars() {
match c {
'\\' => result.push_str("\\\\"),
'"' => result.push('\''),
'\n' => result.push(' '),
'\r' => {}
'|' => result.push_str("\\|"),
'&' => result.push_str("&"),
'<' => result.push_str("<"),
'>' => result.push_str(">"),
'{' => result.push_str("#123;"),
'}' => result.push_str("#125;"),
'[' => result.push_str("#91;"),
']' => result.push_str("#93;"),
'`' => result.push('\''),
';' => result.push(','),
'#' => result.push_str("#35;"),
_ => result.push(c),
}
}
result
}
fn escape_dot_label(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "")
.replace('<', "\\<")
.replace('>', "\\>")
.replace('{', "\\{")
.replace('}', "\\}")
.replace('|', "\\|")
}
#[allow(dead_code)]
fn escape_dot_html_label(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
fn sanitize_dot_id(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
if c.is_alphanumeric() || c == '_' {
result.push(c);
} else {
result.push('_');
}
}
if result.starts_with(|c: char| c.is_ascii_digit()) {
result.insert(0, '_');
}
if result.is_empty() {
result.push_str("_anonymous");
}
result
}
fn sorted_block_ids(cfg: &CFGInfo) -> Vec<BlockId> {
let mut ids: Vec<_> = cfg.blocks.keys().copied().collect();
ids.sort_by_key(|id| id.0);
ids
}
fn sorted_edges(cfg: &CFGInfo) -> Vec<&crate::cfg::types::CFGEdge> {
let mut edges: Vec<_> = cfg.edges.iter().collect();
edges.sort_by_key(|e| (e.from.0, e.to.0));
edges
}
pub fn to_mermaid(cfg: &CFGInfo) -> String {
let mut out = String::from("flowchart TD\n");
for id in sorted_block_ids(cfg) {
let block = &cfg.blocks[&id];
let label = escape_mermaid_label(&block.label);
out.push_str(&format!(" B{}[\"{}\"]\n", id.0, label));
}
for edge in sorted_edges(cfg) {
let label = edge.label();
let arrow = if label.is_empty() {
"-->".to_string()
} else {
format!("-->|{}|", escape_mermaid_label(&label))
};
out.push_str(&format!(" B{} {} B{}\n", edge.from.0, arrow, edge.to.0));
}
out
}
pub fn to_dot(cfg: &CFGInfo) -> String {
let graph_name = sanitize_dot_id(&cfg.function_name);
let mut out = format!("digraph {} {{\n", graph_name);
out.push_str(" rankdir=TB;\n");
out.push_str(" node [shape=box, fontname=\"monospace\"];\n");
out.push_str(&format!(
" B{} [style=filled, fillcolor=lightgreen];\n",
cfg.entry.0
));
for exit in &cfg.exits {
out.push_str(&format!(
" B{} [style=filled, fillcolor=lightcoral];\n",
exit.0
));
}
out.push('\n');
for id in sorted_block_ids(cfg) {
let block = &cfg.blocks[&id];
let label = escape_dot_label(&block.label);
out.push_str(&format!(" B{} [label=\"{}\"];\n", id.0, label));
}
out.push('\n');
for edge in sorted_edges(cfg) {
let label = edge.label();
let label_attr = if label.is_empty() {
String::new()
} else {
format!(" [label=\"{}\"]", escape_dot_label(&label))
};
out.push_str(&format!(
" B{} -> B{}{};\n",
edge.from.0, edge.to.0, label_attr
));
}
out.push_str("}\n");
out
}
#[allow(dead_code)]
pub fn to_ascii(cfg: &CFGInfo) -> String {
let mut out = format!("CFG: {}\n", cfg.function_name);
out.push_str(&"=".repeat(40));
out.push('\n');
out.push_str(&format!("Blocks: {}\n", cfg.blocks.len()));
out.push_str(&format!("Edges: {}\n", cfg.edges.len()));
out.push_str(&format!("Complexity: {}\n", cfg.cyclomatic_complexity()));
out.push_str(&format!("Entry: B{}\n", cfg.entry.0));
out.push_str(&format!(
"Exits: {}\n",
cfg.exits
.iter()
.map(|id| format!("B{}", id.0))
.collect::<Vec<_>>()
.join(", ")
));
out.push('\n');
for id in sorted_block_ids(cfg) {
let block = &cfg.blocks[&id];
out.push_str(&format!(
"[B{}] {} (lines {}-{})\n",
id.0, block.label, block.start_line, block.end_line
));
for stmt in &block.statements {
out.push_str(&format!(" {}\n", stmt));
}
if !block.statements.is_empty() {
out.push('\n');
}
}
out.push_str("\nEdges:\n");
for edge in sorted_edges(cfg) {
let label = edge.label();
let label_str = if label.is_empty() {
String::new()
} else {
format!(" [{}]", label)
};
out.push_str(&format!(
" B{} -> B{}{}\n",
edge.from.0, edge.to.0, label_str
));
}
out
}
#[allow(dead_code)]
pub fn to_json(cfg: &CFGInfo) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(cfg)
}
#[allow(dead_code)]
pub fn to_json_compact(cfg: &CFGInfo) -> Result<String, serde_json::Error> {
serde_json::to_string(cfg)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cfg::types::{BlockType, CFGBlock, CFGEdge, EdgeType};
use std::collections::HashMap;
fn sample_cfg() -> CFGInfo {
let mut blocks = HashMap::new();
blocks.insert(
BlockId(0),
CFGBlock {
id: BlockId(0),
label: "entry".to_string(),
block_type: BlockType::Entry,
statements: vec!["x = 1".to_string()],
func_calls: Vec::new(),
start_line: 1,
end_line: 1,
},
);
blocks.insert(
BlockId(1),
CFGBlock {
id: BlockId(1),
label: "if x > 0".to_string(),
block_type: BlockType::Branch,
statements: vec![],
func_calls: Vec::new(),
start_line: 2,
end_line: 2,
},
);
blocks.insert(
BlockId(2),
CFGBlock {
id: BlockId(2),
label: "exit".to_string(),
block_type: BlockType::Exit,
statements: vec!["return x".to_string()],
func_calls: Vec::new(),
start_line: 3,
end_line: 3,
},
);
CFGInfo {
function_name: "test_func".to_string(),
blocks,
edges: vec![
CFGEdge::unconditional(BlockId(0), BlockId(1)),
CFGEdge::new(BlockId(1), BlockId(2), EdgeType::True),
],
entry: BlockId(0),
exits: vec![BlockId(2)],
decision_points: 1, comprehension_decision_points: 0,
nested_cfgs: HashMap::new(),
is_async: false,
await_points: 0,
blocking_calls: Vec::new(),
..Default::default()
}
}
#[test]
fn test_mermaid_output() {
let cfg = sample_cfg();
let mermaid = to_mermaid(&cfg);
assert!(mermaid.starts_with("flowchart TD"));
assert!(mermaid.contains("B0[\"entry\"]"));
assert!(mermaid.contains("B1[\"if x > 0\"]"));
assert!(mermaid.contains("B0 --> B1"));
assert!(mermaid.contains("-->|true|"));
}
#[test]
fn test_mermaid_escaping() {
let label = "test \"quoted\" label\nwith newline|and pipe";
let escaped = escape_mermaid_label(label);
assert!(!escaped.contains('"'));
assert!(!escaped.contains('\n'));
assert!(!escaped.contains('|') || escaped.contains("\\|"));
assert!(escaped.contains("\\|"));
let html_label = "x < y && y > z";
let escaped_html = escape_mermaid_label(html_label);
assert!(escaped_html.contains("<"));
assert!(escaped_html.contains(">"));
assert!(escaped_html.contains("&"));
assert!(!escaped_html.contains(" < "));
assert!(!escaped_html.contains(" > "));
let shape_label = "dict{key} and list[0]";
let escaped_shape = escape_mermaid_label(shape_label);
assert!(!escaped_shape.contains('{'));
assert!(!escaped_shape.contains('}'));
assert!(!escaped_shape.contains('['));
assert!(!escaped_shape.contains(']'));
assert!(escaped_shape.contains("#123;")); assert!(escaped_shape.contains("#125;")); assert!(escaped_shape.contains("#91;")); assert!(escaped_shape.contains("#93;"));
let code_label = "func(`arg`); next";
let escaped_code = escape_mermaid_label(code_label);
assert!(!escaped_code.contains('`'));
assert!(escaped_code.contains(", next")); assert!(!escaped_code.contains("); "));
let hash_label = "comment # here";
let escaped_hash = escape_mermaid_label(hash_label);
assert!(!escaped_hash.contains(" # "));
assert!(escaped_hash.contains("#35;"));
let backslash_label = "path\\to\\file";
let escaped_backslash = escape_mermaid_label(backslash_label);
assert!(escaped_backslash.contains("\\\\"));
let cr_label = "line1\r\nline2";
let escaped_cr = escape_mermaid_label(cr_label);
assert!(!escaped_cr.contains('\r'));
assert!(!escaped_cr.contains('\n'));
let all_special = "\"<>{}`[];#&|\\test\n\r";
let escaped_all = escape_mermaid_label(all_special);
assert!(!escaped_all.contains('"'));
assert!(!escaped_all.contains('<'));
assert!(!escaped_all.contains('>'));
assert!(!escaped_all.contains('{'));
assert!(!escaped_all.contains('}'));
assert!(!escaped_all.contains('`'));
assert!(!escaped_all.contains('['));
assert!(!escaped_all.contains(']'));
assert!(escaped_all.contains(",#35;")); assert!(!escaped_all.contains('\n'));
assert!(!escaped_all.contains('\r'));
assert!(escaped_all.contains("<"));
assert!(escaped_all.contains(">"));
assert!(escaped_all.contains("&"));
assert!(escaped_all.contains("\\|"));
assert!(escaped_all.contains("\\\\"));
}
#[test]
fn test_dot_output() {
let cfg = sample_cfg();
let dot = to_dot(&cfg);
assert!(dot.starts_with("digraph test_func"));
assert!(dot.contains("rankdir=TB"));
assert!(dot.contains("B0 [label=\"entry\"]"));
assert!(dot.contains("B0 -> B1"));
assert!(dot.contains("[label=\"true\"]"));
}
#[test]
fn test_dot_escaping() {
let label = "test \"quoted\"\nwith newline";
let escaped = escape_dot_label(label);
assert_eq!(escaped, "test \\\"quoted\\\"\\nwith newline");
let html_label = "x < y && y > z";
let escaped_html = escape_dot_label(html_label);
assert!(escaped_html.contains("\\<"));
assert!(escaped_html.contains("\\>"));
assert!(!escaped_html.contains(" < "));
assert!(!escaped_html.contains(" > "));
let record_label = "dict{key} = value";
let escaped_record = escape_dot_label(record_label);
assert!(escaped_record.contains("\\{"));
assert!(escaped_record.contains("\\}"));
assert!(!escaped_record.contains("dict{"));
let pipe_label = "field1 | field2";
let escaped_pipe = escape_dot_label(pipe_label);
assert!(escaped_pipe.contains("\\|"));
assert!(!escaped_pipe.contains(" | "));
let backslash_label = "path\\to\\file";
let escaped_backslash = escape_dot_label(backslash_label);
assert_eq!(escaped_backslash, "path\\\\to\\\\file");
let cr_label = "line1\r\nline2";
let escaped_cr = escape_dot_label(cr_label);
assert!(!escaped_cr.contains('\r'));
assert!(escaped_cr.contains("\\n"));
let all_special = "\"<>{}`|\\test\n\r";
let escaped_all = escape_dot_label(all_special);
assert!(escaped_all.contains("\\\""));
assert!(escaped_all.contains("\\<"));
assert!(escaped_all.contains("\\>"));
assert!(escaped_all.contains("\\{"));
assert!(escaped_all.contains("\\}"));
assert!(escaped_all.contains("\\|"));
assert!(escaped_all.contains("\\\\"));
assert!(escaped_all.contains("\\n"));
assert!(!escaped_all.contains('\r'));
}
#[test]
fn test_dot_html_label_escaping() {
let label = "x < y && z > 0";
let escaped = escape_dot_html_label(label);
assert!(escaped.contains("<"));
assert!(escaped.contains(">"));
assert!(escaped.contains("&"));
assert!(!escaped.contains(" < "));
assert!(!escaped.contains(" > "));
assert!(!escaped.contains(" && "));
let quote_label = "attr=\"value\"";
let escaped_quote = escape_dot_html_label(quote_label);
assert!(escaped_quote.contains("""));
assert!(!escaped_quote.contains("=\""));
let amp_label = "a & b < c";
let escaped_amp = escape_dot_html_label(amp_label);
assert_eq!(escaped_amp, "a & b < c");
}
#[test]
fn test_dot_id_sanitization() {
assert_eq!(sanitize_dot_id("simple"), "simple");
assert_eq!(sanitize_dot_id("with spaces"), "with_spaces");
assert_eq!(sanitize_dot_id("123start"), "_123start");
assert_eq!(sanitize_dot_id(""), "_anonymous");
assert_eq!(sanitize_dot_id("a::b"), "a__b");
}
#[test]
fn test_ascii_output() {
let cfg = sample_cfg();
let ascii = to_ascii(&cfg);
assert!(ascii.contains("CFG: test_func"));
assert!(ascii.contains("Blocks: 3"));
assert!(ascii.contains("Edges: 2"));
assert!(ascii.contains("[B0] entry"));
assert!(ascii.contains("B0 -> B1"));
assert!(ascii.contains("[true]"));
}
#[test]
fn test_json_output() {
let cfg = sample_cfg();
let json = to_json(&cfg).unwrap();
assert!(json.contains("\"function_name\": \"test_func\""));
assert!(json.contains("\"entry\""));
assert!(json.contains("\"exits\""));
}
#[test]
fn test_deterministic_output() {
let cfg = sample_cfg();
let mermaid1 = to_mermaid(&cfg);
let mermaid2 = to_mermaid(&cfg);
assert_eq!(mermaid1, mermaid2);
let dot1 = to_dot(&cfg);
let dot2 = to_dot(&cfg);
assert_eq!(dot1, dot2);
}
#[test]
fn test_edge_ordering_deterministic() {
let mut blocks = HashMap::new();
for i in 0..4 {
blocks.insert(
BlockId(i),
CFGBlock {
id: BlockId(i),
label: format!("block_{}", i),
block_type: BlockType::Body,
statements: vec![],
func_calls: Vec::new(),
start_line: i as usize + 1,
end_line: i as usize + 1,
},
);
}
let unsorted_edges = vec![
CFGEdge::unconditional(BlockId(2), BlockId(3)),
CFGEdge::with_condition(BlockId(0), BlockId(2), EdgeType::True, "branch_a".to_string()),
CFGEdge::unconditional(BlockId(1), BlockId(3)),
CFGEdge::with_condition(BlockId(0), BlockId(1), EdgeType::False, "branch_b".to_string()),
];
let cfg = CFGInfo {
function_name: "test_edge_order".to_string(),
blocks,
edges: unsorted_edges,
entry: BlockId(0),
exits: vec![BlockId(3)],
decision_points: 1,
comprehension_decision_points: 0,
nested_cfgs: HashMap::new(),
is_async: false,
await_points: 0,
blocking_calls: Vec::new(),
..Default::default()
};
let mermaid = to_mermaid(&cfg);
let edge_lines: Vec<&str> = mermaid
.lines()
.filter(|l| l.contains("-->"))
.collect();
assert!(edge_lines[0].contains("B0") && edge_lines[0].contains("B1"));
assert!(edge_lines[1].contains("B0") && edge_lines[1].contains("B2"));
assert!(edge_lines[2].contains("B1") && edge_lines[2].contains("B3"));
assert!(edge_lines[3].contains("B2") && edge_lines[3].contains("B3"));
let dot = to_dot(&cfg);
let dot_edge_lines: Vec<&str> = dot
.lines()
.filter(|l| l.contains("->") && !l.contains("rankdir"))
.collect();
assert!(dot_edge_lines[0].contains("B0 -> B1"));
assert!(dot_edge_lines[1].contains("B0 -> B2"));
assert!(dot_edge_lines[2].contains("B1 -> B3"));
assert!(dot_edge_lines[3].contains("B2 -> B3"));
let ascii = to_ascii(&cfg);
let ascii_edge_section: &str = ascii.split("Edges:\n").nth(1).unwrap();
let ascii_edge_lines: Vec<&str> = ascii_edge_section
.lines()
.filter(|l| l.contains("->"))
.collect();
assert!(ascii_edge_lines[0].contains("B0 -> B1"));
assert!(ascii_edge_lines[1].contains("B0 -> B2"));
assert!(ascii_edge_lines[2].contains("B1 -> B3"));
assert!(ascii_edge_lines[3].contains("B2 -> B3"));
}
}