Skip to main content

normalize_languages/
rust.rs

1//! Rust language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use tree_sitter::Node;
8
9// ============================================================================
10// Rust external package resolution
11// ============================================================================
12
13/// Get Rust version.
14pub fn get_rust_version() -> Option<String> {
15    let output = Command::new("rustc").args(["--version"]).output().ok()?;
16
17    if output.status.success() {
18        let version_str = String::from_utf8_lossy(&output.stdout);
19        // "rustc 1.75.0 (82e1608df 2023-12-21)" -> "1.75"
20        for part in version_str.split_whitespace() {
21            if part.chars().next().is_some_and(|c| c.is_ascii_digit()) {
22                let parts: Vec<&str> = part.split('.').collect();
23                if parts.len() >= 2 {
24                    return Some(format!("{}.{}", parts[0], parts[1]));
25                }
26            }
27        }
28    }
29
30    None
31}
32
33/// Find cargo registry source directory.
34/// Structure: ~/.cargo/registry/src/
35pub fn find_cargo_registry() -> Option<PathBuf> {
36    // Check CARGO_HOME env var
37    if let Ok(cargo_home) = std::env::var("CARGO_HOME") {
38        let registry = PathBuf::from(cargo_home).join("registry").join("src");
39        if registry.is_dir() {
40            return Some(registry);
41        }
42    }
43
44    // Fall back to ~/.cargo/registry/src
45    if let Ok(home) = std::env::var("HOME") {
46        let registry = PathBuf::from(home)
47            .join(".cargo")
48            .join("registry")
49            .join("src");
50        if registry.is_dir() {
51            return Some(registry);
52        }
53    }
54
55    // Windows fallback
56    if let Ok(home) = std::env::var("USERPROFILE") {
57        let registry = PathBuf::from(home)
58            .join(".cargo")
59            .join("registry")
60            .join("src");
61        if registry.is_dir() {
62            return Some(registry);
63        }
64    }
65
66    None
67}
68
69/// Resolve a Rust crate import to its source location.
70fn resolve_rust_crate(crate_name: &str, registry: &Path) -> Option<ResolvedPackage> {
71    // Registry structure: registry/src/index.crates.io-*/crate-version/
72    if let Ok(indices) = std::fs::read_dir(registry) {
73        for index_entry in indices.flatten() {
74            let index_path = index_entry.path();
75            if !index_path.is_dir() {
76                continue;
77            }
78
79            if let Ok(crates) = std::fs::read_dir(&index_path) {
80                for crate_entry in crates.flatten() {
81                    let crate_dir = crate_entry.path();
82                    let dir_name = crate_entry.file_name().to_string_lossy().to_string();
83
84                    // Check if this is our crate (name-version pattern)
85                    if dir_name.starts_with(&format!("{}-", crate_name)) {
86                        let lib_rs = crate_dir.join("src").join("lib.rs");
87                        if lib_rs.is_file() {
88                            return Some(ResolvedPackage {
89                                path: lib_rs,
90                                name: crate_name.to_string(),
91                                is_namespace: false,
92                            });
93                        }
94                    }
95                }
96            }
97        }
98    }
99
100    None
101}
102
103// ============================================================================
104// Rust language support
105// ============================================================================
106
107/// Rust language support.
108pub struct Rust;
109
110impl Language for Rust {
111    fn name(&self) -> &'static str {
112        "Rust"
113    }
114    fn extensions(&self) -> &'static [&'static str] {
115        &["rs"]
116    }
117    fn grammar_name(&self) -> &'static str {
118        "rust"
119    }
120
121    fn has_symbols(&self) -> bool {
122        true
123    }
124
125    fn container_kinds(&self) -> &'static [&'static str] {
126        &["impl_item", "trait_item", "mod_item"]
127    }
128
129    fn function_kinds(&self) -> &'static [&'static str] {
130        &["function_item"]
131    }
132
133    fn type_kinds(&self) -> &'static [&'static str] {
134        &["struct_item", "enum_item", "type_item", "trait_item"]
135    }
136
137    fn import_kinds(&self) -> &'static [&'static str] {
138        &["use_declaration"]
139    }
140
141    fn public_symbol_kinds(&self) -> &'static [&'static str] {
142        &["function_item", "struct_item", "enum_item", "trait_item"]
143    }
144
145    fn visibility_mechanism(&self) -> VisibilityMechanism {
146        VisibilityMechanism::AccessModifier
147    }
148
149    fn complexity_nodes(&self) -> &'static [&'static str] {
150        &[
151            "if_expression",
152            "match_expression",
153            "for_expression",
154            "while_expression",
155            "loop_expression",
156            "match_arm",
157            "binary_expression", // for && and ||
158        ]
159    }
160
161    fn nesting_nodes(&self) -> &'static [&'static str] {
162        &[
163            "if_expression",
164            "match_expression",
165            "for_expression",
166            "while_expression",
167            "loop_expression",
168            "function_item",
169            "impl_item",
170            "trait_item",
171            "mod_item",
172        ]
173    }
174
175    fn signature_suffix(&self) -> &'static str {
176        " {}"
177    }
178
179    fn scope_creating_kinds(&self) -> &'static [&'static str] {
180        // Additional scope-creating nodes beyond functions and containers
181        &[
182            "block",
183            "for_expression",
184            "while_expression",
185            "loop_expression",
186            "closure_expression",
187        ]
188    }
189
190    fn control_flow_kinds(&self) -> &'static [&'static str] {
191        &[
192            "if_expression",
193            "match_expression",
194            "for_expression",
195            "while_expression",
196            "loop_expression",
197            "return_expression",
198            "break_expression",
199            "continue_expression",
200        ]
201    }
202
203    fn extract_function(&self, node: &Node, content: &str, in_container: bool) -> Option<Symbol> {
204        let name = self.node_name(node, content)?;
205
206        // Get visibility modifier
207        let mut vis = String::new();
208        let mut cursor = node.walk();
209        for child in node.children(&mut cursor) {
210            if child.kind() == "visibility_modifier" {
211                vis = format!("{} ", &content[child.byte_range()]);
212                break;
213            }
214        }
215
216        let params = node
217            .child_by_field_name("parameters")
218            .map(|p| content[p.byte_range()].to_string())
219            .unwrap_or_else(|| "()".to_string());
220
221        let return_type = node
222            .child_by_field_name("return_type")
223            .map(|r| format!(" -> {}", &content[r.byte_range()]))
224            .unwrap_or_default();
225
226        let signature = format!("{}fn {}{}{}", vis, name, params, return_type);
227
228        Some(Symbol {
229            name: name.to_string(),
230            kind: if in_container {
231                SymbolKind::Method
232            } else {
233                SymbolKind::Function
234            },
235            signature,
236            docstring: self.extract_docstring(node, content),
237            attributes: self.extract_attributes(node, content),
238            start_line: node.start_position().row + 1,
239            end_line: node.end_position().row + 1,
240            visibility: self.get_visibility(node, content),
241            children: Vec::new(),
242            is_interface_impl: false,
243            implements: Vec::new(),
244        })
245    }
246
247    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
248        match node.kind() {
249            "impl_item" => {
250                let type_node = node.child_by_field_name("type")?;
251                let type_name = &content[type_node.byte_range()];
252
253                // Check if this is a trait impl (impl Trait for Type)
254                let is_trait_impl = node.child_by_field_name("trait").is_some();
255
256                let signature = if let Some(trait_node) = node.child_by_field_name("trait") {
257                    let trait_name = &content[trait_node.byte_range()];
258                    format!("impl {} for {}", trait_name, type_name)
259                } else {
260                    format!("impl {}", type_name)
261                };
262
263                Some(Symbol {
264                    name: type_name.to_string(),
265                    kind: SymbolKind::Module, // impl blocks are like modules
266                    signature,
267                    docstring: None,
268                    attributes: self.extract_attributes(node, content),
269                    start_line: node.start_position().row + 1,
270                    end_line: node.end_position().row + 1,
271                    visibility: Visibility::Public,
272                    children: Vec::new(),
273                    is_interface_impl: is_trait_impl,
274                    implements: Vec::new(),
275                })
276            }
277            "trait_item" => {
278                let name = self.node_name(node, content)?;
279                let vis = self.extract_visibility_prefix(node, content);
280
281                Some(Symbol {
282                    name: name.to_string(),
283                    kind: SymbolKind::Trait,
284                    signature: format!("{}trait {}", vis, name),
285                    docstring: self.extract_docstring(node, content),
286                    attributes: self.extract_attributes(node, content),
287                    start_line: node.start_position().row + 1,
288                    end_line: node.end_position().row + 1,
289                    visibility: self.get_visibility(node, content),
290                    children: Vec::new(),
291                    is_interface_impl: false,
292                    implements: Vec::new(),
293                })
294            }
295            "mod_item" => {
296                // Only extract inline mod blocks (with declaration_list), not `mod foo;` declarations
297                node.child_by_field_name("body")?;
298                let name = self.node_name(node, content)?;
299                let vis = self.extract_visibility_prefix(node, content);
300
301                Some(Symbol {
302                    name: name.to_string(),
303                    kind: SymbolKind::Module,
304                    signature: format!("{}mod {}", vis, name),
305                    docstring: self.extract_docstring(node, content),
306                    attributes: self.extract_attributes(node, content),
307                    start_line: node.start_position().row + 1,
308                    end_line: node.end_position().row + 1,
309                    visibility: self.get_visibility(node, content),
310                    children: Vec::new(),
311                    is_interface_impl: false,
312                    implements: Vec::new(),
313                })
314            }
315            _ => None,
316        }
317    }
318
319    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
320        let name = self.node_name(node, content)?;
321        let vis = self.extract_visibility_prefix(node, content);
322
323        let (kind, keyword) = match node.kind() {
324            "struct_item" => (SymbolKind::Struct, "struct"),
325            "enum_item" => (SymbolKind::Enum, "enum"),
326            "type_item" => (SymbolKind::Type, "type"),
327            "trait_item" => (SymbolKind::Trait, "trait"),
328            _ => return None,
329        };
330
331        Some(Symbol {
332            name: name.to_string(),
333            kind,
334            signature: format!("{}{} {}", vis, keyword, name),
335            docstring: self.extract_docstring(node, content),
336            attributes: self.extract_attributes(node, content),
337            start_line: node.start_position().row + 1,
338            end_line: node.end_position().row + 1,
339            visibility: self.get_visibility(node, content),
340            children: Vec::new(),
341            is_interface_impl: false,
342            implements: Vec::new(),
343        })
344    }
345
346    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
347        // Look for doc comments in the attributes child
348        let mut cursor = node.walk();
349        for child in node.children(&mut cursor) {
350            if child.kind() == "attributes" {
351                let mut doc_lines = Vec::new();
352                let mut attr_cursor = child.walk();
353                for attr_child in child.children(&mut attr_cursor) {
354                    if attr_child.kind() == "line_outer_doc_comment" {
355                        let text = &content[attr_child.byte_range()];
356                        let doc = text.trim_start_matches("///").trim();
357                        if !doc.is_empty() {
358                            doc_lines.push(doc.to_string());
359                        }
360                    }
361                }
362                if !doc_lines.is_empty() {
363                    return Some(doc_lines.join("\n"));
364                }
365            }
366        }
367        None
368    }
369
370    fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
371        let mut attrs = Vec::new();
372
373        // Check for attributes child (e.g., #[test], #[cfg(test)])
374        if let Some(attr_node) = node.child_by_field_name("attributes") {
375            let mut cursor = attr_node.walk();
376            for child in attr_node.children(&mut cursor) {
377                if child.kind() == "attribute_item" {
378                    attrs.push(content[child.byte_range()].to_string());
379                }
380            }
381        }
382
383        // Also check preceding siblings for outer attributes
384        let mut prev = node.prev_sibling();
385        while let Some(sibling) = prev {
386            if sibling.kind() == "attribute_item" {
387                // Insert at beginning to maintain order
388                attrs.insert(0, content[sibling.byte_range()].to_string());
389                prev = sibling.prev_sibling();
390            } else {
391                break;
392            }
393        }
394
395        attrs
396    }
397
398    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
399        if node.kind() != "use_declaration" {
400            return Vec::new();
401        }
402
403        let line = node.start_position().row + 1;
404        let text = &content[node.byte_range()];
405        let module = text.trim_start_matches("use ").trim_end_matches(';').trim();
406
407        // Check for braced imports: use foo::{bar, baz}
408        let mut names = Vec::new();
409        let is_relative = module.starts_with("crate")
410            || module.starts_with("self")
411            || module.starts_with("super");
412
413        if let Some(brace_start) = module.find('{') {
414            let prefix = module[..brace_start].trim_end_matches("::");
415            if let Some(brace_end) = module.find('}') {
416                let items = &module[brace_start + 1..brace_end];
417                for item in items.split(',') {
418                    let trimmed = item.trim();
419                    if !trimmed.is_empty() {
420                        names.push(trimmed.to_string());
421                    }
422                }
423            }
424            vec![Import {
425                module: prefix.to_string(),
426                names,
427                alias: None,
428                is_wildcard: false,
429                is_relative,
430                line,
431            }]
432        } else {
433            // Simple import: use foo::bar or use foo::bar as baz
434            let (module_part, alias) = if let Some(as_pos) = module.find(" as ") {
435                (&module[..as_pos], Some(module[as_pos + 4..].to_string()))
436            } else {
437                (module, None)
438            };
439
440            vec![Import {
441                module: module_part.to_string(),
442                names: Vec::new(),
443                alias,
444                is_wildcard: module_part.ends_with("::*"),
445                is_relative,
446                line,
447            }]
448        }
449    }
450
451    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
452        let names_to_use: Vec<&str> = names
453            .map(|n| n.to_vec())
454            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
455
456        if import.is_wildcard {
457            // Module already contains ::* from parsing
458            format!("use {};", import.module)
459        } else if names_to_use.is_empty() {
460            format!("use {};", import.module)
461        } else if names_to_use.len() == 1 {
462            format!("use {}::{};", import.module, names_to_use[0])
463        } else {
464            format!("use {}::{{{}}};", import.module, names_to_use.join(", "))
465        }
466    }
467
468    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
469        let line = node.start_position().row + 1;
470
471        // Only export pub items
472        if !self.is_public(node, content) {
473            return Vec::new();
474        }
475
476        let name = match self.node_name(node, content) {
477            Some(n) => n.to_string(),
478            None => return Vec::new(),
479        };
480
481        let kind = match node.kind() {
482            "function_item" => SymbolKind::Function,
483            "struct_item" => SymbolKind::Struct,
484            "enum_item" => SymbolKind::Enum,
485            "trait_item" => SymbolKind::Trait,
486            _ => return Vec::new(),
487        };
488
489        vec![Export { name, kind, line }]
490    }
491
492    fn is_public(&self, node: &Node, content: &str) -> bool {
493        let mut cursor = node.walk();
494        for child in node.children(&mut cursor) {
495            if child.kind() == "visibility_modifier" {
496                let vis = &content[child.byte_range()];
497                return vis.starts_with("pub");
498            }
499        }
500        false
501    }
502
503    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
504        let mut cursor = node.walk();
505        for child in node.children(&mut cursor) {
506            if child.kind() == "visibility_modifier" {
507                let vis = &content[child.byte_range()];
508                if vis == "pub" {
509                    return Visibility::Public;
510                } else if vis.starts_with("pub(crate)") {
511                    return Visibility::Internal;
512                } else if vis.starts_with("pub(super)") || vis.starts_with("pub(in") {
513                    return Visibility::Protected;
514                }
515            }
516        }
517        Visibility::Private
518    }
519
520    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
521        let in_attrs = symbol
522            .attributes
523            .iter()
524            .any(|a| a.contains("#[test]") || a.contains("#[cfg(test)]"));
525        let in_sig =
526            symbol.signature.contains("#[test]") || symbol.signature.contains("#[cfg(test)]");
527        if in_attrs || in_sig {
528            return true;
529        }
530        match symbol.kind {
531            crate::SymbolKind::Function | crate::SymbolKind::Method => {
532                symbol.name.starts_with("test_")
533            }
534            crate::SymbolKind::Module => symbol.name == "tests",
535            _ => false,
536        }
537    }
538
539    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
540        None
541    }
542
543    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
544        node.child_by_field_name("body")
545    }
546
547    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
548        // Rust doesn't have body docstrings, only outer doc comments
549        false
550    }
551
552    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
553        let name_node = node.child_by_field_name("name")?;
554        Some(&content[name_node.byte_range()])
555    }
556
557    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
558        // Only Rust files
559        if path.extension()?.to_str()? != "rs" {
560            return None;
561        }
562
563        let path_str = path.to_str()?;
564
565        // Strip src/ prefix if present
566        let rel_path = path_str.strip_prefix("src/").unwrap_or(path_str);
567
568        // Remove .rs extension
569        let module_path = rel_path.strip_suffix(".rs")?;
570
571        // Handle mod.rs and lib.rs - use parent directory as module
572        let module_path = if module_path.ends_with("/mod") || module_path.ends_with("/lib") {
573            module_path.rsplit_once('/')?.0
574        } else {
575            module_path
576        };
577
578        // Convert path separators to ::
579        Some(module_path.replace('/', "::"))
580    }
581
582    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
583        let rel_path = module.replace("::", "/");
584
585        vec![
586            format!("src/{}.rs", rel_path),
587            format!("src/{}/mod.rs", rel_path),
588        ]
589    }
590
591    // === Import Resolution ===
592
593    fn lang_key(&self) -> &'static str {
594        "rust"
595    }
596
597    fn resolve_local_import(
598        &self,
599        module: &str,
600        current_file: &Path,
601        project_root: &Path,
602    ) -> Option<PathBuf> {
603        // Find the crate root (directory containing Cargo.toml)
604        let crate_root = find_crate_root(current_file, project_root)?;
605
606        if module.starts_with("crate::") {
607            // crate::foo::bar -> src/foo/bar.rs or src/foo/bar/mod.rs
608            let path_part = module.strip_prefix("crate::")?.replace("::", "/");
609            let src_dir = crate_root.join("src");
610
611            // Try foo/bar.rs
612            let direct = src_dir.join(format!("{}.rs", path_part));
613            if direct.exists() {
614                return Some(direct);
615            }
616
617            // Try foo/bar/mod.rs
618            let mod_file = src_dir.join(&path_part).join("mod.rs");
619            if mod_file.exists() {
620                return Some(mod_file);
621            }
622        } else if module.starts_with("super::") {
623            // super::foo -> parent directory's foo
624            let current_dir = current_file.parent()?;
625            let parent_dir = current_dir.parent()?;
626            let path_part = module.strip_prefix("super::")?.replace("::", "/");
627
628            // Try parent/foo.rs
629            let direct = parent_dir.join(format!("{}.rs", path_part));
630            if direct.exists() {
631                return Some(direct);
632            }
633
634            // Try parent/foo/mod.rs
635            let mod_file = parent_dir.join(&path_part).join("mod.rs");
636            if mod_file.exists() {
637                return Some(mod_file);
638            }
639        } else if module.starts_with("self::") {
640            // self::foo -> same directory's foo
641            let current_dir = current_file.parent()?;
642            let path_part = module.strip_prefix("self::")?.replace("::", "/");
643
644            // Try dir/foo.rs
645            let direct = current_dir.join(format!("{}.rs", path_part));
646            if direct.exists() {
647                return Some(direct);
648            }
649
650            // Try dir/foo/mod.rs
651            let mod_file = current_dir.join(&path_part).join("mod.rs");
652            if mod_file.exists() {
653                return Some(mod_file);
654            }
655        }
656
657        None
658    }
659
660    fn resolve_external_import(
661        &self,
662        crate_name: &str,
663        _project_root: &Path,
664    ) -> Option<ResolvedPackage> {
665        let registry = find_cargo_registry()?;
666        resolve_rust_crate(crate_name, &registry)
667    }
668
669    fn get_version(&self, _project_root: &Path) -> Option<String> {
670        get_rust_version()
671    }
672
673    fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
674        find_cargo_registry()
675    }
676
677    fn indexable_extensions(&self) -> &'static [&'static str] {
678        &["rs"]
679    }
680
681    fn is_stdlib_import(&self, _import_name: &str, _project_root: &Path) -> bool {
682        // Rust stdlib is part of the compiler, no separate source to index
683        false
684    }
685
686    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
687        // Rust stdlib is part of the compiler, no separate path
688        None
689    }
690
691    fn package_sources(&self, project_root: &Path) -> Vec<crate::PackageSource> {
692        use crate::{PackageSource, PackageSourceKind};
693        let mut sources = Vec::new();
694        if let Some(cache) = self.find_package_cache(project_root) {
695            sources.push(PackageSource {
696                name: "cargo-registry",
697                path: cache,
698                kind: PackageSourceKind::Cargo,
699                version_specific: false,
700            });
701        }
702        sources
703    }
704
705    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
706        use crate::traits::{has_extension, skip_dotfiles};
707        if skip_dotfiles(name) {
708            return true;
709        }
710        // Skip target, tests directories
711        if is_dir
712            && (name == "target" || name == "tests" || name == "benches" || name == "examples")
713        {
714            return true;
715        }
716        // Only index .rs files
717        !is_dir && !has_extension(name, self.indexable_extensions())
718    }
719
720    fn discover_packages(&self, source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
721        if source.kind != crate::PackageSourceKind::Cargo {
722            return Vec::new();
723        }
724        discover_cargo_packages(&source.path)
725    }
726
727    fn package_module_name(&self, entry_name: &str) -> String {
728        // Strip .rs extension
729        entry_name
730            .strip_suffix(".rs")
731            .unwrap_or(entry_name)
732            .to_string()
733    }
734
735    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
736        if path.is_file() {
737            return Some(path.to_path_buf());
738        }
739        // Rust packages use src/lib.rs as entry point
740        let lib_rs = path.join("src").join("lib.rs");
741        if lib_rs.is_file() {
742            return Some(lib_rs);
743        }
744        // Or mod.rs in the directory itself
745        let mod_rs = path.join("mod.rs");
746        if mod_rs.is_file() {
747            return Some(mod_rs);
748        }
749        None
750    }
751}
752
753/// Discover packages in Cargo registry structure.
754/// Structure: ~/.cargo/registry/src/index.crates.io-*/crate-version/
755fn discover_cargo_packages(registry: &Path) -> Vec<(String, PathBuf)> {
756    let mut packages = Vec::new();
757
758    // Registry structure: registry/src/index.crates.io-*/crate-version/
759    let indices = match std::fs::read_dir(registry) {
760        Ok(e) => e,
761        Err(_) => return packages,
762    };
763
764    for index_entry in indices.flatten() {
765        let index_path = index_entry.path();
766        if !index_path.is_dir() {
767            continue;
768        }
769
770        let crates = match std::fs::read_dir(&index_path) {
771            Ok(e) => e,
772            Err(_) => continue,
773        };
774
775        for crate_entry in crates.flatten() {
776            let crate_path = crate_entry.path();
777            let crate_name = crate_entry.file_name().to_string_lossy().to_string();
778
779            if !crate_path.is_dir() {
780                continue;
781            }
782
783            // Extract crate name (remove version suffix: "foo-1.2.3" -> "foo")
784            let name = crate_name
785                .rsplit_once('-')
786                .map(|(n, _)| n)
787                .unwrap_or(&crate_name);
788
789            // Find src/lib.rs
790            let lib_rs = crate_path.join("src").join("lib.rs");
791            if lib_rs.is_file() {
792                packages.push((name.to_string(), lib_rs));
793            }
794        }
795    }
796
797    packages
798}
799
800/// Find the crate root (directory containing Cargo.toml).
801fn find_crate_root(start: &Path, root: &Path) -> Option<PathBuf> {
802    let mut current = start.parent()?;
803    while current.starts_with(root) {
804        if current.join("Cargo.toml").exists() {
805            return Some(current.to_path_buf());
806        }
807        current = current.parent()?;
808    }
809    None
810}
811
812impl Rust {
813    fn extract_visibility_prefix(&self, node: &Node, content: &str) -> String {
814        let mut cursor = node.walk();
815        for child in node.children(&mut cursor) {
816            if child.kind() == "visibility_modifier" {
817                return format!("{} ", &content[child.byte_range()]);
818            }
819        }
820        String::new()
821    }
822}
823
824#[cfg(test)]
825mod tests {
826    use super::*;
827    use crate::validate_unused_kinds_audit;
828
829    /// Documents node kinds that exist in the Rust grammar but aren't used in trait methods.
830    /// Run `cross_check_node_kinds` in registry.rs to see all potentially useful kinds.
831    #[test]
832    fn unused_node_kinds_audit() {
833        // Categories:
834        // - STRUCTURAL: Internal/wrapper nodes
835        // - CLAUSE: Sub-parts of larger constructs
836        // - EXPRESSION: Expressions (we track statements/definitions)
837        // - TYPE: Type-related nodes
838        // - MODIFIER: Visibility/async/unsafe modifiers
839        // - PATTERN: Pattern matching internals
840        // - MACRO: Macro-related nodes
841        // - TODO: Potentially useful
842
843        #[rustfmt::skip]
844        let documented_unused: &[&str] = &[
845            // STRUCTURAL
846            "block_comment",           // comments
847            "declaration_list",        // extern block contents
848            "field_declaration",       // struct field
849            "field_declaration_list",  // struct body
850            "field_expression",        // foo.bar
851            "field_identifier",        // field name
852            "identifier",              // too common
853            "lifetime",                // 'a
854            "lifetime_parameter",      // <'a>
855            "ordered_field_declaration_list", // tuple struct fields
856            "scoped_identifier",       // path::to::thing
857            "scoped_type_identifier",  // path::to::Type
858            "shorthand_field_identifier", // struct init shorthand
859            "type_identifier",         // type names
860            "visibility_modifier",     // pub, pub(crate)
861
862            // CLAUSE
863            "else_clause",             // part of if
864            "enum_variant",            // enum variant
865            "enum_variant_list",       // enum body
866            "match_block",             // match body
867            "match_pattern",           // match arm pattern
868            "trait_bounds",            // T: Foo + Bar
869            "where_clause",            // where T: Foo
870
871            // EXPRESSION
872            "array_expression",        // [1, 2, 3]
873            "assignment_expression",   // x = y
874            "async_block",             // async { }
875            "await_expression",        // foo.await
876            "call_expression",         // foo()
877            "generic_function",        // foo::<T>()
878            "index_expression",        // arr[i]
879            "parenthesized_expression",// (expr)
880            "range_expression",        // 0..10
881            "reference_expression",    // &x
882            "struct_expression",       // Foo { x: 1 }
883            "try_expression",          // foo?
884            "tuple_expression",        // (a, b)
885            "type_cast_expression",    // x as T
886            "unary_expression",        // -x, !x
887            "unit_expression",         // ()
888            "yield_expression",        // yield x
889
890            // TYPE
891            "abstract_type",           // impl Trait
892            "array_type",              // [T; N]
893            "bounded_type",            // T: Foo
894            "bracketed_type",          // <T>
895            "dynamic_type",            // dyn Trait
896            "function_type",           // fn(T) -> U
897            "generic_type",            // Vec<T>
898            "generic_type_with_turbofish", // Vec::<T>
899            "higher_ranked_trait_bound", // for<'a>
900            "never_type",              // !
901            "pointer_type",            // *const T
902            "primitive_type",          // i32, bool
903            "qualified_type",          // <T as Trait>::Item
904            "reference_type",          // &T
905            "removed_trait_bound",     // ?Sized
906            "tuple_type",              // (A, B)
907            "type_arguments",          // <T, U>
908            "type_binding",            // Item = T
909            "type_parameter",          // T
910            "type_parameters",         // <T, U>
911            "unit_type",               // ()
912            "unsafe_bound_type",       // unsafe trait bound
913
914            // MODIFIER
915            "block_outer_doc_comment", // //!
916            "extern_modifier",         // extern "C"
917            "function_modifiers",      // async, const, unsafe
918            "mutable_specifier",       // mut
919
920            // PATTERN
921            "struct_pattern",          // Foo { x, y }
922            "tuple_struct_pattern",    // Foo(x, y)
923
924            // MACRO
925            "fragment_specifier",      // $x:expr
926            "macro_arguments_declaration", // macro args
927            "macro_body_v2",           // macro body
928            "macro_definition",        // macro_rules!
929            "macro_definition_v2",     // macro 2.0
930
931            // OTHER
932            "block_expression_with_attribute", // #[attr] { }
933            "const_block",             // const { }
934            "expression_statement",    // expr;
935            "expression_with_attribute", // #[attr] expr
936            "extern_crate_declaration",// extern crate
937            "foreign_mod_item",        // extern block item
938            "function_signature_item", // fn signature in trait
939            "gen_block",               // gen { }
940            "let_declaration",         // let x = y
941            "try_block",               // try { }
942            "unsafe_block",            // unsafe { }
943            "use_as_clause",           // use foo as bar
944            "empty_statement",         // ;
945        ];
946
947        validate_unused_kinds_audit(&Rust, documented_unused)
948            .expect("Rust unused node kinds audit failed");
949    }
950}