Skip to main content

nichlink/registry_core/source/
walk.rs

1//! Recursive source-tree walking driven by caller-supplied filesystem facts.
2//! 由调用方提供的文件系统事实驱动的递归源码树遍历。
3//!
4//! The kernel owns the algorithm and never touches the filesystem; an execution
5//! surface supplies [`SourceTree`] and chooses a [`SourceWalk`] policy.
6//! 内核拥有算法且从不接触文件系统;执行面提供 [`SourceTree`],并选择 [`SourceWalk`]
7//! 策略。
8
9use std::path::{Path, PathBuf};
10
11/// The filesystem facts a recursive source walk needs.
12/// 递归源码遍历所需的文件系统事实。
13///
14/// The kernel owns the algorithm and never touches the filesystem; an execution
15/// surface supplies these three operations.
16/// 内核拥有算法且从不接触文件系统;三项操作由执行面提供。
17pub trait SourceTree {
18    /// Whether `path` names a directory; the walk recurses only when this is true.
19    /// `path` 是否为目录;仅当为真时遍历才递归进入。
20    fn is_directory(&self, path: &Path) -> bool;
21    /// The direct entries of `path`; an error aborts the whole walk.
22    /// `path` 的直接条目;返回错误会中止整次遍历。
23    fn entries(&self, path: &Path) -> Result<Vec<PathBuf>, String>;
24    /// Read a `.rs` file when the keep callback asks for its text; an error
25    /// skips only that file.
26    /// 当 keep 回调索取文本时读取 `.rs` 文件;返回错误只跳过该文件。
27    fn read_text(&self, path: &Path) -> Result<String, String>;
28}
29
30/// Which subtrees a source walk never enters.
31/// 源码遍历永不进入的子树。
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub struct SourceWalk {
34    /// Build output, never source.
35    /// 构建产物,绝不是源码。
36    pub skip_target: bool,
37    /// The checked-in registration machinery.
38    /// 随仓库提交的注册机制。
39    pub skip_registry_core: bool,
40    /// The compile-error demonstration tree.
41    /// 编译错误演示树。
42    pub skip_compile_error_demo: bool,
43}
44
45impl SourceWalk {
46    /// Enter everything; keep every `.rs` file.
47    /// 进入一切目录;保留每个 `.rs` 文件。
48    pub const EVERYTHING: Self = Self {
49        skip_target: false,
50        skip_registry_core: false,
51        skip_compile_error_demo: false,
52    };
53
54    fn skips(&self, path: &Path) -> bool {
55        let named = |name: &str| path.file_name().and_then(|value| value.to_str()) == Some(name);
56        let within = |name: &str| path.components().any(|part| part.as_os_str() == name);
57        (self.skip_target && named("target"))
58            || (self.skip_registry_core && within("registry_core"))
59            || (self.skip_compile_error_demo && within("compile_error_demo"))
60    }
61}
62
63/// What a walk should do with one `.rs` file it found.
64/// 遍历发现一个 `.rs` 文件后应当做什么。
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum Keep {
67    /// Take it without reading it.
68    /// 直接收下,不读取内容。
69    Yes,
70    /// Skip it.
71    /// 跳过。
72    No,
73    /// Read the text and ask again with it.
74    /// 读出文本后再问一次。
75    NeedSource,
76}
77
78/// How deep the walk nests before it assumes the tree is cyclic.
79/// 遍历在假定树存在环之前允许达到的深度。
80///
81/// The kernel takes filesystem facts from its caller and cannot canonicalize a
82/// path, so it cannot tell a link that points at an ancestor from a directory
83/// that is simply deep. A bound is what keeps the workspace's only recursive
84/// traversal from overflowing the stack; 128 directories is far past any real
85/// source layout, and a tree that reaches it is reported rather than followed.
86/// 内核的文件系统事实来自调用方,且无法 canonicalize 路径,因此它分不出"指向祖先的链接"
87/// 与"确实很深的目录"。能阻止 workspace 唯一的递归遍历栈溢出的东西就是一条深度上限;
88/// 128 层远超任何真实源码布局,而达到它的树会被报告而不是继续跟随。
89pub const MAX_DEPTH: usize = 128;
90
91/// Every `.rs` file under `root`, depth-first, in directory order.
92/// `root` 下每个 `.rs` 文件,深度优先,按目录顺序。
93///
94/// The walk is the only recursive source traversal in the workspace: the build
95/// step, the authoring surface and the MCP index all call it with their own
96/// [`SourceTree`] and their own [`SourceWalk`] options.
97/// 这是整个 workspace 唯一的递归源码遍历:构建步骤、创作面与 MCP 索引都用各自的
98/// [`SourceTree`] 与 [`SourceWalk`] 选项调用它。
99pub fn collect_rust_sources(
100    tree: &impl SourceTree,
101    root: &Path,
102    walk: SourceWalk,
103    mut keep: impl FnMut(&Path, Option<&str>) -> Keep,
104    collected: &mut Vec<PathBuf>,
105) -> Result<(), String> {
106    fn visit(
107        tree: &impl SourceTree,
108        directory: &Path,
109        depth: usize,
110        walk: SourceWalk,
111        keep: &mut dyn FnMut(&Path, Option<&str>) -> Keep,
112        collected: &mut Vec<PathBuf>,
113    ) -> Result<(), String> {
114        if depth > MAX_DEPTH {
115            return Err(format!(
116                "source tree nests deeper than {MAX_DEPTH} directories at {}; it is cyclic, or too deep for this bound",
117                directory.display()
118            ));
119        }
120        for path in tree.entries(directory)? {
121            if walk.skips(&path) {
122                continue;
123            }
124            if tree.is_directory(&path) {
125                visit(tree, &path, depth + 1, walk, keep, collected)?;
126                continue;
127            }
128            if path.extension().and_then(|extension| extension.to_str()) != Some("rs") {
129                continue;
130            }
131            match keep(&path, None) {
132                Keep::No => {}
133                Keep::Yes => collected.push(path),
134                Keep::NeedSource => {
135                    let Ok(source) = tree.read_text(&path) else {
136                        continue;
137                    };
138                    if keep(&path, Some(&source)) == Keep::Yes {
139                        collected.push(path);
140                    }
141                }
142            }
143        }
144        Ok(())
145    }
146    visit(tree, root, 0, walk, &mut keep, collected)
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    /// A tree that points back at itself must stop with a report, not recurse
154    /// until the stack overflows. The kernel cannot canonicalize a path, so the
155    /// bound is the only thing standing between a link loop and a crash.
156    /// 指向自身的树必须带着报告停下来,而不是递归到栈溢出。内核无法 canonicalize 路径,
157    /// 因此这条上限是链接环与崩溃之间唯一的东西。
158    #[test]
159    fn a_cyclic_tree_stops_at_the_depth_bound() {
160        struct Cyclic;
161
162        impl SourceTree for Cyclic {
163            fn is_directory(&self, _path: &Path) -> bool {
164                true
165            }
166
167            fn entries(&self, path: &Path) -> Result<Vec<PathBuf>, String> {
168                // Every directory holds one more directory, forever.
169                // 每个目录里都还有一个目录,永远如此。
170                Ok(vec![path.join("loop")])
171            }
172
173            fn read_text(&self, _path: &Path) -> Result<String, String> {
174                Ok(String::new())
175            }
176        }
177
178        let mut collected = Vec::new();
179        let error = collect_rust_sources(
180            &Cyclic,
181            Path::new("/root"),
182            SourceWalk::EVERYTHING,
183            |_path, _source| Keep::No,
184            &mut collected,
185        )
186        .expect_err("a cyclic tree must be reported");
187        assert!(error.contains("deeper than"), "{error}");
188        assert!(collected.is_empty(), "{collected:?}");
189    }
190
191    /// A tree that stays inside the bound is walked as before.
192    /// 停在上限之内的树照常被遍历。
193    #[test]
194    fn a_flat_tree_yields_every_rust_file() {
195        struct Flat(Vec<PathBuf>, Vec<PathBuf>);
196
197        impl SourceTree for Flat {
198            fn is_directory(&self, path: &Path) -> bool {
199                self.0.iter().any(|directory| directory == path)
200            }
201
202            fn entries(&self, path: &Path) -> Result<Vec<PathBuf>, String> {
203                Ok(self
204                    .1
205                    .iter()
206                    .filter(|entry| entry.parent() == Some(path))
207                    .cloned()
208                    .collect())
209            }
210
211            fn read_text(&self, _path: &Path) -> Result<String, String> {
212                Ok(String::new())
213            }
214        }
215
216        let root = PathBuf::from("/src");
217        let tree = Flat(
218            vec![root.clone()],
219            vec![root.join("a.rs"), root.join("b.txt"), root.join("nested")],
220        );
221        let mut collected = Vec::new();
222        collect_rust_sources(
223            &tree,
224            &root,
225            SourceWalk::EVERYTHING,
226            |_path, _| Keep::Yes,
227            &mut collected,
228        )
229        .expect("a flat tree walks");
230        assert_eq!(collected, vec![root.join("a.rs")]);
231    }
232}