1use 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
49pub mod relations;
51
52pub use relations::DartGraphBuilder;
53
54pub struct DartPlugin {
58 graph_builder: DartGraphBuilder,
59}
60
61impl DartPlugin {
62 #[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 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 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}