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_ast_struct() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct Simple {
      field: NodeId<crate::Type>,
    }
  };

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

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

  ferrotype::assert!(snapshot);
}

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

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct Complex {
      simple_field: NodeId<crate::Type>,
      optional_field: Option<NodeId<crate::Type>>,
      vec_field: Vec<crate::Item>,
      field_field: Field<bool>,
      enum_field: Field<Enum<crate::Visibility>>,
      enum_node: EnumNodeId<crate::Expression>,
    }
  };

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

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

  ferrotype::assert!(snapshot);
}

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

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct MultiType {
      union_field: NodeId<crate::Type1, crate::Type2>,
      triple_field: NodeId<crate::Type1, crate::Type2, crate::Type3>,
      vec_multi: Vec<crate::Type1, crate::Type2>,
    }
  };

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

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

  ferrotype::assert!(snapshot);
}

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

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct Documented {
      /// This is a documented field
      field1: NodeId<crate::Type>,

      /// Multi-line documentation
      /// Second line
      field2: Option<NodeId<crate::Type>>,
    }
  };

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

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

  ferrotype::assert!(snapshot);
}

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

  let input = quote! {
    #[laburnum_syntax(AST, error)]
    pub struct ErrorNode {
      message: String,
      span: Span,
    }
  };

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

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

  ferrotype::assert!(snapshot);
}

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

  let input = quote! {
    #[laburnum_syntax(AST, allow_semantic)]
    pub struct SemanticNode {
      token: NodeId<crate::Token>,
    }
  };

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

  match process(quote!(AST, 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_ast_error_not_struct() {
  let mut snapshot = Ferrotype::new();
  snapshot.set_expect_errors(true);

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub enum NotAStruct {
      Variant1,
      Variant2,
    }
  };

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

  match process(quote!(AST), 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_ast_error_invalid_field_type() {
  let mut snapshot = Ferrotype::new();
  snapshot.set_expect_errors(true);

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct InvalidField {
      bad_field: HashMap<String, i32>,
    }
  };

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

  match process(quote!(AST), input) {
    | Ok(_) => {
      panic!("Expected error for invalid field type");
    },
    | Err(e) => {
      snapshot.add_debug("Error", &e);
      let error_message = e.to_string();
      assert!(
        error_message.contains("literal types do not take any arguments"),
        "Expected error message about literal types, but got: {error_message}",
      );
    },
  }

  ferrotype::assert!(snapshot);
}

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

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct Empty {}
  };

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

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

  ferrotype::assert!(snapshot);
}

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

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct Generic<T> {
      field: NodeId<T>,
    }
  };

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

  match process(quote!(AST), 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_ast_complex_real_world_example() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(AST)]
    pub struct Function {
      visibility: Field<Enum<crate::modifier::Visibility>>,
      ident: NodeId<crate::symbol::Ident>,
      generic_parameters: Option<NodeId<crate::ty::GenericParameters>>,
      parameters: Vec<crate::ty::Parameters>,
      return_ty: Option<NodeId<crate::ty::Path>>,
      effects_required: Vec<NodeId<crate::ty::Path>>,
      effects_handled: Vec<NodeId<crate::ty::Path>>,
      body: NodeId<crate::expression::Block>,
    }
  };

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

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

  ferrotype::assert!(snapshot);
}

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

  let input = quote! {
    #[derive(Debug, Clone)]
    #[laburnum_syntax(AST)]
    pub struct MultiAttr {
      field: NodeId<crate::Type>,
    }
  };

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

  match process(quote!(AST), 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_ast_private_struct() {
  let mut snapshot = Ferrotype::new();

  let input = quote! {
    #[laburnum_syntax(AST)]
    struct Private {
      field: NodeId<crate::Type>,
    }
  };

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

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

  ferrotype::assert!(snapshot);
}