Skip to main content

codemov_parser/
typescript.rs

1use codemov_core::{ImportEdge, ImportKind, Language, Symbol, SymbolKind};
2use tree_sitter::Node;
3
4use crate::ParseError;
5
6pub fn extract(source: &[u8], language: Language) -> Result<Vec<Symbol>, ParseError> {
7    let mut parser = tree_sitter::Parser::new();
8    let lang = match language {
9        Language::TypeScript => tree_sitter_typescript::language_typescript(),
10        _ => tree_sitter_typescript::language_tsx(),
11    };
12    parser
13        .set_language(&lang)
14        .map_err(|e| ParseError::Parse(e.to_string()))?;
15
16    let tree = parser
17        .parse(source, None)
18        .ok_or_else(|| ParseError::Parse("tree-sitter returned None".into()))?;
19
20    let mut symbols = Vec::new();
21    walk(tree.root_node(), source, false, &mut symbols);
22    Ok(symbols)
23}
24
25fn walk(node: Node, source: &[u8], inside_export: bool, out: &mut Vec<Symbol>) {
26    match node.kind() {
27        "function_declaration" | "function" | "generator_function_declaration" => {
28            if let Some(sym) = named(node, source, SymbolKind::Function, "name") {
29                out.push(sym);
30                return; // don't recurse into function body for top-level symbols
31            }
32        }
33        "class_declaration" | "abstract_class_declaration" => {
34            if let Some(sym) = named(node, source, SymbolKind::Class, "name") {
35                out.push(sym);
36                return;
37            }
38        }
39        "interface_declaration" => {
40            if let Some(sym) = named(node, source, SymbolKind::Interface, "name") {
41                out.push(sym);
42                return;
43            }
44        }
45        "type_alias_declaration" => {
46            if let Some(sym) = named(node, source, SymbolKind::TypeAlias, "name") {
47                out.push(sym);
48                return;
49            }
50        }
51        "lexical_declaration" | "variable_declaration" => {
52            extract_variable_decl(node, source, inside_export, out);
53            return;
54        }
55        "export_statement" => {
56            let mut cursor = node.walk();
57            for child in node.children(&mut cursor) {
58                walk(child, source, true, out);
59            }
60            return;
61        }
62        _ => {}
63    }
64
65    let mut cursor = node.walk();
66    for child in node.children(&mut cursor) {
67        walk(child, source, inside_export, out);
68    }
69}
70
71fn extract_variable_decl(node: Node, source: &[u8], inside_export: bool, out: &mut Vec<Symbol>) {
72    let mut cursor = node.walk();
73    for child in node.children(&mut cursor) {
74        if child.kind() == "variable_declarator" {
75            if let Some(name_node) = child.child_by_field_name("name") {
76                let value = child.child_by_field_name("value");
77                let kind = match value.map(|v| v.kind()) {
78                    Some("arrow_function") | Some("function") => SymbolKind::Function,
79                    _ if inside_export => SymbolKind::Export,
80                    _ => continue,
81                };
82                if let Ok(name) = name_node.utf8_text(source) {
83                    out.push(Symbol {
84                        name: name.to_string(),
85                        kind,
86                        start_line: node.start_position().row as u32 + 1,
87                        end_line: node.end_position().row as u32 + 1,
88                    });
89                }
90            }
91        }
92    }
93}
94
95fn named(node: Node, source: &[u8], kind: SymbolKind, field: &str) -> Option<Symbol> {
96    let name = node
97        .child_by_field_name(field)?
98        .utf8_text(source)
99        .ok()?
100        .to_string();
101    Some(Symbol {
102        name,
103        kind,
104        start_line: node.start_position().row as u32 + 1,
105        end_line: node.end_position().row as u32 + 1,
106    })
107}
108
109pub fn extract_imports(
110    source: &[u8],
111    language: Language,
112) -> Result<Vec<ImportEdge>, crate::ParseError> {
113    let mut parser = tree_sitter::Parser::new();
114    let lang = match language {
115        Language::TypeScript => tree_sitter_typescript::language_typescript(),
116        _ => tree_sitter_typescript::language_tsx(),
117    };
118    parser
119        .set_language(&lang)
120        .map_err(|e| crate::ParseError::Parse(e.to_string()))?;
121    let tree = parser
122        .parse(source, None)
123        .ok_or_else(|| crate::ParseError::Parse("tree-sitter returned None".into()))?;
124
125    let mut edges = Vec::new();
126    collect_import_nodes(tree.root_node(), source, &mut edges);
127    Ok(edges)
128}
129
130fn collect_import_nodes(node: Node, source: &[u8], out: &mut Vec<ImportEdge>) {
131    match node.kind() {
132        "import_statement" => {
133            if let Some(src_node) = node.child_by_field_name("source") {
134                if let Ok(raw) = src_node.utf8_text(source) {
135                    let target = raw.trim_matches(|c| c == '\'' || c == '"').to_string();
136                    out.push(ImportEdge {
137                        source_path: std::path::PathBuf::new(),
138                        target_raw: target,
139                        resolved_path: None,
140                        kind: ImportKind::Import,
141                        line: node.start_position().row as u32 + 1,
142                    });
143                }
144            }
145        }
146        "export_statement" => {
147            // re-exports: export { ... } from "..."
148            if let Some(src_node) = node.child_by_field_name("source") {
149                if let Ok(raw) = src_node.utf8_text(source) {
150                    let target = raw.trim_matches(|c| c == '\'' || c == '"').to_string();
151                    out.push(ImportEdge {
152                        source_path: std::path::PathBuf::new(),
153                        target_raw: target,
154                        resolved_path: None,
155                        kind: ImportKind::Export,
156                        line: node.start_position().row as u32 + 1,
157                    });
158                }
159            }
160        }
161        "call_expression" => {
162            // require("...") calls
163            if let Some(fn_node) = node.child_by_field_name("function") {
164                if fn_node.utf8_text(source).ok() == Some("require") {
165                    if let Some(args) = node.child_by_field_name("arguments") {
166                        let mut cur = args.walk();
167                        for child in args.children(&mut cur) {
168                            if matches!(child.kind(), "string" | "template_string") {
169                                if let Ok(raw) = child.utf8_text(source) {
170                                    let target =
171                                        raw.trim_matches(|c| c == '\'' || c == '"').to_string();
172                                    out.push(ImportEdge {
173                                        source_path: std::path::PathBuf::new(),
174                                        target_raw: target,
175                                        resolved_path: None,
176                                        kind: ImportKind::Require,
177                                        line: node.start_position().row as u32 + 1,
178                                    });
179                                }
180                            }
181                        }
182                    }
183                }
184            }
185        }
186        _ => {}
187    }
188
189    let mut cursor = node.walk();
190    for child in node.children(&mut cursor) {
191        collect_import_nodes(child, source, out);
192    }
193}