use serde::{Deserialize, Serialize};
#[doc(hidden)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Record {
pub kind: RecordKind,
pub name: String,
pub content: String,
pub source_location: SourceLocation,
#[serde(skip_serializing_if = "Option::is_none")]
pub cfg: Option<String>,
}
#[doc(hidden)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct SourceLocation {
pub file: String,
pub line: usize,
pub column: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub crate_name: Option<String>,
}
impl std::fmt::Display for SourceLocation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
impl SourceLocation {
pub fn has_position(&self) -> bool {
!self.file.is_empty()
}
pub fn from_span(span: &proc_macro2::Span) -> Self {
if_rust_version::if_rust_version! { >= 1.88 {
#[allow(clippy::incompatible_msrv)]
{
Self {
file: span.unwrap().file(),
line: span.unwrap().line(),
column: span.unwrap().column(),
crate_name: None,
}
}
} else {
let _ = span; Self {
file: "<unknown>".to_string(),
line: 0,
column: 0,
crate_name: None,
}
}}
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum RecordKind {
Struct,
Enum,
Union,
Function,
TypeAlias,
Const,
}
impl RecordKind {
pub fn is_type(&self) -> bool {
matches!(
self,
RecordKind::Struct | RecordKind::Enum | RecordKind::Union | RecordKind::TypeAlias
)
}
}
impl From<&(syn::Item, SourceLocation)> for RecordKind {
fn from((item, source_location): &(syn::Item, SourceLocation)) -> Self {
match item {
syn::Item::Struct(_) => RecordKind::Struct,
syn::Item::Enum(_) => RecordKind::Enum,
syn::Item::Union(_) => RecordKind::Union,
syn::Item::Fn(_) => RecordKind::Function,
syn::Item::Type(_) => RecordKind::TypeAlias,
syn::Item::Const(_) => RecordKind::Const,
_ => panic!("Unknown syn::Item variant for RecordKind at {source_location}"),
}
}
}
impl std::fmt::Display for RecordKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RecordKind::Struct => write!(f, "struct"),
RecordKind::Enum => write!(f, "enum"),
RecordKind::Union => write!(f, "union"),
RecordKind::Function => write!(f, "function"),
RecordKind::TypeAlias => write!(f, "type"),
RecordKind::Const => write!(f, "const"),
}
}
}
impl Record {
#[doc(hidden)]
pub fn new(
kind: RecordKind,
name: String,
content: String,
source_location: SourceLocation,
cfg: Option<String>,
) -> Self {
Self {
kind,
name,
content,
source_location,
cfg,
}
}
#[doc(hidden)]
pub fn to_jsonl_string(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
pub(crate) fn parse(self: &Record) -> (syn::Item, SourceLocation) {
let parsed = syn::parse_file(&self.content)
.map_err(|e| {
panic!(
"Failed to parse record content at {}: {}",
self.source_location, e
)
})
.unwrap();
let mut items = parsed.items.into_iter();
let mut item = items.next().unwrap_or_else(|| {
panic!(
"Expected exactly one item in record, found 0 at {}",
self.source_location
)
});
if items.next().is_some() {
panic!(
"Expected exactly one item in record, found more than 1 at {}",
self.source_location
);
}
if let Some(cfg_value) = &self.cfg {
let cfg_tokens: proc_macro2::TokenStream = cfg_value
.parse()
.unwrap_or_else(|_| panic!("Invalid cfg condition: {}", cfg_value));
let cfg_attr: syn::Attribute = syn::parse_quote! { #[cfg(#cfg_tokens)] };
match &mut item {
syn::Item::Struct(s) => s.attrs.insert(0, cfg_attr),
syn::Item::Enum(e) => e.attrs.insert(0, cfg_attr),
syn::Item::Union(u) => u.attrs.insert(0, cfg_attr),
syn::Item::Fn(f) => f.attrs.insert(0, cfg_attr),
syn::Item::Type(t) => t.attrs.insert(0, cfg_attr),
syn::Item::Const(c) => c.attrs.insert(0, cfg_attr),
_ => {}
}
}
let record_syn = (item, self.source_location.clone());
let actual_kind: RecordKind = (&record_syn).into();
if actual_kind != self.kind {
panic!(
"Record kind mismatch at {}: expected {}, found {}",
self.source_location, self.kind, actual_kind
);
}
record_syn
}
}