1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct Dart;
10
11impl Language for Dart {
12 fn name(&self) -> &'static str {
13 "Dart"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["dart"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "dart"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &[
28 "class_definition",
29 "enum_declaration",
30 "mixin_declaration",
31 "extension_declaration",
32 ]
33 }
34
35 fn function_kinds(&self) -> &'static [&'static str] {
36 &[
37 "function_signature",
38 "method_signature",
39 "function_body",
40 "getter_signature",
41 "setter_signature",
42 ]
43 }
44
45 fn type_kinds(&self) -> &'static [&'static str] {
46 &[
47 "class_definition",
48 "enum_declaration",
49 "mixin_declaration",
50 "type_alias",
51 ]
52 }
53
54 fn import_kinds(&self) -> &'static [&'static str] {
55 &["import_specification", "library_export"]
56 }
57
58 fn public_symbol_kinds(&self) -> &'static [&'static str] {
59 &[
60 "class_definition",
61 "function_signature",
62 "method_signature",
63 "enum_declaration",
64 ]
65 }
66
67 fn visibility_mechanism(&self) -> VisibilityMechanism {
68 VisibilityMechanism::NamingConvention }
70
71 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
72 let name = match self.node_name(node, content) {
73 Some(n) => n,
74 None => return Vec::new(),
75 };
76
77 if name.starts_with('_') {
79 return Vec::new();
80 }
81
82 let kind = match node.kind() {
83 "class_definition" => SymbolKind::Class,
84 "enum_declaration" => SymbolKind::Enum,
85 "mixin_declaration" => SymbolKind::Class,
86 "function_signature" | "function_body" => SymbolKind::Function,
87 "method_signature" => SymbolKind::Method,
88 _ => return Vec::new(),
89 };
90
91 vec![Export {
92 name: name.to_string(),
93 kind,
94 line: node.start_position().row + 1,
95 }]
96 }
97
98 fn scope_creating_kinds(&self) -> &'static [&'static str] {
99 &[
100 "block",
101 "for_statement",
102 "while_statement",
103 "do_statement",
104 "switch_statement",
105 "try_statement",
106 ]
107 }
108
109 fn control_flow_kinds(&self) -> &'static [&'static str] {
110 &[
111 "if_statement",
112 "for_statement",
113 "while_statement",
114 "do_statement",
115 "switch_statement",
116 "try_statement",
117 "return_statement",
118 "break_statement",
119 "continue_statement",
120 "throw_expression",
121 "rethrow_expression",
122 ]
123 }
124
125 fn complexity_nodes(&self) -> &'static [&'static str] {
126 &[
127 "if_statement",
128 "for_statement",
129 "while_statement",
130 "do_statement",
131 "switch_statement_case",
132 "catch_clause",
133 "conditional_expression",
134 "logical_and_expression",
135 "logical_or_expression",
136 ]
137 }
138
139 fn nesting_nodes(&self) -> &'static [&'static str] {
140 &[
141 "if_statement",
142 "for_statement",
143 "while_statement",
144 "do_statement",
145 "switch_statement",
146 "try_statement",
147 "function_body",
148 "class_definition",
149 "function_expression",
150 ]
151 }
152
153 fn signature_suffix(&self) -> &'static str {
154 " {}"
155 }
156
157 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
158 let name = self.node_name(node, content)?;
159
160 let return_type = node
161 .child_by_field_name("return_type")
162 .map(|t| content[t.byte_range()].to_string());
163
164 let params = node
165 .child_by_field_name("formal_parameters")
166 .or_else(|| node.child_by_field_name("parameters"))
167 .map(|p| content[p.byte_range()].to_string())
168 .unwrap_or_else(|| "()".to_string());
169
170 let is_method = node.kind().contains("method");
171 let kind = if is_method {
172 SymbolKind::Method
173 } else {
174 SymbolKind::Function
175 };
176
177 let signature = if let Some(ret) = return_type {
178 format!("{} {}{}", ret, name, params)
179 } else {
180 format!("{}{}", name, params)
181 };
182
183 Some(Symbol {
184 name: name.to_string(),
185 kind,
186 signature,
187 docstring: self.extract_docstring(node, content),
188 attributes: Vec::new(),
189 start_line: node.start_position().row + 1,
190 end_line: node.end_position().row + 1,
191 visibility: self.get_visibility(node, content),
192 children: Vec::new(),
193 is_interface_impl: false,
194 implements: Vec::new(),
195 })
196 }
197
198 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
199 let name = self.node_name(node, content)?;
200 let (kind, keyword) = match node.kind() {
201 "enum_declaration" => (SymbolKind::Enum, "enum"),
202 "mixin_declaration" => (SymbolKind::Class, "mixin"),
203 "extension_declaration" => (SymbolKind::Class, "extension"),
204 _ => (SymbolKind::Class, "class"),
205 };
206
207 let is_abstract = node
209 .parent()
210 .map(|p| {
211 let text = &content[p.byte_range()];
212 text.contains("abstract ")
213 })
214 .unwrap_or(false);
215
216 let prefix = if is_abstract {
217 format!("abstract {}", keyword)
218 } else {
219 keyword.to_string()
220 };
221
222 Some(Symbol {
223 name: name.to_string(),
224 kind,
225 signature: format!("{} {}", prefix, name),
226 docstring: self.extract_docstring(node, content),
227 attributes: Vec::new(),
228 start_line: node.start_position().row + 1,
229 end_line: node.end_position().row + 1,
230 visibility: self.get_visibility(node, content),
231 children: Vec::new(),
232 is_interface_impl: false,
233 implements: Vec::new(),
234 })
235 }
236
237 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
238 self.extract_container(node, content)
239 }
240
241 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
242 let mut prev = node.prev_sibling();
244 let mut doc_lines = Vec::new();
245
246 while let Some(sibling) = prev {
247 let text = &content[sibling.byte_range()];
248 if sibling.kind() == "documentation_comment" || text.starts_with("///") {
249 let line = text.strip_prefix("///").unwrap_or(text).trim();
250 doc_lines.push(line.to_string());
251 prev = sibling.prev_sibling();
252 } else {
253 break;
254 }
255 }
256
257 if doc_lines.is_empty() {
258 return None;
259 }
260
261 doc_lines.reverse();
262 Some(doc_lines.join(" "))
263 }
264
265 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
266 Vec::new()
267 }
268
269 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
270 if node.kind() != "import_specification" && node.kind() != "library_export" {
271 return Vec::new();
272 }
273
274 let text = &content[node.byte_range()];
275 let line = node.start_position().row + 1;
276
277 if let Some(start) = text.find('\'').or_else(|| text.find('"')) {
279 let quote = text.chars().nth(start).unwrap();
280 let rest = &text[start + 1..];
281 if let Some(end) = rest.find(quote) {
282 let module = rest[..end].to_string();
283 let is_relative = module.starts_with('.') || module.starts_with('/');
284
285 let alias = if text.contains(" as ") {
287 text.split(" as ")
288 .nth(1)
289 .and_then(|s| s.split(';').next())
290 .map(|s| s.trim().to_string())
291 } else {
292 None
293 };
294
295 return vec![Import {
296 module,
297 names: Vec::new(),
298 alias,
299 is_wildcard: text.contains(" show ") || text.contains(" hide "),
300 is_relative,
301 line,
302 }];
303 }
304 }
305
306 Vec::new()
307 }
308
309 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
310 let names_to_use: Vec<&str> = names
312 .map(|n| n.to_vec())
313 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
314 if names_to_use.is_empty() {
315 format!("import '{}';", import.module)
316 } else {
317 format!(
318 "import '{}' show {};",
319 import.module,
320 names_to_use.join(", ")
321 )
322 }
323 }
324
325 fn is_public(&self, node: &Node, content: &str) -> bool {
326 if let Some(name) = self.node_name(node, content) {
327 !name.starts_with('_')
328 } else {
329 true
330 }
331 }
332
333 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
334 if self.is_public(node, content) {
335 Visibility::Public
336 } else {
337 Visibility::Private
338 }
339 }
340
341 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
342 let name = symbol.name.as_str();
343 match symbol.kind {
344 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
345 crate::SymbolKind::Module => name == "tests" || name == "test",
346 _ => false,
347 }
348 }
349
350 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
351 None
352 }
353
354 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
355 node.child_by_field_name("body")
356 }
357
358 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
359 false
360 }
361
362 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
363 node.child_by_field_name("name")
364 .map(|n| &content[n.byte_range()])
365 }
366
367 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
368 let ext = path.extension()?.to_str()?;
369 if ext != "dart" {
370 return None;
371 }
372 let stem = path.file_stem()?.to_str()?;
373 Some(stem.to_string())
374 }
375
376 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
377 vec![
378 format!("lib/{}.dart", module),
379 format!("lib/src/{}.dart", module),
380 format!("{}.dart", module),
381 ]
382 }
383
384 fn lang_key(&self) -> &'static str {
385 "dart"
386 }
387
388 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
389 import_name.starts_with("dart:")
390 }
391
392 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
393 None
394 }
395
396 fn resolve_local_import(
397 &self,
398 import: &str,
399 current_file: &Path,
400 project_root: &Path,
401 ) -> Option<PathBuf> {
402 if import.starts_with("package:") {
404 let path_part = import.strip_prefix("package:")?;
405 let parts: Vec<&str> = path_part.splitn(2, '/').collect();
406 if parts.len() == 2 {
407 let full = project_root.join("lib").join(parts[1]);
409 if full.is_file() {
410 return Some(full);
411 }
412 }
413 return None;
414 }
415
416 if import.starts_with('.') || import.starts_with('/') {
418 if let Some(dir) = current_file.parent() {
419 let full = dir.join(import);
420 if full.is_file() {
421 return Some(full);
422 }
423 }
424 }
425
426 None
427 }
428
429 fn resolve_external_import(
430 &self,
431 _import_name: &str,
432 _project_root: &Path,
433 ) -> Option<ResolvedPackage> {
434 None
436 }
437
438 fn get_version(&self, project_root: &Path) -> Option<String> {
439 let pubspec = project_root.join("pubspec.yaml");
440 if pubspec.is_file() {
441 if let Ok(content) = std::fs::read_to_string(&pubspec) {
442 for line in content.lines() {
443 if line.starts_with("version:") {
444 return Some(line.strip_prefix("version:")?.trim().to_string());
445 }
446 }
447 }
448 }
449 None
450 }
451
452 fn find_package_cache(&self, project_root: &Path) -> Option<PathBuf> {
453 let packages = project_root.join(".dart_tool/package_config.json");
455 if packages.is_file() {
456 return Some(project_root.join(".dart_tool"));
457 }
458 None
459 }
460
461 fn indexable_extensions(&self) -> &'static [&'static str] {
462 &["dart"]
463 }
464 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
465 Vec::new()
466 }
467
468 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
469 use crate::traits::{has_extension, skip_dotfiles};
470 if skip_dotfiles(name) {
471 return true;
472 }
473 if is_dir && (name == "build" || name == ".dart_tool" || name == ".pub-cache") {
474 return true;
475 }
476 !is_dir && !has_extension(name, self.indexable_extensions())
477 }
478
479 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
480 Vec::new()
481 }
482
483 fn package_module_name(&self, entry_name: &str) -> String {
484 entry_name
485 .strip_suffix(".dart")
486 .unwrap_or(entry_name)
487 .to_string()
488 }
489
490 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
491 if path.is_file() {
492 return Some(path.to_path_buf());
493 }
494 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
496 let lib = path.join("lib").join(format!("{}.dart", name));
497 if lib.is_file() {
498 return Some(lib);
499 }
500 }
501 None
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use crate::validate_unused_kinds_audit;
509
510 #[test]
511 fn unused_node_kinds_audit() {
512 #[rustfmt::skip]
513 let documented_unused: &[&str] = &[
514 "additive_expression", "additive_operator", "annotation", "as_operator",
515 "assert_statement", "assignable_expression", "assignment_expression",
516 "assignment_expression_without_cascade", "await_expression", "binary_operator",
517 "bitwise_and_expression", "bitwise_operator", "bitwise_or_expression",
518 "bitwise_xor_expression", "cascade_section", "case_builtin",
519 "catch_parameters", "class_body", "const_object_expression",
520 "constant_constructor_signature", "constructor_invocation",
521 "constructor_param", "constructor_signature", "constructor_tearoff",
522 "declaration", "dotted_identifier_list", "enum_body", "enum_constant",
523 "equality_expression", "equality_operator", "expression_statement",
524 "extension_body", "extension_type_declaration", "factory_constructor_signature",
525 "finally_clause", "for_element", "for_loop_parts", "formal_parameter",
526 "formal_parameter_list", "function_expression_body", "function_type",
527 "identifier", "identifier_dollar_escaped", "identifier_list",
528 "if_element", "if_null_expression", "import_or_export", "increment_operator",
529 "inferred_type", "initialized_identifier", "initialized_identifier_list",
530 "initialized_variable_definition", "initializer_list_entry", "interface",
531 "interfaces", "is_operator", "label", "lambda_expression",
532 "library_import", "library_name", "local_function_declaration",
533 "local_variable_declaration", "logical_and_operator", "logical_or_operator",
534 "minus_operator", "mixin_application_class", "multiplicative_expression",
535 "multiplicative_operator", "named_parameter_types", "negation_operator",
536 "new_expression", "normal_parameter_type", "nullable_type",
537 "operator_signature", "optional_formal_parameters", "optional_parameter_types",
538 "optional_positional_parameter_types", "parameter_type_list",
539 "parenthesized_expression", "pattern_variable_declaration",
540 "postfix_expression", "postfix_operator", "prefix_operator", "qualified",
541 "record_type", "record_type_field", "record_type_named_field",
542 "redirecting_factory_constructor_signature", "relational_expression",
543 "relational_operator", "representation_declaration", "rethrow_builtin",
544 "scoped_identifier", "shift_expression", "shift_operator", "spread_element",
545 "static_final_declaration", "static_final_declaration_list", "superclass",
546 "super_formal_parameter", "switch_block", "switch_expression",
547 "switch_expression_case", "switch_statement_default", "symbol_literal",
548 "throw_expression_without_cascade", "tilde_operator", "type_arguments",
549 "type_bound", "type_cast", "type_cast_expression", "type_identifier",
550 "type_parameter", "type_parameters", "type_test", "type_test_expression",
551 "typed_identifier", "unary_expression", "void_type", "yield_each_statement",
552 "yield_statement",
553 ];
554 validate_unused_kinds_audit(&Dart, documented_unused)
555 .expect("Dart unused node kinds audit failed");
556 }
557}