laburnum-syntax-macro 0.1.0

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 {
  crate::error::{
    Error,
    ErrorAccumulator,
  },
  proc_macro2::TokenStream,
  quote::{
    ToTokens,
    spanned::Spanned as quote_spanned,
  },
  syn::{
    Item,
    Type,
    spanned::Spanned,
  },
};

// The AST is very simple for this macro, as you can _only_ attach it to
// a function.
#[derive(Debug, Clone)]
pub struct Ast {
  pub name:   String,
  pub fields: Vec<Field>,
}

#[derive(Debug, Clone)]
pub struct Field {
  pub(crate) name:      String,
  pub(crate) ty:        FieldType,
  pub(crate) is_option: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FieldType {
  Unknown(String),
  Literal(&'static str),
  NodeId,
  Span,
  NodeList,
  ControlSpan,
  KeywordSpan,
  ScalarSpan,
  LiteralSpan,
  TriviaSpan,
  StringSpan,
  Ident,
}

fn ident_to_type(s: &str) -> FieldType {
  match s {
    | "CstNodeId" => FieldType::NodeId,
    | "Span" => FieldType::Span,
    | "NodeList" => FieldType::NodeList,
    | "ControlSpan" => FieldType::ControlSpan,
    | "KeywordSpan" => FieldType::KeywordSpan,
    | "ScalarSpan" => FieldType::ScalarSpan,
    | "LiteralSpan" => FieldType::LiteralSpan,
    | "TriviaSpan" => FieldType::TriviaSpan,
    | "String" => FieldType::StringSpan,
    | "Ident" => FieldType::Ident,

    | "bool" => FieldType::Literal("bool"),
    | "usize" => FieldType::Literal("usize"),
    | "u8" => FieldType::Literal("u8"),
    | "u16" => FieldType::Literal("u16"),
    | "u32" => FieldType::Literal("u32"),
    | "u64" => FieldType::Literal("u64"),
    | "u128" => FieldType::Literal("u128"),
    | "isize" => FieldType::Literal("isize"),
    | "i8" => FieldType::Literal("i8"),
    | "i16" => FieldType::Literal("i16"),
    | "i32" => FieldType::Literal("i32"),
    | "i64" => FieldType::Literal("i64"),
    | "i128" => FieldType::Literal("i128"),
    | "f32" => FieldType::Literal("f32"),
    | "f64" => FieldType::Literal("f64"),
    | _ => FieldType::Unknown(s.to_owned()),
  }
}

pub fn parse(ts: TokenStream) -> Result<Ast, syn::Error> {
  let item = ts;
  let span = item.__span();

  match syn::parse2::<Item>(item) {
    | Ok(Item::Struct(item_stct)) => {
      let name = item_stct.ident.clone().to_string();

      let fields = {
        match item_stct.fields {
          | syn::Fields::Named(fields_named) => {
            parse_fields_with_accumulation(&fields_named.named)?
          },
          | _ => unimplemented!(),
        }
      };
      // dbg!(&name, &fields);
      Ok(Ast { name, fields })
    },
    | Ok(_) => {
      let error_details = Error::ParseItemNotStruct(span).get();
      let mut syn_error =
        syn::Error::new(error_details.span, &error_details.message);

      // Add help and hints if available
      if let Some(help) = &error_details.help {
        let help_error =
          syn::Error::new(error_details.span, format!("help: {help}"));
        syn_error.combine(help_error);
      }
      if let Some(hints) = &error_details.hints {
        let hints_error =
          syn::Error::new(error_details.span, format!("hint: {hints}"));
        syn_error.combine(hints_error);
      }

      Err(syn_error)
    },
    | Err(parse_error) => {
      let error_details = Error::Parser(span).get();
      let mut combined_error =
        syn::Error::new(error_details.span, &error_details.message);
      combined_error.combine(parse_error);
      Err(combined_error)
    },
  }
}

/// Parse all fields and accumulate any errors found
fn parse_fields_with_accumulation(
  fields: &syn::punctuated::Punctuated<syn::Field, syn::Token![,]>,
) -> Result<Vec<Field>, syn::Error> {
  let mut accumulator = ErrorAccumulator::new();
  let mut parsed_fields = Vec::new();

  for field in fields {
    match parse_field(field) {
      | Ok(parsed_field) => {
        parsed_fields.push(parsed_field);
      },
      | Err(error) => {
        accumulator.add_error(error);
      },
    }
  }

  accumulator.into_result(parsed_fields)
}

fn parse_field(f: &syn::Field) -> Result<Field, Error> {
  let name = match &f.ident {
    | Some(ident) => ident.to_string(),
    | None => {
      return Err(Error::MissingFieldIdentifier(f.span()));
    },
  };

  let (is_option, found_type) = {
    match &f.ty {
      | Type::Path(type_path) => {
        let mut segments = type_path.path.segments.clone().into_iter();

        if let Some(first) = segments.next() {
          let s = first.ident.clone().to_string();

          if &s == "Option" {
            let tty = match first.arguments {
              | syn::PathArguments::AngleBracketed(ref args) => {
                match args.args.first() {
                  | Some(syn::GenericArgument::Type(ty)) => {
                    match ty {
                      | Type::Path(type_path) => {
                        // Use the full type path string to handle multi-segment
                        // paths like `laburnum::Ident`
                        ident_to_type(&type_path.into_token_stream().to_string())
                      },
                      | _ => {
                        return Err(Error::UnsupportedFieldType(
                          f.ty.span(),
                          "expected angle-bracketed arguments".to_string(),
                        ));
                      },
                    }
                  },
                  | _ => {
                    return Err(Error::UnsupportedFieldType(
                      f.ty.span(),
                      "expected type argument".to_string(),
                    ));
                  },
                }
              },
              | _ => {
                return Err(Error::UnsupportedFieldType(
                  f.ty.span(),
                  "expected type argument".to_string(),
                ));
              },
            };

            (true, tty)
          } else {
            // panic!("type_path: {}", type_path.into_token_stream());
            (
              false,
              ident_to_type(&type_path.into_token_stream().to_string()),
            )
          }
        } else {
          return Err(Error::EmptyTypePath(type_path.span()));
        }
      },
      | _ => {
        return Err(Error::UnsupportedFieldType(
          f.ty.span(),
          format!("{:?}", f.ty),
        ));
      },
    }
  };

  Ok(Field {
    name,
    ty: found_type,
    is_option,
  })
}