Skip to main content

nichlink/registry_core/syntax/
face.rs

1//! Registration-face parser shared by build-time checks and live authoring.
2//! 构建期检查与实时编辑共用的注册面解析器。
3//!
4//! This page owns the face grammar: finding declarations in a Rust file,
5//! exposing their fields, and splicing a replacement macro back into the source.
6//! The lexical helpers live in `tokens` and the field-value decoders in
7//! `fields`; both are re-exported here, so `syntax::*` keeps its one namespace.
8//! 本页拥有注册面语法:在 Rust 文件中发现声明、暴露其字段,并把替换用宏拼回源码。
9//! 词法辅助函数位于 `tokens`,字段取值解码位于 `fields`;两者都在此再导出,
10//! `syntax::*` 因此仍只有一个命名空间。
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::fmt;
14
15use proc_macro2::{TokenStream, TokenTree};
16
17use syn::spanned::Spanned;
18use syn::visit::Visit;
19
20#[path = "tokens.rs"]
21mod tokens;
22use tokens::end_location;
23pub use tokens::{
24    compact_tokens, location, path_to_string, split_face_fields, split_top_level, syntax_error,
25};
26
27#[path = "fields.rs"]
28mod fields;
29use fields::parse_fields;
30
31/// A position inside a parsed source file, one-based in both axes.
32/// 已解析源文件里的位置,两个轴都从 1 开始。
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct SyntaxLocation {
35    /// One-based line number.
36    /// 从 1 开始的行号。
37    pub line: usize,
38    /// One-based column number, counted in characters.
39    /// 从 1 开始的列号,按字符计。
40    pub column: usize,
41}
42
43#[derive(Clone, Debug)]
44struct FieldSyntax {
45    tokens: TokenStream,
46    location: SyntaxLocation,
47}
48
49/// Parsed registration declaration, independent of the macro that consumes it.
50/// 已解析的注册声明,与消费它的宏实现无关。
51#[derive(Clone, Debug)]
52pub struct FaceSyntax {
53    /// The registration macro's name exactly as written (`control_object`,
54    /// `root_object`, a generated `*_object!` alias, …).
55    /// 注册宏的名字,按源码原文(`control_object`、`root_object`、生成的
56    /// `*_object!` 别名等)。
57    pub macro_name: String,
58    /// The `cfg` gates the declaration carries, if any, exactly as written.
59    /// 声明携带的 `cfg` 门控(若有),按原文保留。
60    pub cfg: Option<String>,
61    /// Where the macro invocation starts.
62    /// 宏调用的起点位置。
63    pub location: SyntaxLocation,
64    /// Exclusive end of the macro invocation in the source file.
65    /// 宏调用在源码中的排他结束位置。
66    pub end: SyntaxLocation,
67    fields: BTreeMap<String, FieldSyntax>,
68}
69
70/// How a declaration names its parent, kept as written rather than resolved.
71/// 声明如何命名其父级,按原文保留而不在此解析。
72///
73/// The three shapes exist because the three registrations that accept a parent
74/// spell it differently; resolving them is the build-time identity code's job.
75/// 之所以有三种形状,是因为接受父级的三类注册写法不同;解析它们是构建期身份代码的事。
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum ParentSyntax {
78    /// The package root, written as `crate::root_node_id(env!("CARGO_PKG_NAME"))`.
79    /// 包根,写成 `crate::root_node_id(env!("CARGO_PKG_NAME"))`。
80    Root,
81    /// The parent is named by a path and a kind, as `NodeId::from_path` takes them.
82    /// 父级由路径与 kind 命名,即 `NodeId::from_path` 接收的两个参数。
83    FromPath {
84        /// The `source` literal.
85        /// `source` 字面量。
86        source: String,
87        /// The `kind` literal.
88        /// `kind` 字面量。
89        kind: String,
90    },
91    /// The parent is named by an expression path (typically `crate::x::NODE_ID`).
92    /// 父级由一个表达式路径命名(通常是 `crate::x::NODE_ID`)。
93    NodePath(String),
94}
95
96/// Why a declaration could not be parsed, and where when that is known.
97/// 声明为何解析不了;位置已知时一并给出。
98#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct FaceSyntaxError {
100    /// Human-readable reason.
101    /// 人类可读的原因。
102    pub message: String,
103    /// Where parsing failed; `None` when the failure is about the file as a whole.
104    /// 解析失败的位置;失败针对整个文件时为 `None`。
105    pub location: Option<SyntaxLocation>,
106}
107
108/// Paths one file's executable expressions mention, and how much the scan could
109/// actually see.
110/// 一个文件的可执行表达式提到的路径,以及这次扫描实际能看到多少。
111///
112/// A caller that prunes faces from this list must respect `conservative`: the
113/// scan is lexical, so anything it cannot follow has to widen the result instead
114/// of silently shrinking it.
115/// 依据这份清单裁剪注册面的调用方必须尊重 `conservative`:扫描是词法层面的,凡它跟不下去
116/// 的东西都必须让结果变宽,而不是悄悄变窄。
117#[derive(Clone, Debug, Default, PartialEq, Eq)]
118pub struct SourceReferences {
119    /// Every path reached through an expression, with imports excluded.
120    /// 经表达式触达的每个路径,不含导入。
121    pub paths: BTreeSet<String>,
122    /// Set when the scan met something it cannot follow (macro expansion, a trait
123    /// object, `include!`), so the caller must not prune on this list alone.
124    /// 扫描遇到跟不下去的东西(宏展开、trait object、`include!`)时置位,调用方不得仅凭
125    /// 这份清单裁剪。
126    pub conservative: bool,
127}
128
129impl fmt::Display for FaceSyntaxError {
130    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
131        if let Some(location) = &self.location {
132            write!(
133                formatter,
134                "{}:{}: {}",
135                location.line, location.column, self.message
136            )
137        } else {
138            formatter.write_str(&self.message)
139        }
140    }
141}
142
143impl std::error::Error for FaceSyntaxError {}
144
145/// Parse every NichLink face declaration in one Rust source file.
146/// 解析一个 Rust 源文件中的全部 NichLink 注册面声明。
147pub fn parse_faces(source: &str) -> Result<Vec<FaceSyntax>, FaceSyntaxError> {
148    let file = super::nesting::parse_file(source)?;
149    let mut visitor = FaceVisitor {
150        faces: Vec::new(),
151        error: None,
152    };
153    visitor.visit_file(&file);
154    if let Some(error) = visitor.error {
155        Err(error)
156    } else {
157        Ok(visitor.faces)
158    }
159}
160
161/// Whether `source` declares a registration face.
162/// 源码是否声明了注册面。
163///
164/// A generated-marker line short-circuits the answer: a snapshot this tooling
165/// wrote is a face by construction, and parsing it again is wasted work. For
166/// every other file the parse decides.
167/// 生成标记行会短路答案:本工具写出的快照按构造就是注册面,再解析一遍是白费。其余
168/// 文件一律由解析裁决。
169pub fn is_face_source(source: &str, marker: &str) -> bool {
170    source.lines().any(|line| line == marker) || matches!(parse_face(source), Ok(Some(_)))
171}
172
173/// Parse exactly one face, returning `None` when the file has no declaration.
174/// 解析唯一注册面;文件没有声明时返回 `None`。
175pub fn parse_face(source: &str) -> Result<Option<FaceSyntax>, FaceSyntaxError> {
176    let mut faces = parse_faces(source)?;
177    if faces.len() > 1 {
178        return Err(FaceSyntaxError {
179            message: "expected one registration face in this file".to_owned(),
180            location: faces.get(1).map(|face| face.location.clone()),
181        });
182    }
183    Ok(faces.pop())
184}
185
186/// Replace only the registration macro, preserving the surrounding Rust code.
187/// 只替换注册宏,保留同一文件里的其余 Rust 实现。
188pub fn replace_face_macro(source: &str, replacement: &str) -> Result<String, FaceSyntaxError> {
189    let current = parse_face(source)?.ok_or_else(|| FaceSyntaxError {
190        message: "source has no registration face".to_owned(),
191        location: None,
192    })?;
193    let next = parse_face(replacement)?.ok_or_else(|| FaceSyntaxError {
194        message: "replacement has no registration face".to_owned(),
195        location: None,
196    })?;
197    let current_start = source_offset(source, &current.location)?;
198    let current_end = source_offset(source, &current.end)?;
199    let replacement_start = source_offset(replacement, &next.location)?;
200    let replacement_end = source_offset(replacement, &next.end)?;
201    let mut output =
202        String::with_capacity(source.len() + replacement_end.saturating_sub(replacement_start));
203    output.push_str(&source[..current_start]);
204    output.push_str(&replacement[replacement_start..replacement_end]);
205    output.push_str(&source[current_end..]);
206    Ok(output)
207}
208
209fn source_offset(source: &str, location: &SyntaxLocation) -> Result<usize, FaceSyntaxError> {
210    let line_start = if location.line <= 1 {
211        0
212    } else {
213        source
214            .match_indices('\n')
215            .nth(location.line - 2)
216            .map(|(index, _)| index + 1)
217            .ok_or_else(|| FaceSyntaxError {
218                message: "macro span points outside source".to_owned(),
219                location: Some(location.clone()),
220            })?
221    };
222    let offset = line_start + location.column.saturating_sub(1);
223    source
224        .is_char_boundary(offset)
225        .then_some(offset)
226        .ok_or_else(|| FaceSyntaxError {
227            message: "macro span is not on a UTF-8 boundary".to_owned(),
228            location: Some(location.clone()),
229        })
230}
231
232/// Collect paths used by executable expressions, excluding imports, comments,
233/// strings, and registration-macro metadata.
234/// 收集可执行表达式使用的路径,排除导入、注释、字符串和注册宏元数据。
235pub fn source_references(source: &str) -> Result<SourceReferences, FaceSyntaxError> {
236    let file = super::nesting::parse_file(source)?;
237    Ok(super::reference_scan::scan(&file))
238}
239
240struct FaceVisitor {
241    faces: Vec<FaceSyntax>,
242    error: Option<FaceSyntaxError>,
243}
244
245impl<'ast> Visit<'ast> for FaceVisitor {
246    fn visit_item_macro(&mut self, item: &'ast syn::ItemMacro) {
247        if self.error.is_some() {
248            return;
249        }
250        let Some(segment) = item.mac.path.segments.last() else {
251            return;
252        };
253        let macro_name = segment.ident.to_string();
254        let is_face_macro = matches!(macro_name.as_str(), "control_object" | "external_object")
255            || macro_name.ends_with("_object");
256        if !is_face_macro {
257            return;
258        }
259        match parse_fields(item.mac.tokens.clone(), item.mac.span()) {
260            Ok(fields) => {
261                let span = item.span();
262                let cfg = item.attrs.iter().find_map(|attribute| {
263                    attribute
264                        .path()
265                        .is_ident("cfg")
266                        .then(|| {
267                            attribute
268                                .parse_args::<TokenStream>()
269                                .ok()
270                                .map(|tokens| compact(&tokens))
271                        })
272                        .flatten()
273                });
274                self.faces.push(FaceSyntax {
275                    macro_name,
276                    cfg,
277                    location: location(span),
278                    end: end_location(span),
279                    fields,
280                })
281            }
282            Err(error) => self.error = Some(error),
283        }
284    }
285}
286
287/// Render a token stream as compact Rust source text.
288/// 把 token 流渲染成紧凑的 Rust 源码文本。
289///
290/// The by-reference twin of [`compact_tokens`], kept for the field reader.
291/// [`compact_tokens`] 的按引用版本,供字段读取器使用。
292pub(super) fn compact(tokens: &TokenStream) -> String {
293    compact_tokens(tokens.clone())
294}
295
296/// Split a typed cut's tokens on a top-level `to`, returning both sides as
297/// compact source text: `cut(a to b)` names a sibling range verbatim.
298/// 在类型化切口的 token 里按顶层 `to` 切分,返回两侧的紧凑源码文本:
299/// `cut(a to b)` 就是用原样 Rust 表达兄弟区间。
300pub(super) fn split_typed_range(tokens: Vec<TokenTree>) -> Option<(String, String)> {
301    let position = tokens
302        .iter()
303        .position(|token| matches!(token, TokenTree::Ident(value) if value == "to"))?;
304    let start = tokens[..position].iter().cloned().collect::<TokenStream>();
305    let finish = tokens[position + 1..]
306        .iter()
307        .cloned()
308        .collect::<TokenStream>();
309    if start.is_empty() || finish.is_empty() {
310        return None;
311    }
312    Some((compact_tokens(start), compact_tokens(finish)))
313}
314#[cfg(test)]
315#[path = "face_tests.rs"]
316mod tests;