@macroforge/mcp-server
Version:
MCP server for Macroforge documentation and code analysis
173 lines (129 loc) • 5.05 kB
Markdown
# ts\_macro\_attribute
`` registers an attribute macro, the analog of Rust's
``. Users invoke it with a JSDoc `@name` decorator on a declaration; the
macro rewrites the entire declaration.
## Basic Syntax
Rust
```
use macroforge_ts::macros::ts_macro_attribute;
use macroforge_ts::ts_syn::{
MacroforgeError, Patch, TargetIR, TsStream,
};
pub fn traced_macro(input: TsStream) -> Result<TsStream, MacroforgeError> {
let ctx = input
.context()
.ok_or_else(|| MacroforgeError::new_global("no macro context"))?;
let TargetIR::Function(f) = &ctx.target else {
return Err(MacroforgeError::new_global(
"@traced can only be applied to functions",
));
};
let full = input.source();
let open = full.find('{').unwrap();
let close = full.rfind('}').unwrap();
let sig = &full[..open];
let body = &full[open + 1..close].trim();
let replacement = format!(
"{sig}{{\\n (globalThis as any).__traced ??= {{}};\\n \\
(globalThis as any).__traced[{name:?}] = \\
((globalThis as any).__traced[{name:?}] || 0) + 1;\\n {body}\\n}}",
sig = sig,
name = f.name,
body = body,
);
let mut out = TsStream::from_string(String::new());
out.runtime_patches.push(Patch::Replace {
span: f.span,
code: replacement,
source_macro: Some("traced".to_string()),
});
Ok(out)
}
```
## Call Site
Consumers decorate a declaration with a JSDoc `@name`:
TypeScript
```
/** import macro { traced } from "@my/macro-package" */
/** @traced */
export function add(a: number, b: number): number {
return a + b;
}
```
## Patch-Based Output
Warning
Attribute macros must emit their rewrite as an explicit `Patch::Replace` over the target span.
Unlike derive macros, the expander does **not** auto-convert a returned `TsStream`'s tokens into
patches for attribute macros: your macro is responsible for describing the edit.
Minimal pattern:
Rust
```
let ctx = input.context().ok_or_else(|| ...)?;
let TargetIR::Function(f) = &ctx.target else { ... };
// Build the new declaration text...
let replacement = format!("{sig}{{ /* new body */ }}", sig = signature);
// Emit a Replace patch covering the function's full span:
let mut out = TsStream::from_string(String::new());
out.runtime_patches.push(Patch::Replace {
span: f.span,
code: replacement,
source_macro: Some("my_macro".to_string()),
});
Ok(out)
```
## Target Kinds
Attribute macros can be applied to any top-level declaration, and to class methods. `ctx.target` is
a `TargetIR` enum; the structured `IR` types give you access to names, spans, parameters, fields,
etc.
| Decoration | TargetIR variant |
| ------------ | ---------------------------------- |
| Function | `TargetIR::Function(FunctionIR)` |
| Class method | `TargetIR::Function(FunctionIR)` |
| Class | `TargetIR::Class(ClassIR)` |
| Interface | `TargetIR::Interface(InterfaceIR)` |
| Enum | `TargetIR::Enum(EnumIR)` |
| Type alias | `TargetIR::TypeAlias(TypeAliasIR)` |
## Attribute Options
### name (required)
Rust
```
// users write /** @reactive */
// users write /** @traced */
```
### description
Rust
```
```
## No-op Exports
Each `` auto-generates a no-op void export in the WASM build so TypeScript can
resolve the decorator name during type-checking:
TypeScript
```
// Generated automatically for #[ts_macro_attribute(traced)]
export function traced(): void;
```
## Example Walkthrough
The `@traced` macro in `tooling/testground/macro/src/attrs.rs` wraps a function so every call
increments a counter on `globalThis.__traced[fnName]`:
TypeScript
```
/** import macro { traced } from "@testground/macro" */
/** @traced */
export function add(a: number, b: number): number {
return a + b;
}
// Expanded output:
export function add(a: number, b: number): number {
(globalThis as any).__traced ??= {};
(globalThis as any).__traced["add"] =
((globalThis as any).__traced["add"] || 0) + 1;
return a + b;
}
```
The wrapper preserves the original signature (including `export`, `async`, parameters, and return
type) and only injects new statements at the top of the body.