Skip to main content

debtmap/core/
ast.rs

1//! AST representation and pattern extraction data structures.
2//!
3//! This module provides language-agnostic AST representations and specialized
4//! structures for pattern detection in different programming languages.
5//!
6//! # Design Pattern Extraction
7//!
8//! The module supports extracting design patterns from source code:
9//! - Class hierarchies with inheritance and decorators
10//! - Method definitions with abstract/override tracking
11//! - Module-level singleton instances
12//! - Assignment and expression analysis
13//!
14//! # Example
15//!
16//! ```ignore
17//! use debtmap::core::ast::{ClassDef, ModuleScopeAnalysis};
18//!
19//! // Extract classes from Python AST
20//! let classes: Vec<ClassDef> = extractor.extract_classes(&module);
21//!
22//! // Analyze module scope for singletons
23//! let scope: ModuleScopeAnalysis = extractor.extract_module_scope(&module);
24//! ```
25//!
26//! # Performance Considerations
27//!
28//! Pattern extraction is designed to add minimal overhead (< 5%) to existing
29//! parsing operations by extracting data during the single-pass AST traversal.
30
31use serde::{Deserialize, Serialize};
32use std::path::PathBuf;
33
34#[derive(Clone, Debug)]
35pub enum Ast {
36    Rust(RustAst),
37    Python(PythonAst),
38    TypeScript(TypeScriptAst),
39    Unknown,
40}
41
42/// JavaScript/TypeScript language variant
43#[derive(Clone, Debug, PartialEq, Eq, Copy)]
44pub enum JsLanguageVariant {
45    JavaScript,
46    TypeScript,
47    Jsx,
48    Tsx,
49}
50
51impl JsLanguageVariant {
52    /// Determine variant from file extension
53    pub fn from_extension(ext: &str) -> Option<Self> {
54        match ext {
55            "js" | "mjs" | "cjs" => Some(JsLanguageVariant::JavaScript),
56            "jsx" => Some(JsLanguageVariant::Jsx),
57            "ts" | "mts" | "cts" => Some(JsLanguageVariant::TypeScript),
58            "tsx" => Some(JsLanguageVariant::Tsx),
59            _ => None,
60        }
61    }
62
63    /// Check if this variant uses JSX syntax
64    pub fn has_jsx(&self) -> bool {
65        matches!(self, JsLanguageVariant::Jsx | JsLanguageVariant::Tsx)
66    }
67
68    /// Check if this variant uses TypeScript types
69    pub fn has_types(&self) -> bool {
70        matches!(self, JsLanguageVariant::TypeScript | JsLanguageVariant::Tsx)
71    }
72}
73
74/// TypeScript/JavaScript AST wrapper
75#[derive(Clone)]
76pub struct TypeScriptAst {
77    /// The tree-sitter parse tree
78    pub tree: tree_sitter::Tree,
79    /// Path to the source file
80    pub path: PathBuf,
81    /// Original source code
82    pub source: String,
83    /// JavaScript/TypeScript variant
84    pub language_variant: JsLanguageVariant,
85}
86
87impl std::fmt::Debug for TypeScriptAst {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        f.debug_struct("TypeScriptAst")
90            .field("path", &self.path)
91            .field("language_variant", &self.language_variant)
92            .field("source_len", &self.source.len())
93            .finish()
94    }
95}
96
97// Pattern recognition data structures
98
99/// Represents a class definition with its metadata.
100///
101/// Captures information about class decorators, inheritance hierarchy,
102/// methods, and abstract status for design pattern recognition.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct ClassDef {
105    pub name: String,
106    pub base_classes: Vec<String>,
107    pub methods: Vec<MethodDef>,
108    pub is_abstract: bool,
109    pub decorators: Vec<String>,
110    pub line: usize,
111}
112
113/// Represents a method definition within a class.
114///
115/// Tracks method decorators, abstract status, and whether it overrides
116/// a base class method for inheritance pattern detection.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct MethodDef {
119    pub name: String,
120    pub is_abstract: bool,
121    pub decorators: Vec<String>,
122    pub overrides_base: bool,
123    pub line: usize,
124}
125
126/// Analysis of module-level scope for pattern detection.
127///
128/// Captures module-level assignments and identifies singleton instances
129/// (module-level class instantiations that follow the singleton pattern).
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ModuleScopeAnalysis {
132    pub assignments: Vec<Assignment>,
133    pub singleton_instances: Vec<SingletonInstance>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct Assignment {
138    pub name: String,
139    pub value: Expression,
140    pub scope: Scope,
141    pub line: usize,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145pub enum Scope {
146    Module,
147    Class,
148    Function,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub enum Expression {
153    ClassInstantiation {
154        class_name: String,
155        args: Vec<String>,
156    },
157    FunctionCall {
158        function_name: String,
159        args: Vec<String>,
160    },
161    ClassReference {
162        class_name: String,
163    },
164    Literal {
165        value: String,
166    },
167    Other,
168}
169
170impl Expression {
171    pub fn is_class_instantiation(&self) -> bool {
172        matches!(self, Expression::ClassInstantiation { .. })
173    }
174
175    pub fn is_class_reference(&self) -> bool {
176        matches!(self, Expression::ClassReference { .. })
177    }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct SingletonInstance {
182    pub variable_name: String,
183    pub class_name: String,
184    pub line: usize,
185}
186
187#[derive(Clone, Debug)]
188pub struct RustAst {
189    pub file: syn::File,
190    pub path: PathBuf,
191    pub source: String,
192}
193
194// Stub for removed Python AST support
195#[derive(Clone, Debug)]
196pub struct PythonAst {
197    pub path: PathBuf,
198    pub source: String,
199}
200
201#[derive(Clone, Debug)]
202pub struct AstNode {
203    pub kind: NodeKind,
204    pub name: Option<String>,
205    pub line: usize,
206    pub children: Vec<AstNode>,
207}
208
209#[derive(Clone, Debug, PartialEq)]
210pub enum NodeKind {
211    Function,
212    Method,
213    Class,
214    Module,
215    If,
216    While,
217    For,
218    Match,
219    Try,
220    Block,
221}
222
223impl Ast {
224    pub fn transform<F>(self, f: F) -> Self
225    where
226        F: Fn(Self) -> Self,
227    {
228        f(self)
229    }
230
231    pub fn map_functions<F, T>(&self, f: F) -> Vec<T>
232    where
233        F: Fn(&AstNode) -> Option<T>,
234    {
235        let nodes = self.extract_nodes();
236        nodes
237            .iter()
238            .filter(|n| matches!(n.kind, NodeKind::Function | NodeKind::Method))
239            .filter_map(f)
240            .collect()
241    }
242
243    pub fn extract_nodes(&self) -> Vec<AstNode> {
244        match self {
245            Ast::Rust(_) => self.extract_rust_nodes(),
246            Ast::Python(_) => self.extract_python_nodes(),
247            Ast::TypeScript(_) => self.extract_typescript_nodes(),
248            Ast::Unknown => vec![],
249        }
250    }
251
252    fn extract_rust_nodes(&self) -> Vec<AstNode> {
253        vec![]
254    }
255
256    fn extract_python_nodes(&self) -> Vec<AstNode> {
257        vec![]
258    }
259
260    fn extract_typescript_nodes(&self) -> Vec<AstNode> {
261        // Will be populated with tree-sitter traversal in phase 2
262        vec![]
263    }
264
265    pub fn count_branches(&self) -> usize {
266        self.extract_nodes()
267            .iter()
268            .filter(|n| {
269                matches!(
270                    n.kind,
271                    NodeKind::If | NodeKind::While | NodeKind::For | NodeKind::Match
272                )
273            })
274            .count()
275    }
276}
277
278pub fn combine_asts(asts: Vec<Ast>) -> Vec<Ast> {
279    asts
280}
281
282pub fn filter_ast<F>(ast: Ast, predicate: F) -> Option<Ast>
283where
284    F: Fn(&Ast) -> bool,
285{
286    if predicate(&ast) {
287        Some(ast)
288    } else {
289        None
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_map_functions_extracts_functions() {
299        let ast = Ast::Unknown;
300        let results = ast.map_functions(|node| {
301            if matches!(node.kind, NodeKind::Function | NodeKind::Method) {
302                Some(node.name.clone().unwrap_or_else(|| "anonymous".to_string()))
303            } else {
304                None
305            }
306        });
307
308        // Since Unknown AST has no functions, expect empty
309        assert_eq!(results.len(), 0);
310    }
311
312    #[test]
313    fn test_ast_transform() {
314        let ast = Ast::Unknown;
315        let transformed = ast.clone().transform(|a| {
316            // Simple identity transform
317            a
318        });
319
320        assert!(matches!(transformed, Ast::Unknown));
321    }
322
323    #[test]
324    fn test_count_branches() {
325        let ast = Ast::Unknown;
326        let count = ast.count_branches();
327        assert_eq!(count, 0);
328    }
329
330    #[test]
331    fn test_extract_nodes_unknown() {
332        let ast = Ast::Unknown;
333        let nodes = ast.extract_nodes();
334        assert_eq!(nodes.len(), 0);
335    }
336
337    #[test]
338    fn test_combine_asts() {
339        let asts = vec![Ast::Unknown, Ast::Unknown];
340        let combined = combine_asts(asts);
341        assert_eq!(combined.len(), 2);
342    }
343
344    #[test]
345    fn test_filter_ast_matches() {
346        let ast = Ast::Unknown;
347        let filtered = filter_ast(ast, |a| matches!(a, Ast::Unknown));
348        assert!(filtered.is_some());
349    }
350
351    #[test]
352    fn test_filter_ast_no_match() {
353        let ast = Ast::Unknown;
354        let filtered = filter_ast(ast, |a| matches!(a, Ast::Rust(_)));
355        assert!(filtered.is_none());
356    }
357
358    #[test]
359    fn test_ast_node_creation() {
360        let node = AstNode {
361            kind: NodeKind::Function,
362            name: Some("test_func".to_string()),
363            line: 10,
364            children: vec![],
365        };
366
367        assert_eq!(node.kind, NodeKind::Function);
368        assert_eq!(node.name, Some("test_func".to_string()));
369        assert_eq!(node.line, 10);
370        assert_eq!(node.children.len(), 0);
371    }
372
373    #[test]
374    fn test_map_functions_filters_correctly() {
375        // Create a mock AST with both function and non-function nodes
376        let nodes = [
377            AstNode {
378                kind: NodeKind::Function,
379                name: Some("func1".to_string()),
380                line: 1,
381                children: vec![],
382            },
383            AstNode {
384                kind: NodeKind::If,
385                name: None,
386                line: 2,
387                children: vec![],
388            },
389            AstNode {
390                kind: NodeKind::Method,
391                name: Some("method1".to_string()),
392                line: 3,
393                children: vec![],
394            },
395        ];
396
397        // Since we cannot easily construct a real AST with nodes,
398        // test the filtering logic directly
399        let function_nodes: Vec<_> = nodes
400            .iter()
401            .filter(|n| matches!(n.kind, NodeKind::Function | NodeKind::Method))
402            .collect();
403
404        assert_eq!(function_nodes.len(), 2);
405    }
406
407    #[test]
408    fn test_extract_rust_nodes() {
409        let ast = Ast::Rust(RustAst {
410            file: syn::File {
411                shebang: None,
412                attrs: vec![],
413                items: vec![],
414            },
415            path: PathBuf::from("test.rs"),
416            source: String::new(),
417        });
418
419        // Currently returns empty vec, test that it doesn't panic
420        let nodes = ast.extract_rust_nodes();
421        assert_eq!(nodes.len(), 0);
422    }
423
424    #[test]
425    fn test_extract_python_nodes() {
426        // Test with Unknown AST since creating PythonAst requires complex structures
427        let ast = Ast::Unknown;
428        let nodes = ast.extract_python_nodes();
429        assert_eq!(nodes.len(), 0);
430    }
431
432    #[test]
433    fn test_extract_nodes_rust() {
434        let ast = Ast::Rust(RustAst {
435            file: syn::File {
436                shebang: None,
437                attrs: vec![],
438                items: vec![],
439            },
440            path: PathBuf::from("test.rs"),
441            source: String::new(),
442        });
443
444        let nodes = ast.extract_nodes();
445        assert_eq!(nodes.len(), 0); // Expected since extract_rust_nodes returns empty
446    }
447
448    #[test]
449    fn test_extract_nodes_python() {
450        // Test extraction logic without creating complex AST
451        let ast = Ast::Unknown;
452        let nodes = ast.extract_nodes();
453        assert_eq!(nodes.len(), 0);
454    }
455
456    #[test]
457    fn test_ast_node_with_children() {
458        let child1 = AstNode {
459            kind: NodeKind::Block,
460            name: None,
461            line: 5,
462            children: vec![],
463        };
464
465        let child2 = AstNode {
466            kind: NodeKind::If,
467            name: None,
468            line: 6,
469            children: vec![],
470        };
471
472        let parent = AstNode {
473            kind: NodeKind::Function,
474            name: Some("parent_func".to_string()),
475            line: 4,
476            children: vec![child1, child2],
477        };
478
479        assert_eq!(parent.children.len(), 2);
480        assert_eq!(parent.children[0].kind, NodeKind::Block);
481        assert_eq!(parent.children[1].kind, NodeKind::If);
482    }
483
484    #[test]
485    fn test_node_kind_equality() {
486        assert_eq!(NodeKind::Function, NodeKind::Function);
487        assert_ne!(NodeKind::Function, NodeKind::Method);
488        assert_ne!(NodeKind::If, NodeKind::While);
489    }
490
491    #[test]
492    fn test_count_branches_with_different_node_types() {
493        // Test that count_branches correctly identifies branch nodes
494        let branch_kinds = vec![
495            NodeKind::If,
496            NodeKind::While,
497            NodeKind::For,
498            NodeKind::Match,
499        ];
500
501        let non_branch_kinds = vec![
502            NodeKind::Function,
503            NodeKind::Method,
504            NodeKind::Class,
505            NodeKind::Module,
506            NodeKind::Try,
507            NodeKind::Block,
508        ];
509
510        for kind in branch_kinds {
511            assert!(
512                matches!(
513                    kind,
514                    NodeKind::If | NodeKind::While | NodeKind::For | NodeKind::Match
515                ),
516                "Expected {kind:?} to be a branch node"
517            );
518        }
519
520        for kind in non_branch_kinds {
521            assert!(
522                !matches!(
523                    kind,
524                    NodeKind::If | NodeKind::While | NodeKind::For | NodeKind::Match
525                ),
526                "Expected {kind:?} to not be a branch node"
527            );
528        }
529    }
530
531    #[test]
532    fn test_transform_preserves_type() {
533        let rust_ast = Ast::Rust(RustAst {
534            file: syn::File {
535                shebang: None,
536                attrs: vec![],
537                items: vec![],
538            },
539            path: PathBuf::from("test.rs"),
540            source: String::new(),
541        });
542
543        let transformed = rust_ast.transform(|a| a);
544        assert!(matches!(transformed, Ast::Rust(_)));
545    }
546
547    #[test]
548    fn test_combine_asts_preserves_order() {
549        let ast1 = Ast::Unknown;
550        let ast2 = Ast::Unknown;
551        let ast3 = Ast::Unknown;
552
553        let combined = combine_asts(vec![ast1, ast2, ast3]);
554        assert_eq!(combined.len(), 3);
555    }
556
557    #[test]
558    fn test_combine_asts_empty() {
559        let combined = combine_asts(vec![]);
560        assert_eq!(combined.len(), 0);
561    }
562
563    #[test]
564    fn test_filter_ast_with_multiple_predicates() {
565        let ast = Ast::Unknown;
566
567        // Test with always true predicate
568        let result1 = filter_ast(ast.clone(), |_| true);
569        assert!(result1.is_some());
570
571        // Test with always false predicate
572        let result2 = filter_ast(ast.clone(), |_| false);
573        assert!(result2.is_none());
574
575        // Test with specific type check
576        let result3 = filter_ast(ast.clone(), |a| matches!(a, Ast::Unknown));
577        assert!(result3.is_some());
578    }
579}