spydecy_debugger/
visualize.rs1use anyhow::{Context, Result};
6use colored::Colorize;
7use spydecy_python::parser::PythonAST;
8use std::fs;
9use std::path::Path;
10
11pub fn visualize_python(file_path: &Path) -> Result<String> {
17 let source = fs::read_to_string(file_path)
19 .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
20
21 let filename = file_path.to_string_lossy().to_string();
23 let ast = spydecy_python::parser::parse(&source, &filename)
24 .context("Failed to parse Python source")?;
25
26 let mut output = String::new();
28
29 output.push_str(&format!(
31 "{}",
32 "╔══════════════════════════════════════════════════════════╗\n".cyan()
33 ));
34 output.push_str(&format!(
35 "{}",
36 "║ Spydecy Debugger: Python AST Visualization ║\n".cyan()
37 ));
38 output.push_str(&format!(
39 "{}",
40 "╚══════════════════════════════════════════════════════════╝\n".cyan()
41 ));
42 output.push('\n');
43
44 output.push_str(&format!("{} {}\n", "File:".bold(), file_path.display()));
46 output.push_str(&format!(
47 "{} {} lines\n",
48 "Size:".bold(),
49 source.lines().count()
50 ));
51 output.push('\n');
52
53 output.push_str(&format!("{}\n", "═══ Source Code ═══".yellow().bold()));
55 for (i, line) in source.lines().enumerate() {
56 output.push_str(&format!("{:3} │ {}\n", (i + 1).to_string().dimmed(), line));
57 }
58 output.push('\n');
59
60 output.push_str(&format!(
62 "{}\n",
63 "═══ Abstract Syntax Tree ═══".green().bold()
64 ));
65 format_ast_node(&ast, 0, &mut output);
66 output.push('\n');
67
68 output.push_str(&format!("{}\n", "═══ Statistics ═══".blue().bold()));
70 let node_count = count_nodes(&ast);
71 output.push_str(&format!(" {} {}\n", "Total AST nodes:".bold(), node_count));
72 output.push_str(&format!(
73 " {} {}\n",
74 "Root node type:".bold(),
75 ast.node_type
76 ));
77 if !ast.children.is_empty() {
78 output.push_str(&format!(
79 " {} {}\n",
80 "Direct children:".bold(),
81 ast.children.len()
82 ));
83 }
84
85 Ok(output)
86}
87
88fn format_ast_node(node: &PythonAST, depth: usize, output: &mut String) {
90 let indent = " ".repeat(depth);
91 let connector = if depth > 0 { "├─ " } else { "" };
92
93 let node_type_colored = match node.node_type.as_str() {
95 "Module" => node.node_type.cyan().bold(),
96 "FunctionDef" => node.node_type.green().bold(),
97 "ClassDef" => node.node_type.yellow().bold(),
98 "Call" => node.node_type.magenta(),
99 "Return" => node.node_type.red(),
100 "Name" => node.node_type.blue(),
101 _ => node.node_type.white(),
102 };
103
104 output.push_str(&format!("{}{}{}", indent, connector, node_type_colored));
105
106 if !node.attributes.is_empty() {
108 output.push_str(" (");
109 let mut first = true;
110 for (key, value) in &node.attributes {
111 if !first {
112 output.push_str(", ");
113 }
114 output.push_str(&format!("{}={}", key.dimmed(), value.bright_white()));
115 first = false;
116 }
117 output.push(')');
118 }
119
120 if let Some(lineno) = node.lineno {
122 output.push_str(&format!(" {}", format!("@L{lineno}").dimmed()));
123 }
124
125 output.push('\n');
126
127 for child in &node.children {
129 format_ast_node(child, depth + 1, output);
130 }
131}
132
133fn count_nodes(node: &PythonAST) -> usize {
135 1 + node.children.iter().map(count_nodes).sum::<usize>()
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use std::io::Write;
142 use tempfile::NamedTempFile;
143
144 #[test]
145 fn test_visualize_simple_function() {
146 let mut temp_file = NamedTempFile::new().unwrap();
147 writeln!(temp_file, "def my_len(x):\n return len(x)").unwrap();
148
149 let result = visualize_python(temp_file.path());
150 assert!(result.is_ok());
151
152 let output = result.unwrap();
153 assert!(output.contains("Module"));
154 assert!(output.contains("FunctionDef"));
155 assert!(output.contains("Return"));
156 assert!(output.contains("Call"));
157 assert!(output.contains("my_len"));
158 }
159
160 #[test]
161 fn test_count_nodes() {
162 let ast = PythonAST {
163 node_type: "Module".to_string(),
164 lineno: None,
165 col_offset: None,
166 children: vec![
167 PythonAST::new("FunctionDef".to_string()),
168 PythonAST::new("FunctionDef".to_string()),
169 ],
170 attributes: std::collections::HashMap::new(),
171 };
172
173 assert_eq!(count_nodes(&ast), 3); }
175}