1use crate::docstring::extract_preceding_prefix_comments;
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use tree_sitter::Node;
6
7pub struct Dart;
9
10impl Language for Dart {
11 fn name(&self) -> &'static str {
12 "Dart"
13 }
14 fn extensions(&self) -> &'static [&'static str] {
15 &["dart"]
16 }
17 fn grammar_name(&self) -> &'static str {
18 "dart"
19 }
20
21 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
22 Some(self)
23 }
24
25 fn signature_suffix(&self) -> &'static str {
26 " {}"
27 }
28
29 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
30 extract_preceding_prefix_comments(node, content, "///")
31 }
32
33 fn refine_kind(
34 &self,
35 node: &Node,
36 _content: &str,
37 tag_kind: crate::SymbolKind,
38 ) -> crate::SymbolKind {
39 match node.kind() {
40 "enum_declaration" => crate::SymbolKind::Enum,
41 "mixin_declaration" => crate::SymbolKind::Trait,
42 _ => tag_kind,
43 }
44 }
45
46 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
47 extract_dart_annotations(node, content)
48 }
49
50 fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
51 let mut implements = Vec::new();
52 let mut cursor = node.walk();
53 for child in node.children(&mut cursor) {
54 if child.kind() == "superclass" || child.kind() == "interfaces" {
55 let mut ic = child.walk();
56 for t in child.children(&mut ic) {
57 if t.kind() == "type_identifier" {
58 implements.push(content[t.byte_range()].to_string());
59 }
60 }
61 }
62 }
63 crate::ImplementsInfo {
64 is_interface: false,
65 implements,
66 }
67 }
68
69 fn build_signature(&self, node: &Node, content: &str) -> String {
70 let name = match self.node_name(node, content) {
71 Some(n) => n,
72 None => {
73 return content[node.byte_range()]
74 .lines()
75 .next()
76 .unwrap_or("")
77 .trim()
78 .to_string();
79 }
80 };
81 match node.kind() {
82 k if k.contains("function") || k.contains("method") => {
83 let return_type = node
84 .child_by_field_name("return_type")
85 .map(|t| content[t.byte_range()].to_string());
86 let params = node
87 .child_by_field_name("formal_parameters")
88 .or_else(|| node.child_by_field_name("parameters"))
89 .map(|p| content[p.byte_range()].to_string())
90 .unwrap_or_else(|| "()".to_string());
91 if let Some(ret) = return_type {
92 format!("{} {}{}", ret, name, params)
93 } else {
94 format!("{}{}", name, params)
95 }
96 }
97 "class_declaration" => {
98 let is_abstract = node
99 .parent()
100 .map(|p| content[p.byte_range()].contains("abstract "))
101 .unwrap_or(false);
102 if is_abstract {
103 format!("abstract class {}", name)
104 } else {
105 format!("class {}", name)
106 }
107 }
108 "enum_declaration" => format!("enum {}", name),
109 "mixin_declaration" => format!("mixin {}", name),
110 "extension_declaration" => format!("extension {}", name),
111 _ => {
112 let text = &content[node.byte_range()];
113 text.lines().next().unwrap_or(text).trim().to_string()
114 }
115 }
116 }
117
118 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
119 if node.kind() != "import_specification" && node.kind() != "library_export" {
120 return Vec::new();
121 }
122
123 let text = &content[node.byte_range()];
124 let line = node.start_position().row + 1;
125
126 if let Some(start) = text.find('\'').or_else(|| text.find('"')) {
128 let Some(quote) = text[start..].chars().next() else {
130 return Vec::new();
131 };
132 let rest = &text[start + 1..];
133 if let Some(end) = rest.find(quote) {
134 let module = rest[..end].to_string();
135 let is_relative = module.starts_with('.') || module.starts_with('/');
136
137 let alias = if text.contains(" as ") {
139 text.split(" as ")
140 .nth(1)
141 .and_then(|s| s.split(';').next())
142 .map(|s| s.trim().to_string())
143 } else {
144 None
145 };
146
147 let (names, is_wildcard) = if text.contains(" show ") {
151 let show_names: Vec<String> = text
152 .split(" show ")
153 .nth(1)
154 .unwrap_or("")
155 .split(';')
156 .next()
157 .unwrap_or("")
158 .split(',')
159 .map(|s| s.trim().to_string())
160 .filter(|s| !s.is_empty())
161 .collect();
162 (show_names, false)
163 } else {
164 (Vec::new(), false)
165 };
166
167 return vec![Import {
168 module,
169 names,
170 alias,
171 is_wildcard,
172 is_relative,
173 line,
174 }];
175 }
176 }
177
178 Vec::new()
179 }
180
181 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
182 let names_to_use: Vec<&str> = names
184 .map(|n| n.to_vec())
185 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
186 if names_to_use.is_empty() {
187 format!("import '{}';", import.module)
188 } else {
189 format!(
190 "import '{}' show {};",
191 import.module,
192 names_to_use.join(", ")
193 )
194 }
195 }
196
197 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
198 if let Some(name) = self.node_name(node, content) {
199 if name.starts_with('_') {
200 Visibility::Private
201 } else {
202 Visibility::Public
203 }
204 } else {
205 Visibility::Public
206 }
207 }
208
209 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
210 let name = symbol.name.as_str();
211 match symbol.kind {
212 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
213 crate::SymbolKind::Module => name == "tests" || name == "test",
214 _ => false,
215 }
216 }
217
218 fn test_file_globs(&self) -> &'static [&'static str] {
219 &["**/test/**/*.dart", "**/*_test.dart"]
220 }
221
222 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
223 node.child_by_field_name("body")
224 }
225
226 fn analyze_container_body(
227 &self,
228 body_node: &Node,
229 content: &str,
230 inner_indent: &str,
231 ) -> Option<ContainerBody> {
232 crate::body::analyze_brace_body(body_node, content, inner_indent)
233 }
234}
235
236impl LanguageSymbols for Dart {}
237
238fn extract_dart_annotations(node: &Node, content: &str) -> Vec<String> {
240 let mut attrs = Vec::new();
241 let mut cursor = node.walk();
242 for child in node.children(&mut cursor) {
243 if child.kind() == "annotation" {
244 let text = content[child.byte_range()].trim().to_string();
245 if !text.is_empty() {
246 attrs.push(text);
247 }
248 }
249 }
250 let mut prev = node.prev_sibling();
251 while let Some(sibling) = prev {
252 if sibling.kind() == "annotation" {
253 let text = content[sibling.byte_range()].trim().to_string();
254 if !text.is_empty() {
255 attrs.insert(0, text);
256 }
257 prev = sibling.prev_sibling();
258 } else {
259 break;
260 }
261 }
262 attrs
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::validate_unused_kinds_audit;
269
270 #[test]
271 fn unused_node_kinds_audit() {
272 #[rustfmt::skip]
273 let documented_unused: &[&str] = &[
274 "additive_expression", "additive_operator", "annotation", "as_operator",
275 "assert_statement", "assignable_expression", "assignment_expression",
276 "assignment_expression_without_cascade", "await_expression", "binary_operator",
277 "bitwise_and_expression", "bitwise_operator", "bitwise_or_expression",
278 "bitwise_xor_expression", "cascade_section", "case_builtin",
279 "catch_parameters", "class_body", "const_object_expression",
280 "constant_constructor_signature", "constructor_invocation",
281 "constructor_param", "constructor_signature", "constructor_tearoff",
282 "declaration", "dotted_identifier_list", "enum_body", "enum_constant",
283 "equality_expression", "equality_operator", "expression_statement",
284 "extension_body", "extension_type_declaration", "factory_constructor_signature",
285 "finally_clause", "for_element", "for_loop_parts", "formal_parameter",
286 "formal_parameter_list", "function_expression_body", "function_type",
287 "identifier_dollar_escaped", "identifier_list",
288 "if_element", "if_null_expression", "import_or_export", "increment_operator",
289 "inferred_type", "initialized_identifier", "initialized_identifier_list",
290 "initialized_variable_definition", "initializer_list_entry", "interface",
291 "interfaces", "is_operator", "label", "lambda_expression",
292 "library_import", "library_name", "local_function_declaration",
293 "local_variable_declaration", "logical_and_operator", "logical_or_operator",
294 "minus_operator", "mixin_application_class", "multiplicative_expression",
295 "multiplicative_operator", "named_parameter_types", "negation_operator",
296 "new_expression", "normal_parameter_type", "nullable_type",
297 "operator_signature", "optional_formal_parameters", "optional_parameter_types",
298 "optional_positional_parameter_types", "parameter_type_list",
299 "parenthesized_expression", "pattern_variable_declaration",
300 "postfix_expression", "postfix_operator", "prefix_operator", "qualified",
301 "record_type", "record_type_field", "record_type_named_field",
302 "redirecting_factory_constructor_signature", "relational_expression",
303 "relational_operator", "representation_declaration", "rethrow_builtin",
304 "scoped_identifier", "shift_expression", "shift_operator", "spread_element",
305 "static_final_declaration", "static_final_declaration_list", "superclass",
306 "super_formal_parameter", "switch_block", "switch_expression",
307 "switch_expression_case", "switch_statement_default", "symbol_literal",
308 "throw_expression_without_cascade", "tilde_operator", "type_arguments",
309 "type_bound", "type_cast", "type_cast_expression", "type_identifier",
310 "type_parameter", "type_parameters", "type_test", "type_test_expression",
311 "typed_identifier", "unary_expression", "void_type", "yield_each_statement",
312 "yield_statement",
313 "logical_and_expression",
315 "for_statement",
316 "do_statement",
317 "try_statement",
318 "return_statement",
319 "continue_statement",
320 "catch_clause",
321 "conditional_expression",
322 "break_statement",
323 "switch_statement_case",
324 "block",
325 "switch_statement",
326 "if_statement",
327 "throw_expression",
328 "rethrow_expression",
329 "function_body",
330 "function_expression",
331 "logical_or_expression",
332 "library_export",
333 "while_statement",
334 "import_specification",
335 "method_signature",
336 "type_alias",
337 ];
338 validate_unused_kinds_audit(&Dart, documented_unused)
339 .expect("Dart unused node kinds audit failed");
340 }
341}