Skip to main content

aptu_coder_core/languages/
javascript.rs

1// SPDX-FileCopyrightText: 2026 aptu-coder contributors
2// SPDX-License-Identifier: Apache-2.0
3/// Tree-sitter query for extracting JavaScript elements (functions, classes, and related).
4pub const ELEMENT_QUERY: &str = r"
5(function_declaration) @function
6(class_declaration) @class
7(method_definition) @function
8(generator_function_declaration) @function
9
10";
11
12/// Tree-sitter query for extracting function calls.
13pub const CALL_QUERY: &str = r"
14(call_expression
15  function: (identifier) @call)
16(call_expression
17  function: (member_expression property: (property_identifier) @call))
18";
19
20/// Tree-sitter query for extracting JavaScript imports (ESM and CommonJS).
21pub const IMPORT_QUERY: &str = r#"
22(import_statement) @import_path
23(call_expression
24  function: (identifier) @_fn (#eq? @_fn "require")
25  arguments: (arguments (string) @import_path))
26"#;
27
28/// Tree-sitter query for extracting definition and use sites.
29pub const DEFUSE_QUERY: &str = r"
30(variable_declarator name: (identifier) @write.declarator)
31(assignment_expression left: (identifier) @write.assign)
32(augmented_assignment_expression left: (identifier) @writeread.augmented)
33(update_expression argument: (identifier) @writeread.update)
34(identifier) @read.usage
35";
36
37// JavaScript intentionally has no REFERENCE_QUERY. JavaScript's dynamic typing
38// makes static type reference extraction low-value: most "type" references in JS
39// are just identifiers that appear in many non-type contexts, producing excessive
40// false positives with no meaningful signal. The `reference_query` field is set
41// to `None` for the JavaScript handler in `mod.rs`.
42
43use tree_sitter::Node;
44
45/// Extract function name from a JavaScript function or method declaration.
46#[must_use]
47pub fn extract_function_name(node: &Node, source: &str, _lang: &str) -> Option<String> {
48    if node.kind() != "function_declaration" && node.kind() != "method_definition" {
49        return None;
50    }
51    node.child_by_field_name("name").and_then(|n| {
52        let end = n.end_byte();
53        if end <= source.len() {
54            Some(source[n.start_byte()..end].to_string())
55        } else {
56            None
57        }
58    })
59}
60
61/// Find receiver type (enclosing class) for a JavaScript method.
62#[must_use]
63pub fn find_receiver_type(node: &Node, source: &str) -> Option<String> {
64    if node.kind() != "method_definition" {
65        return None;
66    }
67
68    // Walk ancestors to find enclosing class_declaration
69    let mut current = *node;
70    while let Some(parent) = current.parent() {
71        if parent.kind() == "class_declaration" {
72            // Found the enclosing class, extract its name
73            return parent.child_by_field_name("name").and_then(|n| {
74                let end = n.end_byte();
75                if end <= source.len() {
76                    Some(source[n.start_byte()..end].to_string())
77                } else {
78                    None
79                }
80            });
81        }
82        current = parent;
83    }
84
85    None
86}
87
88/// Find method name when inside a class body.
89#[must_use]
90pub fn find_method_for_receiver(
91    node: &Node,
92    source: &str,
93    _depth: Option<usize>,
94) -> Option<String> {
95    if node.kind() != "method_definition" {
96        return None;
97    }
98
99    // Verify that the method is inside a class_declaration
100    let mut current = *node;
101    let mut in_class = false;
102    while let Some(parent) = current.parent() {
103        if parent.kind() == "class_declaration" {
104            in_class = true;
105            break;
106        }
107        current = parent;
108    }
109
110    if !in_class {
111        return None;
112    }
113
114    // Return the method name
115    node.child_by_field_name("name").and_then(|n| {
116        let end = n.end_byte();
117        if end <= source.len() {
118            Some(source[n.start_byte()..end].to_string())
119        } else {
120            None
121        }
122    })
123}
124
125/// Extract inheritance information from a JavaScript class node.
126#[must_use]
127pub fn extract_inheritance(node: &Node, source: &str) -> Vec<String> {
128    let mut inherits = Vec::new();
129    // Walk children to find class_heritage node
130    for i in 0..node.named_child_count() {
131        if let Some(child) = node.named_child(u32::try_from(i).unwrap_or(u32::MAX))
132            && child.kind() == "class_heritage"
133        {
134            // Walk class_heritage children for extends_clause
135            for j in 0..child.named_child_count() {
136                if let Some(clause) = child.named_child(u32::try_from(j).unwrap_or(u32::MAX))
137                    && clause.kind() == "extends_clause"
138                    && let Some(value) = clause.child_by_field_name("value")
139                {
140                    let text = &source[value.start_byte()..value.end_byte()];
141                    inherits.push(format!("extends {text}"));
142                }
143            }
144        }
145    }
146    inherits
147}
148
149#[cfg(all(test, feature = "lang-javascript"))]
150mod tests {
151    use crate::DefUseKind;
152    use crate::parser::SemanticExtractor;
153    use tree_sitter::Parser;
154
155    fn parse_js(src: &str) -> tree_sitter::Tree {
156        let mut parser = Parser::new();
157        parser
158            .set_language(&tree_sitter_javascript::LANGUAGE.into())
159            .unwrap();
160        parser.parse(src, None).unwrap()
161    }
162
163    fn find_node_by_kind<'a>(
164        node: tree_sitter::Node<'a>,
165        kind: &str,
166    ) -> Option<tree_sitter::Node<'a>> {
167        if node.kind() == kind {
168            return Some(node);
169        }
170        for i in 0..node.child_count() {
171            if let Some(child) = node.child(u32::try_from(i).unwrap_or(u32::MAX)) {
172                if let Some(found) = find_node_by_kind(child, kind) {
173                    return Some(found);
174                }
175            }
176        }
177        None
178    }
179
180    #[test]
181    fn test_extract_function_name() {
182        // Arrange: free function declaration
183        let src = "function foo() {}";
184        let tree = parse_js(src);
185        let root = tree.root_node();
186
187        // Find function_declaration node
188        let func_node =
189            find_node_by_kind(root, "function_declaration").expect("expected function_declaration");
190
191        // Act
192        let result = super::extract_function_name(&func_node, src, "javascript");
193
194        // Assert
195        assert_eq!(result, Some("foo".to_string()));
196    }
197
198    #[test]
199    fn test_extract_method_name() {
200        // Arrange: method inside a class
201        let src = "class C { bar() {} }";
202        let tree = parse_js(src);
203        let root = tree.root_node();
204
205        // Find method_definition node
206        let method_node =
207            find_node_by_kind(root, "method_definition").expect("expected method_definition");
208
209        // Act
210        let result = super::extract_function_name(&method_node, src, "javascript");
211
212        // Assert
213        assert_eq!(result, Some("bar".to_string()));
214    }
215
216    #[test]
217    fn test_find_receiver_type() {
218        // Arrange: method inside a class
219        let src = "class MyClass { baz() {} }";
220        let tree = parse_js(src);
221        let root = tree.root_node();
222
223        // Find method_definition node
224        let method_node =
225            find_node_by_kind(root, "method_definition").expect("expected method_definition");
226
227        // Act
228        let result = super::find_receiver_type(&method_node, src);
229
230        // Assert
231        assert_eq!(result, Some("MyClass".to_string()));
232    }
233
234    #[test]
235    fn test_find_method_for_receiver() {
236        // Arrange: method inside a class
237        let src = "class C { qux() {} }";
238        let tree = parse_js(src);
239        let root = tree.root_node();
240
241        // Find method_definition node
242        let method_node =
243            find_node_by_kind(root, "method_definition").expect("expected method_definition");
244
245        // Act
246        let result = super::find_method_for_receiver(&method_node, src, None);
247
248        // Assert
249        assert_eq!(result, Some("qux".to_string()));
250    }
251
252    #[test]
253    fn test_function_declaration() {
254        let src = "function greet() { return 42; }";
255        let tree = parse_js(src);
256        let root = tree.root_node();
257        let func = find_node_by_kind(root, "function_declaration");
258        assert!(func.is_some(), "expected to find function_declaration");
259    }
260
261    #[test]
262    fn test_arrow_function() {
263        let src = "const add = (a, b) => a + b;";
264        let tree = parse_js(src);
265        let root = tree.root_node();
266        let arrow = find_node_by_kind(root, "arrow_function");
267        assert!(arrow.is_some(), "expected to find arrow_function");
268    }
269
270    #[test]
271    fn test_class_declaration() {
272        let src = "class Foo extends Bar { method() {} }";
273        let tree = parse_js(src);
274        let root = tree.root_node();
275        let class = find_node_by_kind(root, "class_declaration");
276        assert!(class.is_some(), "expected to find class_declaration");
277    }
278
279    #[test]
280    fn test_es_import() {
281        let src = "import {x} from 'module';";
282        let tree = parse_js(src);
283        let root = tree.root_node();
284        let import = find_node_by_kind(root, "import_statement");
285        assert!(import.is_some(), "expected to find import_statement");
286    }
287
288    #[test]
289    fn test_commonjs_require() {
290        let src = "const lib = require('lib');";
291        let tree = parse_js(src);
292        let root = tree.root_node();
293        let call = find_node_by_kind(root, "call_expression");
294        assert!(call.is_some(), "expected to find call_expression");
295    }
296
297    #[test]
298    fn test_defuse_query_write_site() {
299        // Arrange
300        let src = "let y = 10;\n";
301        let sites =
302            SemanticExtractor::extract_def_use_for_file(src, "javascript", "y", "test.js", None);
303        assert!(!sites.is_empty(), "defuse sites should not be empty");
304        let has_write = sites.iter().any(|s| matches!(s.kind, DefUseKind::Write));
305        assert!(has_write, "should contain a Write DefUseSite");
306    }
307}