spydecy_debugger/
visualize.rs

1//! AST visualization for debugging
2//!
3//! This module provides formatted visualization of ASTs for debugging purposes.
4
5use anyhow::{Context, Result};
6use colored::Colorize;
7use spydecy_python::parser::PythonAST;
8use std::fs;
9use std::path::Path;
10
11/// Visualize Python source as AST
12///
13/// # Errors
14///
15/// Returns an error if the file cannot be read or parsed
16pub fn visualize_python(file_path: &Path) -> Result<String> {
17    // Read the source file
18    let source = fs::read_to_string(file_path)
19        .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
20
21    // Parse to AST
22    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    // Format the output
27    let mut output = String::new();
28
29    // Header
30    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    // File info
45    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    // Source code preview
54    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    // AST tree
61    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    // Statistics
69    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
88/// Format an AST node with indentation
89fn format_ast_node(node: &PythonAST, depth: usize, output: &mut String) {
90    let indent = "  ".repeat(depth);
91    let connector = if depth > 0 { "├─ " } else { "" };
92
93    // Node type (colored)
94    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    // Node attributes
107    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    // Source location
121    if let Some(lineno) = node.lineno {
122        output.push_str(&format!(" {}", format!("@L{lineno}").dimmed()));
123    }
124
125    output.push('\n');
126
127    // Recursively format children
128    for child in &node.children {
129        format_ast_node(child, depth + 1, output);
130    }
131}
132
133/// Count total nodes in AST
134fn 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); // Module + 2 FunctionDef
174    }
175}