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 proc_macro2::TokenStream;

#[derive(Debug, Clone)]
pub(crate) struct Args {
  /// Span of the CST token if present
  pub(crate) cst_span:      Option<proc_macro2::Span>,
  /// Span of the AST token if present
  pub(crate) ast_span:      Option<proc_macro2::Span>,
  /// Span of the error token if present
  pub(crate) error_span:    Option<proc_macro2::Span>,
  /// Span of the allow_semantic token if present
  pub(crate) semantic_span: Option<proc_macro2::Span>,
}
impl Args {
  pub(crate) fn parse(ts: TokenStream) -> Self {
    let mut args = Args {
      cst_span:      None,
      ast_span:      None,
      error_span:    None,
      semantic_span: None,
    };

    let tokens_iter = ts.clone().into_iter().peekable();
    let mut collected_tokens = Vec::new();
    let mut items = Vec::new();

    for token in tokens_iter {
      match &token {
        | proc_macro2::TokenTree::Punct(punct) if punct.as_char() == ',' => {
          if !collected_tokens.is_empty() {
            items.push(collected_tokens.to_vec());
            collected_tokens.clear();
          }
        },
        | _ => {
          collected_tokens.push(token);
        },
      }
    }

    if !collected_tokens.is_empty() {
      items.push(collected_tokens.to_vec());
    }

    for item in items {
      for tkn in item.iter() {
        match tkn {
          | proc_macro2::TokenTree::Ident(ident) => {
            match ident.to_string().as_str() {
              | "CST" => {
                args.cst_span = Some(ident.span());
              },
              | "AST" => {
                args.ast_span = Some(ident.span());
              },
              | "error" => {
                args.error_span = Some(ident.span());
              },
              | "allow_semantic" => {
                args.semantic_span = Some(ident.span());
              },
              | _ => (),
            }
          },
          | _ => (),
        }
      }
    }

    args
  }

  /// Check if CST flag is set
  pub(crate) fn is_cst(&self) -> bool {
    self.cst_span.is_some()
  }

  /// Check if AST flag is set
  pub(crate) fn is_ast(&self) -> bool {
    self.ast_span.is_some()
  }

  /// Check if error flag is set
  pub(crate) fn is_error_node(&self) -> bool {
    self.error_span.is_some()
  }

  /// Check if semantic tokens flag is set
  pub(crate) fn has_semantic_tokens(&self) -> bool {
    self.semantic_span.is_some()
  }

  /// Get the span for error reporting when both AST and CST are specified
  pub(crate) fn conflicting_span(&self) -> proc_macro2::Span {
    // Return the span of whichever was parsed first, or call_site as fallback
    self
      .ast_span
      .or(self.cst_span)
      .unwrap_or_else(proc_macro2::Span::call_site)
  }

  /// Get the span for error reporting when neither AST nor CST is specified
  pub(crate) fn missing_type_span(&self) -> proc_macro2::Span {
    // If we have other spans, use them for context, otherwise call_site
    self
      .error_span
      .or(self.semantic_span)
      .unwrap_or_else(proc_macro2::Span::call_site)
  }

  // Test helper constructors for cleaner test code
  #[cfg(test)]
  #[allow(dead_code)]
  pub(crate) fn ast() -> Self {
    Self {
      cst_span:      None,
      ast_span:      Some(proc_macro2::Span::call_site()),
      error_span:    None,
      semantic_span: None,
    }
  }

  #[cfg(test)]
  #[allow(dead_code)]
  pub(crate) fn cst() -> Self {
    Self {
      cst_span:      Some(proc_macro2::Span::call_site()),
      ast_span:      None,
      error_span:    None,
      semantic_span: None,
    }
  }

  #[cfg(test)]
  #[allow(dead_code)]
  pub(crate) fn ast_with_error() -> Self {
    Self {
      cst_span:      None,
      ast_span:      Some(proc_macro2::Span::call_site()),
      error_span:    Some(proc_macro2::Span::call_site()),
      semantic_span: None,
    }
  }

  #[cfg(test)]
  #[allow(dead_code)]
  pub(crate) fn cst_with_error() -> Self {
    Self {
      cst_span:      Some(proc_macro2::Span::call_site()),
      ast_span:      None,
      error_span:    Some(proc_macro2::Span::call_site()),
      semantic_span: None,
    }
  }

  #[cfg(test)]
  #[allow(dead_code)]
  pub(crate) fn ast_with_semantic() -> Self {
    Self {
      cst_span:      None,
      ast_span:      Some(proc_macro2::Span::call_site()),
      error_span:    None,
      semantic_span: Some(proc_macro2::Span::call_site()),
    }
  }

  #[cfg(test)]
  #[allow(dead_code)]
  pub(crate) fn cst_with_semantic() -> Self {
    Self {
      cst_span:      Some(proc_macro2::Span::call_site()),
      ast_span:      None,
      error_span:    None,
      semantic_span: Some(proc_macro2::Span::call_site()),
    }
  }

  #[cfg(test)]
  #[allow(dead_code)]
  pub(crate) fn ast_with_error_and_semantic() -> Self {
    Self {
      cst_span:      None,
      ast_span:      Some(proc_macro2::Span::call_site()),
      error_span:    Some(proc_macro2::Span::call_site()),
      semantic_span: Some(proc_macro2::Span::call_site()),
    }
  }

  #[cfg(test)]
  #[allow(dead_code)]
  pub(crate) fn cst_with_error_and_semantic() -> Self {
    Self {
      cst_span:      Some(proc_macro2::Span::call_site()),
      ast_span:      None,
      error_span:    Some(proc_macro2::Span::call_site()),
      semantic_span: Some(proc_macro2::Span::call_site()),
    }
  }
}