nichlink/registry_core/syntax/
face.rs1use 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#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct SyntaxLocation {
35 pub line: usize,
38 pub column: usize,
41}
42
43#[derive(Clone, Debug)]
44struct FieldSyntax {
45 tokens: TokenStream,
46 location: SyntaxLocation,
47}
48
49#[derive(Clone, Debug)]
52pub struct FaceSyntax {
53 pub macro_name: String,
58 pub cfg: Option<String>,
61 pub location: SyntaxLocation,
64 pub end: SyntaxLocation,
67 fields: BTreeMap<String, FieldSyntax>,
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum ParentSyntax {
78 Root,
81 FromPath {
84 source: String,
87 kind: String,
90 },
91 NodePath(String),
94}
95
96#[derive(Clone, Debug, PartialEq, Eq)]
99pub struct FaceSyntaxError {
100 pub message: String,
103 pub location: Option<SyntaxLocation>,
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq)]
118pub struct SourceReferences {
119 pub paths: BTreeSet<String>,
122 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
145pub 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
161pub fn is_face_source(source: &str, marker: &str) -> bool {
170 source.lines().any(|line| line == marker) || matches!(parse_face(source), Ok(Some(_)))
171}
172
173pub 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
186pub 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, ¤t.location)?;
198 let current_end = source_offset(source, ¤t.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
232pub 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
287pub(super) fn compact(tokens: &TokenStream) -> String {
293 compact_tokens(tokens.clone())
294}
295
296pub(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;