Skip to main content

normalize_languages/
dart.rs

1//! Dart language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Dart language support.
9pub 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 // _ prefix = private
69    }
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        // _ prefix means private
78        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        // Check for abstract
208        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        // Dart uses /// for doc comments
243        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        // Extract the import URI
278        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                // Check for 'as' alias
286                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        // Dart: import 'package:name/name.dart'; or import '...' show a, b, c;
311        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        // Handle package: imports
403        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                // Look in lib/
408                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        // Relative imports
417        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        // pub.dev package resolution would go here
435        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        // Check .dart_tool/package_config.json for package locations
454        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        // Look for lib/<name>.dart
495        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}