laburnum-syntax-macro 0.1.1

Proc-macros for defining CST and AST node types in language frontends built with the laburnum LSP framework.
Documentation
// Copyright Two Neutron Stars Incorporated and contributors
// SPDX-License-Identifier: BlueOak-1.0.0

use {
  super::parse::Ast,
  crate::{args::Args, cst::parse::FieldType},
  proc_macro2::{self, TokenStream},
  quote::quote,
};

pub(crate) fn codegen(
  args: Args,
  ast: Ast,
) -> Result<TokenStream, crate::error::Error> {
  let Ast { name, fields } = ast;

  // Ensure there is a field called "span" with a type of Span.
  if !fields
    .iter()
    .any(|f| f.name == "span" && f.ty == FieldType::Span && !f.is_option)
  {
    return Err(crate::error::Error::MissingRequiredSpanField(
      proc_macro2::Span::call_site(),
    ));
  }

  let field_data = &fields
    .iter()
    .map(|f| -> Result<TokenStream, crate::error::Error> {
      let (name, ty) = match f.ty {
        | FieldType::Unknown(ref ty) => (&f.name, ty.as_str()),
        | FieldType::Literal(ty_name) => (&f.name, ty_name),
        | FieldType::NodeId => (&f.name, "crate::CstNodeId"),
        | FieldType::Span => (&f.name, "laburnum::Span"),
        | FieldType::NodeList => (&f.name, "crate::node::NodeList"),
        | FieldType::ControlSpan => {
          if !f.name.ends_with("_token") {
            return Err(crate::error::Error::InvalidFieldNamingConvention(
              proc_macro2::Span::call_site(),
              f.name.clone(),
              "_token".to_string(),
            ));
          }

          (&f.name, "crate::lexer::control::ControlSpan")
        },
        | FieldType::KeywordSpan => {
          if !f.name.ends_with("_keyword") {
            return Err(crate::error::Error::InvalidFieldNamingConvention(
              proc_macro2::Span::call_site(),
              f.name.clone(),
              "_keyword".to_string(),
            ));
          }

          (&f.name, "crate::lexer::keyword::KeywordSpan")
        },
        | FieldType::ScalarSpan => {
          if !f.name.ends_with("_scalar") {
            return Err(crate::error::Error::InvalidFieldNamingConvention(
              proc_macro2::Span::call_site(),
              f.name.clone(),
              "_scalar".to_string(),
            ));
          }

          (&f.name, "crate::lexer::scalar::ScalarSpan")
        },
        | FieldType::LiteralSpan => {
          if !f.name.ends_with("_literal") {
            return Err(crate::error::Error::InvalidFieldNamingConvention(
              proc_macro2::Span::call_site(),
              f.name.clone(),
              "_literal".to_string(),
            ));
          }

          (&f.name, "crate::primitive::literal::LiteralSpan")
        },
        | FieldType::TriviaSpan => {
          if !f.name.ends_with("_trivia") {
            return Err(crate::error::Error::InvalidFieldNamingConvention(
              proc_macro2::Span::call_site(),
              f.name.clone(),
              "_trivia".to_string(),
            ));
          }

          (&f.name, "crate::errata::TriviaSpan")
        },
        | FieldType::StringSpan => (&f.name, "laburnum::Span"),
        | FieldType::Ident => (&f.name, "laburnum::Ident"),
      };

      let ty = {
        if f.is_option {
          format!("Option<{ty}>")
        } else {
          ty.to_string()
        }
      };

      let field_name: syn::Ident = syn::parse_str(name).unwrap();
      let path: syn::Path = syn::parse_str(&ty).unwrap();

      Ok(quote::quote!(pub #field_name: #path))
    })
    .collect::<Result<Vec<TokenStream>, _>>()?;

  let struct_name =
    proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());

  let ts_struct = quote! {
    #[derive(Clone, PartialEq)]
    pub struct #struct_name {
      #(#field_data),*
    }

    impl #struct_name {
      pub fn span(&self) -> laburnum::Span {
        self.span
      }
    }
  };

  // TODO: create a method for each field that returns a reference to the field (gold-fik)

  let mod_name = quote! {
    {
      let __mp = module_path!();
      let __stripped = __mp.find("::").map(|i| &__mp[i+2..]).unwrap_or(__mp);
      // Remove trailing module segment when it matches the struct name
      // (case-insensitive, ignoring underscores) to avoid stutter like
      // `expression::inline::Inline` or `expression::field_access::FieldAccess`
      if let Some(__last_sep) = __stripped.rfind("::") {
        let __last_seg = &__stripped[__last_sep + 2..];
        let __name = stringify!(#struct_name);
        let __seg_norm: std::string::String = __last_seg.chars().filter(|c| *c != '_').collect();
        let __name_norm: std::string::String = __name.chars().filter(|c| *c != '_').collect();
        if __seg_norm.eq_ignore_ascii_case(&__name_norm) {
          &__stripped[..__last_sep]
        } else {
          __stripped
        }
      } else {
        __stripped
      }
    }
  };

  // -- Display, Debug impl ----------------------------------------------------

  let ts_impl_display = quote! {
      #[allow(unused)]
      impl std::fmt::Display for #struct_name {
        fn fmt(&self,f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
          f.debug_struct(&format!("{}::{}", #mod_name,  stringify!(#struct_name)))
            .field("span", &self.span)
            .finish()
        }
      }
  };

  let ts_impl_debug = quote! {
    #[allow(unused)]
    impl std::fmt::Debug for #struct_name {
      fn fmt(&self,f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct(&format!("{}::{}", #mod_name,  stringify!(#struct_name)))
          .field("span", &self.span)
          .finish()
      }
    }
  };

  // -- Bluegum impl -----------------------------------------------------------

  let bgname = {
    if args.is_error_node() {
      quote! {
        &format!("{}::{}", #mod_name, stringify!(#struct_name).red().bold().to_string())
      }
    } else {
      quote! {
        &format!("{}::{}", #mod_name, stringify!(#struct_name))
      }
    }
  };

  let ts_impl_bluegum = quote! {


    #[allow(unused)]
    impl bluegum::Bluegum for #struct_name {
      fn node(&self,b: &mut bluegum::Builder){
        use owo_colors::OwoColorize;

        b.name(#bgname)
         .debug("span",self.span);
      }
    }
  };

  let ts_impl_bluegum_with_state = {
    let mut fields = fields
      .iter()
      // filter out the span field
      .filter(|f| f.name != "span")
      .map(|f| {
        let field_name: syn::Ident = syn::parse_str(&f.name).unwrap();
        let field = match f.ty {
          | FieldType::NodeId => {
            quote! {
              b.add_node_with_state(state, stringify!(#field_name), #field_name);
            }
          },
          | FieldType::NodeList => {
            quote! {
              b.add_nodes_with_state::<
                crate::Printer<'_>,
              >(state, stringify!(#field_name), #field_name.as_slice());
            }
          },
          | FieldType::ControlSpan => {
            quote! {
              let mut t = bluegum::Builder::new();
              t.name("ControlSpan").debug("span", #field_name.span());

              let s = format!("{}", #field_name.value());
              if s.width() > 80 {
                t.alt(format!("{:.80}...", s).yellow());
              } else {
                t.alt(format!("{:.80}", s).yellow());
              };

              b.add_named_builder(stringify!(#field_name), t);
            }
          },
          | FieldType::ScalarSpan => {
            quote! {
              let mut t = bluegum::Builder::new();
              t.name("ScalarSpan").debug("span", #field_name.span());

              let s = format!("{}", #field_name.value());
              if s.width() > 80 {
                t.alt(format!("{:.80}...", s).bright_green());
              } else {
                t.alt(format!("{:.80}", s).bright_green());
              };

              b.add_named_builder(stringify!(#field_name), t);
            }
          },
          | FieldType::LiteralSpan => {
            quote! {
              b.child(stringify!(#field_name), #field_name) ;
            }
          },
          | FieldType::KeywordSpan => {
            quote! {
              let mut t = bluegum::Builder::new();
              t.name("KeywordSpan").debug("span", #field_name.span());

              let s = format!("{}", #field_name.value());
              if s.width() > 80 {
                t.alt(format!("{:.80}...", s).blue().italic());
              } else {
                t.alt(format!("{:.80}", s).blue().italic());
              };

              b.add_named_builder(stringify!(#field_name), t);

            }
          },
          | FieldType::StringSpan => {
            quote! {
              use {owo_colors::OwoColorize, unicode_width::UnicodeWidthStr};
              let mut t = bluegum::Builder::new();
              t.name("StringSpan");
              let val = state.span_text(#field_name);
              if val.width() > 80 {
                t.alt(format!("{:.80}...", val).bright_white());
              } else {
                t.alt(format!("{:.80}", val).bright_white());
              };
              b.add_named_builder(stringify!(#field_name), t);
            }
          },
          | FieldType::Ident => {
            quote! {
              use {owo_colors::OwoColorize, unicode_width::UnicodeWidthStr};
              let mut t = bluegum::Builder::new();
              t.name("Ident");
              let val = state.span_text(&self.span);
              if val.width() > 80 {
                t.alt(format!("{:.80}...", val).bright_white());
              } else {
                t.alt(format!("{:.80}", val).bright_white());
              };
              b.add_named_builder(stringify!(#field_name), t);
            }
          },
          | FieldType::Literal(_) => {
            quote! {
              let mut t = bluegum::Builder::new();
              t.name("Literal");

              let s = format!("{}", #field_name);
              if s.width() > 80 {
                t.alt(format!("{:.80}...", s).bright_green());
              } else {
                t.alt(format!("{:.80}", s).bright_green());
              };

              b.add_named_builder(stringify!(#field_name), t);

            }
          },
          | FieldType::Unknown(_) => {
            quote! {
              let mut t = bluegum::Builder::new();
              t.name("Literal");

              let s = format!("{}", #field_name);
              if s.width() > 80 {
                t.alt(format!("{:.80}...", s).bright_green());
              } else {
                t.alt(format!("{:.80}", s).bright_green());
              };

              b.add_named_builder(stringify!(#field_name), t);
            }
          },
          | _ => {
            quote! {}
          },
        };

        if f.is_option {
          quote! {
            if let Some(ref #field_name) = self.#field_name {
              #field
            }
          }
        } else {
          quote! {
          let #field_name = &self.#field_name;
          #field
          }
        }
      })
      .collect::<Vec<TokenStream>>();

    if args.is_error_node() {
      fields.push(quote! {
        b.alt(format!("ERROR").red());
      })
    }

    quote! {
    #[allow(unused)]
    impl bluegum::BluegumWithState<crate::Printer<'_>>for #struct_name {
      fn node_with_state(
        &self,
        b: &mut bluegum::Builder,
        state: &crate::Printer<'_>,
      ){
        use owo_colors::OwoColorize;
        use unicode_width::UnicodeWidthStr;

        b.name(#bgname)
         .debug("span", self.span);

        { #(#fields)* };

        }
      }
    }
  };

  let ts = quote! {
    #ts_struct

    #ts_impl_display
    #ts_impl_debug
    #ts_impl_bluegum
    #ts_impl_bluegum_with_state
  };

  Ok(ts)
}