Skip to main content

normalize_languages/
dart.rs

1//! Dart language support.
2
3use crate::docstring::extract_preceding_prefix_comments;
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use tree_sitter::Node;
6
7/// Dart language support.
8pub 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        // Extract the import URI
127        if let Some(start) = text.find('\'').or_else(|| text.find('"')) {
128            // start is a byte offset; slice at it (safe: ASCII quote is single-byte) then take first char
129            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                // Check for 'as' alias
138                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                // Extract names from 'show' clause; 'hide' imports are named but we
148                // don't suppress names so they remain empty (caller can query show node).
149                // Neither show nor hide is a wildcard import.
150                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        // Dart: import 'package:name/name.dart'; or import '...' show a, b, c;
183        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
238/// Extract Dart annotations from child and preceding sibling nodes.
239fn 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            // control flow — not extracted as symbols
314            "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}