Skip to main content

deagle_parse/
typescript_parser.rs

1//! TypeScript/TSX language parser using tree-sitter-typescript.
2
3use deagle_core::{DeagleError, EdgeKind, Language, Node, NodeKind, Result};
4use std::path::Path;
5
6use crate::ParseResult;
7
8/// Parse a TypeScript source file and extract definitions.
9pub fn parse(path: &Path, content: &str) -> Result<Vec<Node>> {
10    parse_with_edges(path, content).map(|r| r.nodes)
11}
12
13/// Parse with edge extraction.
14pub fn parse_with_edges(path: &Path, content: &str) -> Result<ParseResult> {
15    let mut parser = tree_sitter::Parser::new();
16
17    // Use TSX parser (superset of TS — handles both .ts and .tsx)
18    let language = tree_sitter_typescript::LANGUAGE_TSX;
19    parser.set_language(&language.into()).map_err(|e| DeagleError::Parse {
20        file: path.display().to_string(),
21        message: format!("Failed to set language: {}", e),
22    })?;
23
24    let tree = parser.parse(content, None).ok_or_else(|| DeagleError::Parse {
25        file: path.display().to_string(),
26        message: "Failed to parse file".into(),
27    })?;
28
29    let mut nodes = Vec::new();
30    let file_path = path.to_string_lossy().to_string();
31    let lang = if path.extension().and_then(|e| e.to_str()) == Some("tsx") {
32        Language::TypeScript
33    } else {
34        Language::TypeScript
35    };
36
37    nodes.push(Node {
38        id: 0,
39        name: path.file_name().and_then(|n| n.to_str()).unwrap_or("unknown").to_string(),
40        kind: NodeKind::File,
41        language: lang,
42        file_path: file_path.clone(),
43        line_start: 1,
44        line_end: content.lines().count() as u32,
45        content: None,
46    });
47
48    extract_definitions(tree.root_node(), content, &file_path, lang, &mut nodes, false);
49
50    let mut edges = Vec::new();
51    for i in 1..nodes.len() {
52        edges.push((0, i, EdgeKind::Contains));
53    }
54
55    Ok(ParseResult { nodes, edges })
56}
57
58fn extract_definitions(
59    node: tree_sitter::Node,
60    source: &str,
61    file_path: &str,
62    lang: Language,
63    results: &mut Vec<Node>,
64    inside_class: bool,
65) {
66    let kind = match node.kind() {
67        "function_declaration" => Some(NodeKind::Function),
68        "method_definition" => Some(NodeKind::Method),
69        "class_declaration" => Some(NodeKind::Class),
70        "interface_declaration" => Some(NodeKind::Interface),
71        "type_alias_declaration" => Some(NodeKind::TypeAlias),
72        "enum_declaration" => Some(NodeKind::Enum),
73        "import_statement" => Some(NodeKind::Import),
74        "export_statement" => None, // recurse into children
75        "lexical_declaration" => {
76            // const/let/var — check for arrow functions or UPPER_CASE constants
77            if !inside_class {
78                extract_lexical(node, source, file_path, lang, results);
79            }
80            None
81        }
82        _ => None,
83    };
84
85    if let Some(kind) = kind {
86        if let Some(name) = extract_name(node, source, kind) {
87            let start = node.start_position();
88            let end = node.end_position();
89            let content = node.utf8_text(source.as_bytes()).ok().map(|s| {
90                if s.len() > 500 { format!("{}...", &s[..500]) } else { s.to_string() }
91            });
92
93            results.push(Node {
94                id: 0,
95                name,
96                kind,
97                language: lang,
98                file_path: file_path.to_string(),
99                line_start: (start.row + 1) as u32,
100                line_end: (end.row + 1) as u32,
101                content,
102            });
103
104            // Recurse into class body for methods
105            if kind == NodeKind::Class {
106                if let Some(body) = node.child_by_field_name("body") {
107                    let mut cursor = body.walk();
108                    for child in body.children(&mut cursor) {
109                        extract_definitions(child, source, file_path, lang, results, true);
110                    }
111                }
112                return;
113            }
114        }
115    }
116
117    // Recurse
118    if node.kind() != "class_declaration" {
119        let mut cursor = node.walk();
120        for child in node.children(&mut cursor) {
121            extract_definitions(child, source, file_path, lang, results, inside_class);
122        }
123    }
124}
125
126fn extract_lexical(
127    node: tree_sitter::Node,
128    source: &str,
129    file_path: &str,
130    lang: Language,
131    results: &mut Vec<Node>,
132) {
133    let mut cursor = node.walk();
134    for child in node.children(&mut cursor) {
135        if child.kind() == "variable_declarator" {
136            if let Some(name_node) = child.child_by_field_name("name") {
137                let name = name_node.utf8_text(source.as_bytes()).unwrap_or_default().to_string();
138                // Check if value is an arrow function
139                let is_arrow = child.child_by_field_name("value")
140                    .map(|v| v.kind() == "arrow_function")
141                    .unwrap_or(false);
142
143                let kind = if is_arrow {
144                    NodeKind::Function
145                } else if name.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) && !name.is_empty() {
146                    NodeKind::Constant
147                } else {
148                    return; // skip regular variables
149                };
150
151                let start = node.start_position();
152                let end = node.end_position();
153                let content = node.utf8_text(source.as_bytes()).ok().map(|s| {
154                    if s.len() > 500 { format!("{}...", &s[..500]) } else { s.to_string() }
155                });
156
157                results.push(Node {
158                    id: 0,
159                    name,
160                    kind,
161                    language: lang,
162                    file_path: file_path.to_string(),
163                    line_start: (start.row + 1) as u32,
164                    line_end: (end.row + 1) as u32,
165                    content,
166                });
167            }
168        }
169    }
170}
171
172fn extract_name(node: tree_sitter::Node, source: &str, kind: NodeKind) -> Option<String> {
173    match kind {
174        NodeKind::Import => {
175            node.utf8_text(source.as_bytes())
176                .ok()
177                .map(|s| s.trim().to_string())
178        }
179        _ => {
180            node.child_by_field_name("name")
181                .and_then(|n| n.utf8_text(source.as_bytes()).ok())
182                .map(|s| s.to_string())
183        }
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use std::path::PathBuf;
191
192    const SAMPLE_TS: &str = r#"
193import { Router } from 'express';
194import type { Request, Response } from 'express';
195
196const MAX_SIZE = 1024;
197
198interface Config {
199    name: string;
200    values: Record<string, string>;
201}
202
203type Status = 'active' | 'inactive';
204
205enum Direction {
206    Up,
207    Down,
208    Left,
209    Right,
210}
211
212class Server {
213    private config: Config;
214
215    constructor(config: Config) {
216        this.config = config;
217    }
218
219    start(): void {
220        console.log('starting');
221    }
222
223    getConfig(): Config {
224        return this.config;
225    }
226}
227
228function createServer(name: string): Server {
229    return new Server({ name, values: {} });
230}
231
232const handler = (req: Request, res: Response) => {
233    res.send('ok');
234};
235
236export function main() {
237    const server = createServer('test');
238    server.start();
239}
240"#;
241
242    #[test]
243    fn test_parse_ts_finds_all_definitions() {
244        let path = PathBuf::from("app.ts");
245        let nodes = parse(&path, SAMPLE_TS).unwrap();
246        let kinds: Vec<_> = nodes.iter().map(|n| n.kind).collect();
247        assert!(kinds.contains(&NodeKind::Import), "should find import");
248        assert!(kinds.contains(&NodeKind::Constant), "should find constant");
249        assert!(kinds.contains(&NodeKind::Interface), "should find interface");
250        assert!(kinds.contains(&NodeKind::TypeAlias), "should find type alias");
251        assert!(kinds.contains(&NodeKind::Enum), "should find enum");
252        assert!(kinds.contains(&NodeKind::Class), "should find class");
253        assert!(kinds.contains(&NodeKind::Function), "should find function");
254    }
255
256    #[test]
257    fn test_parse_ts_class_methods() {
258        let path = PathBuf::from("app.ts");
259        let nodes = parse(&path, SAMPLE_TS).unwrap();
260        let methods: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Method).collect();
261        assert!(methods.iter().any(|m| m.name == "start"));
262        assert!(methods.iter().any(|m| m.name == "getConfig"));
263        assert!(methods.iter().any(|m| m.name == "constructor"));
264    }
265
266    #[test]
267    fn test_parse_ts_arrow_function() {
268        let path = PathBuf::from("app.ts");
269        let nodes = parse(&path, SAMPLE_TS).unwrap();
270        let fns: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Function).collect();
271        assert!(fns.iter().any(|f| f.name == "handler"), "arrow function should be captured");
272        assert!(fns.iter().any(|f| f.name == "createServer"));
273        assert!(fns.iter().any(|f| f.name == "main"));
274    }
275
276    #[test]
277    fn test_parse_ts_interface() {
278        let path = PathBuf::from("app.ts");
279        let nodes = parse(&path, SAMPLE_TS).unwrap();
280        let ifaces: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Interface).collect();
281        assert_eq!(ifaces.len(), 1);
282        assert_eq!(ifaces[0].name, "Config");
283    }
284
285    #[test]
286    fn test_parse_ts_enum() {
287        let path = PathBuf::from("app.ts");
288        let nodes = parse(&path, SAMPLE_TS).unwrap();
289        let enums: Vec<_> = nodes.iter().filter(|n| n.kind == NodeKind::Enum).collect();
290        assert_eq!(enums.len(), 1);
291        assert_eq!(enums[0].name, "Direction");
292    }
293
294    #[test]
295    fn test_parse_ts_edges() {
296        let path = PathBuf::from("app.ts");
297        let result = parse_with_edges(&path, SAMPLE_TS).unwrap();
298        assert!(!result.edges.is_empty());
299        for &(from, _, ref kind) in &result.edges {
300            assert_eq!(from, 0);
301            assert_eq!(*kind, EdgeKind::Contains);
302        }
303    }
304
305    #[test]
306    fn test_parse_empty_ts() {
307        let path = PathBuf::from("empty.ts");
308        let nodes = parse(&path, "").unwrap();
309        assert!(nodes.len() <= 1);
310    }
311}