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