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 Rust;
13
14impl Language for Rust {
15 fn name(&self) -> &'static str {
16 "Rust"
17 }
18 fn extensions(&self) -> &'static [&'static str] {
19 &["rs"]
20 }
21 fn grammar_name(&self) -> &'static str {
22 "rust"
23 }
24
25 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
26 Some(self)
27 }
28
29 fn signature_suffix(&self) -> &'static str {
30 " {}"
31 }
32
33 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
34 extract_docstring(node, content)
35 }
36
37 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
38 extract_attributes(node, content)
39 }
40
41 fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
42 if node.kind() == "impl_item" {
43 let type_node = match node.child_by_field_name("type") {
44 Some(n) => n,
45 None => return crate::ImplementsInfo::default(),
46 };
47 let _ = &content[type_node.byte_range()]; let is_interface = node.child_by_field_name("trait").is_some();
49 let implements = if let Some(trait_node) = node.child_by_field_name("trait") {
50 vec![content[trait_node.byte_range()].to_string()]
51 } else {
52 Vec::new()
53 };
54 crate::ImplementsInfo {
55 is_interface,
56 implements,
57 }
58 } else {
59 crate::ImplementsInfo::default()
60 }
61 }
62
63 fn refine_kind(
64 &self,
65 node: &Node,
66 _content: &str,
67 tag_kind: crate::SymbolKind,
68 ) -> crate::SymbolKind {
69 match node.kind() {
70 "struct_item" => crate::SymbolKind::Struct,
71 "enum_item" => crate::SymbolKind::Enum,
72 "type_item" => crate::SymbolKind::Type,
73 "union_item" => crate::SymbolKind::Struct,
74 "trait_item" => crate::SymbolKind::Trait,
75 _ => tag_kind,
76 }
77 }
78
79 fn build_signature(&self, node: &Node, content: &str) -> String {
80 match node.kind() {
81 "function_item" | "function_signature_item" => {
82 let name = match self.node_name(node, content) {
83 Some(n) => n,
84 None => {
85 return content[node.byte_range()]
86 .lines()
87 .next()
88 .unwrap_or("")
89 .trim()
90 .to_string();
91 }
92 };
93 let vis = self.extract_visibility_prefix(node, content);
94 let params = node
95 .child_by_field_name("parameters")
96 .map(|p| content[p.byte_range()].to_string())
97 .unwrap_or_else(|| "()".to_string());
98 let return_type = node
99 .child_by_field_name("return_type")
100 .map(|r| format!(" -> {}", &content[r.byte_range()]))
101 .unwrap_or_default();
102 format!("{}fn {}{}{}", vis, name, params, return_type)
103 }
104 "impl_item" => {
105 let type_node = node.child_by_field_name("type");
106 let type_name = type_node
107 .map(|n| content[n.byte_range()].to_string())
108 .unwrap_or_default();
109 if let Some(trait_node) = node.child_by_field_name("trait") {
110 let trait_name = &content[trait_node.byte_range()];
111 format!("impl {} for {}", trait_name, type_name)
112 } else {
113 format!("impl {}", type_name)
114 }
115 }
116 "trait_item" => {
117 let name = self.node_name(node, content).unwrap_or("");
118 let vis = self.extract_visibility_prefix(node, content);
119 format!("{}trait {}", vis, name)
120 }
121 "mod_item" => {
122 let name = self.node_name(node, content).unwrap_or("");
123 let vis = self.extract_visibility_prefix(node, content);
124 format!("{}mod {}", vis, name)
125 }
126 "struct_item" => {
127 let name = self.node_name(node, content).unwrap_or("");
128 let vis = self.extract_visibility_prefix(node, content);
129 format!("{}struct {}", vis, name)
130 }
131 "enum_item" => {
132 let name = self.node_name(node, content).unwrap_or("");
133 let vis = self.extract_visibility_prefix(node, content);
134 format!("{}enum {}", vis, name)
135 }
136 "type_item" => {
137 let name = self.node_name(node, content).unwrap_or("");
138 let vis = self.extract_visibility_prefix(node, content);
139 format!("{}type {}", vis, name)
140 }
141 _ => {
142 let text = &content[node.byte_range()];
143 text.lines().next().unwrap_or(text).trim().to_string()
144 }
145 }
146 }
147
148 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
149 if node.kind() != "use_declaration" {
150 return Vec::new();
151 }
152
153 let line = node.start_position().row + 1;
154 let text = &content[node.byte_range()];
155 let module = text.trim_start_matches("use ").trim_end_matches(';').trim();
156
157 let mut names = Vec::new();
159 let is_relative = module.starts_with("crate")
160 || module.starts_with("self")
161 || module.starts_with("super");
162
163 if let Some(brace_start) = module.find('{') {
164 let prefix = module[..brace_start].trim_end_matches("::");
165 let brace_end = {
168 let mut depth = 0u32;
169 let mut end = None;
170 for (i, c) in module[brace_start..].char_indices() {
171 match c {
172 '{' => depth += 1,
173 '}' => {
174 depth -= 1;
175 if depth == 0 {
176 end = Some(brace_start + i);
177 break;
178 }
179 }
180 _ => {}
181 }
182 }
183 end
184 };
185 if let Some(brace_end) = brace_end {
186 let items = &module[brace_start + 1..brace_end];
187 for item in items.split(',') {
188 let trimmed = item.trim();
189 if !trimmed.is_empty() {
190 names.push(trimmed.to_string());
191 }
192 }
193 }
194 vec![Import {
195 module: prefix.to_string(),
196 names,
197 alias: None,
198 is_wildcard: false,
199 is_relative,
200 line,
201 }]
202 } else {
203 let (module_part, alias) = if let Some(as_pos) = module.find(" as ") {
205 (&module[..as_pos], Some(module[as_pos + 4..].to_string()))
206 } else {
207 (module, None)
208 };
209
210 vec![Import {
211 module: module_part.to_string(),
212 names: Vec::new(),
213 alias,
214 is_wildcard: module_part.ends_with("::*"),
215 is_relative,
216 line,
217 }]
218 }
219 }
220
221 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
222 let names_to_use: Vec<&str> = names
223 .map(|n| n.to_vec())
224 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
225
226 if import.is_wildcard {
227 format!("use {};", import.module)
229 } else if names_to_use.is_empty() {
230 format!("use {};", import.module)
231 } else if names_to_use.len() == 1 {
232 format!("use {}::{};", import.module, names_to_use[0])
233 } else {
234 format!("use {}::{{{}}};", import.module, names_to_use.join(", "))
235 }
236 }
237
238 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
239 let mut cursor = node.walk();
240 for child in node.children(&mut cursor) {
241 if child.kind() == "visibility_modifier" {
242 let vis = &content[child.byte_range()];
243 if vis == "pub" {
244 return Visibility::Public;
245 } else if vis.starts_with("pub(crate)") {
246 return Visibility::Internal;
247 } else if vis.starts_with("pub(super)") || vis.starts_with("pub(in") {
248 return Visibility::Protected;
249 }
250 }
251 }
252 Visibility::Private
253 }
254
255 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
256 let in_attrs = symbol
257 .attributes
258 .iter()
259 .any(|a| a.contains("#[test]") || a.contains("#[cfg(test)]"));
260 let in_sig =
261 symbol.signature.contains("#[test]") || symbol.signature.contains("#[cfg(test)]");
262 if in_attrs || in_sig {
263 return true;
264 }
265 match symbol.kind {
266 crate::SymbolKind::Function | crate::SymbolKind::Method => {
267 symbol.name.starts_with("test_")
268 }
269 crate::SymbolKind::Module => symbol.name == "tests",
270 _ => false,
271 }
272 }
273
274 fn test_file_globs(&self) -> &'static [&'static str] {
275 &[
276 "**/tests/**",
277 "**/test_*.rs",
278 "**/*_test.rs",
279 "**/*_tests.rs",
280 ]
281 }
282
283 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
284 node.child_by_field_name("body")
285 }
286
287 fn analyze_container_body(
288 &self,
289 body_node: &Node,
290 content: &str,
291 inner_indent: &str,
292 ) -> Option<ContainerBody> {
293 crate::body::analyze_brace_body(body_node, content, inner_indent)
294 }
295
296 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
297 let name_node = node
299 .child_by_field_name("name")
300 .or_else(|| node.child_by_field_name("type"))?;
301 Some(&content[name_node.byte_range()])
302 }
303
304 fn extract_module_doc(&self, src: &str) -> Option<String> {
305 extract_rust_module_doc(src)
306 }
307
308 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
309 static RESOLVER: RustModuleResolver = RustModuleResolver;
310 Some(&RESOLVER)
311 }
312}
313
314impl LanguageSymbols for Rust {}
315
316pub struct RustModuleResolver;
318
319impl ModuleResolver for RustModuleResolver {
320 fn workspace_config(&self, root: &Path) -> ResolverConfig {
321 let cargo_toml = root.join("Cargo.toml");
322 let mut path_mappings: Vec<(String, PathBuf)> = Vec::new();
323
324 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
325 if let Ok(val) = content.parse::<toml::Value>() {
327 if let Some(members) = val
328 .get("workspace")
329 .and_then(|w| w.get("members"))
330 .and_then(|m| m.as_array())
331 {
332 for member in members {
333 if let Some(member_str) = member.as_str() {
334 let member_path = root.join(member_str);
336 let member_cargo = member_path.join("Cargo.toml");
337 if let Ok(mc) = std::fs::read_to_string(&member_cargo)
338 && let Ok(mv) = mc.parse::<toml::Value>()
339 && let Some(name) = mv
340 .get("package")
341 .and_then(|p| p.get("name"))
342 .and_then(|n| n.as_str())
343 {
344 path_mappings.push((name.to_string(), member_path));
345 }
346 }
347 }
348 }
349
350 if path_mappings.is_empty()
352 && let Some(name) = val
353 .get("package")
354 .and_then(|p| p.get("name"))
355 .and_then(|n| n.as_str())
356 {
357 path_mappings.push((name.to_string(), root.to_path_buf()));
358 }
359 }
360 }
361
362 ResolverConfig {
363 workspace_root: root.to_path_buf(),
364 path_mappings,
365 search_roots: Vec::new(),
366 }
367 }
368
369 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
370 for (crate_name, crate_dir) in &cfg.path_mappings {
372 let src_dir = crate_dir.join("src");
373 if let Ok(rel) = file.strip_prefix(&src_dir) {
374 let components: Vec<&str> = rel
375 .components()
376 .filter_map(|c| {
377 if let std::path::Component::Normal(s) = c {
378 s.to_str()
379 } else {
380 None
381 }
382 })
383 .collect();
384
385 if components.is_empty() {
386 continue;
387 }
388
389 let last = *components.last().unwrap();
390 let module_path =
391 if components.len() == 1 && (last == "lib.rs" || last == "main.rs") {
392 crate_name.clone()
394 } else {
395 let mut parts = vec![crate_name.as_str()];
397 for c in &components[..components.len() - 1] {
398 parts.push(c);
399 }
400 let stem = if last == "mod.rs" {
402 None
405 } else {
406 last.strip_suffix(".rs")
407 };
408 if let Some(s) = stem {
409 parts.push(s);
410 }
411 parts.join("::")
412 };
413
414 return vec![ModuleId {
415 canonical_path: module_path,
416 }];
417 }
418 }
419 Vec::new()
420 }
421
422 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
423 if from_file.extension().and_then(|e| e.to_str()) != Some("rs") {
425 return Resolution::NotApplicable;
426 }
427
428 let raw = &spec.raw;
429
430 let resolved_raw = if raw.starts_with("super::") || raw.starts_with("self::") {
432 let crate_name = self.crate_name_for_file(from_file, cfg);
434 if let Some(cn) = crate_name {
435 let module_path = self.module_path_for_file(from_file, cfg);
436 if let Some(suffix) = raw.strip_prefix("super::") {
437 let parent = module_path
439 .rsplit_once("::")
440 .map(|(p, _)| p.to_string())
441 .unwrap_or_else(|| cn.clone());
442 format!("{}::{}", parent, suffix)
443 } else {
444 let suffix = raw.strip_prefix("self::").unwrap_or(raw);
445 format!("{}::{}", module_path, suffix)
446 }
447 } else {
448 return Resolution::NotFound;
449 }
450 } else {
451 raw.clone()
452 };
453
454 let (crate_name, rest) = if let Some(pos) = resolved_raw.find("::") {
456 let cn = &resolved_raw[..pos];
457 let rest = &resolved_raw[pos + 2..];
458 (cn.to_string(), rest.to_string())
459 } else {
460 (resolved_raw.clone(), String::new())
461 };
462
463 let crate_dir = cfg
465 .path_mappings
466 .iter()
467 .find(|(name, _)| name == &crate_name)
468 .map(|(_, dir)| dir.clone());
469
470 let crate_dir = match crate_dir {
471 Some(d) => d,
472 None => {
473 return Resolution::NotFound;
475 }
476 };
477
478 let exported_name = if let Some(pos) = rest.rfind("::") {
480 rest[pos + 2..].to_string()
481 } else {
482 rest.clone()
483 };
484
485 let module_part = if let Some(pos) = rest.rfind("::") {
486 rest[..pos].to_string()
487 } else {
488 String::new()
489 };
490
491 let candidate = self.module_to_file(&crate_dir, &module_part);
492 if let Some(path) = candidate {
493 Resolution::Resolved(path, exported_name)
494 } else {
495 Resolution::NotFound
496 }
497 }
498}
499
500impl RustModuleResolver {
501 fn crate_name_for_file(&self, file: &Path, cfg: &ResolverConfig) -> Option<String> {
503 for (crate_name, crate_dir) in &cfg.path_mappings {
504 if file.starts_with(crate_dir) {
505 return Some(crate_name.clone());
506 }
507 }
508 None
509 }
510
511 fn module_path_for_file(&self, file: &Path, cfg: &ResolverConfig) -> String {
513 for (crate_name, crate_dir) in &cfg.path_mappings {
514 let src_dir = crate_dir.join("src");
515 if let Ok(rel) = file.strip_prefix(&src_dir) {
516 let components: Vec<&str> = rel
517 .components()
518 .filter_map(|c| {
519 if let std::path::Component::Normal(s) = c {
520 s.to_str()
521 } else {
522 None
523 }
524 })
525 .collect();
526 if components.is_empty() {
527 return crate_name.clone();
528 }
529 let last = *components.last().unwrap();
530 if components.len() == 1 && (last == "lib.rs" || last == "main.rs") {
531 return crate_name.clone();
532 }
533 let mut parts = vec![crate_name.as_str()];
534 for c in &components[..components.len() - 1] {
535 parts.push(c);
536 }
537 if last != "mod.rs"
538 && let Some(s) = last.strip_suffix(".rs")
539 {
540 parts.push(s);
541 }
542 return parts.join("::");
543 }
544 }
545 String::new()
546 }
547
548 fn module_to_file(&self, crate_dir: &Path, module_path: &str) -> Option<PathBuf> {
552 let src_dir = crate_dir.join("src");
553
554 if module_path.is_empty() {
555 let lib = src_dir.join("lib.rs");
557 if lib.exists() {
558 return Some(lib);
559 }
560 let main = src_dir.join("main.rs");
561 if main.exists() {
562 return Some(main);
563 }
564 return None;
565 }
566
567 let parts: Vec<&str> = module_path.split("::").collect();
568 let mut path = src_dir.clone();
569 for part in &parts {
570 path = path.join(part);
571 }
572
573 let rs_path = path.with_extension("rs");
575 if rs_path.exists() {
576 return Some(rs_path);
577 }
578
579 let mod_path = path.join("mod.rs");
581 if mod_path.exists() {
582 return Some(mod_path);
583 }
584
585 None
586 }
587}
588
589impl Rust {
590 fn extract_visibility_prefix(&self, node: &Node, content: &str) -> String {
591 let mut cursor = node.walk();
592 for child in node.children(&mut cursor) {
593 if child.kind() == "visibility_modifier" {
594 return format!("{} ", &content[child.byte_range()]);
595 }
596 }
597 String::new()
598 }
599}
600
601fn extract_docstring(node: &Node, content: &str) -> Option<String> {
605 let mut cursor = node.walk();
606 for child in node.children(&mut cursor) {
607 if child.kind() == "attributes" {
608 let mut doc_lines = Vec::new();
609 let mut attr_cursor = child.walk();
610 for attr_child in child.children(&mut attr_cursor) {
611 if attr_child.kind() == "line_outer_doc_comment" {
612 let text = &content[attr_child.byte_range()];
613 let doc = text.trim_start_matches("///").trim();
614 if !doc.is_empty() {
615 doc_lines.push(doc.to_string());
616 }
617 }
618 }
619 if !doc_lines.is_empty() {
620 return Some(doc_lines.join("\n"));
621 }
622 }
623 }
624 None
625}
626
627fn extract_attributes(node: &Node, content: &str) -> Vec<String> {
631 let mut attrs = Vec::new();
632
633 if let Some(attr_node) = node.child_by_field_name("attributes") {
635 let mut cursor = attr_node.walk();
636 for child in attr_node.children(&mut cursor) {
637 if child.kind() == "attribute_item" {
638 attrs.push(content[child.byte_range()].to_string());
639 }
640 }
641 }
642
643 let mut prev = node.prev_sibling();
645 while let Some(sibling) = prev {
646 if sibling.kind() == "attribute_item" {
647 attrs.insert(0, content[sibling.byte_range()].to_string());
649 prev = sibling.prev_sibling();
650 } else {
651 break;
652 }
653 }
654
655 attrs
656}
657
658fn extract_rust_module_doc(src: &str) -> Option<String> {
663 let mut lines = Vec::new();
664 for line in src.lines() {
665 let trimmed = line.trim();
666 if trimmed.starts_with("//!") {
667 let text = trimmed.strip_prefix("//!").unwrap_or("").trim_start();
668 lines.push(text.to_string());
669 } else if trimmed.is_empty() && lines.is_empty() {
670 } else {
672 break;
673 }
674 }
675 if lines.is_empty() {
676 return None;
677 }
678 while lines.last().map(|l: &String| l.is_empty()).unwrap_or(false) {
680 lines.pop();
681 }
682 if lines.is_empty() {
683 None
684 } else {
685 Some(lines.join("\n"))
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692 use crate::validate_unused_kinds_audit;
693
694 #[test]
697 fn unused_node_kinds_audit() {
698 #[rustfmt::skip]
709 let documented_unused: &[&str] = &[
710 "block_comment", "field_declaration", "field_declaration_list", "field_expression", "field_identifier", "identifier", "lifetime", "lifetime_parameter", "ordered_field_declaration_list", "scoped_identifier", "scoped_type_identifier", "shorthand_field_identifier", "type_identifier", "visibility_modifier", "else_clause", "enum_variant", "enum_variant_list", "match_block", "match_pattern", "trait_bounds", "where_clause", "array_expression", "assignment_expression", "async_block", "await_expression", "generic_function", "index_expression", "parenthesized_expression","range_expression", "reference_expression", "struct_expression", "try_expression", "tuple_expression", "type_cast_expression", "unary_expression", "unit_expression", "yield_expression", "abstract_type", "array_type", "bounded_type", "bracketed_type", "dynamic_type", "function_type", "generic_type", "generic_type_with_turbofish", "higher_ranked_trait_bound", "never_type", "pointer_type", "primitive_type", "qualified_type", "reference_type", "removed_trait_bound", "tuple_type", "type_arguments", "type_binding", "type_parameter", "type_parameters", "unit_type", "unsafe_bound_type", "block_outer_doc_comment", "extern_modifier", "function_modifiers", "mutable_specifier", "struct_pattern", "tuple_struct_pattern", "fragment_specifier", "macro_arguments_declaration", "macro_body_v2", "macro_definition_v2", "block_expression_with_attribute", "const_block", "expression_statement", "expression_with_attribute", "extern_crate_declaration","foreign_mod_item", "function_signature_item", "gen_block", "let_declaration", "try_block", "unsafe_block", "use_as_clause", "empty_statement", "closure_expression",
809 "continue_expression",
810 "match_expression",
811 "use_declaration",
812 "for_expression",
813 "match_arm",
814 "break_expression",
815 "while_expression",
816 "loop_expression",
817 "return_expression",
818 "if_expression",
819 "block",
820 "binary_expression",
821 ];
822
823 validate_unused_kinds_audit(&Rust, documented_unused)
824 .expect("Rust unused node kinds audit failed");
825 }
826}