1use 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
11pub struct D;
13
14impl D {
15 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 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 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 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
259pub 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 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 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 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 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 "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 "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 "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 "aggregate_foreach", "foreach", "foreach_aggregate", "foreach_type",
391 "foreach_type_attribute", "foreach_type_attributes", "foreach_type_list",
392 "range_foreach", "static_foreach",
393 "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_type_parameter", "template_type_parameter_default",
402 "template_type_parameter_specialization", "type_specialization",
403 "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", "import_bind", "import_bind_list", "import_bindings", "import_list",
412 "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 "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 "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}