Skip to main content

sqry_lang_dart/
lib.rs

1//! Dart language plugin for sqry
2//!
3//! Implements the `LanguagePlugin` trait for Dart, providing:
4//! - Graph-native node/edge extraction via `DartGraphBuilder`
5//! - AST parsing with tree-sitter
6//! - Scope extraction for Dart constructs
7//!
8//! # Supported Features
9//!
10//! - Classes (`class`, `abstract class`)
11//! - Functions (`void`, typed functions)
12//! - Methods (instance and static)
13//! - Variables (`var`, `final`, `const`)
14//! - Async/await support
15//! - Visibility modifiers (public via no underscore, private via underscore)
16//!
17//! # Node Attributes
18//!
19//! All modifiers are detected via AST node walking, avoiding false positives
20//! from comments, strings, or identifiers containing modifier keywords:
21//!
22//! - **`is_async`**: Detected via `async` or `async*` tokens in function body
23//! - **`is_static`**: Detected via `static` keyword node
24//! - **visibility**: Determined by identifier name prefix (`_` = private)
25//!
26//! # Example
27//!
28//! ```
29//! use sqry_lang_dart::DartPlugin;
30//! use sqry_core::plugin::LanguagePlugin;
31//!
32//! let plugin = DartPlugin::default();
33//! let metadata = plugin.metadata();
34//! assert_eq!(metadata.id, "dart");
35//! assert_eq!(metadata.name, "Dart");
36//! ```
37
38use sqry_core::ast::{Scope, ScopeId, link_nested_scopes};
39use sqry_core::plugin::{
40    LanguageMetadata, LanguagePlugin,
41    error::{ParseError, ScopeError},
42};
43use std::path::Path;
44use streaming_iterator::StreamingIterator;
45use tree_sitter::{Language, Parser, Query, QueryCursor, Tree};
46
47const PLUGIN_ID: &str = "dart";
48
49/// Dart relation extraction and graph building
50pub mod relations;
51
52pub use relations::DartGraphBuilder;
53
54/// Dart language plugin
55///
56/// Provides language support for Dart source files (.dart).
57pub struct DartPlugin {
58    graph_builder: DartGraphBuilder,
59}
60
61impl DartPlugin {
62    /// Create a new Dart plugin instance.
63    #[must_use]
64    pub fn new() -> Self {
65        Self {
66            graph_builder: DartGraphBuilder::new(),
67        }
68    }
69}
70
71impl Default for DartPlugin {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl LanguagePlugin for DartPlugin {
78    fn metadata(&self) -> LanguageMetadata {
79        LanguageMetadata {
80            id: PLUGIN_ID,
81            name: "Dart",
82            version: env!("CARGO_PKG_VERSION"),
83            author: "Verivus",
84            description: "Dart language support for sqry",
85            tree_sitter_version: "0.22",
86        }
87    }
88
89    fn extensions(&self) -> &'static [&'static str] {
90        &["dart"]
91    }
92
93    fn language(&self) -> Language {
94        tree_sitter_dart::language()
95    }
96
97    fn parse_ast(&self, content: &[u8]) -> Result<Tree, ParseError> {
98        let mut parser = Parser::new();
99        let language = self.language();
100
101        parser.set_language(&language).map_err(|e| {
102            ParseError::LanguageSetFailed(format!("Failed to set Dart language: {e}"))
103        })?;
104
105        parser
106            .parse(content, None)
107            .ok_or(ParseError::TreeSitterFailed)
108    }
109
110    fn extract_scopes(
111        &self,
112        tree: &Tree,
113        content: &[u8],
114        file_path: &Path,
115    ) -> Result<Vec<Scope>, ScopeError> {
116        Self::extract_dart_scopes(tree, content, file_path)
117    }
118
119    fn graph_builder(&self) -> Option<&dyn sqry_core::graph::GraphBuilder> {
120        Some(&self.graph_builder)
121    }
122}
123
124impl DartPlugin {
125    /// Extract scope information from Dart code
126    fn extract_dart_scopes(
127        tree: &Tree,
128        content: &[u8],
129        file_path: &Path,
130    ) -> Result<Vec<Scope>, ScopeError> {
131        let root_node = tree.root_node();
132        let language = tree_sitter_dart::language();
133
134        let scope_query = Self::scope_query_source();
135
136        let query = Query::new(&language, scope_query)
137            .map_err(|e| ScopeError::QueryCompilationFailed(e.to_string()))?;
138
139        let mut scopes = Vec::new();
140        let mut cursor = QueryCursor::new();
141        let mut query_matches = cursor.matches(&query, root_node, content);
142
143        while let Some(m) = query_matches.next() {
144            let mut scope_type = None;
145            let mut scope_name = None;
146            let mut scope_start = None;
147            let mut scope_end = None;
148
149            for capture in m.captures {
150                let capture_name = query.capture_names()[capture.index as usize];
151                let node = capture.node;
152
153                if let Some((prefix, suffix)) = capture_name.rsplit_once('.') {
154                    match suffix {
155                        "type" => {
156                            scope_type = Some(prefix.to_string());
157                            scope_start = Some(node.start_position());
158                            scope_end = Some(node.end_position());
159                        }
160                        "name" => {
161                            scope_name = node
162                                .utf8_text(content)
163                                .ok()
164                                .map(std::string::ToString::to_string);
165                        }
166                        _ => {}
167                    }
168                }
169            }
170
171            if let (Some(stype), Some(sname), Some(start), Some(end)) =
172                (scope_type, scope_name, scope_start, scope_end)
173            {
174                let scope = Scope {
175                    id: ScopeId::new(0),
176                    scope_type: stype,
177                    name: sname,
178                    file_path: file_path.to_path_buf(),
179                    start_line: start.row + 1,
180                    start_column: start.column,
181                    end_line: end.row + 1,
182                    end_column: end.column,
183                    parent_id: None,
184                };
185                scopes.push(scope);
186            }
187        }
188
189        scopes.sort_by_key(|s| (s.start_line, s.start_column));
190
191        link_nested_scopes(&mut scopes);
192
193        Ok(scopes)
194    }
195
196    /// Returns tree-sitter query source for scope extraction
197    fn scope_query_source() -> &'static str {
198        r"
199; Function scopes (includes both top-level functions and class methods)
200(function_signature
201  name: (identifier) @function.name
202) @function.type
203
204; Class scopes
205(class_definition
206  name: (identifier) @class.name
207) @class.type
208
209; Getter scopes
210(getter_signature
211  name: (identifier) @getter.name
212) @getter.type
213
214; Setter scopes
215(setter_signature
216  name: (identifier) @setter.name
217) @setter.type
218"
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use std::path::Path;
226
227    #[test]
228    fn test_metadata() {
229        let plugin = DartPlugin::default();
230        let metadata = plugin.metadata();
231
232        assert_eq!(metadata.id, "dart");
233        assert_eq!(metadata.name, "Dart");
234        assert_eq!(metadata.author, "Verivus");
235    }
236
237    #[test]
238    fn test_extensions() {
239        let plugin = DartPlugin::default();
240        let extensions = plugin.extensions();
241
242        assert_eq!(extensions.len(), 1);
243        assert!(extensions.contains(&"dart"));
244    }
245
246    #[test]
247    fn test_language() {
248        let plugin = DartPlugin::default();
249        let language = plugin.language();
250
251        assert!(language.abi_version() > 0);
252    }
253
254    #[test]
255    fn test_parse_ast_simple() {
256        let plugin = DartPlugin::default();
257        let source = b"void main() {}";
258
259        let tree = plugin.parse_ast(source).unwrap();
260        assert!(!tree.root_node().has_error());
261    }
262
263    #[test]
264    fn test_extract_scopes_basic() {
265        let plugin = DartPlugin::default();
266        let source = b"class User { void greet() {} } void main() {}";
267        let tree = plugin.parse_ast(source).unwrap();
268        let scopes = plugin
269            .extract_scopes(&tree, source, Path::new("test.dart"))
270            .unwrap();
271
272        assert!(
273            scopes
274                .iter()
275                .any(|scope| scope.scope_type == "class" && scope.name == "User"),
276            "class scope not found"
277        );
278        assert!(
279            scopes
280                .iter()
281                .any(|scope| scope.scope_type == "function" && scope.name == "main"),
282            "function scope not found"
283        );
284    }
285}