@macroforge/mcp-server
Version:
MCP server for Macroforge documentation and code analysis
137 lines (102 loc) • 5.23 kB
Markdown
# Testing and Debugging
A macro is ordinary Rust, so it can be unit-tested like any function. For a quick look at what it
generates, run it on a file.
## Running a Macro on a File
After `macroforge build`, expand one file of a project that uses the macro and print the result:
Bash
```
macroforge expand src/user.ts --print
```
Diagnostics are printed with their file, line and column, followed by any notes and help. `--out`
writes the expansion to a file instead, and `--types-out` writes the generated type declarations.
## Unit Tests
A test builds the input a macro would get, calls the macro function and checks the stream it
returns. To build the input, lower a TypeScript snippet to IR and wrap it in a context:
Rust
```
use macroforge_ts::ts_syn::abi::{MacroContextIR, SpanIR};
use macroforge_ts::ts_syn::oxc::allocator::Allocator;
use macroforge_ts::ts_syn::oxc::parser::Parser;
use macroforge_ts::ts_syn::oxc::span::SourceType;
use macroforge_ts::ts_syn::{TsStream, lower_classes};
/// The input a derive gets for the first class in \`source\`.
fn class_input(source: &str) -> TsStream {
let allocator = Allocator::default();
let parsed = Parser::new(&allocator, source, SourceType::ts()).parse();
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let class = lower_classes(&parsed.program, source, None)
.expect("the class lowers")
.into_iter()
.next()
.expect("the source has a class");
let context = MacroContextIR::new_derive_class(
"Validate".to_string(),
"my-macros".to_string(),
SpanIR::new(0, 0),
class.span,
"test.ts".to_string(),
class,
source.to_string(),
);
TsStream::with_context(source, "test.ts", context).expect("the input stream builds")
}
fn validate_checks_every_field() {
let output = derive_validate(class_input("class User { name: string; email: string; }"))
.expect("the macro succeeds");
assert!(output.source().contains("this.name"));
assert!(output.source().contains("this.email"));
}
```
`lower_interfaces`, `lower_enums`, `lower_type_aliases` and `lower_functions` lower the other kinds,
and `MacroContextIR` has a matching constructor for each: `new_derive_interface`, `new_derive_enum`,
`new_derive_type_alias`, and `new_attribute_class`, `new_attribute_function` and the rest for
attribute macros.
The context starts with empty imports and an empty type registry. To test a macro that reads them,
fill them in: `context.import_registry.install_source_imports(...)` gives the file imports, and
`context.type_registry.insert(...)` adds project types.
## Debug Logging
`macroforge_ts::debug` writes timestamped lines to the project's `.macroforge/debug.log`, in the
nearest directory holding a `macroforge.config`:
Rust
```
use macroforge_ts::debug;
debug::log("Validate", "starting");
debug::log_ctx("Validate", &input.context); // macro, file, target and field count
macroforge_ts::debug_log!("Validate", "{} has {} fields", input.name(), class.fields().len());
let result = output.clone().into_result();
debug::log_result("Validate", &result); // patch, token and diagnostic counts
debug::clear(); // empty the log
```
A macro package runs in a WebAssembly sandbox with no file access, so its lines travel back with its
result. When the CLI runs the macro they are written to `debug.log` as usual; in a JavaScript host
such as the Vite plugin they are printed to the console instead.
## Implementing a Macro by Hand
The `` attributes generate an implementation of the `Macroforge` trait, plus the
registration and exports a macro package needs. Writing the trait yourself is only needed when you
embed Macroforge in your own Rust program:
Rust
```
use std::sync::Arc;
use macroforge_ts::host::{Macroforge, MacroRegistry, Result};
use macroforge_ts::ts_syn::{MacroKind, MacroResult, TsStream};
struct Stamp;
impl Macroforge for Stamp {
fn name(&self) -> &str { "Stamp" }
fn kind(&self) -> MacroKind { MacroKind::Derive }
fn run(&self, input: TsStream) -> MacroResult {
let name = input.context().map_or("Unknown", |ctx| ctx.macro_name.as_str());
TsStream::from_string(format!("export const stampedBy = \"{name}\";")).into_result()
}
}
fn register(registry: &MacroRegistry) -> Result<()> {
registry.register("my-macros", "Stamp", Arc::new(Stamp))
}
macroforge_ts::register_macro_package!("my-macros", register);
```
Warning
`register_macro_package!` registers macros with the expander in the same program. A package built
with `macroforge build` exposes only the macros declared with the `` attributes.
## Next Steps
- [Errors and Diagnostics](../../docs/custom-macros/diagnostics)
- [Output and Imports](../../docs/custom-macros/output)