1use anyhow::{Context, Result};
6use colored::Colorize;
7use spydecy_c::{cpython, parser::CAST};
8use spydecy_python::parser::PythonAST;
9use std::fs;
10use std::path::Path;
11
12pub fn visualize_python(file_path: &Path) -> Result<String> {
18 let source = fs::read_to_string(file_path)
20 .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
21
22 let filename = file_path.to_string_lossy().to_string();
24 let ast = spydecy_python::parser::parse(&source, &filename)
25 .context("Failed to parse Python source")?;
26
27 let mut output = String::new();
29
30 output.push_str(&format!(
32 "{}",
33 "╔══════════════════════════════════════════════════════════╗\n".cyan()
34 ));
35 output.push_str(&format!(
36 "{}",
37 "║ Spydecy Debugger: Python AST Visualization ║\n".cyan()
38 ));
39 output.push_str(&format!(
40 "{}",
41 "╚══════════════════════════════════════════════════════════╝\n".cyan()
42 ));
43 output.push('\n');
44
45 output.push_str(&format!("{} {}\n", "File:".bold(), file_path.display()));
47 output.push_str(&format!(
48 "{} {} lines\n",
49 "Size:".bold(),
50 source.lines().count()
51 ));
52 output.push('\n');
53
54 output.push_str(&format!("{}\n", "═══ Source Code ═══".yellow().bold()));
56 for (i, line) in source.lines().enumerate() {
57 output.push_str(&format!("{:3} │ {}\n", (i + 1).to_string().dimmed(), line));
58 }
59 output.push('\n');
60
61 output.push_str(&format!(
63 "{}\n",
64 "═══ Abstract Syntax Tree ═══".green().bold()
65 ));
66 format_ast_node(&ast, 0, &mut output);
67 output.push('\n');
68
69 output.push_str(&format!("{}\n", "═══ Statistics ═══".blue().bold()));
71 let node_count = count_nodes(&ast);
72 output.push_str(&format!(" {} {}\n", "Total AST nodes:".bold(), node_count));
73 output.push_str(&format!(
74 " {} {}\n",
75 "Root node type:".bold(),
76 ast.node_type
77 ));
78 if !ast.children.is_empty() {
79 output.push_str(&format!(
80 " {} {}\n",
81 "Direct children:".bold(),
82 ast.children.len()
83 ));
84 }
85
86 Ok(output)
87}
88
89fn format_ast_node(node: &PythonAST, depth: usize, output: &mut String) {
91 let indent = " ".repeat(depth);
92 let connector = if depth > 0 { "├─ " } else { "" };
93
94 let node_type_colored = match node.node_type.as_str() {
96 "Module" => node.node_type.cyan().bold(),
97 "FunctionDef" => node.node_type.green().bold(),
98 "ClassDef" => node.node_type.yellow().bold(),
99 "Call" => node.node_type.magenta(),
100 "Return" => node.node_type.red(),
101 "Name" => node.node_type.blue(),
102 _ => node.node_type.white(),
103 };
104
105 output.push_str(&format!("{}{}{}", indent, connector, node_type_colored));
106
107 if !node.attributes.is_empty() {
109 output.push_str(" (");
110 let mut first = true;
111 for (key, value) in &node.attributes {
112 if !first {
113 output.push_str(", ");
114 }
115 output.push_str(&format!("{}={}", key.dimmed(), value.bright_white()));
116 first = false;
117 }
118 output.push(')');
119 }
120
121 if let Some(lineno) = node.lineno {
123 output.push_str(&format!(" {}", format!("@L{lineno}").dimmed()));
124 }
125
126 output.push('\n');
127
128 for child in &node.children {
130 format_ast_node(child, depth + 1, output);
131 }
132}
133
134fn count_nodes(node: &PythonAST) -> usize {
136 1 + node.children.iter().map(count_nodes).sum::<usize>()
137}
138
139pub fn visualize_c(file_path: &Path) -> Result<String> {
145 let source = fs::read_to_string(file_path)
147 .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
148
149 let filename = file_path.to_string_lossy().to_string();
151 let parser = spydecy_c::parser::CParser::new().context("Failed to create C parser")?;
152 let ast = parser
153 .parse(&source, &filename)
154 .context("Failed to parse C source")?;
155
156 let mut output = String::new();
158
159 output.push_str(&format!(
161 "{}",
162 "╔══════════════════════════════════════════════════════════╗\n".cyan()
163 ));
164 output.push_str(&format!(
165 "{}",
166 "║ Spydecy Debugger: C AST Visualization ║\n".cyan()
167 ));
168 output.push_str(&format!(
169 "{}",
170 "╚══════════════════════════════════════════════════════════╝\n".cyan()
171 ));
172 output.push('\n');
173
174 output.push_str(&format!("{} {}\n", "File:".bold(), file_path.display()));
176 output.push_str(&format!(
177 "{} {} lines\n",
178 "Size:".bold(),
179 source.lines().count()
180 ));
181 output.push('\n');
182
183 output.push_str(&format!("{}\n", "═══ Source Code ═══".yellow().bold()));
185 for (i, line) in source.lines().enumerate() {
186 output.push_str(&format!("{:3} │ {}\n", (i + 1).to_string().dimmed(), line));
187 }
188 output.push('\n');
189
190 output.push_str(&format!(
192 "{}\n",
193 "═══ Abstract Syntax Tree ═══".green().bold()
194 ));
195 format_c_ast_node(&ast, 0, &mut output);
196 output.push('\n');
197
198 output.push_str(&format!(
200 "{}\n",
201 "═══ CPython API Analysis ═══".magenta().bold()
202 ));
203 let cpython_calls = collect_cpython_calls(&ast);
204 if cpython_calls.is_empty() {
205 output.push_str(&format!(
206 " {} No CPython API calls detected\n",
207 "ℹ".dimmed()
208 ));
209 } else {
210 for (pattern, name) in cpython_calls {
211 output.push_str(&format!(
212 " {} {} → {:?}\n",
213 "⚡".bright_yellow(),
214 name.bright_white().bold(),
215 pattern
216 ));
217 }
218 }
219 output.push('\n');
220
221 output.push_str(&format!("{}\n", "═══ PyObject* Tracking ═══".blue().bold()));
223 let pyobject_params = collect_pyobject_params(&ast);
224 if pyobject_params.is_empty() {
225 output.push_str(&format!(
226 " {} No PyObject* parameters detected\n",
227 "ℹ".dimmed()
228 ));
229 } else {
230 for (func_name, param_name, param_type) in pyobject_params {
231 output.push_str(&format!(
232 " {} {}::{} ({})\n",
233 "🐍".bright_cyan(),
234 func_name.yellow(),
235 param_name.bright_white(),
236 param_type.dimmed()
237 ));
238 }
239 }
240 output.push('\n');
241
242 output.push_str(&format!("{}\n", "═══ Statistics ═══".blue().bold()));
244 let node_count = count_c_nodes(&ast);
245 output.push_str(&format!(" {} {}\n", "Total AST nodes:".bold(), node_count));
246 output.push_str(&format!(
247 " {} {}\n",
248 "Root node type:".bold(),
249 ast.node_type
250 ));
251 if !ast.children.is_empty() {
252 output.push_str(&format!(
253 " {} {}\n",
254 "Direct children:".bold(),
255 ast.children.len()
256 ));
257 }
258
259 Ok(output)
260}
261
262fn format_c_ast_node(node: &CAST, depth: usize, output: &mut String) {
264 let indent = " ".repeat(depth);
265 let connector = if depth > 0 { "├─ " } else { "" };
266 let pattern = cpython::identify_pattern(node);
267
268 let node_type_colored = colorize_c_node_type(&node.node_type, pattern.is_some());
270 output.push_str(&format!("{indent}{connector}{node_type_colored}"));
271
272 format_c_node_details(node, pattern, output);
274 output.push('\n');
275
276 for child in &node.children {
278 format_c_ast_node(child, depth + 1, output);
279 }
280}
281
282fn colorize_c_node_type(node_type: &str, is_cpython: bool) -> colored::ColoredString {
284 use colored::Colorize;
285
286 match node_type {
287 "TranslationUnit" => node_type.cyan().bold(),
288 "FunctionDecl" if is_cpython => node_type.magenta().bold(),
289 "FunctionDecl" => node_type.green().bold(),
290 "CallExpr" if is_cpython => node_type.magenta(),
291 "CallExpr" => node_type.blue(),
292 "ReturnStmt" => node_type.red(),
293 "VarDecl" => node_type.yellow(),
294 "ParmDecl" => node_type.cyan(),
295 _ => node_type.white(),
296 }
297}
298
299fn format_c_node_details(
301 node: &CAST,
302 pattern: Option<cpython::CPythonPattern>,
303 output: &mut String,
304) {
305 if let Some(ref name) = node.name {
307 output.push_str(&format!(" {}", name.bright_white().bold()));
308 }
309
310 if let Some(p) = pattern {
312 output.push_str(&format!(" {} {p:?}", "⚡".bright_yellow()));
313 }
314
315 if let Some(ref ret_type) = node.return_type {
317 output.push_str(&format!(" → {}", ret_type.dimmed()));
318 }
319
320 if !node.params.is_empty() {
322 format_c_parameters(&node.params, output);
323 }
324}
325
326fn format_c_parameters(params: &[spydecy_c::parser::CParam], output: &mut String) {
328 output.push_str(" (");
329 for (i, param) in params.iter().enumerate() {
330 if i > 0 {
331 output.push_str(", ");
332 }
333 format_c_parameter(param, output);
334 }
335 output.push(')');
336}
337
338fn format_c_parameter(param: &spydecy_c::parser::CParam, output: &mut String) {
340 let is_pyobject = param.param_type.contains("PyObject") || param.param_type.contains("PyList");
341 if is_pyobject {
342 output.push_str(&format!(
343 "{}: {}",
344 param.name.bright_cyan().bold(),
345 param.param_type.cyan()
346 ));
347 } else {
348 output.push_str(&format!("{}: {}", param.name, param.param_type.dimmed()));
349 }
350}
351
352fn collect_cpython_calls(node: &CAST) -> Vec<(cpython::CPythonPattern, String)> {
354 let mut calls = Vec::new();
355
356 if let Some(pattern) = cpython::identify_pattern(node) {
357 if let Some(ref name) = node.name {
358 calls.push((pattern, name.clone()));
359 }
360 }
361
362 for child in &node.children {
363 calls.extend(collect_cpython_calls(child));
364 }
365
366 calls
367}
368
369fn collect_pyobject_params(node: &CAST) -> Vec<(String, String, String)> {
371 let mut params = Vec::new();
372
373 if node.node_type == "FunctionDecl" {
374 if let Some(ref func_name) = node.name {
375 for param in &node.params {
376 if param.param_type.contains("PyObject")
377 || param.param_type.contains("PyList")
378 || param.param_type.contains("PyDict")
379 {
380 params.push((
381 func_name.clone(),
382 param.name.clone(),
383 param.param_type.clone(),
384 ));
385 }
386 }
387 }
388 }
389
390 for child in &node.children {
391 params.extend(collect_pyobject_params(child));
392 }
393
394 params
395}
396
397fn count_c_nodes(node: &CAST) -> usize {
399 1 + node.children.iter().map(count_c_nodes).sum::<usize>()
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use std::io::Write;
406 use tempfile::NamedTempFile;
407
408 #[test]
409 fn test_visualize_simple_function() {
410 let mut temp_file = NamedTempFile::new().unwrap();
411 writeln!(temp_file, "def my_len(x):\n return len(x)").unwrap();
412
413 let result = visualize_python(temp_file.path());
414 assert!(result.is_ok());
415
416 let output = result.unwrap();
417 assert!(output.contains("Module"));
418 assert!(output.contains("FunctionDef"));
419 assert!(output.contains("Return"));
420 assert!(output.contains("Call"));
421 assert!(output.contains("my_len"));
422 }
423
424 #[test]
425 fn test_count_nodes() {
426 let ast = PythonAST {
427 node_type: "Module".to_string(),
428 lineno: None,
429 col_offset: None,
430 children: vec![
431 PythonAST::new("FunctionDef".to_string()),
432 PythonAST::new("FunctionDef".to_string()),
433 ],
434 attributes: std::collections::HashMap::new(),
435 };
436
437 assert_eq!(count_nodes(&ast), 3); }
439
440 #[test]
441 fn test_visualize_simple_c_function() {
442 use std::io::Write;
443 use tempfile::Builder;
444
445 let mut temp_file = Builder::new().suffix(".c").tempfile().unwrap();
446 writeln!(temp_file, "int add(int a, int b) {{\n return a + b;\n}}").unwrap();
447 temp_file.flush().unwrap(); let result = visualize_c(temp_file.path());
450 assert!(
451 result.is_ok(),
452 "Should visualize C code: {:?}",
453 result.as_ref().err()
454 );
455
456 let output = result.unwrap();
457 assert!(output.contains("C AST Visualization"));
458 assert!(output.contains("FunctionDecl"));
459 assert!(output.contains("add"));
460 }
461
462 #[test]
463 fn test_visualize_cpython_function() {
464 use std::io::Write;
465 use tempfile::Builder;
466
467 let mut temp_file = Builder::new().suffix(".c").tempfile().unwrap();
468 writeln!(
469 temp_file,
470 "static Py_ssize_t list_length(PyListObject *self) {{\n return Py_SIZE(self);\n}}"
471 )
472 .unwrap();
473 temp_file.flush().unwrap(); let result = visualize_c(temp_file.path());
476 assert!(
477 result.is_ok(),
478 "Should visualize CPython code: {:?}",
479 result.as_ref().err()
480 );
481
482 let output = result.unwrap();
483 assert!(output.contains("CPython API Analysis"));
484 assert!(output.contains("PyObject* Tracking"));
485 assert!(output.contains("list_length"));
486 }
487
488 #[test]
489 fn test_collect_cpython_calls() {
490 let mut ast = CAST::new("FunctionDecl".to_owned());
491 ast.name = Some("list_length".to_owned());
492
493 let mut child = CAST::new("CallExpr".to_owned());
494 child.name = Some("PyList_Append".to_owned());
495 ast.children.push(child);
496
497 let calls = collect_cpython_calls(&ast);
498 assert_eq!(calls.len(), 2);
499 assert!(calls.iter().any(|(_, name)| name == "list_length"));
500 assert!(calls.iter().any(|(_, name)| name == "PyList_Append"));
501 }
502
503 #[test]
504 fn test_collect_pyobject_params() {
505 let mut ast = CAST::new("FunctionDecl".to_owned());
506 ast.name = Some("test_func".to_owned());
507 ast.params.push(spydecy_c::parser::CParam {
508 name: "obj".to_owned(),
509 param_type: "PyObject*".to_owned(),
510 });
511 ast.params.push(spydecy_c::parser::CParam {
512 name: "x".to_owned(),
513 param_type: "int".to_owned(),
514 });
515
516 let params = collect_pyobject_params(&ast);
517 assert_eq!(params.len(), 1);
518 assert_eq!(params[0].1, "obj");
519 assert_eq!(params[0].2, "PyObject*");
520 }
521
522 #[test]
523 fn test_count_c_nodes() {
524 let mut ast = CAST::new("TranslationUnit".to_owned());
525 ast.children.push(CAST::new("FunctionDecl".to_owned()));
526 ast.children.push(CAST::new("FunctionDecl".to_owned()));
527
528 assert_eq!(count_c_nodes(&ast), 3); }
530}