use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use proc_macro2::{TokenStream, TokenTree};
use syn::spanned::Spanned;
use syn::visit::Visit;
#[path = "tokens.rs"]
mod tokens;
use tokens::end_location;
pub use tokens::{
compact_tokens, location, path_to_string, split_face_fields, split_top_level, syntax_error,
};
#[path = "fields.rs"]
mod fields;
use fields::parse_fields;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SyntaxLocation {
pub line: usize,
pub column: usize,
}
#[derive(Clone, Debug)]
struct FieldSyntax {
tokens: TokenStream,
location: SyntaxLocation,
}
#[derive(Clone, Debug)]
pub struct FaceSyntax {
pub macro_name: String,
pub cfg: Option<String>,
pub location: SyntaxLocation,
pub end: SyntaxLocation,
fields: BTreeMap<String, FieldSyntax>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ParentSyntax {
Root,
FromPath {
source: String,
kind: String,
},
NodePath(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FaceSyntaxError {
pub message: String,
pub location: Option<SyntaxLocation>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SourceReferences {
pub paths: BTreeSet<String>,
pub conservative: bool,
}
impl fmt::Display for FaceSyntaxError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(location) = &self.location {
write!(
formatter,
"{}:{}: {}",
location.line, location.column, self.message
)
} else {
formatter.write_str(&self.message)
}
}
}
impl std::error::Error for FaceSyntaxError {}
pub fn parse_faces(source: &str) -> Result<Vec<FaceSyntax>, FaceSyntaxError> {
let file = super::nesting::parse_file(source)?;
let mut visitor = FaceVisitor {
faces: Vec::new(),
error: None,
};
visitor.visit_file(&file);
if let Some(error) = visitor.error {
Err(error)
} else {
Ok(visitor.faces)
}
}
pub fn is_face_source(source: &str, marker: &str) -> bool {
source.lines().any(|line| line == marker) || matches!(parse_face(source), Ok(Some(_)))
}
pub fn parse_face(source: &str) -> Result<Option<FaceSyntax>, FaceSyntaxError> {
let mut faces = parse_faces(source)?;
if faces.len() > 1 {
return Err(FaceSyntaxError {
message: "expected one registration face in this file".to_owned(),
location: faces.get(1).map(|face| face.location.clone()),
});
}
Ok(faces.pop())
}
pub fn replace_face_macro(source: &str, replacement: &str) -> Result<String, FaceSyntaxError> {
let current = parse_face(source)?.ok_or_else(|| FaceSyntaxError {
message: "source has no registration face".to_owned(),
location: None,
})?;
let next = parse_face(replacement)?.ok_or_else(|| FaceSyntaxError {
message: "replacement has no registration face".to_owned(),
location: None,
})?;
let current_start = source_offset(source, ¤t.location)?;
let current_end = source_offset(source, ¤t.end)?;
let replacement_start = source_offset(replacement, &next.location)?;
let replacement_end = source_offset(replacement, &next.end)?;
let mut output =
String::with_capacity(source.len() + replacement_end.saturating_sub(replacement_start));
output.push_str(&source[..current_start]);
output.push_str(&replacement[replacement_start..replacement_end]);
output.push_str(&source[current_end..]);
Ok(output)
}
fn source_offset(source: &str, location: &SyntaxLocation) -> Result<usize, FaceSyntaxError> {
let line_start = if location.line <= 1 {
0
} else {
source
.match_indices('\n')
.nth(location.line - 2)
.map(|(index, _)| index + 1)
.ok_or_else(|| FaceSyntaxError {
message: "macro span points outside source".to_owned(),
location: Some(location.clone()),
})?
};
let offset = line_start + location.column.saturating_sub(1);
source
.is_char_boundary(offset)
.then_some(offset)
.ok_or_else(|| FaceSyntaxError {
message: "macro span is not on a UTF-8 boundary".to_owned(),
location: Some(location.clone()),
})
}
pub fn source_references(source: &str) -> Result<SourceReferences, FaceSyntaxError> {
let file = super::nesting::parse_file(source)?;
Ok(super::reference_scan::scan(&file))
}
struct FaceVisitor {
faces: Vec<FaceSyntax>,
error: Option<FaceSyntaxError>,
}
impl<'ast> Visit<'ast> for FaceVisitor {
fn visit_item_macro(&mut self, item: &'ast syn::ItemMacro) {
if self.error.is_some() {
return;
}
let Some(segment) = item.mac.path.segments.last() else {
return;
};
let macro_name = segment.ident.to_string();
let is_face_macro = matches!(macro_name.as_str(), "control_object" | "external_object")
|| macro_name.ends_with("_object");
if !is_face_macro {
return;
}
match parse_fields(item.mac.tokens.clone(), item.mac.span()) {
Ok(fields) => {
let span = item.span();
let cfg = item.attrs.iter().find_map(|attribute| {
attribute
.path()
.is_ident("cfg")
.then(|| {
attribute
.parse_args::<TokenStream>()
.ok()
.map(|tokens| compact(&tokens))
})
.flatten()
});
self.faces.push(FaceSyntax {
macro_name,
cfg,
location: location(span),
end: end_location(span),
fields,
})
}
Err(error) => self.error = Some(error),
}
}
}
pub(super) fn compact(tokens: &TokenStream) -> String {
compact_tokens(tokens.clone())
}
pub(super) fn split_typed_range(tokens: Vec<TokenTree>) -> Option<(String, String)> {
let position = tokens
.iter()
.position(|token| matches!(token, TokenTree::Ident(value) if value == "to"))?;
let start = tokens[..position].iter().cloned().collect::<TokenStream>();
let finish = tokens[position + 1..]
.iter()
.cloned()
.collect::<TokenStream>();
if start.is_empty() || finish.is_empty() {
return None;
}
Some((compact_tokens(start), compact_tokens(finish)))
}
#[cfg(test)]
#[path = "face_tests.rs"]
mod tests;