1use deagle_core::{DeagleError, EdgeKind, Language, Node, NodeKind, Result};
4use std::path::Path;
5
6use crate::ParseResult;
7
8pub fn parse(path: &Path, content: &str) -> Result<Vec<Node>> {
10 parse_with_edges(path, content).map(|r| r.nodes)
11}
12
13pub fn parse_with_edges(path: &Path, content: &str) -> Result<ParseResult> {
15 let mut parser = tree_sitter::Parser::new();
16
17 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, "lexical_declaration" => {
76 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 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 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 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; };
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}