use proc_macro2::{TokenStream, TokenTree};
use quote::{quote, quote_spanned};
use syn::{
Attribute, Fields, FieldsNamed, Ident, Item, ItemEnum, ItemStruct, PathArguments, PathSegment,
Type, Variant, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma,
};
use crate::{
generated::{
derived_traits::get_trait_crate_and_generics,
enums::{ENUMS, INHERITED_ENUMS},
structs::STRUCTS,
},
variant::make_inherited_variant,
};
pub fn ast(item: &mut Item, args: TokenStream) -> TokenStream {
match item {
Item::Enum(item) => modify_enum(item),
Item::Struct(item) => modify_struct(item, args),
_ => unreachable!(),
}
}
pub struct EnumDetails {
pub is_fieldless: bool,
pub inherits: &'static [u32],
}
pub struct InheritedEnum {
pub doc: &'static str,
pub variants: &'static [EnumVariant],
}
pub struct EnumVariant {
pub name: &'static str,
pub inner_name: &'static str,
pub inner_has_lifetime: bool,
pub is_boxed: bool,
pub discriminant: u8,
}
fn modify_enum(item: &mut ItemEnum) -> TokenStream {
modify_enum_impl(item).unwrap_or_else(|message| {
let error = compile_error(&item.ident, message);
quote! {
#[derive(::oxc_ast_macros::Ast)]
#item
#error
}
})
}
fn modify_enum_impl(item: &mut ItemEnum) -> Result<TokenStream, &'static str> {
let enum_name = item.ident.to_string();
let Some(enum_details) = ENUMS.get(&enum_name) else {
return Err("Enum is unknown. Run `just ast` to re-run the codegen.");
};
let mut inherit_links = TokenStream::new();
if !enum_details.inherits.is_empty() {
let mut markers = Vec::with_capacity(enum_details.inherits.len());
let variants = std::mem::take(&mut item.variants);
item.variants = variants
.into_iter()
.filter(|variant| {
if is_inherit_marker(variant) {
let segment = inherit_marker_inner_segment(variant);
let has_lifetime = !matches!(segment.arguments, PathArguments::None);
markers.push((variant.span(), segment.ident.clone(), has_lifetime));
false
} else {
true
}
})
.collect();
for (&index, (marker_span, inner_ident, has_lifetime)) in
enum_details.inherits.iter().zip(markers)
{
let inherited = &INHERITED_ENUMS[index as usize];
item.variants.extend(
inherited
.variants
.iter()
.map(|variant| make_inherited_variant(variant, inherited.doc, marker_span)),
);
let generics = if has_lifetime { quote!(<'static>) } else { quote!() };
inherit_links.extend(quote! {
const _: Option<#inner_ident #generics> = None;
});
}
}
let repr = if enum_details.is_fieldless { quote!(#[repr(u8)]) } else { quote!(#[repr(C, u8)]) };
let assertions = assert_generated_derives(&item.attrs);
Ok(quote! {
#repr
#[derive(::oxc_ast_macros::Ast)]
#item
#assertions
#inherit_links
})
}
fn is_inherit_marker(variant: &Variant) -> bool {
variant.ident == "INHERIT"
}
fn inherit_marker_inner_segment(variant: &Variant) -> &PathSegment {
let Fields::Unnamed(fields) = &variant.fields else {
unreachable!("`INHERIT` marker must be a tuple variant: `{}`", variant.ident);
};
let Some(Type::Path(type_path)) = fields.unnamed.first().map(|field| &field.ty) else {
unreachable!("`INHERIT` marker must contain a single path type: `{}`", variant.ident);
};
type_path.path.segments.last().expect("`INHERIT` marker type has empty path")
}
pub struct StructDetails {
pub field_order: Option<&'static [u8]>,
pub is_node: bool,
pub is_transparent: bool,
}
fn modify_struct(item: &mut ItemStruct, args: TokenStream) -> TokenStream {
modify_struct_impl(item, args).unwrap_or_else(|message| {
let error = compile_error(&item.ident, message);
quote! {
#[derive(::oxc_ast_macros::Ast)]
#item
#error
}
})
}
fn modify_struct_impl(
item: &mut ItemStruct,
args: TokenStream,
) -> Result<TokenStream, &'static str> {
if let Some(TokenTree::Ident(ident)) = args.into_iter().next()
&& ident == "foreign"
{
return Ok(quote! {
#[derive(::oxc_ast_macros::Ast)]
#item
});
}
let struct_name = item.ident.to_string();
let Some(struct_details) = STRUCTS.get(&struct_name) else {
return Err("Struct is unknown. Run `just ast` to re-run the codegen.");
};
let assertions = assert_generated_derives(&item.attrs);
reorder_struct_fields(item, struct_details)?;
let repr = if struct_details.is_transparent {
quote!(#[repr(transparent)])
} else {
quote!(#[repr(C)])
};
let non_exhaustive =
if struct_details.is_node { Some(quote!(#[non_exhaustive])) } else { None };
Ok(quote! {
#repr
#non_exhaustive
#[derive(::oxc_ast_macros::Ast)]
#item
#assertions
})
}
fn reorder_struct_fields(
item: &mut ItemStruct,
struct_details: &StructDetails,
) -> Result<(), &'static str> {
let Some(field_order) = struct_details.field_order else {
return Ok(());
};
let named = match &mut item.fields {
Fields::Named(FieldsNamed { named, .. }) if named.len() == field_order.len() => named,
_ => {
return Err("Struct has been altered. Run `just ast` to re-run the codegen.");
}
};
let mut fields = named.clone().into_pairs().zip(field_order).collect::<Vec<_>>();
fields.sort_unstable_by_key(|(_, index)| **index);
for field in named.iter_mut() {
field.attrs.insert(0, parse_quote!( #[cfg(doc)]));
}
named.extend(fields.into_iter().map(|(mut pair, _)| {
pair.value_mut().attrs.insert(0, parse_quote!( #[cfg(not(doc))]));
pair
}));
Ok(())
}
fn assert_generated_derives(attrs: &[Attribute]) -> TokenStream {
let mut assertions = quote!();
for attr in attrs {
if !attr.path().is_ident("generate_derive") {
continue;
}
let Ok(parsed) = attr.parse_args_with(Punctuated::<Ident, Comma>::parse_terminated) else {
continue;
};
for trait_ident in parsed {
let trait_name = trait_ident.to_string();
let Some((trait_path, generics)) = get_trait_crate_and_generics(&trait_name) else {
continue;
};
assertions.extend(quote! {{
trait AssertionTrait: #trait_path #generics {}
impl<T: #trait_ident #generics> AssertionTrait for T {}
}});
}
}
quote! {
const _: () = { #assertions };
}
}
fn compile_error<S: Spanned>(spanned: &S, message: &str) -> TokenStream {
quote_spanned! { spanned.span() => compile_error!(#message); }
}