sqry_lang_javascript/
lib.rs1pub 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
21pub 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}