Skip to main content

thread_flow/incremental/extractors/
rust.rs

1// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Rust dependency extractor using tree-sitter AST traversal.
5//!
6//! Extracts `use` declarations and `pub use` re-exports from Rust source files,
7//! producing [`RustImportInfo`] and [`ExportInfo`] records for the dependency
8//! graph. Supports:
9//!
10//! - Simple imports: `use std::collections::HashMap;`
11//! - Nested imports: `use std::collections::{HashMap, HashSet};`
12//! - Wildcard imports: `use module::*;`
13//! - Aliased imports: `use std::io::Result as IoResult;`
14//! - Crate-relative: `use crate::core::Engine;`
15//! - Super-relative: `use super::utils;`
16//! - Self-relative: `use self::types::Config;`
17//! - Re-exports: `pub use types::Config;`, `pub(crate) use internal::Helper;`
18//!
19//! # Examples
20//!
21//! ```rust,ignore
22//! use thread_flow::incremental::extractors::rust::RustDependencyExtractor;
23//! use std::path::Path;
24//!
25//! let extractor = RustDependencyExtractor::new();
26//! let source = "use std::collections::HashMap;\nuse crate::config::Settings;";
27//! let imports = extractor.extract_imports(source, Path::new("src/main.rs")).unwrap();
28//! assert_eq!(imports.len(), 2);
29//! ```
30//!
31//! # Performance
32//!
33//! Target: <5ms per file extraction. Tree-sitter parsing and AST traversal
34//! operate in a single pass without backtracking.
35
36use std::path::{Path, PathBuf};
37
38/// Errors that can occur during Rust dependency extraction.
39#[derive(Debug, thiserror::Error)]
40pub enum ExtractionError {
41    /// Tree-sitter failed to parse the Rust source file.
42    #[error("parse error: failed to parse Rust source")]
43    ParseError,
44
45    /// Module path could not be resolved to a local file path.
46    #[error("unresolved module: {module} from {source_file}: {reason}")]
47    ResolutionError {
48        /// The module path that could not be resolved.
49        module: String,
50        /// The source file containing the use statement.
51        source_file: PathBuf,
52        /// The reason resolution failed.
53        reason: String,
54    },
55}
56
57/// Visibility level of a Rust re-export (`pub use`).
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub enum Visibility {
60    /// `pub use` -- visible to all.
61    Public,
62    /// `pub(crate) use` -- visible within the crate.
63    Crate,
64    /// `pub(super) use` -- visible to the parent module.
65    Super,
66    /// `pub(in path) use` -- visible to a specific path.
67    Restricted,
68}
69
70/// Information extracted from a single Rust `use` declaration.
71///
72/// Represents the parsed form of a `use` statement. The coordinator (Task 3.5)
73/// converts these into [`DependencyEdge`](crate::incremental::types::DependencyEdge)
74/// entries.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct RustImportInfo {
77    /// The module path as written in the source code, excluding the final
78    /// symbol(s).
79    ///
80    /// For `use std::collections::HashMap` this is `"std::collections"`.
81    /// For `use crate::config::Settings` this is `"crate::config"`.
82    /// For `use serde;` (bare crate import) this is `"serde"`.
83    pub module_path: String,
84
85    /// Specific symbols imported from the module.
86    ///
87    /// Contains `["HashMap"]` for `use std::collections::HashMap`.
88    /// Contains `["HashMap", "HashSet"]` for `use std::collections::{HashMap, HashSet}`.
89    /// Empty for bare imports like `use serde;` or wildcard imports.
90    pub symbols: Vec<String>,
91
92    /// Whether this is a wildcard import (`use module::*`).
93    pub is_wildcard: bool,
94
95    /// Aliases for imported names.
96    ///
97    /// Maps original name to alias. For `use std::io::Result as IoResult`,
98    /// contains `[("Result", "IoResult")]`.
99    pub aliases: Vec<(String, String)>,
100}
101
102/// Information extracted from a Rust `pub use` re-export.
103///
104/// Represents a single re-exported symbol. For `pub use types::{Config, Settings}`,
105/// two `ExportInfo` records are produced.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct ExportInfo {
108    /// The name of the re-exported symbol.
109    ///
110    /// For `pub use types::Config` this is `"Config"`.
111    /// For `pub use module::*` this is `"*"`.
112    pub symbol_name: String,
113
114    /// The source module path of the re-export.
115    ///
116    /// For `pub use types::Config` this is `"types"`.
117    pub module_path: String,
118
119    /// The visibility level of this re-export.
120    pub visibility: Visibility,
121}
122
123/// Extracts Rust import and export dependencies using tree-sitter AST traversal.
124///
125/// Uses tree-sitter's Rust grammar to parse `use` and `pub use` declarations
126/// without executing Rust code. Thread-safe and reusable across files.
127///
128/// # Architecture
129///
130/// The extractor operates in two phases:
131/// 1. **Parse**: Tree-sitter parses the source into an AST
132/// 2. **Walk**: Recursive traversal extracts `use_declaration` nodes and their
133///    nested structure (scoped identifiers, use lists, wildcards, aliases)
134///
135/// Module path resolution (converting `"crate::config"` to `"src/config.rs"`)
136/// is handled separately by [`resolve_module_path`](Self::resolve_module_path).
137pub struct RustDependencyExtractor {
138    _private: (),
139}
140
141impl RustDependencyExtractor {
142    /// Creates a new Rust dependency extractor.
143    pub fn new() -> Self {
144        Self { _private: () }
145    }
146
147    /// Parse Rust source code into a tree-sitter tree.
148    fn parse_source(source: &str) -> Result<tree_sitter::Tree, ExtractionError> {
149        let language = thread_language::parsers::language_rust();
150        let mut parser = tree_sitter::Parser::new();
151        parser
152            .set_language(&language)
153            .map_err(|_| ExtractionError::ParseError)?;
154        parser
155            .parse(source, None)
156            .ok_or(ExtractionError::ParseError)
157    }
158
159    /// Extracts all `use` declarations from Rust source code.
160    ///
161    /// Parses the source with tree-sitter and walks the AST to find all
162    /// `use_declaration` nodes. Both public and private `use` statements are
163    /// returned as imports (the caller may filter by visibility if needed).
164    ///
165    /// # Arguments
166    ///
167    /// * `source` - Rust source code to analyze.
168    /// * `_file_path` - Path of the source file (reserved for error context).
169    ///
170    /// # Returns
171    ///
172    /// A vector of [`RustImportInfo`] records, one per `use` declaration.
173    ///
174    /// # Errors
175    ///
176    /// Returns [`ExtractionError::ParseError`] if tree-sitter cannot parse
177    /// the source.
178    pub fn extract_imports(
179        &self,
180        source: &str,
181        _file_path: &Path,
182    ) -> Result<Vec<RustImportInfo>, ExtractionError> {
183        if source.is_empty() {
184            return Ok(Vec::new());
185        }
186
187        let tree = Self::parse_source(source)?;
188        let root = tree.root_node();
189        let src = source.as_bytes();
190        let mut imports = Vec::new();
191
192        self.walk_use_declarations(root, src, &mut imports);
193        self.walk_mod_declarations(root, src, &mut imports);
194
195        Ok(imports)
196    }
197
198    /// Extracts all `pub use` re-export declarations from Rust source code.
199    ///
200    /// Only public or restricted-visibility `use` statements are returned.
201    ///
202    /// # Arguments
203    ///
204    /// * `source` - Rust source code to analyze.
205    /// * `_file_path` - Path of the source file (reserved for error context).
206    ///
207    /// # Returns
208    ///
209    /// A vector of [`ExportInfo`] records, one per re-exported symbol.
210    /// For `pub use types::{Config, Settings}`, two records are returned.
211    ///
212    /// # Errors
213    ///
214    /// Returns [`ExtractionError::ParseError`] if tree-sitter cannot parse
215    /// the source.
216    pub fn extract_exports(
217        &self,
218        source: &str,
219        _file_path: &Path,
220    ) -> Result<Vec<ExportInfo>, ExtractionError> {
221        if source.is_empty() {
222            return Ok(Vec::new());
223        }
224
225        let tree = Self::parse_source(source)?;
226        let root = tree.root_node();
227        let src = source.as_bytes();
228        let mut exports = Vec::new();
229
230        self.walk_export_declarations(root, src, &mut exports);
231
232        Ok(exports)
233    }
234
235    /// Resolves a Rust module path to a filesystem path.
236    ///
237    /// Handles the three Rust-specific path prefixes:
238    /// - `crate::` - resolves from the `src/` root of the crate
239    /// - `super::` - resolves from the parent module directory
240    /// - `self::` - resolves from the current module directory
241    ///
242    /// External crate paths (e.g., `std::collections`) cannot be resolved
243    /// to local files and return an error.
244    ///
245    /// # Arguments
246    ///
247    /// * `source_file` - The file containing the `use` statement.
248    /// * `module_path` - The module path (e.g., `"crate::config"`, `"super::utils"`).
249    ///
250    /// # Returns
251    ///
252    /// The resolved filesystem path to the target module file (e.g., `src/config.rs`).
253    ///
254    /// # Errors
255    ///
256    /// Returns [`ExtractionError::ResolutionError`] if:
257    /// - The path is an external crate (no `crate::`, `super::`, or `self::` prefix)
258    /// - The source file has no parent directory for `super::` resolution
259    pub fn resolve_module_path(
260        &self,
261        source_file: &Path,
262        module_path: &str,
263    ) -> Result<PathBuf, ExtractionError> {
264        if let Some(rest) = module_path.strip_prefix("crate::") {
265            // crate:: resolves from src/ root
266            let relative = rest.replace("::", "/");
267            return Ok(PathBuf::from(format!("src/{relative}.rs")));
268        }
269
270        if let Some(rest) = module_path.strip_prefix("super::") {
271            // super:: resolves relative to the parent module
272            let super_dir = self.super_directory(source_file)?;
273            let relative = rest.replace("::", "/");
274            return Ok(super_dir.join(format!("{relative}.rs")));
275        }
276
277        if module_path == "super" {
278            // Bare `super` -- resolve to the parent module itself
279            let super_dir = self.super_directory(source_file)?;
280            return Ok(super_dir.join("mod.rs"));
281        }
282
283        if let Some(rest) = module_path.strip_prefix("self::") {
284            // self:: resolves from current module directory
285            let dir = self.module_directory(source_file)?;
286            let relative = rest.replace("::", "/");
287            return Ok(dir.join(format!("{relative}.rs")));
288        }
289
290        // Simple module name without prefix (e.g., `mod lib;` in main.rs)
291        // Resolves to sibling file (lib.rs) or directory module (lib/mod.rs)
292        if !module_path.contains("::") && !module_path.is_empty() {
293            let dir = self.module_directory(source_file)?;
294            // Return sibling file path (lib.rs)
295            // Note: Could also be lib/mod.rs, but we prefer the simpler form
296            return Ok(dir.join(format!("{module_path}.rs")));
297        }
298
299        // External crate -- cannot resolve to local file
300        Err(ExtractionError::ResolutionError {
301            module: module_path.to_string(),
302            source_file: source_file.to_path_buf(),
303            reason: "external crate path cannot be resolved to a local file".to_string(),
304        })
305    }
306
307    /// Extract [`DependencyEdge`] values from a Rust source file.
308    ///
309    /// Combines import extraction with path resolution to produce edges
310    /// suitable for the incremental dependency graph. Only resolvable
311    /// internal imports produce edges; external crates are silently skipped.
312    ///
313    /// # Errors
314    ///
315    /// Returns an error if the source file cannot be parsed.
316    pub fn extract_dependency_edges(
317        &self,
318        source: &str,
319        file_path: &Path,
320    ) -> Result<Vec<super::super::types::DependencyEdge>, ExtractionError> {
321        let imports = self.extract_imports(source, file_path)?;
322        let mut edges = Vec::new();
323
324        for import in &imports {
325            // Only create edges for resolvable module paths
326            // External crates are silently skipped per design spec
327            if let Ok(resolved) = self.resolve_module_path(file_path, &import.module_path) {
328                // Create symbol-level tracking if specific symbols are imported
329                let symbol = if !import.symbols.is_empty() && !import.is_wildcard {
330                    // For now, track the first symbol (could be enhanced to create multiple edges)
331                    Some(super::super::types::SymbolDependency {
332                        from_symbol: import.symbols[0].clone(),
333                        to_symbol: import.symbols[0].clone(),
334                        kind: super::super::types::SymbolKind::Module,
335                        strength: super::super::types::DependencyStrength::Strong,
336                    })
337                } else {
338                    None
339                };
340
341                let mut edge = super::super::types::DependencyEdge::new(
342                    file_path.to_path_buf(),
343                    resolved,
344                    super::super::types::DependencyType::Import,
345                );
346                edge.symbol = symbol;
347
348                edges.push(edge);
349            }
350        }
351
352        Ok(edges)
353    }
354
355    /// Determine the module directory for a source file.
356    ///
357    /// For `mod.rs` or `lib.rs`, the module *is* the directory (these files
358    /// define the module that contains sibling files). So `self::` resolves
359    /// to the same directory and `super::` resolves to the parent directory.
360    ///
361    /// For regular files like `auth.rs`, the file is a leaf module. Its parent
362    /// module is the directory it lives in. So `self::` is meaningless (leaf
363    /// modules have no children), and `super::` resolves to the same directory
364    /// (siblings in the parent module).
365    fn module_directory(&self, source_file: &Path) -> Result<PathBuf, ExtractionError> {
366        source_file
367            .parent()
368            .map(|p| p.to_path_buf())
369            .ok_or_else(|| ExtractionError::ResolutionError {
370                module: String::new(),
371                source_file: source_file.to_path_buf(),
372                reason: "source file has no parent directory".to_string(),
373            })
374    }
375
376    /// Check if a source file is a module root (`mod.rs` or `lib.rs`).
377    ///
378    /// Module root files define a module that owns the directory, so `super::`
379    /// from these files goes up one directory level.
380    fn is_module_root(source_file: &Path) -> bool {
381        source_file
382            .file_name()
383            .map(|f| f == "mod.rs" || f == "lib.rs")
384            .unwrap_or(false)
385    }
386
387    /// Determine the directory that `super::` resolves to.
388    ///
389    /// - For `mod.rs`/`lib.rs`: `super::` goes to the parent directory.
390    /// - For regular files (e.g., `auth.rs`): `super::` stays in the same
391    ///   directory (siblings in the parent module).
392    fn super_directory(&self, source_file: &Path) -> Result<PathBuf, ExtractionError> {
393        let dir = self.module_directory(source_file)?;
394        if Self::is_module_root(source_file) {
395            // mod.rs/lib.rs: super is the parent directory
396            dir.parent()
397                .map(|p| p.to_path_buf())
398                .ok_or_else(|| ExtractionError::ResolutionError {
399                    module: String::new(),
400                    source_file: source_file.to_path_buf(),
401                    reason: "no parent directory for super resolution from module root".to_string(),
402                })
403        } else {
404            // Regular file: super is the same directory (parent module)
405            Ok(dir)
406        }
407    }
408
409    // =========================================================================
410    // Import extraction (private helpers)
411    // =========================================================================
412
413    /// Walk the AST looking for `use_declaration` nodes and extract import info.
414    fn walk_use_declarations(
415        &self,
416        node: tree_sitter::Node<'_>,
417        source: &[u8],
418        imports: &mut Vec<RustImportInfo>,
419    ) {
420        if node.kind() == "use_declaration" {
421            self.extract_use_declaration(node, source, imports);
422            return;
423        }
424
425        let mut cursor = node.walk();
426        for child in node.children(&mut cursor) {
427            self.walk_use_declarations(child, source, imports);
428        }
429    }
430
431    /// Walk the AST looking for `mod_item` nodes and extract module dependencies.
432    ///
433    /// Extracts `mod foo;` declarations which create module dependencies.
434    /// Note: This extracts declarations like `mod lib;`, not inline modules `mod lib { ... }`.
435    fn walk_mod_declarations(
436        &self,
437        node: tree_sitter::Node<'_>,
438        source: &[u8],
439        imports: &mut Vec<RustImportInfo>,
440    ) {
441        if node.kind() == "mod_item" {
442            self.extract_mod_declaration(node, source, imports);
443            return;
444        }
445
446        let mut cursor = node.walk();
447        for child in node.children(&mut cursor) {
448            self.walk_mod_declarations(child, source, imports);
449        }
450    }
451
452    /// Extract module dependency from a `mod_item` node.
453    ///
454    /// Handles: `mod foo;` (external module file)
455    /// Skips: `mod foo { ... }` (inline module - no file dependency)
456    fn extract_mod_declaration(
457        &self,
458        node: tree_sitter::Node<'_>,
459        source: &[u8],
460        imports: &mut Vec<RustImportInfo>,
461    ) {
462        // Check if this is an external module (has semicolon) vs inline (has block)
463        let has_block = node
464            .children(&mut node.walk())
465            .any(|c| c.kind() == "declaration_list");
466        if has_block {
467            // Inline module - no file dependency
468            return;
469        }
470
471        // Extract module name
472        let mut cursor = node.walk();
473        for child in node.children(&mut cursor) {
474            if child.kind() == "identifier" {
475                if let Ok(name) = child.utf8_text(source) {
476                    // Create import info for module dependency
477                    imports.push(RustImportInfo {
478                        module_path: name.to_string(),
479                        symbols: Vec::new(),
480                        is_wildcard: false,
481                        aliases: Vec::new(),
482                    });
483                }
484                return;
485            }
486        }
487    }
488
489    /// Extract import info from a single `use_declaration` node.
490    ///
491    /// Tree-sitter Rust grammar for `use_declaration`:
492    /// ```text
493    /// use_declaration -> visibility_modifier? "use" use_clause ";"
494    /// use_clause -> scoped_identifier | identifier | use_as_clause
495    ///            | scoped_use_list | use_wildcard | use_list
496    /// ```
497    fn extract_use_declaration(
498        &self,
499        node: tree_sitter::Node<'_>,
500        source: &[u8],
501        imports: &mut Vec<RustImportInfo>,
502    ) {
503        let mut cursor = node.walk();
504        for child in node.children(&mut cursor) {
505            match child.kind() {
506                "scoped_identifier" | "scoped_use_list" | "use_as_clause" | "use_wildcard"
507                | "use_list" | "identifier" => {
508                    let mut info = RustImportInfo {
509                        module_path: String::new(),
510                        symbols: Vec::new(),
511                        is_wildcard: false,
512                        aliases: Vec::new(),
513                    };
514                    self.extract_use_clause(child, source, &mut info);
515                    imports.push(info);
516                }
517                _ => {}
518            }
519        }
520    }
521
522    /// Extract use clause details into a [`RustImportInfo`].
523    ///
524    /// Dispatches based on the node kind to handle all Rust use syntax variants.
525    fn extract_use_clause(
526        &self,
527        node: tree_sitter::Node<'_>,
528        source: &[u8],
529        info: &mut RustImportInfo,
530    ) {
531        match node.kind() {
532            "identifier" => {
533                // Bare import: `use serde;`
534                info.module_path = self.node_text(node, source);
535            }
536            "scoped_identifier" => {
537                // `use std::collections::HashMap;`
538                // Split into path (all but last) and name (last identifier)
539                let full_path = self.node_text(node, source);
540                if let Some((path, symbol)) = full_path.rsplit_once("::") {
541                    info.module_path = path.to_string();
542                    info.symbols.push(symbol.to_string());
543                } else {
544                    info.module_path = full_path;
545                }
546            }
547            "use_as_clause" => {
548                self.extract_use_as_clause(node, source, info);
549            }
550            "scoped_use_list" => {
551                self.extract_scoped_use_list(node, source, info);
552            }
553            "use_wildcard" => {
554                self.extract_use_wildcard(node, source, info);
555            }
556            "use_list" => {
557                self.extract_use_list(node, source, info);
558            }
559            _ => {}
560        }
561    }
562
563    /// Extract a `use_as_clause` node: `path as alias`.
564    fn extract_use_as_clause(
565        &self,
566        node: tree_sitter::Node<'_>,
567        source: &[u8],
568        info: &mut RustImportInfo,
569    ) {
570        let mut cursor = node.walk();
571        let children: Vec<_> = node
572            .children(&mut cursor)
573            .filter(|c| c.is_named())
574            .collect();
575
576        // Structure: use_as_clause -> path "as" alias
577        // Named children: [scoped_identifier|identifier, identifier(alias)]
578        if children.len() >= 2 {
579            let path_node = children[0];
580            let alias_node = children[children.len() - 1];
581
582            let full_path = self.node_text(path_node, source);
583            let alias = self.node_text(alias_node, source);
584
585            if let Some((path, symbol)) = full_path.rsplit_once("::") {
586                info.module_path = path.to_string();
587                info.symbols.push(symbol.to_string());
588                info.aliases.push((symbol.to_string(), alias));
589            } else {
590                // `use serde as s;`
591                info.module_path = full_path.clone();
592                info.aliases.push((full_path, alias));
593            }
594        }
595    }
596
597    /// Extract a `scoped_use_list` node: `path::{items}`.
598    fn extract_scoped_use_list(
599        &self,
600        node: tree_sitter::Node<'_>,
601        source: &[u8],
602        info: &mut RustImportInfo,
603    ) {
604        let mut cursor = node.walk();
605        for child in node.children(&mut cursor) {
606            match child.kind() {
607                "identifier" | "scoped_identifier" | "self" | "crate" | "super" => {
608                    info.module_path = self.node_text(child, source);
609                }
610                "use_list" => {
611                    self.extract_use_list(child, source, info);
612                }
613                _ => {}
614            }
615        }
616    }
617
618    /// Extract items from a `use_list` node: `{Item1, Item2, ...}`.
619    fn extract_use_list(
620        &self,
621        node: tree_sitter::Node<'_>,
622        source: &[u8],
623        info: &mut RustImportInfo,
624    ) {
625        let mut cursor = node.walk();
626        for child in node.children(&mut cursor) {
627            match child.kind() {
628                "identifier" => {
629                    info.symbols.push(self.node_text(child, source));
630                }
631                "use_as_clause" => {
632                    // `HashMap as Map` inside a use list
633                    let mut inner_cursor = child.walk();
634                    let named: Vec<_> = child
635                        .children(&mut inner_cursor)
636                        .filter(|c| c.is_named())
637                        .collect();
638                    if named.len() >= 2 {
639                        let original = self.node_text(named[0], source);
640                        let alias = self.node_text(named[named.len() - 1], source);
641                        info.symbols.push(original.clone());
642                        info.aliases.push((original, alias));
643                    }
644                }
645                "self" => {
646                    info.symbols.push("self".to_string());
647                }
648                "use_wildcard" => {
649                    info.is_wildcard = true;
650                }
651                _ => {}
652            }
653        }
654    }
655
656    /// Extract a `use_wildcard` node: `path::*`.
657    fn extract_use_wildcard(
658        &self,
659        node: tree_sitter::Node<'_>,
660        source: &[u8],
661        info: &mut RustImportInfo,
662    ) {
663        info.is_wildcard = true;
664        let mut cursor = node.walk();
665        for child in node.children(&mut cursor) {
666            if child.kind() == "identifier" || child.kind() == "scoped_identifier" {
667                info.module_path = self.node_text(child, source);
668            }
669        }
670    }
671
672    // =========================================================================
673    // Export extraction (private helpers)
674    // =========================================================================
675
676    /// Walk the AST looking for `pub use` declarations and extract export info.
677    fn walk_export_declarations(
678        &self,
679        node: tree_sitter::Node<'_>,
680        source: &[u8],
681        exports: &mut Vec<ExportInfo>,
682    ) {
683        if node.kind() == "use_declaration" {
684            if let Some(vis) = self.get_visibility(node, source) {
685                self.extract_export_from_use(node, source, vis, exports);
686            }
687            return;
688        }
689
690        let mut cursor = node.walk();
691        for child in node.children(&mut cursor) {
692            self.walk_export_declarations(child, source, exports);
693        }
694    }
695
696    /// Check if a `use_declaration` has a visibility modifier.
697    /// Returns `Some(Visibility)` for pub/pub(crate)/pub(super)/pub(in ...).
698    fn get_visibility(&self, node: tree_sitter::Node<'_>, source: &[u8]) -> Option<Visibility> {
699        let mut cursor = node.walk();
700        for child in node.children(&mut cursor) {
701            if child.kind() == "visibility_modifier" {
702                let text = self.node_text(child, source);
703                return Some(self.parse_visibility(&text));
704            }
705        }
706        None
707    }
708
709    /// Parse a visibility modifier string into a [`Visibility`] enum value.
710    fn parse_visibility(&self, text: &str) -> Visibility {
711        let trimmed = text.trim();
712        if trimmed == "pub" {
713            Visibility::Public
714        } else if trimmed.starts_with("pub(crate)") {
715            Visibility::Crate
716        } else if trimmed.starts_with("pub(super)") {
717            Visibility::Super
718        } else if trimmed.starts_with("pub(in") {
719            Visibility::Restricted
720        } else {
721            Visibility::Public
722        }
723    }
724
725    /// Extract export info from a `pub use` declaration.
726    fn extract_export_from_use(
727        &self,
728        node: tree_sitter::Node<'_>,
729        source: &[u8],
730        visibility: Visibility,
731        exports: &mut Vec<ExportInfo>,
732    ) {
733        let mut cursor = node.walk();
734        for child in node.children(&mut cursor) {
735            match child.kind() {
736                "scoped_identifier" => {
737                    let full = self.node_text(child, source);
738                    if let Some((path, symbol)) = full.rsplit_once("::") {
739                        exports.push(ExportInfo {
740                            symbol_name: symbol.to_string(),
741                            module_path: path.to_string(),
742                            visibility,
743                        });
744                    }
745                }
746                "scoped_use_list" => {
747                    let mut module_path = String::new();
748                    let mut symbols = Vec::new();
749
750                    let mut inner_cursor = child.walk();
751                    for inner in child.children(&mut inner_cursor) {
752                        match inner.kind() {
753                            "identifier" | "scoped_identifier" => {
754                                module_path = self.node_text(inner, source);
755                            }
756                            "use_list" => {
757                                let mut list_cursor = inner.walk();
758                                for item in inner.children(&mut list_cursor) {
759                                    if item.kind() == "identifier" {
760                                        symbols.push(self.node_text(item, source));
761                                    }
762                                }
763                            }
764                            _ => {}
765                        }
766                    }
767
768                    for sym in symbols {
769                        exports.push(ExportInfo {
770                            symbol_name: sym,
771                            module_path: module_path.clone(),
772                            visibility,
773                        });
774                    }
775                }
776                "use_wildcard" => {
777                    let mut module_path = String::new();
778                    let mut wc_cursor = child.walk();
779                    for wc_child in child.children(&mut wc_cursor) {
780                        if wc_child.kind() == "identifier" || wc_child.kind() == "scoped_identifier"
781                        {
782                            module_path = self.node_text(wc_child, source);
783                        }
784                    }
785                    exports.push(ExportInfo {
786                        symbol_name: "*".to_string(),
787                        module_path,
788                        visibility,
789                    });
790                }
791                "use_as_clause" => {
792                    let mut inner_cursor = child.walk();
793                    let named: Vec<_> = child
794                        .children(&mut inner_cursor)
795                        .filter(|c| c.is_named())
796                        .collect();
797                    if !named.is_empty() {
798                        let full = self.node_text(named[0], source);
799                        if let Some((path, symbol)) = full.rsplit_once("::") {
800                            exports.push(ExportInfo {
801                                symbol_name: symbol.to_string(),
802                                module_path: path.to_string(),
803                                visibility,
804                            });
805                        }
806                    }
807                }
808                "identifier" => {
809                    let name = self.node_text(child, source);
810                    exports.push(ExportInfo {
811                        symbol_name: name.clone(),
812                        module_path: name,
813                        visibility,
814                    });
815                }
816                _ => {}
817            }
818        }
819    }
820
821    // =========================================================================
822    // Utility helpers
823    // =========================================================================
824
825    /// Get the UTF-8 text of a tree-sitter node.
826    fn node_text(&self, node: tree_sitter::Node<'_>, source: &[u8]) -> String {
827        node.utf8_text(source).unwrap_or("").to_string()
828    }
829}
830
831impl Default for RustDependencyExtractor {
832    fn default() -> Self {
833        Self::new()
834    }
835}
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840
841    /// Verify AST node kinds for Rust use declarations to validate grammar assumptions.
842    #[test]
843    fn verify_ast_structure() {
844        let source = "use std::collections::HashMap;";
845        let tree = RustDependencyExtractor::parse_source(source).unwrap();
846        let root = tree.root_node();
847        assert_eq!(root.kind(), "source_file");
848        let use_decl = root.child(0).unwrap();
849        assert_eq!(use_decl.kind(), "use_declaration");
850    }
851}