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 {
  crate::process,
  ferrotype::Ferrotype,
  quote::quote,
};

#[test_log::test]
fn test_simple_cst_struct() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST)]
    pub struct SimpleCST {
      span: Span,
      token: NodeId<crate::Token>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
      // Should generate impl with CST-specific methods
      let output_str = output.to_string();
      assert!(output_str.contains("impl SimpleCST"));
    },
    | Err(e) => {
      panic!("Unexpected error: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_with_field_types() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST)]
    pub struct ComplexCST {
      span: Span,
      start_token: NodeId<crate::Token>,
      children: Vec<NodeId>,
      end_token: Option<NodeId<crate::Token>>,
      trivia: Vec<crate::Trivia>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      panic!("Unexpected error: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_with_multiple_node_types() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST)]
    pub struct MultiNodeCST {
      span: Span,
      expr: NodeId<crate::Expression, crate::Statement>,
      mixed: NodeId<crate::Token, crate::Trivia, crate::Comment>,
      optional_multi: Option<NodeId<crate::Type1, crate::Type2>>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      panic!("Unexpected error: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_with_doc_comments() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST)]
    pub struct DocumentedCST {
      span: Span,
      /// The opening token
      open: NodeId<crate::Token>,

      /// The content nodes
      /// Can be multiple types
      content: Vec<NodeId>,

      /// The closing token
      close: NodeId<crate::Token>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      panic!("Unexpected error: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_error_node() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST, error)]
    pub struct CSTErrorNode {
      message: String,
      span: Span,
      tokens: Vec<NodeId<crate::Token>>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST, error), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      panic!("Unexpected error: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_with_semantic_tokens() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST, allow_semantic)]
    pub struct SemanticCST {
      span: Span,
      token: NodeId<crate::SemanticToken>,
      kind: Field<TokenKind>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST, allow_semantic), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      panic!("Unexpected error: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

// Error test cases

#[test_log::test]
fn test_cst_error_not_struct() {
  let mut snapshot = Ferrotype::new();
  snapshot.set_expect_errors(true);

  let input = quote! {
    #[laburnum_syntax(CST)]
    pub enum NotAStruct {
      Token(NodeId),
      Trivia(NodeId),
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(_) => {
      panic!("Expected error for non-struct item");
    },
    | Err(e) => {
      snapshot.add_debug("Error", &e);
      let error_message = e.to_string();
      assert!(
        error_message.contains("struct"),
        "Error message should mention 'struct', got: {error_message}",
      );
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_empty_struct() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST)]
    pub struct EmptyCST {
      span: Span,
    }
  };

  snapshot.add_token_stream("Input", &input);

  // Empty structs might be valid, depends on implementation
  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      snapshot.add_debug("Error", &e);
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_with_generics() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST)]
    pub struct GenericCST<T> {
      span: Span,
      data: NodeId<T>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      // Generics might not be supported
      snapshot.add_debug("Error", &e);
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_complex_real_world_example() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST)]
    pub struct FunctionCST {
      span: Span,
      visibility_token: Option<NodeId<crate::Token>>,
      fn_keyword: NodeId<crate::Token>,
      name: NodeId<crate::Identifier>,
      generic_params: Option<NodeId<crate::GenericParams>>,
      lparen: NodeId<crate::Token>,
      params: Vec<crate::Param>,
      rparen: NodeId<crate::Token>,
      return_type: Option<NodeId<crate::ReturnType>>,
      body: NodeId<crate::Block>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
      let output_str = output.to_string();
      assert!(output_str.contains("impl FunctionCST"));
    },
    | Err(e) => {
      panic!("Unexpected error: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_multiple_attributes() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[derive(Debug, Clone)]
    #[laburnum_syntax(CST)]
    pub struct MultiAttrCST {
      span: Span,
      token: NodeId<crate::Token>,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
      // Other attributes should be preserved
      let output_str = output.to_string();
      assert!(output_str.contains("derive"));
    },
    | Err(e) => {
      panic!("Unexpected error: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}

#[test_log::test]
fn test_cst_private_struct() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(CST)]
    struct PrivateCST {
      span: Span,
      field: NodeId,
    }
  };

  snapshot.add_token_stream("Input", &input);

  match process(quote!(CST), input) {
    | Ok(output) => {
      snapshot.add_token_stream("Output", &output);
    },
    | Err(e) => {
      panic!("Unexpected error: {e:?}");
    },
  }

  ferrotype::assert!(snapshot);
}