Skip to main content

sqry_lang_typescript/
lib.rs

1//! TypeScript language plugin for sqry
2//!
3//! Implements the `LanguagePlugin` trait for TypeScript, providing:
4//! - AST parsing with tree-sitter
5//! - Scope extraction
6//! - Relation extraction via `TypeScriptGraphBuilder` (calls, imports, exports, OOP edges)
7
8pub 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
21/// TypeScript language plugin
22pub struct TypeScriptPlugin {
23    graph_builder: TypeScriptGraphBuilder,
24}
25
26impl TypeScriptPlugin {
27    /// Create a new TypeScript plugin instance
28    #[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}