nichlink/registry_core/source/source.rs
1//! Pure Rust-source lexer shared by search, callgraph, and indexing tools.
2//! 供搜索、调用图与索引工具共用的纯 Rust 源码词法器。
3//!
4//! Every function here is a text transformation only: callers own file I/O.
5//! 这里的所有函数只做文本变换,文件 I/O 由调用方负责。
6//!
7//! Function discovery lives here; `calls` scans call sites inside an extracted
8//! body and `walk` owns the recursive source traversal.
9//! 函数发现位于本页;`calls` 扫描已提取函数体内的调用点,`walk` 拥有递归源码遍历。
10#[path = "lex.rs"]
11pub(crate) mod lex;
12
13#[path = "calls.rs"]
14mod calls;
15pub use calls::*;
16#[path = "walk.rs"]
17mod walk;
18pub use walk::*;
19
20/// One Rust function discovered in a source file.
21/// 在源码文件中发现的一个 Rust 函数。
22#[derive(Clone, Debug, Eq, PartialEq)]
23pub struct SourceFunction {
24 /// Function name as written after the `fn` token.
25 /// `fn` token 之后书写的函数名。
26 pub name: String,
27 /// Source text from the start of the line through the opening brace, trimmed.
28 /// 从行首到左花括号的源码文本,已去除首尾空白。
29 pub signature: String,
30 /// Source text between the opening and closing braces.
31 /// 左、右花括号之间的源码文本。
32 pub body: String,
33 /// 1-based line of the opening `fn` token.
34 /// 以 1 起始的 `fn` 起始行行号。
35 pub line: u32,
36 /// 1-based line of the closing brace.
37 /// 以 1 起始的右花括号所在行行号。
38 pub end_line: u32,
39}
40
41/// Find the 0-based inclusive line range of a function by name.
42/// 按名称查找函数的 0 起始闭区间行范围。
43pub fn function_source_range(lines: &[&str], name: &str) -> Option<(usize, usize)> {
44 // The start test runs on masked text and requires the name to be a whole
45 // identifier. On the raw line, `// fn ghost() {` started a range for `ghost` that
46 // closed on the next real function, and `fn renew(` matched the name `new`.
47 // 起点判断跑在屏蔽后的文本上,并要求名字是完整标识符。按原始行时,`// fn ghost() {`
48 // 会为 `ghost` 开出一个到下一个真实函数才闭合的范围,而 `fn renew(` 会匹配名字 `new`。
49 let start = lines.iter().position(|line| {
50 let masked = mask_non_code(line);
51 let mut from = 0usize;
52 while let Some(offset) = masked[from..].find("fn ") {
53 let at = from + offset;
54 let after = masked[at + "fn ".len()..].trim_start();
55 if let Some(rest) = after.strip_prefix(name)
56 && rest.trim_start().starts_with('(')
57 {
58 return true;
59 }
60 from = at + "fn ".len();
61 }
62 false
63 })?;
64 let mut depth = 0usize;
65 let mut opened = false;
66 for (index, line) in lines.iter().enumerate().skip(start) {
67 // Braces inside a string or a comment are not code. Counting them on the
68 // raw line let `let s = "{";` open a range that closed on a later `}` —
69 // and with no closing brace at all the old fallback claimed a one-line
70 // function. The masking below is the same rule the function index uses.
71 // 字符串或注释里的花括号不是代码。按原始行计数会让 `let s = "{";` 打开一个在更后面的
72 // `}` 处闭合的范围——而完全没有闭合花括号时,旧的兜底会声称这是个单行函数。下面的
73 // 屏蔽与函数索引用的是同一条规则。
74 for character in mask_non_code(line).chars() {
75 match character {
76 '{' => {
77 depth += 1;
78 opened = true;
79 }
80 '}' if opened => depth = depth.saturating_sub(1),
81 _ => {}
82 }
83 }
84 if opened && depth == 0 {
85 return Some((start, index));
86 }
87 }
88 // Unbalanced at the end of `lines`: the function does not close inside what
89 // the caller handed over. `Some((start, start))` reported that as a one-line
90 // function, which is the opposite of what the caller needs to know.
91 // 在 `lines` 末尾仍未配平:该函数没有在调用方给出的范围内闭合。过去用
92 // `Some((start, start))` 把它报成单行函数,而这与调用方需要知道的事实相反。
93 None
94}
95
96/// Advance `cursor` past one UTF-8 character, when there is one.
97/// 若存在,把 `cursor` 推进一个 UTF-8 字符。
98fn skip_one_character(bytes: &[u8], cursor: &mut usize) {
99 if *cursor < bytes.len() {
100 *cursor += 1;
101 while bytes.get(*cursor).is_some_and(|byte| byte & 0xC0 == 0x80) {
102 *cursor += 1;
103 }
104 }
105}
106
107/// Index function bodies without treating comments, strings, or macro text as Rust.
108/// 扫描函数体时屏蔽注释、字符串和宏文本,避免把它们误认成 Rust 函数。
109pub fn function_symbols(source: &str) -> Vec<SourceFunction> {
110 let masked = mask_non_code(source);
111 let bytes = masked.as_bytes();
112 let mut result = Vec::new();
113 // Line numbers come from one forward scan. Both offsets this reports are
114 // non-decreasing — the loop resumes at the end of the function it just took —
115 // so counting from the last offset instead of from zero makes the whole pass
116 // linear; counting from zero per function made it quadratic in the number of
117 // functions, which is the shape MCP's index walks.
118 // 行号来自一次前向扫描。它报告的两个偏移都是非递减的——循环从刚取下的函数末尾继续——
119 // 因此从上次的偏移继续数、而不是每次从 0 数,使整趟是线性的;过去每个函数都从 0 数,
120 // 于是复杂度与函数个数相乘,而 MCP 的索引正是按那种形状遍历的。
121 //
122 // The `target < counted_to` arm is a correctness fallback, not the path any
123 // caller takes today: a future caller that asks about an earlier offset gets
124 // the right answer at the old cost instead of a wrong one.
125 // `target < counted_to` 那一支是正确性兜底,不是今天任何调用方会走的路径:将来若有
126 // 调用方问一个更早的偏移,它会以旧代价拿到正确答案,而不是拿到一个错答案。
127 let mut counted_to = 0usize;
128 let mut counted_lines = 1u32;
129 let line_at = |target: usize, counted_to: &mut usize, counted_lines: &mut u32| -> u32 {
130 if target < *counted_to {
131 return source[..target]
132 .bytes()
133 .filter(|byte| *byte == b'\n')
134 .count() as u32
135 + 1;
136 }
137 *counted_lines += source[*counted_to..target]
138 .bytes()
139 .filter(|byte| *byte == b'\n')
140 .count() as u32;
141 *counted_to = target;
142 *counted_lines
143 };
144 let mut index = 0usize;
145 while index < bytes.len() {
146 if !is_ident_start(bytes[index]) {
147 index += 1;
148 continue;
149 }
150 let token_start = index;
151 index += 1;
152 while index < bytes.len() && is_ident_continue(bytes[index]) {
153 index += 1;
154 }
155 if &masked[token_start..index] != "fn" {
156 continue;
157 }
158 let mut name_start = index;
159 while name_start < bytes.len() && bytes[name_start].is_ascii_whitespace() {
160 name_start += 1;
161 }
162 if name_start >= bytes.len() || !is_ident_start(bytes[name_start]) {
163 continue;
164 }
165 let mut name_end = name_start + 1;
166 while name_end < bytes.len() && is_ident_continue(bytes[name_end]) {
167 name_end += 1;
168 }
169 let name = masked[name_start..name_end].to_owned();
170 let mut open = name_end;
171 let mut angle_depth = 0usize;
172 while open < bytes.len() {
173 match bytes[open] {
174 b'<' => angle_depth += 1,
175 b'>' if angle_depth > 0 => angle_depth -= 1,
176 b'{' if angle_depth == 0 => break,
177 b';' if angle_depth == 0 => break,
178 _ => {}
179 }
180 open += 1;
181 }
182 if open >= bytes.len() || bytes[open] != b'{' {
183 continue;
184 }
185 let mut depth = 1usize;
186 let mut close = open + 1;
187 while close < bytes.len() && depth > 0 {
188 match bytes[close] {
189 b'{' => depth += 1,
190 b'}' => depth = depth.saturating_sub(1),
191 _ => {}
192 }
193 close += 1;
194 }
195 if depth != 0 {
196 continue;
197 }
198 let line_start = source[..token_start].rfind('\n').map_or(0, |line| line + 1);
199 let line = line_at(token_start, &mut counted_to, &mut counted_lines);
200 let end_line = line_at(close, &mut counted_to, &mut counted_lines);
201 let signature = source[line_start..open].trim().to_owned();
202 result.push(SourceFunction {
203 name,
204 signature,
205 body: source[open + 1..close - 1].to_owned(),
206 line,
207 end_line,
208 });
209 index = close;
210 }
211 result
212}
213
214fn is_ident_start(byte: u8) -> bool {
215 byte.is_ascii_alphabetic() || byte == b'_'
216}
217
218fn is_ident_continue(byte: u8) -> bool {
219 byte.is_ascii_alphanumeric() || byte == b'_'
220}
221
222/// Replace comments and quoted literals with spaces while preserving offsets.
223/// 用空格替换注释和引号字面量,同时保留原始偏移量。
224/// Blank the contents of comments, string literals and character literals.
225/// 把注释、字符串字面量与字符字面量的内容抹成空白。
226///
227/// The workspace's one text rule for "what is code": the source scanners use it so
228/// a `fn` inside a comment or a brace inside a string is not read as Rust, and the
229/// `conventions` gates use it so a workspace test's *fixture string* mentioning
230/// `include!`/`std::fs` is not read as a violation. Macro bodies are deliberately
231/// left alone — the macro name and its delimiter are code, and a gate that looks
232/// for a macro invocation has to see them.
233/// 本工作区关于"什么算代码"的唯一文本规则:源码扫描器用它,使注释里的 `fn` 或字符串里的花括号不被
234/// 读成 Rust;`conventions` 的门禁也用它,使工作区测试里**夹具字符串**中提到的
235/// `include!`/`std::fs` 不被读成违规。宏内容有意保留——宏名与它的定界符是代码,而查找宏调用的
236/// 门禁必须看见它们。
237pub fn mask_non_code(source: &str) -> String {
238 mask(source, true)
239}
240
241/// Replace quoted literals with spaces while preserving offsets, keeping comments.
242/// 用空格替换引号字面量,同时保留原始偏移量,并保留注释。
243///
244/// The same scan as [`mask_non_code`], with the comment writes disabled: what
245/// survives is code *plus comment text*, which is what a rule about what a
246/// comment says needs. A gate that read the raw lines instead was fooled by a
247/// test fixture whose string literal carried a `///` line — the fixture's text
248/// looked like a doc comment and was reported as one.
249/// 与 [`mask_non_code`] 同一次扫描,只是关掉写注释的空白:留下的是代码**加上注释文本**,
250/// 而一条关于注释说了什么的规则正需要这个。直接读原始行的门禁曾被一个测试夹具骗过:那个字符串
251/// 字面量里带着一行 `///`,夹具文本看起来像文档注释,于是被当成文档注释报了出来。
252pub fn mask_literals(source: &str) -> String {
253 mask(source, false)
254}
255
256/// The shared scan behind [`mask_non_code`] and [`mask_literals`].
257/// [`mask_non_code`] 与 [`mask_literals`] 共用的扫描。
258///
259/// `blank_comments` decides whether comment text is replaced. Literals are always
260/// replaced, because a quote inside a comment must not be read as the start of a
261/// string and a quote inside a string must not be read as code.
262/// `blank_comments` 决定注释文本是否被替换。字面量总会被替换,因为注释里的引号不能被读成字符串的
263/// 开始,字符串里的引号也不能被读成代码。
264fn mask(source: &str, blank_comments: bool) -> String {
265 let bytes = source.as_bytes();
266 let mut masked = bytes.to_vec();
267 let mut index = 0usize;
268 while index < bytes.len() {
269 if bytes[index] == b'/' && bytes.get(index + 1) == Some(&b'/') {
270 index += 2;
271 while index < bytes.len() && bytes[index] != b'\n' {
272 if blank_comments {
273 masked[index] = b' ';
274 }
275 index += 1;
276 }
277 continue;
278 }
279 if bytes[index] == b'/' && bytes.get(index + 1) == Some(&b'*') {
280 if blank_comments {
281 masked[index] = b' ';
282 if index + 1 < bytes.len() {
283 masked[index + 1] = b' ';
284 }
285 }
286 index += 2;
287 let mut depth = 1usize;
288 while index < bytes.len() && depth > 0 {
289 if bytes[index] == b'/' && bytes.get(index + 1) == Some(&b'*') {
290 depth += 1;
291 if blank_comments {
292 masked[index] = b' ';
293 masked[index + 1] = b' ';
294 }
295 index += 2;
296 } else if bytes[index] == b'*' && bytes.get(index + 1) == Some(&b'/') {
297 depth = depth.saturating_sub(1);
298 if blank_comments {
299 masked[index] = b' ';
300 masked[index + 1] = b' ';
301 }
302 index += 2;
303 } else {
304 if blank_comments && bytes[index] != b'\n' {
305 masked[index] = b' ';
306 }
307 index += 1;
308 }
309 }
310 continue;
311 }
312 // A raw string is not closed by the quote that follows its opening `#`s, so the
313 // ordinary branch below stopped at the first interior `"` and the rest of the
314 // literal — braces, `fn`, call sites — was read as code. `r#"…"#` is ordinary
315 // Rust, and JSON payloads inside it are full of interior quotes.
316 // 原始字符串不由其开头 `#` 之后的那个引号闭合,因此下面的普通分支会在第一个内部 `"`
317 // 处停下,而字面量剩下的部分——花括号、`fn`、调用点——会被读成代码。`r#"…"#` 是普通
318 // Rust,而它里面的 JSON 载荷满是内部引号。
319 if let Some(end) = lex::raw_string_end(bytes, index) {
320 for byte in &mut masked[index..end] {
321 if *byte != b'\n' {
322 *byte = b' ';
323 }
324 }
325 index = end;
326 continue;
327 }
328 if bytes[index] == b'"' {
329 let quote = bytes[index];
330 masked[index] = b' ';
331 index += 1;
332 while index < bytes.len() {
333 let escaped = bytes[index] == b'\\';
334 if bytes[index] != b'\n' {
335 masked[index] = b' ';
336 }
337 index += 1;
338 if escaped && index < bytes.len() {
339 if bytes[index] != b'\n' {
340 masked[index] = b' ';
341 }
342 index += 1;
343 } else if bytes[index - 1] == quote {
344 break;
345 }
346 }
347 continue;
348 }
349 // A `'` opens a character literal only when that literal closes. A
350 // lifetime (`'a`), a label (`'outer`) or the apostrophe of `&'static` has
351 // no closing quote, and treating it as one masked everything up to the
352 // next apostrophe — a function's `{` included — so every function with a
353 // lifetime parameter vanished from the index, and every call after a
354 // `&'static` was missed. The rule here is the lexer's: `'\…'`, or one
355 // character followed by `'`.
356 // `'` 只有在字符字面量闭合时才是它的起始。生命周期(`'a`)、标签(`'outer`)或
357 // `&'static` 的撇号没有闭合引号,把它当成引号会一路遮到下一个撇号——包括函数的 `{`
358 // ——于是每个带生命周期参数的函数都从索引里消失,`&'static` 之后的调用也全部漏掉。
359 // 这里的规则与词法器相同:`'\…'`,或一个字符后紧跟 `'`。
360 if bytes[index] == b'\'' {
361 let mut cursor = index + 1;
362 if bytes.get(cursor) == Some(&b'\\') {
363 cursor += 1;
364 match bytes.get(cursor) {
365 // `\u{…}`: the escape is delimited by braces.
366 Some(&b'u') if bytes.get(cursor + 1) == Some(&b'{') => {
367 cursor += 2;
368 while cursor < bytes.len() && bytes[cursor] != b'}' {
369 cursor += 1;
370 }
371 cursor = (cursor + 1).min(bytes.len());
372 }
373 // `\xNN`: exactly two hex digits.
374 Some(&b'x') => cursor = (cursor + 3).min(bytes.len()),
375 // `\n`, `\'`, `\\`, an escaped multi-byte character: one.
376 _ => skip_one_character(bytes, &mut cursor),
377 }
378 } else {
379 skip_one_character(bytes, &mut cursor);
380 }
381 if bytes.get(cursor) == Some(&b'\'') {
382 for byte in &mut masked[index..=cursor] {
383 if *byte != b'\n' {
384 *byte = b' ';
385 }
386 }
387 index = cursor + 1;
388 } else {
389 // A lifetime or a label: an apostrophe is not a token either
390 // scanner looks for, so masking it alone is enough.
391 // 生命周期或标签:撇号不是两个扫描器要找的 token,只遮掉它本身即可。
392 masked[index] = b' ';
393 index += 1;
394 }
395 continue;
396 }
397 index += 1;
398 }
399 String::from_utf8(masked).unwrap_or_else(|_| source.to_owned())
400}
401
402/// Collect the deduplicated `kind:` values used in registration declarations.
403/// 收集注册声明中出现过的 `kind:` 取值,去重并排序。
404pub fn registration_kinds(source: &str) -> Vec<String> {
405 let mut kinds = Vec::new();
406 for line in source.lines() {
407 // A `kind:` in a comment or a string is prose, not a declaration: the raw line
408 // scan reported a kind named `Ghost` for `// kind: Ghost`.
409 // 注释或字符串里的 `kind:` 是散文而不是声明:按原始行扫描会为 `// kind: Ghost`
410 // 报出一个名叫 `Ghost` 的 kind。
411 let masked = mask_non_code(line);
412 let trimmed = masked.trim();
413 if let Some(kind) = trimmed.split_once("kind:").map(|(_, remainder)| remainder) {
414 let kind = kind
415 .trim()
416 .split(|character: char| {
417 character == ',' || character == '}' || character.is_whitespace()
418 })
419 .next()
420 .unwrap_or_default();
421 if !kind.is_empty()
422 && kind
423 .chars()
424 .all(|character| character.is_ascii_alphanumeric() || character == '_')
425 {
426 kinds.push(kind.to_owned());
427 }
428 }
429 }
430 kinds.sort();
431 kinds.dedup();
432 kinds
433}
434#[cfg(test)]
435#[path = "source_tests.rs"]
436mod tests;