Skip to main content

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;