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}