quickjs-jit-macro 0.12.2

Procedural macros for the quickjs-jit distribution
Documentation
use convert_case::Case as ConvertCase;
use proc_macro2::Span;
use proc_macro_crate::FoundCrate;
use quote::{ToTokens, TokenStreamExt};
use syn::{
    fold::Fold,
    parse::{Parse, ParseStream},
    Error, Generics, Lifetime, LifetimeParam, LitStr, Result, Type,
};

/// prefix for getter implementations
pub const GET_PREFIX: &str = "__impl_get_";
/// prefix for setter implementations
pub const SET_PREFIX: &str = "__impl_set_";
/// the base prefix for type which should be accessed by macro users.
pub const BASE_PREFIX: &str = "js_";
/// the base prefix for type which should remain macro internal.
pub const IMPL_PREFIX: &str = "__impl_";

/// Casing for mass case convert.
///
/// Only allowing casings which are valid js identifiers.
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
pub enum Case {
    Lower,
    Upper,
    Camel,
    Pascal,
    Snake,
    ScreamingSnake,
}

impl Parse for Case {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let str_lit: LitStr = input.parse()?;
        let value = str_lit.value();
        match value.as_str() {
            "lowercase" => Ok(Case::Lower),
            "UPPERCASE" => Ok(Case::Upper),
            "camelCase" => Ok(Case::Camel),
            "PascalCase" => Ok(Case::Pascal),
            "snake_case" => Ok(Case::Snake),
            "SCREAMING_SNAKE" => Ok(Case::ScreamingSnake),
            _ => Err(syn::Error::new(str_lit.span(), "Invalid casing, expected one of 'lowercase', 'UPPERCASE', 'camelCase','PascalCase','snake_case','SCREAMING_SNAKE'"))
        }
    }
}

impl ToTokens for Case {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        match self {
            Case::Lower => tokens.append_all(["lowercase"]),
            Case::Upper => tokens.append_all(["UPPERCASE"]),
            Case::Camel => tokens.append_all(["camelCase"]),
            Case::Pascal => tokens.append_all(["PascalCase"]),
            Case::Snake => tokens.append_all(["snake_case"]),
            Case::ScreamingSnake => tokens.append_all(["SCREAMING_SNAKE"]),
        }
    }
}

impl Case {
    pub fn to_convert_case(self) -> ConvertCase<'static> {
        match self {
            Case::Lower => ConvertCase::Lower,
            Case::Upper => ConvertCase::Upper,
            Case::Camel => ConvertCase::Camel,
            Case::Pascal => ConvertCase::Pascal,
            Case::Snake => ConvertCase::Snake,
            Case::ScreamingSnake => ConvertCase::Constant,
        }
    }
}

const JIT_PACKAGE: &str = "quickjs-jit";
const UPSTREAM_PACKAGE: &str = "rquickjs";
const JIT_DEFAULT_CRATE_IDENT: &str = "quickjs_jit";
const COMPATIBLE_LIB_IDENT: &str = "rquickjs";

fn top_level_package_candidates() -> [&'static str; 2] {
    [JIT_PACKAGE, UPSTREAM_PACKAGE]
}

fn normalize_crate_name(name: &str) -> &str {
    // proc-macro-crate derives this identifier from the package name when the
    // dependency is not explicitly aliased, but our compatible lib target is
    // intentionally still named `rquickjs`.
    match name {
        JIT_DEFAULT_CRATE_IDENT => COMPATIBLE_LIB_IDENT,
        name => name,
    }
}

pub(crate) fn crate_ident() -> Result<String> {
    let mut last_error = None;
    for package in top_level_package_candidates() {
        match proc_macro_crate::crate_name(package) {
            Ok(FoundCrate::Itself) => return Ok(COMPATIBLE_LIB_IDENT.to_owned()),
            Ok(FoundCrate::Name(name)) => return Ok(normalize_crate_name(&name).to_owned()),
            Err(error) => last_error = Some(error),
        }
    }
    Err(Error::new(
        Span::call_site(),
        format_args!(
            "could not find quickjs-jit or rquickjs package: {}",
            last_error.expect("package candidates are non-empty")
        ),
    ))
}

/// Add the 'js lifetime to a list of existing lifetimes, if it doesn't already exits.
pub fn add_js_lifetime(generics: &Generics) -> Generics {
    let mut generics = generics.clone();
    let has_js_lifetime = generics.lifetimes().any(|lt| lt.lifetime.ident == "js");
    if !has_js_lifetime {
        generics.params.insert(
            0,
            syn::GenericParam::Lifetime(LifetimeParam::new(Lifetime::new(
                "'js",
                Span::call_site(),
            ))),
        );
    }
    generics
}

pub struct SelfReplacer<'a> {
    pub ty: &'a Type,
}

impl<'a> SelfReplacer<'a> {
    pub fn with(ty: &'a Type) -> Self {
        Self { ty }
    }
}

impl<'a> Fold for SelfReplacer<'a> {
    fn fold_type(&mut self, i: Type) -> Type {
        let Type::Path(x) = i else { return i };
        if x.path.segments.len() != 1 {
            return Type::Path(self.fold_type_path(x));
        }
        if x.path.segments.first().unwrap().ident == "Self" {
            self.ty.clone()
        } else {
            Type::Path(self.fold_type_path(x))
        }
    }
}

pub(crate) mod kw {
    syn::custom_keyword!(frozen);
    syn::custom_keyword!(skip_trace);
    syn::custom_keyword!(rename);
    syn::custom_keyword!(rename_all);
    syn::custom_keyword!(rename_vars);
    syn::custom_keyword!(rename_types);
    syn::custom_keyword!(get);
    syn::custom_keyword!(set);
    syn::custom_keyword!(constructor);
    syn::custom_keyword!(skip);
    syn::custom_keyword!(configurable);
    syn::custom_keyword!(enumerable);
    syn::custom_keyword!(writable);
    syn::custom_keyword!(prop);
    syn::custom_keyword!(prefix);
    syn::custom_keyword!(declare);
    syn::custom_keyword!(evaluate);
    syn::custom_keyword!(exotic);
    syn::custom_keyword!(delete);
    syn::custom_keyword!(has);
    syn::custom_keyword!(get_own_property);
    syn::custom_keyword!(get_own_property_names);
}

#[cfg(test)]
mod tests {
    #[test]
    fn top_level_package_candidates_prefer_jit_distribution() {
        assert_eq!(
            super::top_level_package_candidates(),
            ["quickjs-jit", "rquickjs"]
        );
    }

    #[test]
    fn default_jit_package_name_maps_to_compatible_library_name() {
        assert_eq!(super::normalize_crate_name("quickjs_jit"), "rquickjs");
        assert_eq!(super::normalize_crate_name("js"), "js");
    }
}