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;