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

/// Create a test case for the laburnum_syntax macro
///
/// This macro supports testing both AST and CST variants, with optional error
/// expectations.
///
/// # Examples
///
/// ```
/// syntax_test_case! {
///   test_name(AST,
///     #[laburnum_syntax(AST)]
///     pub struct MyStruct {
///       field: NodeId<crate::Type>,
///     }
///   )
/// }
/// ```
///
/// For error cases:
/// ```
/// syntax_test_case! {
///   #[error]
///   test_name(AST,
///     #[laburnum_syntax(AST)]
///     pub struct MyStruct {
///       invalid_field: InvalidType,
///     }
///   )
/// }
/// ```
#[allow(unused_macros)]
macro_rules! syntax_test_case {
  ( $( $(#[$meta:meta])* $name:ident($syntax_type:ident $(, $($extra_args:tt)*)? , $($input:tt)* ) $(,)? )+ ) => {
    $(
      #[test_log::test]
      fn $name() {
        let mut snapshot = ferrotype::Ferrotype::new();

        let expect_errs = $crate::test_macros::attr_error!{$($meta)*};
        snapshot.set_expect_errors(expect_errs);

        let src = quote::quote!($( $input )* );
        snapshot.add_token_stream("Source", &src);

        let args = $crate::test_macros::make_args!($syntax_type $(, $($extra_args)*)?);

        match $crate::process(
          quote::quote!(),  // attr
          quote::quote!(#src),  // item
        ) {
          | Ok(ts) => {
            snapshot.add_token_stream("Output", &ts);

            if expect_errs {
              snapshot.print();
              panic!("Expected error, but got: {:#?}", ts);
            }
          },
          | Err(e) => {
            snapshot.add_debug("Error", &e);
            if !expect_errs {
              snapshot.print();
              panic!("Unexpected error: {:#?}", e);
            }
          },
        }

        ferrotype::assert!(snapshot);
      }
    )+
  };
}

/// Create Args struct based on the syntax type
#[allow(unused_macros)]
macro_rules! make_args {
  (AST) => {
    $crate::Args::ast()
  };

  (CST) => {
    $crate::Args::cst()
  };

  (AST,error) => {
    $crate::Args::ast_with_error()
  };

  (CST,error) => {
    $crate::Args::cst_with_error()
  };

  (AST,allow_semantic) => {
    $crate::Args::ast_with_semantic()
  };

  (CST,allow_semantic) => {
    $crate::Args::cst_with_semantic()
  };

  (AST,error,allow_semantic) => {
    $crate::Args::ast_with_error_and_semantic()
  };

  (CST,error,allow_semantic) => {
    $crate::Args::cst_with_error_and_semantic()
  };
}

/// Helper macro to determine if we expect an error based on attributes
#[allow(unused_macros)]
macro_rules! attr_error {
  () => {
    false
  };

  (error) => {
    true
  };

  (expect_error) => {
    true
  };

  ($other:meta) => {
    false
  };
}

/// Create a compile-fail test case
///
/// These tests expect the macro to produce a compile error
#[allow(unused_macros)]
macro_rules! compile_fail_test {
  ($name:ident, $syntax_type:ident, $($input:tt)*) => {
    #[test_log::test]
    fn $name() {
      let t = trybuild::TestCases::new();
      let test_name = concat!(stringify!($name), ".rs");
      let test_path = std::path::Path::new("tests/compile-fail").join(test_name);

      // Write the test file
      std::fs::create_dir_all("tests/compile-fail").ok();
      std::fs::write(
        &test_path,
        format!(
          r#"// Auto-generated test case
use laburnum_syntax_macro::laburnum_syntax;

{}
"#,
          stringify!($($input)*)
        )
      ).expect("Failed to write test file");

      t.compile_fail(test_path);
    }
  };
}

/// Test helper to validate the generated output matches expected patterns
#[allow(unused_macros)]
macro_rules! assert_output_contains {
  ($output:expr, $($pattern:tt)*) => {
    let output_str = $output.to_string();
    let pattern = stringify!($($pattern)*);
    assert!(
      output_str.contains(pattern),
      "Output does not contain expected pattern.\nPattern: {}\nOutput: {}",
      pattern,
      output_str
    );
  };
}

/// Test helper to validate error messages
#[allow(unused_macros)]
macro_rules! assert_error_contains {
  ($error:expr, $expected:expr) => {
    let error_details = $error.get();
    assert!(
      error_details.message.contains($expected),
      "Error message does not contain expected text.\nExpected: {}\nActual: {}",
      $expected,
      error_details.message
    );
  };
}

#[allow(unused_imports)]
pub(crate) use {
  assert_error_contains,
  assert_output_contains,
  attr_error,
  compile_fail_test,
  make_args,
  syntax_test_case,
};