Skip to main content

normalize_languages/
d.rs

1//! D language support.
2
3use std::path::{Path, PathBuf};
4
5use crate::{
6    ContainerBody, Import, ImportSpec, Language, LanguageSymbols, ModuleId, ModuleResolver,
7    Resolution, ResolverConfig, Visibility,
8};
9use tree_sitter::Node;
10
11/// D language support.
12pub struct D;
13
14impl D {
15    /// Recursively collect type names from a D inheritance clause.
16    /// D nests: base_class_list > super_class_or_interface/interfaces/interface >
17    /// qualified_identifier > identifier(s)
18    fn collect_identifiers(node: &Node, content: &str, out: &mut Vec<String>) {
19        if node.kind() == "qualified_identifier" {
20            out.push(content[node.byte_range()].to_string());
21            return;
22        }
23        let mut cursor = node.walk();
24        for child in node.children(&mut cursor) {
25            Self::collect_identifiers(&child, content, out);
26        }
27    }
28}
29
30impl Language for D {
31    fn name(&self) -> &'static str {
32        "D"
33    }
34    fn extensions(&self) -> &'static [&'static str] {
35        &["d", "di"]
36    }
37    fn grammar_name(&self) -> &'static str {
38        "d"
39    }
40
41    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
42        Some(self)
43    }
44
45    fn signature_suffix(&self) -> &'static str {
46        " {}"
47    }
48
49    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
50        let mut prev = node.prev_sibling();
51        let mut doc_lines = Vec::new();
52        while let Some(sibling) = prev {
53            let text = &content[sibling.byte_range()];
54            match sibling.kind() {
55                "comment" => {
56                    if text.starts_with("///") {
57                        let line = text.strip_prefix("///").unwrap_or(text).trim();
58                        if !line.is_empty() {
59                            doc_lines.push(line.to_string());
60                        }
61                        prev = sibling.prev_sibling();
62                    } else {
63                        break;
64                    }
65                }
66                "block_comment" => {
67                    if text.starts_with("/**") {
68                        let inner = text
69                            .strip_prefix("/**")
70                            .unwrap_or(text)
71                            .strip_suffix("*/")
72                            .unwrap_or(text);
73                        for line in inner.lines() {
74                            let clean = line.trim().strip_prefix('*').unwrap_or(line).trim();
75                            if !clean.is_empty() {
76                                doc_lines.push(clean.to_string());
77                            }
78                        }
79                    }
80                    break;
81                }
82                "nesting_block_comment" => {
83                    if text.starts_with("/++") {
84                        let inner = text
85                            .strip_prefix("/++")
86                            .unwrap_or(text)
87                            .strip_suffix("+/")
88                            .unwrap_or(text);
89                        for line in inner.lines() {
90                            let clean = line.trim().strip_prefix('+').unwrap_or(line).trim();
91                            if !clean.is_empty() {
92                                doc_lines.push(clean.to_string());
93                            }
94                        }
95                    }
96                    break;
97                }
98                _ => break,
99            }
100        }
101        if doc_lines.is_empty() {
102            return None;
103        }
104        doc_lines.reverse();
105        Some(doc_lines.join(" "))
106    }
107
108    fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
109        let mut attrs = Vec::new();
110        let mut cursor = node.walk();
111        for child in node.children(&mut cursor) {
112            if child.kind() == "attribute_specifier" {
113                let text = content[child.byte_range()].trim().to_string();
114                if !text.is_empty() {
115                    attrs.push(text);
116                }
117            }
118        }
119        let mut prev = node.prev_sibling();
120        while let Some(sibling) = prev {
121            if sibling.kind() == "attribute_specifier" {
122                let text = content[sibling.byte_range()].trim().to_string();
123                if !text.is_empty() {
124                    attrs.insert(0, text);
125                }
126                prev = sibling.prev_sibling();
127            } else {
128                break;
129            }
130        }
131        attrs
132    }
133
134    fn build_signature(&self, node: &Node, content: &str) -> String {
135        let name = self.node_name(node, content).unwrap_or("");
136        match node.kind() {
137            "module_declaration" => format!("module {}", name),
138            _ => {
139                let text = &content[node.byte_range()];
140                text.lines().next().unwrap_or(text).trim().to_string()
141            }
142        }
143    }
144
145    fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
146        let mut implements = Vec::new();
147        let mut cursor = node.walk();
148        for child in node.children(&mut cursor) {
149            if child.kind() == "base_class_list" {
150                D::collect_identifiers(&child, content, &mut implements);
151            }
152        }
153        crate::ImplementsInfo {
154            is_interface: false,
155            implements,
156        }
157    }
158
159    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
160        if node.kind() != "import_declaration" {
161            return Vec::new();
162        }
163
164        let text = &content[node.byte_range()];
165        // Strip "import " prefix and trailing ";"
166        let module = text
167            .trim()
168            .strip_prefix("import ")
169            .unwrap_or(text.trim())
170            .trim_end_matches(';')
171            .trim()
172            .to_string();
173        let is_wildcard = module.contains(':');
174        vec![Import {
175            module,
176            names: Vec::new(),
177            alias: None,
178            is_wildcard,
179            is_relative: false,
180            line: node.start_position().row + 1,
181        }]
182    }
183
184    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
185        // D: import module; or import module : a, b, c;
186        let names_to_use: Vec<&str> = names
187            .map(|n| n.to_vec())
188            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
189        if names_to_use.is_empty() {
190            format!("import {};", import.module)
191        } else {
192            format!("import {} : {};", import.module, names_to_use.join(", "))
193        }
194    }
195
196    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
197        let text = &content[node.byte_range()];
198        if text.starts_with("private ") {
199            Visibility::Private
200        } else if text.starts_with("protected ") {
201            Visibility::Protected
202        } else {
203            Visibility::Public
204        }
205    }
206
207    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
208        let name = symbol.name.as_str();
209        match symbol.kind {
210            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
211            crate::SymbolKind::Module => name == "tests" || name == "test",
212            _ => false,
213        }
214    }
215
216    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
217        node.child_by_field_name("body")
218    }
219
220    fn analyze_container_body(
221        &self,
222        body_node: &Node,
223        content: &str,
224        inner_indent: &str,
225    ) -> Option<ContainerBody> {
226        crate::body::analyze_brace_body(body_node, content, inner_indent)
227    }
228
229    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
230        if let Some(name_node) = node.child_by_field_name("name") {
231            return Some(&content[name_node.byte_range()]);
232        }
233        let mut cursor = node.walk();
234        for child in node.children(&mut cursor) {
235            if child.kind() == "identifier" {
236                return Some(&content[child.byte_range()]);
237            }
238            // func_declaration: name is inside func_declarator
239            if child.kind() == "func_declarator" {
240                let mut inner = child.walk();
241                for grandchild in child.children(&mut inner) {
242                    if grandchild.kind() == "identifier" {
243                        return Some(&content[grandchild.byte_range()]);
244                    }
245                }
246            }
247        }
248        None
249    }
250
251    fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
252        static RESOLVER: DModuleResolver = DModuleResolver;
253        Some(&RESOLVER)
254    }
255}
256
257impl LanguageSymbols for D {}
258
259// =============================================================================
260// D Module Resolver
261// =============================================================================
262
263/// Module resolver for D.
264///
265/// Uses `dub.json` at the workspace root to find source paths (`sourcePaths`,
266/// default `["source"]`). Module names map to file paths: `mypackage.utils` →
267/// `mypackage/utils.d` under a source root.
268pub struct DModuleResolver;
269
270impl ModuleResolver for DModuleResolver {
271    fn workspace_config(&self, root: &Path) -> ResolverConfig {
272        let mut search_roots: Vec<PathBuf> = Vec::new();
273
274        // Try dub.json first
275        let dub_json = root.join("dub.json");
276        if let Ok(content) = std::fs::read_to_string(&dub_json)
277            && let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&content)
278            && let Some(paths) = parsed.get("sourcePaths").and_then(|v| v.as_array())
279        {
280            for path in paths {
281                if let Some(s) = path.as_str() {
282                    search_roots.push(root.join(s));
283                }
284            }
285        }
286
287        // Default to source/ (dub convention) if not found in config
288        if search_roots.is_empty() {
289            search_roots.push(root.join("source"));
290        }
291
292        ResolverConfig {
293            workspace_root: root.to_path_buf(),
294            path_mappings: Vec::new(),
295            search_roots,
296        }
297    }
298
299    fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
300        let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
301        if ext != "d" && ext != "di" {
302            return Vec::new();
303        }
304
305        for root in &cfg.search_roots {
306            if let Ok(rel) = file.strip_prefix(root) {
307                let rel_str = rel.to_string_lossy();
308                // Strip .d/.di extension and replace / with .
309                let base = rel_str
310                    .strip_suffix(".di")
311                    .or_else(|| rel_str.strip_suffix(".d"))
312                    .unwrap_or(&rel_str);
313                let canonical = if cfg!(windows) {
314                    base.replace('\\', ".")
315                } else {
316                    base.replace('/', ".")
317                };
318                if !canonical.is_empty() {
319                    return vec![ModuleId {
320                        canonical_path: canonical,
321                    }];
322                }
323            }
324        }
325
326        Vec::new()
327    }
328
329    fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
330        let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
331        if ext != "d" && ext != "di" {
332            return Resolution::NotApplicable;
333        }
334
335        // `mypackage.utils` → `mypackage/utils.d`
336        let file_path = spec.raw.replace('.', "/") + ".d";
337
338        for root in &cfg.search_roots {
339            let candidate = root.join(&file_path);
340            if candidate.exists() {
341                return Resolution::Resolved(candidate, String::new());
342            }
343        }
344
345        Resolution::NotFound
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::validate_unused_kinds_audit;
353
354    #[test]
355    fn unused_node_kinds_audit() {
356        #[rustfmt::skip]
357        let documented_unused: &[&str] = &[
358            // Expressions
359            "add_expression", "and_and_expression", "and_expression", "assign_expression",
360            "assert_expression", "cat_expression", "cast_expression", "comma_expression",
361            "complement_expression", "conditional_expression", "delete_expression", "equal_expression",
362            "expression", "identity_expression", "import_expression", "in_expression",
363            "index_expression", "is_expression", "key_expression", "lwr_expression",
364            "mixin_expression", "mul_expression", "new_anon_class_expression", "new_expression",
365            "or_expression", "or_or_expression", "postfix_expression", "pow_expression",
366            "primary_expression", "qualified_identifier", "rel_expression", "shift_expression",
367            "slice_expression", "traits_expression", "typeid_expression", "unary_expression",
368            "upr_expression", "value_expression", "xor_expression",
369            // Statements
370            "asm_statement", "break_statement", "case_range_statement", "case_statement",
371            "conditional_statement", "continue_statement", "declaration_statement", "default_statement",
372            "do_statement", "empty_statement", "expression_statement", "final_switch_statement",
373            "foreach_range_statement", "goto_statement", "labeled_statement", "mixin_statement",
374            "out_statement", "pragma_statement", "return_statement", "scope_block_statement",
375            "scope_guard_statement", "scope_statement_list", "statement_list",
376            "statement_list_no_case_no_default", "static_foreach_statement", "synchronized_statement",
377            "then_statement", "throw_statement", "try_statement", "with_statement",
378            // Declarations
379            "anonymous_enum_declaration", "anonymous_enum_member",
380            "anonymous_enum_members", "anon_struct_declaration", "anon_union_declaration",
381            "auto_func_declaration", "class_template_declaration",
382            "conditional_declaration", "debug_specification", "destructor", "empty_declaration",
383            "enum_body", "enum_member", "enum_member_attribute", "enum_member_attributes",
384            "enum_members", "interface_template_declaration", "mixin_declaration",
385            "module", "shared_static_constructor", "shared_static_destructor", "static_constructor",
386            "static_destructor", "static_foreach_declaration", "struct_template_declaration",
387            "template_declaration", "template_mixin_declaration", "union_declaration",
388            "union_template_declaration", "var_declarations", "version_specification",
389            // Foreach-related
390            "aggregate_foreach", "foreach", "foreach_aggregate", "foreach_type",
391            "foreach_type_attribute", "foreach_type_attributes", "foreach_type_list",
392            "range_foreach", "static_foreach",
393            // Function-related
394            "constructor_args", "constructor_template", "function_attribute_kwd",
395            "function_attributes", "function_contracts", "function_literal_body",
396            "function_literal_body2", "member_function_attribute", "member_function_attributes",
397            "missing_function_body", "out_contract_expression", "in_contract_expression",
398            "in_statement", "parameter_with_attributes", "parameter_with_member_attributes",
399            "shortened_function_body", "specified_function_body",
400            // Template-related
401            "template_type_parameter", "template_type_parameter_default",
402            "template_type_parameter_specialization", "type_specialization",
403            // Type-related
404            "aggregate_body", "basic_type", "catch_parameter", "catches", "constructor",
405            "else_statement", "enum_base_type", "finally_statement", "fundamental_type",
406            "if_condition", "interfaces", "linkage_type", "module_alias_identifier",
407            "module_attributes", "module_fully_qualified_name", "module_name", "mixin_type",
408            "mixin_qualified_identifier", "storage_class", "storage_classes", "type",
409            "type_ctor", "type_ctors", "type_suffix", "type_suffixes", "typeof", "interface",
410            // Import-related
411            "import", "import_bind", "import_bind_list", "import_bindings", "import_list",
412            // ASM-related
413            "asm_instruction", "asm_instruction_list", "asm_shift_exp", "asm_type_prefix",
414            "gcc_asm_instruction_list", "gcc_asm_statement", "gcc_basic_asm_instruction",
415            "gcc_ext_asm_instruction", "gcc_goto_asm_instruction",
416            // Misc
417            "alt_declarator_identifier", "base_class_list", "base_interface_list",
418            "block_comment", "declaration_block", "declarator_identifier_list", "dot_identifier", "nesting_block_comment", "static_if_condition", "struct_initializer",
419            "struct_member_initializer", "struct_member_initializers", "super_class_or_interface",
420            "traits_arguments", "traits_keyword", "var_declarator_identifier", "vector_base_type",
421            "attribute_specifier",
422            // structural node, not extracted as symbols
423            "alias_declaration",
424            "auto_declaration",
425            "module_declaration",
426            "block_statement",
427            "import_declaration",
428            "while_statement",
429            "switch_statement",
430            "if_statement",
431            "function_literal",
432            "for_statement",
433            "foreach_statement",
434            "catch",
435        ];
436        validate_unused_kinds_audit(&D, documented_unused)
437            .expect("D unused node kinds audit failed");
438    }
439}