1use crate::docstring::extract_preceding_prefix_comments;
4use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
5use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
6use std::path::{Path, PathBuf};
7use tree_sitter::Node;
8
9pub struct Dart;
11
12impl Language for Dart {
13 fn name(&self) -> &'static str {
14 "Dart"
15 }
16 fn extensions(&self) -> &'static [&'static str] {
17 &["dart"]
18 }
19 fn grammar_name(&self) -> &'static str {
20 "dart"
21 }
22
23 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
24 Some(self)
25 }
26
27 fn signature_suffix(&self) -> &'static str {
28 " {}"
29 }
30
31 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
32 extract_preceding_prefix_comments(node, content, "///")
33 }
34
35 fn refine_kind(
36 &self,
37 node: &Node,
38 _content: &str,
39 tag_kind: crate::SymbolKind,
40 ) -> crate::SymbolKind {
41 match node.kind() {
42 "enum_declaration" => crate::SymbolKind::Enum,
43 "mixin_declaration" => crate::SymbolKind::Trait,
44 _ => tag_kind,
45 }
46 }
47
48 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
49 extract_dart_annotations(node, content)
50 }
51
52 fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
53 let mut implements = Vec::new();
54 let mut cursor = node.walk();
55 for child in node.children(&mut cursor) {
56 if child.kind() == "superclass" || child.kind() == "interfaces" {
57 let mut ic = child.walk();
58 for t in child.children(&mut ic) {
59 if t.kind() == "type_identifier" {
60 implements.push(content[t.byte_range()].to_string());
61 }
62 }
63 }
64 }
65 crate::ImplementsInfo {
66 is_interface: false,
67 implements,
68 }
69 }
70
71 fn build_signature(&self, node: &Node, content: &str) -> String {
72 let name = match self.node_name(node, content) {
73 Some(n) => n,
74 None => {
75 return content[node.byte_range()]
76 .lines()
77 .next()
78 .unwrap_or("")
79 .trim()
80 .to_string();
81 }
82 };
83 match node.kind() {
84 k if k.contains("function") || k.contains("method") => {
85 let return_type = node
86 .child_by_field_name("return_type")
87 .map(|t| content[t.byte_range()].to_string());
88 let params = node
89 .child_by_field_name("formal_parameters")
90 .or_else(|| node.child_by_field_name("parameters"))
91 .map(|p| content[p.byte_range()].to_string())
92 .unwrap_or_else(|| "()".to_string());
93 if let Some(ret) = return_type {
94 format!("{} {}{}", ret, name, params)
95 } else {
96 format!("{}{}", name, params)
97 }
98 }
99 "class_declaration" => {
100 let is_abstract = node
101 .parent()
102 .map(|p| content[p.byte_range()].contains("abstract "))
103 .unwrap_or(false);
104 if is_abstract {
105 format!("abstract class {}", name)
106 } else {
107 format!("class {}", name)
108 }
109 }
110 "enum_declaration" => format!("enum {}", name),
111 "mixin_declaration" => format!("mixin {}", name),
112 "extension_declaration" => format!("extension {}", name),
113 _ => {
114 let text = &content[node.byte_range()];
115 text.lines().next().unwrap_or(text).trim().to_string()
116 }
117 }
118 }
119
120 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
121 if node.kind() != "import_specification" && node.kind() != "library_export" {
122 return Vec::new();
123 }
124
125 let text = &content[node.byte_range()];
126 let line = node.start_position().row + 1;
127
128 if let Some(start) = text.find('\'').or_else(|| text.find('"')) {
130 let Some(quote) = text[start..].chars().next() else {
132 return Vec::new();
133 };
134 let rest = &text[start + 1..];
135 if let Some(end) = rest.find(quote) {
136 let module = rest[..end].to_string();
137 let is_relative = module.starts_with('.') || module.starts_with('/');
138
139 let alias = if text.contains(" as ") {
141 text.split(" as ")
142 .nth(1)
143 .and_then(|s| s.split(';').next())
144 .map(|s| s.trim().to_string())
145 } else {
146 None
147 };
148
149 let (names, is_wildcard) = if text.contains(" show ") {
153 let show_names: Vec<String> = text
154 .split(" show ")
155 .nth(1)
156 .unwrap_or("")
157 .split(';')
158 .next()
159 .unwrap_or("")
160 .split(',')
161 .map(|s| s.trim().to_string())
162 .filter(|s| !s.is_empty())
163 .collect();
164 (show_names, false)
165 } else {
166 (Vec::new(), false)
167 };
168
169 return vec![Import {
170 module,
171 names,
172 alias,
173 is_wildcard,
174 is_relative,
175 line,
176 }];
177 }
178 }
179
180 Vec::new()
181 }
182
183 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
184 let names_to_use: Vec<&str> = names
186 .map(|n| n.to_vec())
187 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
188 if names_to_use.is_empty() {
189 format!("import '{}';", import.module)
190 } else {
191 format!(
192 "import '{}' show {};",
193 import.module,
194 names_to_use.join(", ")
195 )
196 }
197 }
198
199 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
200 if let Some(name) = self.node_name(node, content) {
201 if name.starts_with('_') {
202 Visibility::Private
203 } else {
204 Visibility::Public
205 }
206 } else {
207 Visibility::Public
208 }
209 }
210
211 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
212 let name = symbol.name.as_str();
213 match symbol.kind {
214 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
215 crate::SymbolKind::Module => name == "tests" || name == "test",
216 _ => false,
217 }
218 }
219
220 fn test_file_globs(&self) -> &'static [&'static str] {
221 &["**/test/**/*.dart", "**/*_test.dart"]
222 }
223
224 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
225 node.child_by_field_name("body")
226 }
227
228 fn analyze_container_body(
229 &self,
230 body_node: &Node,
231 content: &str,
232 inner_indent: &str,
233 ) -> Option<ContainerBody> {
234 crate::body::analyze_brace_body(body_node, content, inner_indent)
235 }
236
237 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
238 static RESOLVER: DartModuleResolver = DartModuleResolver;
239 Some(&RESOLVER)
240 }
241}
242
243impl LanguageSymbols for Dart {}
244
245pub struct DartModuleResolver;
256
257impl ModuleResolver for DartModuleResolver {
258 fn workspace_config(&self, root: &Path) -> ResolverConfig {
259 let mut path_mappings: Vec<(String, PathBuf)> = Vec::new();
260
261 let pubspec = root.join("pubspec.yaml");
262 if let Ok(content) = std::fs::read_to_string(&pubspec) {
263 for line in content.lines() {
264 let trimmed = line.trim();
265 if let Some(rest) = trimmed.strip_prefix("name:") {
266 let name = rest.trim().trim_matches('"').trim_matches('\'');
267 if !name.is_empty() {
268 path_mappings.push((name.to_string(), root.join("lib")));
269 break;
270 }
271 }
272 }
273 }
274
275 ResolverConfig {
276 workspace_root: root.to_path_buf(),
277 path_mappings,
278 search_roots: vec![root.join("lib")],
279 }
280 }
281
282 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
283 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
284 if ext != "dart" {
285 return Vec::new();
286 }
287 for (pkg_name, lib_dir) in &cfg.path_mappings {
288 if let Ok(rel) = file.strip_prefix(lib_dir) {
289 let rel_str = rel.to_str().unwrap_or("").replace('\\', "/");
290 let canonical = format!("package:{}/{}", pkg_name, rel_str);
291 return vec![ModuleId {
292 canonical_path: canonical,
293 }];
294 }
295 }
296 if let Ok(rel) = file.strip_prefix(&cfg.workspace_root) {
298 return vec![ModuleId {
299 canonical_path: rel.to_str().unwrap_or("").replace('\\', "/"),
300 }];
301 }
302 Vec::new()
303 }
304
305 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
306 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
307 if ext != "dart" {
308 return Resolution::NotApplicable;
309 }
310 let raw = &spec.raw;
311
312 if raw.starts_with("dart:") {
314 return Resolution::NotFound;
315 }
316
317 if raw.starts_with('.') {
319 if let Some(parent) = from_file.parent() {
320 let resolved = parent.join(raw);
321 if resolved.exists() {
322 let name = resolved
323 .file_stem()
324 .and_then(|s| s.to_str())
325 .unwrap_or("")
326 .to_string();
327 return Resolution::Resolved(resolved, name);
328 }
329 }
330 return Resolution::NotFound;
331 }
332
333 if let Some(rest) = raw.strip_prefix("package:") {
335 let slash = rest.find('/');
337 let (pkg, path_in_pkg) = if let Some(idx) = slash {
338 (&rest[..idx], &rest[idx + 1..])
339 } else {
340 (rest, "")
341 };
342
343 for (own_pkg, lib_dir) in &cfg.path_mappings {
345 if pkg == own_pkg {
346 let candidate = lib_dir.join(path_in_pkg);
347 if candidate.exists() {
348 let name = candidate
349 .file_stem()
350 .and_then(|s| s.to_str())
351 .unwrap_or("")
352 .to_string();
353 return Resolution::Resolved(candidate, name);
354 }
355 return Resolution::NotFound;
356 }
357 }
358 }
359
360 Resolution::NotFound
361 }
362}
363
364fn extract_dart_annotations(node: &Node, content: &str) -> Vec<String> {
366 let mut attrs = Vec::new();
367 let mut cursor = node.walk();
368 for child in node.children(&mut cursor) {
369 if child.kind() == "annotation" {
370 let text = content[child.byte_range()].trim().to_string();
371 if !text.is_empty() {
372 attrs.push(text);
373 }
374 }
375 }
376 let mut prev = node.prev_sibling();
377 while let Some(sibling) = prev {
378 if sibling.kind() == "annotation" {
379 let text = content[sibling.byte_range()].trim().to_string();
380 if !text.is_empty() {
381 attrs.insert(0, text);
382 }
383 prev = sibling.prev_sibling();
384 } else {
385 break;
386 }
387 }
388 attrs
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394 use crate::validate_unused_kinds_audit;
395
396 #[test]
397 fn unused_node_kinds_audit() {
398 #[rustfmt::skip]
399 let documented_unused: &[&str] = &[
400 "additive_expression", "additive_operator", "annotation", "as_operator",
401 "assert_statement", "assignable_expression", "assignment_expression",
402 "assignment_expression_without_cascade", "await_expression", "binary_operator",
403 "bitwise_and_expression", "bitwise_operator", "bitwise_or_expression",
404 "bitwise_xor_expression", "cascade_section", "case_builtin",
405 "catch_parameters", "class_body", "const_object_expression",
406 "constant_constructor_signature", "constructor_invocation",
407 "constructor_param", "constructor_signature", "constructor_tearoff",
408 "declaration", "dotted_identifier_list", "enum_body", "enum_constant",
409 "equality_expression", "equality_operator", "expression_statement",
410 "extension_body", "extension_type_declaration", "factory_constructor_signature",
411 "finally_clause", "for_element", "for_loop_parts", "formal_parameter",
412 "formal_parameter_list", "function_expression_body", "function_type",
413 "identifier_dollar_escaped", "identifier_list",
414 "if_element", "if_null_expression", "import_or_export", "increment_operator",
415 "inferred_type", "initialized_identifier", "initialized_identifier_list",
416 "initialized_variable_definition", "initializer_list_entry", "interface",
417 "interfaces", "is_operator", "label", "lambda_expression",
418 "library_import", "library_name", "local_function_declaration",
419 "local_variable_declaration", "logical_and_operator", "logical_or_operator",
420 "minus_operator", "mixin_application_class", "multiplicative_expression",
421 "multiplicative_operator", "named_parameter_types", "negation_operator",
422 "new_expression", "normal_parameter_type", "nullable_type",
423 "operator_signature", "optional_formal_parameters", "optional_parameter_types",
424 "optional_positional_parameter_types", "parameter_type_list",
425 "parenthesized_expression", "pattern_variable_declaration",
426 "postfix_expression", "postfix_operator", "prefix_operator", "qualified",
427 "record_type", "record_type_field", "record_type_named_field",
428 "redirecting_factory_constructor_signature", "relational_expression",
429 "relational_operator", "representation_declaration", "rethrow_builtin",
430 "scoped_identifier", "shift_expression", "shift_operator", "spread_element",
431 "static_final_declaration", "static_final_declaration_list", "superclass",
432 "super_formal_parameter", "switch_block", "switch_expression",
433 "switch_expression_case", "switch_statement_default", "symbol_literal",
434 "throw_expression_without_cascade", "tilde_operator", "type_arguments",
435 "type_bound", "type_cast", "type_cast_expression", "type_identifier",
436 "type_parameter", "type_parameters", "type_test", "type_test_expression",
437 "typed_identifier", "unary_expression", "void_type", "yield_each_statement",
438 "yield_statement",
439 "logical_and_expression",
441 "for_statement",
442 "do_statement",
443 "try_statement",
444 "return_statement",
445 "continue_statement",
446 "catch_clause",
447 "conditional_expression",
448 "break_statement",
449 "switch_statement_case",
450 "block",
451 "switch_statement",
452 "if_statement",
453 "throw_expression",
454 "rethrow_expression",
455 "function_body",
456 "function_expression",
457 "logical_or_expression",
458 "library_export",
459 "while_statement",
460 "import_specification",
461 "method_signature",
462 "type_alias",
463 ];
464 validate_unused_kinds_audit(&Dart, documented_unused)
465 .expect("Dart unused node kinds audit failed");
466 }
467}