Skip to main content

sqry_lang_javascript/
lib.rs

1//! JavaScript language plugin for sqry
2//!
3//! Implements the `LanguagePlugin` trait for JavaScript, providing:
4//! - AST parsing with tree-sitter
5//! - Scope extraction
6//! - Relation extraction via `JavaScriptGraphBuilder` (calls, imports, exports, OOP edges)
7
8pub mod relations;
9
10pub use relations::JavaScriptGraphBuilder;
11
12use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
13use sqry_core::plugin::{
14    LanguageMetadata, LanguagePlugin,
15    error::{ParseError, ScopeError},
16};
17use std::path::Path;
18use streaming_iterator::StreamingIterator;
19use tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
20
21/// JavaScript language plugin
22pub struct JavaScriptPlugin {
23    graph_builder: JavaScriptGraphBuilder,
24}
25
26impl JavaScriptPlugin {
27    #[must_use]
28    pub fn new() -> Self {
29        Self {
30            graph_builder: JavaScriptGraphBuilder::default(),
31        }
32    }
33}
34
35impl Default for JavaScriptPlugin {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl LanguagePlugin for JavaScriptPlugin {
42    fn metadata(&self) -> LanguageMetadata {
43        LanguageMetadata {
44            id: "javascript",
45            name: "JavaScript",
46            version: env!("CARGO_PKG_VERSION"),
47            author: "Verivus Pty Ltd",
48            description: "JavaScript language support for sqry",
49            tree_sitter_version: "0.24",
50        }
51    }
52
53    fn extensions(&self) -> &'static [&'static str] {
54        &["js", "jsx", "mjs"]
55    }
56
57    fn language(&self) -> Language {
58        tree_sitter_javascript::LANGUAGE.into()
59    }
60
61    fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
62        let mut parser = Parser::new();
63        parser
64            .set_language(&self.language())
65            .map_err(|e| ParseError::LanguageSetFailed(e.to_string()))?;
66
67        parser
68            .parse(content, None)
69            .ok_or(ParseError::TreeSitterFailed)
70    }
71
72    fn extract_scopes(
73        &self,
74        tree: &Tree,
75        content: &[u8],
76        file_path: &Path,
77    ) -> Result<Vec<Scope>, ScopeError> {
78        Self::extract_javascript_scopes(tree, content, file_path)
79    }
80
81    fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
82        Some(&self.graph_builder)
83    }
84}
85
86impl JavaScriptPlugin {
87    fn extract_javascript_scopes(
88        tree: &Tree,
89        content: &[u8],
90        file_path: &Path,
91    ) -> Result<Vec<Scope>, ScopeError> {
92        let root_node = tree.root_node();
93        let language = tree_sitter_javascript::LANGUAGE.into();
94
95        let scope_query = Self::scope_query_source();
96        let query = Query::new(&language, scope_query)
97            .map_err(|e| ScopeError::QueryCompilationFailed(e.to_string()))?;
98
99        let mut scopes = Vec::new();
100        let mut cursor = QueryCursor::new();
101        let mut query_matches = cursor.matches(&query, root_node, content);
102
103        while let Some(m) = query_matches.next() {
104            let mut scope_type = None;
105            let mut scope_name = None;
106            let mut scope_start = None;
107            let mut scope_end = None;
108
109            for capture in m.captures {
110                let capture_name = query.capture_names()[capture.index as usize];
111                let node = capture.node;
112
113                if std::path::Path::new(capture_name)
114                    .extension()
115                    .is_some_and(|ext| ext.eq_ignore_ascii_case("type"))
116                {
117                    scope_type = Some(capture_name.trim_end_matches(".type").to_string());
118                    scope_start = Some(node.start_position());
119                    scope_end = Some(node.end_position());
120                } else if std::path::Path::new(capture_name)
121                    .extension()
122                    .is_some_and(|ext| ext.eq_ignore_ascii_case("name"))
123                {
124                    scope_name = node
125                        .utf8_text(content)
126                        .ok()
127                        .map(std::string::ToString::to_string);
128                }
129            }
130
131            if let (Some(stype), Some(sname), Some(start), Some(end)) =
132                (scope_type, scope_name, scope_start, scope_end)
133            {
134                let scope = Scope {
135                    id: ScopeId::new(0),
136                    scope_type: stype,
137                    name: sname,
138                    file_path: file_path.to_path_buf(),
139                    start_line: start.row + 1,
140                    start_column: start.column,
141                    end_line: end.row + 1,
142                    end_column: end.column,
143                    parent_id: None,
144                };
145                scopes.push(scope);
146            }
147        }
148
149        scopes.sort_by_key(|s| (s.start_line, s.start_column));
150
151        link_nested_scopes(&mut scopes);
152        Ok(scopes)
153    }
154
155    fn scope_query_source() -> &'static str {
156        r"
157; Function scopes
158(function_declaration
159  name: (identifier) @function.name
160) @function.type
161
162; Class scopes
163(class_declaration
164  name: (identifier) @class.name
165) @class.type
166
167; Method scopes
168(method_definition
169  name: (property_identifier) @method.name
170) @method.type
171"
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_metadata() {
181        let plugin = JavaScriptPlugin::default();
182        let metadata = plugin.metadata();
183
184        assert_eq!(metadata.id, "javascript");
185        assert_eq!(metadata.name, "JavaScript");
186    }
187
188    #[test]
189    fn test_extensions() {
190        let plugin = JavaScriptPlugin::default();
191        let extensions = plugin.extensions();
192
193        assert_eq!(extensions.len(), 3);
194        assert!(extensions.contains(&"js"));
195        assert!(extensions.contains(&"jsx"));
196        assert!(extensions.contains(&"mjs"));
197    }
198
199    #[test]
200    fn test_parse_ast_simple() {
201        let plugin = JavaScriptPlugin::default();
202        let source = b"function hello() {}";
203
204        let tree = plugin.parse_ast(source).unwrap();
205        assert!(!tree.root_node().has_error());
206    }
207}