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