1use super::{
2 add_contains_edge, add_node_if_missing, make_file_node, run_query_matches_ranged,
3 run_query_named, run_tree_walk_docstrings,
4};
5use crate::error::Result;
6use crate::extract::{make_id, ImportNode, LanguageExtractor};
7use crate::types::{Edge, ExtractionFragment, Node};
8use std::collections::HashMap;
9use std::path::Path;
10
11pub struct TsJavaScriptExtractor;
12
13impl TsJavaScriptExtractor {
14 pub fn extract(source: &[u8], path: &Path) -> Result<ExtractionFragment> {
15 let (file_id, _, file_node) = make_file_node(path);
16 let mut fragment = ExtractionFragment {
17 nodes: vec![file_node],
18 edges: vec![],
19 };
20
21 let lang = tree_sitter_javascript::LANGUAGE.into();
22 let content = std::str::from_utf8(source).unwrap_or("");
23
24 let docstrings = run_tree_walk_docstrings(source, &lang, &["class_declaration"]);
25
26 let class_query = r#"
28 (class_declaration
29 name: (identifier) @class.name
30 (class_heritage
31 (identifier) @class.base
32 )?
33 ) @class.node
34 "#;
35 if let Ok(matches) = run_query_matches_ranged(source, &lang, class_query) {
36 for (m, ranges) in &matches {
37 let name = match m.get("class.name") {
38 Some(n) => n.clone(),
39 None => continue,
40 };
41 let class_id = make_id(&[&file_id, &name]);
42 let source_location = ranges
43 .get("class.node")
44 .map(|(start, end)| format!("{}:{}", start, end));
45 let class_node = Node {
46 id: class_id.clone(),
47 label: name.to_string(),
48 file_type: "class".to_string(),
49 source_file: path.to_string_lossy().to_string(),
50 source_location,
51 community: None,
52 rationale: None,
53 docstring: docstrings.get(name.as_str()).cloned(),
54 metadata: HashMap::new(),
55 };
56 add_node_if_missing(&mut fragment, class_node);
57 add_contains_edge(&mut fragment, &file_id, class_id.clone(), path);
58
59 if let Some(base_name) = m.get("class.base") {
60 let base_id = make_id(&[base_name]);
61 let base_node_ = Node {
62 id: base_id.clone(),
63 label: base_name.to_string(),
64 file_type: "code".to_string(),
65 source_file: path.to_string_lossy().to_string(),
66 source_location: None,
67 community: None,
68 rationale: None,
69 docstring: None,
70 metadata: HashMap::new(),
71 };
72 add_node_if_missing(&mut fragment, base_node_);
73 fragment.edges.push(Edge {
74 source: class_id.clone(),
75 target: base_id,
76 relation: "inherits".to_string(),
77 confidence: "EXTRACTED".to_string(),
78 source_file: Some(path.to_string_lossy().to_string()),
79 weight: 1.0,
80 context: None,
81 });
82 }
83 }
84 }
85
86 let fn_query = r#"
88 (function_declaration
89 name: (identifier) @func.name
90 ) @func.node
91 "#;
92 if let Ok(matches) = run_query_matches_ranged(source, &lang, fn_query) {
93 for (m, ranges) in &matches {
94 let name = match m.get("func.name") {
95 Some(n) => n.clone(),
96 None => continue,
97 };
98 let fn_id = make_id(&[&file_id, &name, "()"]);
99 let source_location = ranges
100 .get("func.node")
101 .map(|(start, end)| format!("{}:{}", start, end));
102 let fn_node = Node {
103 id: fn_id.clone(),
104 label: format!("{}()", name),
105 file_type: "function".to_string(),
106 source_file: path.to_string_lossy().to_string(),
107 source_location,
108 community: None,
109 rationale: None,
110 docstring: None,
111 metadata: HashMap::new(),
112 };
113 add_node_if_missing(&mut fragment, fn_node);
114 add_contains_edge(&mut fragment, &file_id, fn_id, path);
115 }
116 }
117
118 let method_query = r#"
120 (method_definition
121 name: (property_identifier) @method.name
122 ) @method.node
123 "#;
124 if let Ok(matches) = run_query_matches_ranged(source, &lang, method_query) {
125 for (m, ranges) in &matches {
126 let name = match m.get("method.name") {
127 Some(n) => n.clone(),
128 None => continue,
129 };
130 let method_id = make_id(&[&file_id, &name, "()"]);
131 let source_location = ranges
132 .get("method.node")
133 .map(|(start, end)| format!("{}:{}", start, end));
134 let method_node = Node {
135 id: method_id.clone(),
136 label: format!("{}()", name),
137 file_type: "method".to_string(),
138 source_file: path.to_string_lossy().to_string(),
139 source_location,
140 community: None,
141 rationale: None,
142 docstring: None,
143 metadata: HashMap::new(),
144 };
145 add_node_if_missing(&mut fragment, method_node);
146 add_contains_edge(&mut fragment, &file_id, method_id, path);
147 }
148 }
149
150 let arrow_query = r#"
152 (variable_declarator
153 name: (identifier) @func.name
154 value: [
155 (arrow_function)
156 (function_expression)
157 ] @func.node
158 )
159 "#;
160 if let Ok(matches) = run_query_matches_ranged(source, &lang, arrow_query) {
161 for (m, ranges) in &matches {
162 let name = match m.get("func.name") {
163 Some(n) => n.clone(),
164 None => continue,
165 };
166 let fn_id = make_id(&[&file_id, &name, "()"]);
167 let source_location = ranges
168 .get("func.node")
169 .map(|(start, end)| format!("{}:{}", start, end));
170 let fn_node = Node {
171 id: fn_id.clone(),
172 label: format!("{}()", name),
173 file_type: "function".to_string(),
174 source_file: path.to_string_lossy().to_string(),
175 source_location,
176 community: None,
177 rationale: None,
178 docstring: None,
179 metadata: HashMap::new(),
180 };
181 add_node_if_missing(&mut fragment, fn_node);
182 add_contains_edge(&mut fragment, &file_id, fn_id, path);
183 }
184 }
185
186 let import_query = r#"
191 (import_statement
192 (import_clause
193 (named_imports
194 (import_specifier
195 (identifier) @import.name
196 )
197 )?
198 )?
199 source: (string) @import.source
200 )
201 "#;
202 if let Ok(mut captures) = run_query_named(source, &lang, import_query) {
203 let sources = captures.remove("import.source").unwrap_or_default();
204 let import_names = captures.remove("import.name").unwrap_or_default();
205
206 let mut source_modules: Vec<String> = Vec::new();
207 for s in &sources {
208 let s = s.trim().trim_matches('"').trim_matches('\'');
209 source_modules.push(s.to_string());
210 }
211
212 let mut all_import_names: Vec<String> = Vec::new();
213 for name in &import_names {
214 all_import_names.push(name.to_string());
215 }
216
217 for mod_name in source_modules.iter() {
218 let mod_id = make_id(&[&file_id, mod_name]);
219 let mod_node = Node {
220 id: mod_id.clone(),
221 label: mod_name.to_string(),
222 file_type: "code".to_string(),
223 source_file: path.to_string_lossy().to_string(),
224 source_location: None,
225 community: None,
226 rationale: None,
227 docstring: None,
228 metadata: HashMap::new(),
229 };
230 add_node_if_missing(&mut fragment, mod_node);
231 fragment.edges.push(Edge {
232 source: file_id.clone(),
233 target: mod_id,
234 relation: "imports_from".to_string(),
235 confidence: "EXTRACTED".to_string(),
236 source_file: Some(path.to_string_lossy().to_string()),
237 weight: 1.0,
238 context: None,
239 });
240 }
241
242 for name in &all_import_names {
243 let spec_id = make_id(&[&file_id, name]);
244 let spec_node = Node {
245 id: spec_id.clone(),
246 label: name.to_string(),
247 file_type: "code".to_string(),
248 source_file: path.to_string_lossy().to_string(),
249 source_location: None,
250 community: None,
251 rationale: None,
252 docstring: None,
253 metadata: HashMap::new(),
254 };
255 add_node_if_missing(&mut fragment, spec_node);
256 fragment.edges.push(Edge {
257 source: file_id.clone(),
258 target: spec_id,
259 relation: "imports".to_string(),
260 confidence: "EXTRACTED".to_string(),
261 source_file: Some(path.to_string_lossy().to_string()),
262 weight: 1.0,
263 context: None,
264 });
265 }
266 }
267
268 for line in content.lines() {
270 let trimmed = line.trim();
271 if trimmed.contains("import(") {
272 let start = trimmed.find("import(");
273 if let Some(start) = start {
274 let rest = &trimmed[start + 7..];
275 let source_str = rest
276 .split(')')
277 .next()
278 .unwrap_or("")
279 .trim()
280 .trim_matches('\'')
281 .trim_matches('"');
282 if !source_str.is_empty() {
283 let mod_id = make_id(&[&file_id, source_str]);
284 let mod_node = Node {
285 id: mod_id.clone(),
286 label: source_str.to_string(),
287 file_type: "code".to_string(),
288 source_file: path.to_string_lossy().to_string(),
289 source_location: None,
290 community: None,
291 rationale: None,
292 docstring: None,
293 metadata: HashMap::new(),
294 };
295 add_node_if_missing(&mut fragment, mod_node);
296 fragment.edges.push(Edge {
297 source: file_id.clone(),
298 target: mod_id,
299 relation: "imports_from".to_string(),
300 confidence: "EXTRACTED".to_string(),
301 source_file: Some(path.to_string_lossy().to_string()),
302 weight: 1.0,
303 context: None,
304 });
305 }
306 }
307 }
308 if trimmed.contains("require(") {
309 let start = trimmed.find("require(");
310 if let Some(start) = start {
311 let rest = &trimmed[start + 8..];
312 let source_str = rest
313 .split(')')
314 .next()
315 .unwrap_or("")
316 .trim()
317 .trim_matches('\'')
318 .trim_matches('"');
319 if !source_str.is_empty() {
320 let mod_id = make_id(&[&file_id, source_str]);
321 let mod_node = Node {
322 id: mod_id.clone(),
323 label: source_str.to_string(),
324 file_type: "code".to_string(),
325 source_file: path.to_string_lossy().to_string(),
326 source_location: None,
327 community: None,
328 rationale: None,
329 docstring: None,
330 metadata: HashMap::new(),
331 };
332 add_node_if_missing(&mut fragment, mod_node);
333 fragment.edges.push(Edge {
334 source: file_id.clone(),
335 target: mod_id,
336 relation: "imports_from".to_string(),
337 confidence: "EXTRACTED".to_string(),
338 source_file: Some(path.to_string_lossy().to_string()),
339 weight: 1.0,
340 context: None,
341 });
342 }
343 }
344 }
345 }
346
347 Ok(fragment)
348 }
349}
350
351impl LanguageExtractor for TsJavaScriptExtractor {
352 fn file_extensions(&self) -> Vec<&'static str> {
353 vec!["js", "jsx", "mjs", "cjs"]
354 }
355 fn extract(&self, source: &[u8], path: &Path) -> Result<ExtractionFragment> {
356 Self::extract(source, path)
357 }
358 fn resolve_imports(&self, _imports: &[ImportNode]) -> Vec<Edge> {
359 vec![]
360 }
361 fn collect_type_refs(&self, _fragment: &mut ExtractionFragment) {}
362}