Attribute Macros
Attribute macros transform decorated declarations (classes, functions, methods).
When to Use
- Transform classes:
@myMacro class Foo {} - Transform functions:
@myMacro function foo() {} - Add methods or properties
- Generate companion code
Basic Structure
typescript
import { defineAttributeMacro } from "@typesugar/core";
import * as ts from "typescript";
defineAttributeMacro("myMacro", {
expand(ctx, decorator, target, args) {
// decorator: the @myMacro(...) node
// target: the decorated class/function/etc
// args: decorator arguments
// Return the transformed node (or array of nodes)
return target;
},
});Tutorial: Creating @logged
A decorator that logs method entry and exit.
typescript
import { defineAttributeMacro } from "@typesugar/core";
import { quote, quoteStatements, ident, spread } from "@typesugar/core/quote";
defineAttributeMacro("logged", {
expand(ctx, decorator, target) {
if (!ts.isMethodDeclaration(target)) {
ctx.reportError(decorator, "@logged can only be applied to methods");
return target;
}
const methodName = target.name.getText(ctx.sourceFile);
const params = target.parameters;
const body = target.body;
if (!body) return target;
const newBody = quote(ctx)`{
console.log("Entering ${methodName}");
try {
const __result = (() => ${body})();
console.log("Exiting ${methodName}");
return __result;
} catch (e) {
console.log("Error in ${methodName}:", e);
throw e;
}
}`;
return ctx.factory.updateMethodDeclaration(
target,
target.modifiers?.filter((m) => !ts.isDecorator(m)),
target.asteriskToken,
target.name,
target.questionToken,
target.typeParameters,
target.parameters,
target.type,
newBody as ts.Block
);
},
});Class Decorators
Transform entire classes:
typescript
defineAttributeMacro("singleton", {
expand(ctx, decorator, target) {
if (!ts.isClassDeclaration(target)) {
ctx.reportError(decorator, "@singleton requires a class");
return target;
}
const className = target.name?.getText(ctx.sourceFile) ?? "Anonymous";
const instanceName = ctx.generateUniqueName("instance");
return quoteStatements(ctx)`
${target}
const ${instanceName}: ${ident(className)} | undefined = undefined;
${ident(className)}.getInstance = function(): ${ident(className)} {
if (!${instanceName}) {
${instanceName} = new ${ident(className)}();
}
return ${instanceName};
};
`;
},
});Decorator Arguments
Access decorator arguments:
typescript
defineAttributeMacro("route", {
expand(ctx, decorator, target, args) {
// args is ts.NodeArray<ts.Expression>
const path = args[0];
const method = args[1];
if (!path) {
ctx.reportError(decorator, "@route requires a path argument");
return target;
}
// Use the arguments...
const pathValue = ctx.evaluate(path);
console.log("Route path:", pathValue);
return target;
},
});
// Usage: @route("/users", "GET")Returning Multiple Nodes
Attribute macros can return an array:
typescript
defineAttributeMacro("withFactory", {
expand(ctx, decorator, target) {
if (!ts.isClassDeclaration(target)) return target;
const className = target.name?.getText(ctx.sourceFile);
// Return the class plus a factory function
return [
target,
...quoteStatements(ctx)`
function create${ident(className)}(...args: ConstructorParameters<typeof ${ident(className)}>): ${ident(className)} {
return new ${ident(className)}(...args);
}
`,
];
},
});Modifying Class Members
Add methods to a class:
typescript
defineAttributeMacro("addToString", {
expand(ctx, decorator, target) {
if (!ts.isClassDeclaration(target)) return target;
const className = target.name?.getText(ctx.sourceFile) ?? "?";
const members = [...(target.members ?? [])];
// Add toString method
const toStringMethod = ctx.factory.createMethodDeclaration(
undefined,
undefined,
ctx.factory.createIdentifier("toString"),
undefined,
undefined,
[],
ctx.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
ctx.factory.createBlock([
ctx.factory.createReturnStatement(ctx.factory.createStringLiteral(`[${className}]`)),
])
);
members.push(toStringMethod);
return ctx.factory.updateClassDeclaration(
target,
target.modifiers?.filter((m) => !ts.isDecorator(m)),
target.name,
target.typeParameters,
target.heritageClauses,
members
);
},
});Decorator Ordering
Multiple decorators are processed in order:
typescript
@first
@second
@third
class MyClass {}
// Processed: first, then second, then thirdUse expandAfter for dependencies:
typescript
defineAttributeMacro("dependent", {
expandAfter: ["prerequisite"],
expand(ctx, decorator, target) {
// Runs after @prerequisite
return target;
},
});Type Information
Access type information from the decorated item:
typescript
defineAttributeMacro("typeInfo", {
expand(ctx, decorator, target) {
if (ts.isClassDeclaration(target) && target.name) {
const type = ctx.typeChecker.getTypeAtLocation(target);
const props = ctx.getPropertiesOfType(type);
console.log(
"Properties:",
props.map((p) => p.getName())
);
}
return target;
},
});Error Handling
Report errors and return the original:
typescript
defineAttributeMacro("requireClass", {
expand(ctx, decorator, target) {
if (!ts.isClassDeclaration(target)) {
ctx.reportError(decorator, "@requireClass can only decorate classes");
return target; // Return unchanged
}
if (!target.name) {
ctx.reportError(target, "@requireClass requires a named class");
return target;
}
// Transform...
return target;
},
});Best Practices
- Check target type: Validate the decorator is on the right kind of node
- Preserve modifiers: Don't accidentally remove other decorators
- Handle missing body: Methods/functions might be abstract
- Use updateXxx: Preserve node properties you don't change
- Report clear errors: Tell users what went wrong
Next Steps
- Derive Macros — Specialized attribute macros
- Quasiquoting — Building AST with
quote() - Testing Macros — Verifying macro output
