1use super::{node_text, LanguageExtractor};
7use crate::ast::{
8 ExtractedSymbol, FunctionCall, Import, ImportedName, Parameter, SymbolKind, Visibility,
9};
10use crate::error::Result;
11use tree_sitter::{Language, Node, Tree};
12
13pub struct JavaScriptExtractor;
15
16impl LanguageExtractor for JavaScriptExtractor {
17 fn language(&self) -> Language {
18 tree_sitter_javascript::LANGUAGE.into()
19 }
20
21 fn name(&self) -> &'static str {
22 "javascript"
23 }
24
25 fn extensions(&self) -> &'static [&'static str] {
26 &["js", "jsx", "mjs", "cjs"]
27 }
28
29 fn extract_symbols(&self, tree: &Tree, source: &str) -> Result<Vec<ExtractedSymbol>> {
30 let mut symbols = Vec::new();
31 let root = tree.root_node();
32 self.extract_symbols_recursive(&root, source, &mut symbols, None);
33 Ok(symbols)
34 }
35
36 fn extract_imports(&self, tree: &Tree, source: &str) -> Result<Vec<Import>> {
37 let mut imports = Vec::new();
38 let root = tree.root_node();
39 self.extract_imports_recursive(&root, source, &mut imports);
40 Ok(imports)
41 }
42
43 fn extract_calls(
44 &self,
45 tree: &Tree,
46 source: &str,
47 current_function: Option<&str>,
48 ) -> Result<Vec<FunctionCall>> {
49 let mut calls = Vec::new();
50 let root = tree.root_node();
51 self.extract_calls_recursive(&root, source, &mut calls, current_function);
52 Ok(calls)
53 }
54
55 fn extract_doc_comment(&self, node: &Node, source: &str) -> Option<String> {
56 if let Some(prev) = node.prev_sibling() {
57 if prev.kind() == "comment" {
58 let comment = node_text(&prev, source);
59 if comment.starts_with("/**") {
60 return Some(Self::clean_jsdoc(comment));
61 }
62 if let Some(rest) = comment.strip_prefix("//") {
63 return Some(rest.trim().to_string());
64 }
65 }
66 }
67 None
68 }
69}
70
71impl JavaScriptExtractor {
72 fn extract_symbols_recursive(
73 &self,
74 node: &Node,
75 source: &str,
76 symbols: &mut Vec<ExtractedSymbol>,
77 parent: Option<&str>,
78 ) {
79 match node.kind() {
80 "function_declaration" => {
81 if let Some(sym) = self.extract_function(node, source, parent) {
82 symbols.push(sym);
83 }
84 }
85
86 "lexical_declaration" | "variable_declaration" => {
87 self.extract_variable_symbols(node, source, symbols, parent);
88 }
89
90 "class_declaration" => {
91 if let Some(sym) = self.extract_class(node, source, parent) {
92 let class_name = sym.name.clone();
93 symbols.push(sym);
94
95 if let Some(body) = node.child_by_field_name("body") {
96 self.extract_class_members(&body, source, symbols, Some(&class_name));
97 }
98 }
99 }
100
101 "export_statement" => {
102 self.extract_export_symbols(node, source, symbols, parent);
103 }
104
105 _ => {}
106 }
107
108 let mut cursor = node.walk();
109 for child in node.children(&mut cursor) {
110 self.extract_symbols_recursive(&child, source, symbols, parent);
111 }
112 }
113
114 fn extract_function(
115 &self,
116 node: &Node,
117 source: &str,
118 parent: Option<&str>,
119 ) -> Option<ExtractedSymbol> {
120 let name_node = node.child_by_field_name("name")?;
121 let name = node_text(&name_node, source).to_string();
122
123 let mut sym = ExtractedSymbol::new(
124 name,
125 SymbolKind::Function,
126 node.start_position().row + 1,
127 node.end_position().row + 1,
128 )
129 .with_columns(node.start_position().column, node.end_position().column);
130
131 let text = node_text(node, source);
132 if text.starts_with("async") {
133 sym = sym.async_fn();
134 }
135
136 if let Some(params) = node.child_by_field_name("parameters") {
137 self.extract_parameters(¶ms, source, &mut sym);
138 }
139
140 sym.doc_comment = self.extract_doc_comment(node, source);
141
142 if let Some(p) = parent {
143 sym = sym.with_parent(p);
144 }
145
146 sym.signature = Some(self.build_function_signature(node, source));
147
148 Some(sym)
149 }
150
151 fn extract_variable_symbols(
152 &self,
153 node: &Node,
154 source: &str,
155 symbols: &mut Vec<ExtractedSymbol>,
156 parent: Option<&str>,
157 ) {
158 let mut cursor = node.walk();
159 for child in node.children(&mut cursor) {
160 if child.kind() == "variable_declarator" {
161 if let (Some(name_node), Some(value)) = (
162 child.child_by_field_name("name"),
163 child.child_by_field_name("value"),
164 ) {
165 let name = node_text(&name_node, source);
166 let value_kind = value.kind();
167
168 if value_kind == "arrow_function" || value_kind == "function_expression" {
169 let mut sym = ExtractedSymbol::new(
170 name.to_string(),
171 SymbolKind::Function,
172 node.start_position().row + 1,
173 node.end_position().row + 1,
174 );
175
176 let text = node_text(&value, source);
177 if text.starts_with("async") {
178 sym = sym.async_fn();
179 }
180
181 if let Some(params) = value.child_by_field_name("parameters") {
182 self.extract_parameters(¶ms, source, &mut sym);
183 }
184
185 sym.doc_comment = self.extract_doc_comment(node, source);
186
187 if let Some(p) = parent {
188 sym = sym.with_parent(p);
189 }
190
191 if let Some(parent_node) = node.parent() {
192 if parent_node.kind() == "export_statement" {
193 sym = sym.exported();
194 }
195 }
196
197 symbols.push(sym);
198 } else {
199 let kind = if node_text(node, source).starts_with("const") {
200 SymbolKind::Constant
201 } else {
202 SymbolKind::Variable
203 };
204
205 let mut sym = ExtractedSymbol::new(
206 name.to_string(),
207 kind,
208 node.start_position().row + 1,
209 node.end_position().row + 1,
210 );
211
212 if let Some(p) = parent {
213 sym = sym.with_parent(p);
214 }
215
216 symbols.push(sym);
217 }
218 }
219 }
220 }
221 }
222
223 fn extract_class(
224 &self,
225 node: &Node,
226 source: &str,
227 parent: Option<&str>,
228 ) -> Option<ExtractedSymbol> {
229 let name_node = node.child_by_field_name("name")?;
230 let name = node_text(&name_node, source).to_string();
231
232 let mut sym = ExtractedSymbol::new(
233 name,
234 SymbolKind::Class,
235 node.start_position().row + 1,
236 node.end_position().row + 1,
237 )
238 .with_columns(node.start_position().column, node.end_position().column);
239
240 sym.doc_comment = self.extract_doc_comment(node, source);
241
242 if let Some(p) = parent {
243 sym = sym.with_parent(p);
244 }
245
246 Some(sym)
247 }
248
249 fn extract_class_members(
250 &self,
251 body: &Node,
252 source: &str,
253 symbols: &mut Vec<ExtractedSymbol>,
254 class_name: Option<&str>,
255 ) {
256 let mut cursor = body.walk();
257 for child in body.children(&mut cursor) {
258 if child.kind() == "method_definition" {
259 if let Some(sym) = self.extract_method(&child, source, class_name) {
260 symbols.push(sym);
261 }
262 }
263 }
264 }
265
266 fn extract_method(
267 &self,
268 node: &Node,
269 source: &str,
270 class_name: Option<&str>,
271 ) -> Option<ExtractedSymbol> {
272 let name_node = node.child_by_field_name("name")?;
273 let name = node_text(&name_node, source).to_string();
274
275 let is_private = name.starts_with('#');
277
278 let mut sym = ExtractedSymbol::new(
279 name,
280 SymbolKind::Method,
281 node.start_position().row + 1,
282 node.end_position().row + 1,
283 );
284
285 let text = node_text(node, source);
286 if text.contains("static") {
287 sym = sym.static_fn();
288 }
289 if text.contains("async") {
290 sym = sym.async_fn();
291 }
292
293 if is_private {
296 sym.visibility = Visibility::Private;
297 }
298
299 if let Some(params) = node.child_by_field_name("parameters") {
300 self.extract_parameters(¶ms, source, &mut sym);
301 }
302
303 sym.doc_comment = self.extract_doc_comment(node, source);
304
305 if let Some(p) = class_name {
306 sym = sym.with_parent(p);
307 }
308
309 Some(sym)
310 }
311
312 fn extract_export_symbols(
313 &self,
314 node: &Node,
315 source: &str,
316 symbols: &mut Vec<ExtractedSymbol>,
317 parent: Option<&str>,
318 ) {
319 let mut cursor = node.walk();
320 for child in node.children(&mut cursor) {
321 match child.kind() {
322 "function_declaration" => {
323 if let Some(mut sym) = self.extract_function(&child, source, parent) {
324 sym = sym.exported();
325 symbols.push(sym);
326 }
327 }
328 "class_declaration" => {
329 if let Some(mut sym) = self.extract_class(&child, source, parent) {
330 sym = sym.exported();
331 let class_name = sym.name.clone();
332 symbols.push(sym);
333
334 if let Some(body) = child.child_by_field_name("body") {
335 self.extract_class_members(&body, source, symbols, Some(&class_name));
336 }
337 }
338 }
339 "lexical_declaration" | "variable_declaration" => {
340 self.extract_variable_symbols(&child, source, symbols, parent);
341 if let Some(last) = symbols.last_mut() {
342 last.exported = true;
343 }
344 }
345 _ => {}
346 }
347 }
348 }
349
350 fn extract_parameters(&self, params: &Node, source: &str, sym: &mut ExtractedSymbol) {
351 let mut cursor = params.walk();
352 for child in params.children(&mut cursor) {
353 match child.kind() {
354 "identifier" => {
355 sym.add_parameter(Parameter {
356 name: node_text(&child, source).to_string(),
357 type_info: None,
358 default_value: None,
359 is_rest: false,
360 is_optional: false,
361 });
362 }
363 "assignment_pattern" => {
364 let name = child
365 .child_by_field_name("left")
366 .map(|n| node_text(&n, source).to_string())
367 .unwrap_or_default();
368 let default_value = child
369 .child_by_field_name("right")
370 .map(|n| node_text(&n, source).to_string());
371
372 sym.add_parameter(Parameter {
373 name,
374 type_info: None,
375 default_value,
376 is_rest: false,
377 is_optional: true,
378 });
379 }
380 "rest_pattern" => {
381 let name = node_text(&child, source)
382 .trim_start_matches("...")
383 .to_string();
384 sym.add_parameter(Parameter {
385 name,
386 type_info: None,
387 default_value: None,
388 is_rest: true,
389 is_optional: false,
390 });
391 }
392 _ => {}
393 }
394 }
395 }
396
397 fn extract_imports_recursive(&self, node: &Node, source: &str, imports: &mut Vec<Import>) {
398 if node.kind() == "import_statement" {
399 if let Some(import) = self.parse_import(node, source) {
400 imports.push(import);
401 }
402 }
403
404 let mut cursor = node.walk();
405 for child in node.children(&mut cursor) {
406 self.extract_imports_recursive(&child, source, imports);
407 }
408 }
409
410 fn parse_import(&self, node: &Node, source: &str) -> Option<Import> {
411 let source_node = node.child_by_field_name("source")?;
412 let source_path = node_text(&source_node, source)
413 .trim_matches(|c| c == '"' || c == '\'')
414 .to_string();
415
416 let mut import = Import {
417 source: source_path,
418 names: Vec::new(),
419 is_default: false,
420 is_namespace: false,
421 line: node.start_position().row + 1,
422 };
423
424 let mut cursor = node.walk();
425 for child in node.children(&mut cursor) {
426 if child.kind() == "import_clause" {
427 self.parse_import_clause(&child, source, &mut import);
428 }
429 }
430
431 Some(import)
432 }
433
434 fn parse_import_clause(&self, clause: &Node, source: &str, import: &mut Import) {
435 let mut cursor = clause.walk();
436 for child in clause.children(&mut cursor) {
437 match child.kind() {
438 "identifier" => {
439 import.is_default = true;
440 import.names.push(ImportedName {
441 name: "default".to_string(),
442 alias: Some(node_text(&child, source).to_string()),
443 });
444 }
445 "namespace_import" => {
446 import.is_namespace = true;
447 if let Some(name_node) = child.child_by_field_name("name") {
448 import.names.push(ImportedName {
449 name: "*".to_string(),
450 alias: Some(node_text(&name_node, source).to_string()),
451 });
452 }
453 }
454 "named_imports" => {
455 self.parse_named_imports(&child, source, import);
456 }
457 _ => {}
458 }
459 }
460 }
461
462 fn parse_named_imports(&self, node: &Node, source: &str, import: &mut Import) {
463 let mut cursor = node.walk();
464 for child in node.children(&mut cursor) {
465 if child.kind() == "import_specifier" {
466 let name = child
467 .child_by_field_name("name")
468 .map(|n| node_text(&n, source).to_string())
469 .unwrap_or_default();
470
471 let alias = child
472 .child_by_field_name("alias")
473 .map(|n| node_text(&n, source).to_string());
474
475 import.names.push(ImportedName { name, alias });
476 }
477 }
478 }
479
480 fn extract_calls_recursive(
481 &self,
482 node: &Node,
483 source: &str,
484 calls: &mut Vec<FunctionCall>,
485 current_function: Option<&str>,
486 ) {
487 if node.kind() == "call_expression" {
488 if let Some(call) = self.parse_call(node, source, current_function) {
489 calls.push(call);
490 }
491 }
492
493 let func_name = match node.kind() {
494 "function_declaration" | "method_definition" => node
495 .child_by_field_name("name")
496 .map(|n| node_text(&n, source)),
497 _ => None,
498 };
499
500 let current = func_name
501 .map(String::from)
502 .or_else(|| current_function.map(String::from));
503
504 let mut cursor = node.walk();
505 for child in node.children(&mut cursor) {
506 self.extract_calls_recursive(&child, source, calls, current.as_deref());
507 }
508 }
509
510 fn parse_call(
511 &self,
512 node: &Node,
513 source: &str,
514 current_function: Option<&str>,
515 ) -> Option<FunctionCall> {
516 let function = node.child_by_field_name("function")?;
517
518 let (callee, is_method, receiver) = match function.kind() {
519 "member_expression" => {
520 let object = function
521 .child_by_field_name("object")
522 .map(|n| node_text(&n, source).to_string());
523 let property = function
524 .child_by_field_name("property")
525 .map(|n| node_text(&n, source).to_string())?;
526 (property, true, object)
527 }
528 "identifier" => (node_text(&function, source).to_string(), false, None),
529 _ => return None,
530 };
531
532 Some(FunctionCall {
533 caller: current_function.unwrap_or("<module>").to_string(),
534 callee,
535 line: node.start_position().row + 1,
536 is_method,
537 receiver,
538 })
539 }
540
541 fn build_function_signature(&self, node: &Node, source: &str) -> String {
542 let name = node
543 .child_by_field_name("name")
544 .map(|n| node_text(&n, source))
545 .unwrap_or("anonymous");
546
547 let params = node
548 .child_by_field_name("parameters")
549 .map(|n| node_text(&n, source))
550 .unwrap_or("()");
551
552 format!("function {}{}", name, params)
553 }
554
555 fn clean_jsdoc(comment: &str) -> String {
556 comment
557 .trim_start_matches("/**")
558 .trim_end_matches("*/")
559 .lines()
560 .map(|line| line.trim().trim_start_matches('*').trim())
561 .filter(|line| !line.is_empty())
562 .collect::<Vec<_>>()
563 .join("\n")
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 fn parse_js(source: &str) -> (Tree, String) {
572 let mut parser = tree_sitter::Parser::new();
573 parser
574 .set_language(&tree_sitter_javascript::LANGUAGE.into())
575 .unwrap();
576 let tree = parser.parse(source, None).unwrap();
577 (tree, source.to_string())
578 }
579
580 #[test]
581 fn test_extract_function() {
582 let source = r#"
583function greet(name) {
584 return `Hello, ${name}!`;
585}
586"#;
587 let (tree, src) = parse_js(source);
588 let extractor = JavaScriptExtractor;
589 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
590
591 assert_eq!(symbols.len(), 1);
592 assert_eq!(symbols[0].name, "greet");
593 assert_eq!(symbols[0].kind, SymbolKind::Function);
594 }
595
596 #[test]
597 fn test_extract_class() {
598 let source = r#"
599class UserService {
600 constructor(name) {
601 this.name = name;
602 }
603
604 greet() {
605 return `Hello, ${this.name}!`;
606 }
607}
608"#;
609 let (tree, src) = parse_js(source);
610 let extractor = JavaScriptExtractor;
611 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
612
613 assert!(symbols
614 .iter()
615 .any(|s| s.name == "UserService" && s.kind == SymbolKind::Class));
616 assert!(symbols
617 .iter()
618 .any(|s| s.name == "greet" && s.kind == SymbolKind::Method));
619 }
620
621 #[test]
622 fn test_extract_arrow_function() {
623 let source = r#"
624const add = (a, b) => a + b;
625"#;
626 let (tree, src) = parse_js(source);
627 let extractor = JavaScriptExtractor;
628 let symbols = extractor.extract_symbols(&tree, &src).unwrap();
629
630 assert_eq!(symbols.len(), 1);
631 assert_eq!(symbols[0].name, "add");
632 assert_eq!(symbols[0].kind, SymbolKind::Function);
633 }
634}