@blainehansen/macro-ts
Version:
An ergonomic typescript compiler that enables typesafe syntactic macros.
365 lines • 20.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ImportMacro = exports.DecoratorMacro = exports.FunctionMacro = exports.BlockMacro = exports.Transformer = void 0;
const ts = require("typescript");
const monads_1 = require("@ts-std/monads");
const message_1 = require("./message");
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, omitTrailingSemicolon: true });
function printNodes(nodes) {
const resultFile = ts.createSourceFile("", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
let printed = "";
for (const node of nodes)
printed += "\n" + printer.printNode(ts.EmitHint.Unspecified, node, resultFile);
return printed;
}
function nonExistentErr(macroName) {
return [`Macro non-existent`, `The macro "${macroName}" doesn't exist.`];
}
function incorrectTypeErr(macroName, macroType, expectedType) {
return [`Macro type mismatch`, `The macro "${macroName}" is a ${macroType} type, but here it's being used as a ${expectedType} type.`];
}
class Transformer {
macros;
cache = new Map();
factory;
errors = [];
warnings = [];
checkSuccess() {
return message_1.SpanResult.checkSuccess(this.errors, this.warnings);
}
constructor(macros, workingDir, sourceChannel, readFile, joinPath, dirMaker) {
this.macros = macros;
this.factory = macros !== undefined && Object.keys(macros).length !== 0 ? context => sourceFile => {
const { currentDir, currentFile } = dirMaker(sourceFile.fileName);
const ctx = new message_1.SpanResult.Context(sourceFile);
const statements = flatVisitStatements({
macros,
fileContext: { workingDir, currentDir, currentFile },
sourceChannel,
handleScript: ({ path, source }) => {
this.transformSource(path, source);
},
readFile,
joinPath,
subsume: result => ctx.subsume(result),
Err: (node, title, message) => ctx.Err(node, title, message),
macroCtx: {
Ok: (value, warnings) => message_1.SpanResult.Ok(value, warnings),
TsNodeErr: (node, title, ...paragraphs) => message_1.SpanResult.TsNodeErr(ctx.sourceFile, node, title, paragraphs),
Err: (fileName, title, ...paragraphs) => message_1.SpanResult.Err(fileName, title, paragraphs),
tsNodeWarn: (node, title, ...paragraphs) => { ctx.tsNodeWarn(node, title, paragraphs); },
warn: (fileName, title, ...paragraphs) => { ctx.warn(fileName, title, paragraphs); },
subsume: result => ctx.subsume(result)
}
}, sourceFile.statements, context);
const { errors, warnings } = ctx.drop();
this.errors = this.errors.concat(errors);
this.warnings = this.warnings.concat(warnings);
return ts.updateSourceFileNode(sourceFile, statements);
} : () => sourceFile => sourceFile;
}
reset() {
this.cache.clear();
}
has(path) {
return this.cache.has(path);
}
get(path) {
return this.cache.get(path);
}
transformSourceFile(sourceFile) {
const { transformed: [newSourceFile] } = ts.transform(sourceFile, [this.factory]);
const newSource = printer.printFile(newSourceFile);
this.cache.set(sourceFile.fileName, newSource);
return newSource;
}
transformSource(path, source) {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
return this.transformSourceFile(sourceFile);
}
}
exports.Transformer = Transformer;
function BlockMacro(execute) {
return { type: "block", execute };
}
exports.BlockMacro = BlockMacro;
function attemptBlockMacro(ctx, statement, block, context) {
if (!(ts.isExpressionStatement(statement)
&& ts.isNonNullExpression(statement.expression)
&& ts.isNonNullExpression(statement.expression.expression)
&& ts.isIdentifier(statement.expression.expression.expression)))
return undefined;
if (!block || !ts.isBlock(block))
return ctx.Err(statement.expression, "Block macro syntax without block.", `You've used the macro syntax with an identifier as you would when calling a block macro, but without a block.\nThis is likely a mistake.`);
const macroName = statement.expression.expression.expression.text;
const macro = ctx.macros[macroName];
if (!macro)
return ctx.Err(statement.expression, ...nonExistentErr(macroName));
if (macro.type !== "block")
return ctx.Err(statement.expression, ...incorrectTypeErr(macroName, macro.type, "block"));
return ctx.subsume(macro.execute(ctx.macroCtx, flatVisitStatements(ctx, block.statements, context)));
}
function FunctionMacro(execute) {
return { type: "function", execute };
}
exports.FunctionMacro = FunctionMacro;
function attemptFunctionMacro(ctx, node, argumentsVisitor) {
if (!(ts.isCallExpression(node)
&& ts.isNonNullExpression(node.expression)
&& ts.isNonNullExpression(node.expression.expression)
&& ts.isIdentifier(node.expression.expression.expression)))
return undefined;
const macroName = node.expression.expression.expression.text;
const macro = ctx.macros[macroName];
if (!macro)
return ctx.Err(node.expression, ...nonExistentErr(macroName));
if (macro.type !== "function")
return ctx.Err(node.expression, ...incorrectTypeErr(macroName, macro.type, "function"));
return ctx.subsume(macro.execute(ctx.macroCtx, argumentsVisitor(node.arguments), node.typeArguments));
}
function DecoratorMacro(execute) {
return { type: "decorator", execute };
}
exports.DecoratorMacro = DecoratorMacro;
function attemptDecoratorMacros(ctx, statement) {
if (statement.decorators === undefined)
return undefined;
let currentStatement = statement;
const decorators = statement.decorators.slice();
statement.decorators = undefined;
const prepends = [];
const appends = [];
for (const { expression } of decorators) {
if (!(ts.isCallExpression(expression)
&& ts.isNonNullExpression(expression.expression)
&& ts.isNonNullExpression(expression.expression.expression)
&& ts.isIdentifier(expression.expression.expression.expression)))
return ctx.Err(expression, "Disallowed normal decorator", `At this point, macro-ts doesn't allow normal decorators.`);
const macroName = expression.expression.expression.expression.text;
if (!currentStatement)
return ctx.Err(expression, "Decorator conflict", `Can't perform decorator macro ${macroName}. A previous decorator removed the decorated statement.`);
const macro = ctx.macros[macroName];
if (!macro)
return ctx.Err(expression.expression, ...nonExistentErr(macroName));
if (macro.type !== "decorator")
return ctx.Err(expression.expression, ...incorrectTypeErr(macroName, macro.type, "decorator"));
const executionResult = ctx.subsume(macro.execute(ctx.macroCtx, currentStatement, expression.arguments, expression.typeArguments));
if (executionResult.is_err())
return (0, monads_1.Err)(undefined);
const { prepend, replacement, append } = executionResult.value;
currentStatement = replacement;
if (prepend)
Array.prototype.push.apply(prepends, prepend);
if (append)
Array.prototype.push.apply(appends, append);
}
return (0, monads_1.Ok)(prepends.concat(currentStatement ? [currentStatement] : []).concat(appends));
}
function ImportMacro(execute) {
return { type: "import", execute };
}
exports.ImportMacro = ImportMacro;
function attemptImportMacro(ctx, declaration) {
const { macros, fileContext, sourceChannel, handleScript, readFile, joinPath } = ctx;
const moduleSpecifier = declaration.moduleSpecifier;
if (!(moduleSpecifier
&& ts.isCallExpression(moduleSpecifier)
&& ts.isNonNullExpression(moduleSpecifier.expression)
&& ts.isNonNullExpression(moduleSpecifier.expression.expression)
&& ts.isIdentifier(moduleSpecifier.expression.expression.expression)))
return undefined;
if (moduleSpecifier.arguments.length !== 1)
return ctx.Err(moduleSpecifier.arguments, "Macro incorrect arguments", `Import macros have to have exactly one string literal argument.`).ok_undef();
const pathSpecifier = moduleSpecifier.arguments[0];
if (!ts.isStringLiteral(pathSpecifier))
return ctx.Err(pathSpecifier, "Macro incorrect arguments", `Import macros have to have exactly one string literal argument.`).ok_undef();
if (moduleSpecifier.typeArguments)
return ctx.Err(moduleSpecifier.typeArguments, "Macro incorrect arguments", `Import macros don't allow type arguments.`).ok_undef();
const macroName = moduleSpecifier.expression.expression.expression.text;
const macro = macros[macroName];
if (!macro)
return ctx.Err(moduleSpecifier.expression, ...nonExistentErr(macroName)).ok_undef();
if (macro.type !== "import")
return ctx.Err(moduleSpecifier.expression, ...incorrectTypeErr(macroName, macro.type, "import")).ok_undef();
const targetPath = pathSpecifier.text;
const { workingDir, currentDir } = fileContext;
const fullPath = joinPath(workingDir, currentDir, targetPath);
const source = readFile(fullPath);
if (source === undefined)
return ctx.Err(pathSpecifier, "Invalid path", `This path resolved to "${fullPath}", but for some reason that file couldn't be read.`).ok_undef();
const executionResult = ctx.subsume(macro.execute(ctx.macroCtx, source, targetPath, fileContext));
if (executionResult.is_err())
return undefined;
const { sources = {}, statements } = executionResult.value;
sourceChannel(sources);
handleScript({ path: fullPath + ".ts", source: printNodes(statements) });
return ts.createStringLiteral(targetPath);
}
function visitStatement(ctx, statement, context) {
const result = attemptVisitStatement(ctx, statement, context);
if (!result)
throw new Error();
return result;
}
function visitBlock(ctx, block, context) {
return ts.updateBlock(block, flatVisitStatements(ctx, block.statements, context));
}
function visitStatementIntoBlock(ctx, inputStatement, context) {
if (ts.isBlock(inputStatement))
return visitBlock(ctx, inputStatement, context);
const { prepend = [], statement, append = [] } = visitStatement(ctx, inputStatement, context);
if (prepend.length > 0 || append.length > 0)
return ts.createBlock(prepend.concat(statement ? [statement] : []).concat(append));
return statement;
}
function attemptVisitStatement(ctx, statement, context) {
const prepends = [];
const appends = [];
function subsumingVisitor(node) {
const statementResult = attemptVisitStatement(ctx, node, context);
if (statementResult) {
const { prepend, statement, append } = statementResult;
if (prepend)
Array.prototype.push.apply(prepends, prepend);
if (append)
Array.prototype.push.apply(appends, append);
return statement;
}
const macroResult = attemptFunctionMacro(ctx, node, visitArgsSubsuming);
if (macroResult) {
if (macroResult.is_err())
return node;
const { prepend, expression, append } = macroResult.value;
if (prepend)
Array.prototype.push.apply(prepends, prepend);
if (append)
Array.prototype.push.apply(appends, append);
return expression;
}
return visitChildrenSubsuming(node);
}
function visitNodeSubsuming(node) {
const result = ts.visitNode(node, subsumingVisitor);
if (!result)
throw new Error();
return result;
}
function visitArgsSubsuming(args) {
const nodeArray = ts.createNodeArray(args.map(visitNodeSubsuming));
nodeArray.pos = args.pos;
nodeArray.end = args.end;
return nodeArray;
}
function visitChildrenSubsuming(node) {
const result = ts.visitEachChild(node, subsumingVisitor, context);
if (!result)
throw new Error();
return result;
}
function include(statement) {
return { prepend: prepends, statement, append: appends };
}
if (ts.isBlock(statement))
return { statement: visitBlock(ctx, statement, context) };
else if (ts.isVariableStatement(statement))
return include(ts.updateVariableStatement(statement, statement.modifiers, visitNodeSubsuming(statement.declarationList)));
else if (ts.isExpressionStatement(statement))
return include(ts.updateExpressionStatement(statement, visitNodeSubsuming(statement.expression)));
else if (ts.isFunctionDeclaration(statement))
return include(ts.updateFunctionDeclaration(statement, statement.decorators ? statement.decorators.map(visitNodeSubsuming) : undefined, statement.modifiers, statement.asteriskToken, statement.name, statement.typeParameters, statement.parameters.map(parameter => {
return parameter.initializer
? ts.updateParameter(parameter, parameter.decorators, parameter.modifiers, parameter.dotDotDotToken, parameter.name, parameter.questionToken, parameter.type, visitNodeSubsuming(parameter.initializer))
: parameter;
}), statement.type, statement.body ? ts.updateBlock(statement.body, flatVisitStatements(ctx, statement.body.statements, context)) : undefined));
else if (ts.isReturnStatement(statement))
return include(ts.updateReturn(statement, statement.expression ? visitNodeSubsuming(statement.expression) : undefined));
else if (ts.isIfStatement(statement))
return include(ts.updateIf(statement, visitNodeSubsuming(statement.expression), visitStatementIntoBlock(ctx, statement.thenStatement, context), statement.elseStatement ? visitStatementIntoBlock(ctx, statement.elseStatement, context) : undefined));
else if (ts.isSwitchStatement(statement))
return include(ts.updateSwitch(statement, visitNodeSubsuming(statement.expression), ts.updateCaseBlock(statement.caseBlock, statement.caseBlock.clauses.map(clause => {
switch (clause.kind) {
case ts.SyntaxKind.CaseClause: return ts.updateCaseClause(clause, visitNodeSubsuming(clause.expression), flatVisitStatements(ctx, clause.statements, context));
case ts.SyntaxKind.DefaultClause: return ts.updateDefaultClause(clause, flatVisitStatements(ctx, clause.statements, context));
}
}))));
else if (ts.isWhileStatement(statement))
return include(ts.updateWhile(statement, visitNodeSubsuming(statement.expression), visitStatementIntoBlock(ctx, statement.statement, context)));
else if (ts.isForStatement(statement))
return include(ts.updateFor(statement, statement.initializer ? visitNodeSubsuming(statement.initializer) : undefined, statement.condition ? visitNodeSubsuming(statement.condition) : undefined, statement.incrementor ? visitNodeSubsuming(statement.incrementor) : undefined, visitStatementIntoBlock(ctx, statement.statement, context)));
else if (ts.isForInStatement(statement))
return include(ts.updateForIn(statement, visitNodeSubsuming(statement.initializer), visitNodeSubsuming(statement.expression), visitStatementIntoBlock(ctx, statement.statement, context)));
else if (ts.isForOfStatement(statement))
return include(ts.updateForOf(statement, statement.awaitModifier, visitNodeSubsuming(statement.initializer), visitNodeSubsuming(statement.expression), visitStatementIntoBlock(ctx, statement.statement, context)));
else if (ts.isDoStatement(statement))
return include(ts.updateDo(statement, visitStatementIntoBlock(ctx, statement.statement, context), visitNodeSubsuming(statement.expression)));
else if (ts.isThrowStatement(statement))
return statement.expression
? include(ts.updateThrow(statement, visitNodeSubsuming(statement.expression)))
: { statement };
else if (ts.isTryStatement(statement))
return include(ts.updateTry(statement, visitBlock(ctx, statement.tryBlock, context), statement.catchClause ? visitNodeSubsuming(statement.catchClause) : undefined, statement.finallyBlock ? visitBlock(ctx, statement.finallyBlock, context) : undefined));
else if (ts.isClassDeclaration(statement))
return include(ts.updateClassDeclaration(statement, statement.decorators ? statement.decorators.map(visitNodeSubsuming) : undefined, statement.modifiers, statement.name, statement.typeParameters, statement.heritageClauses, statement.members.map(visitNodeSubsuming)));
else if (ts.isWithStatement(statement))
return include(ts.updateWith(statement, visitNodeSubsuming(statement.expression), visitStatementIntoBlock(ctx, statement.statement, context)));
else if (ts.isLabeledStatement(statement))
return include(ts.updateLabel(statement, statement.label, visitNodeSubsuming(statement.statement)));
else if (ts.isEnumDeclaration(statement))
return include(ts.updateEnumDeclaration(statement, statement.decorators ? statement.decorators.map(visitNodeSubsuming) : undefined, statement.modifiers, statement.name, statement.members.map(visitNodeSubsuming)));
else if (ts.isModuleDeclaration(statement))
return include(ts.updateModuleDeclaration(statement, statement.decorators ? statement.decorators.map(visitNodeSubsuming) : undefined, statement.modifiers, statement.name, statement.body ? visitNodeSubsuming(statement.body) : undefined));
else if (ts.isModuleBlock(statement))
return { statement: ts.updateModuleBlock(statement, flatVisitStatements(ctx, statement.statements, context)) };
else if (ts.isExportAssignment(statement))
return include(ts.updateExportAssignment(statement, statement.decorators ? statement.decorators.map(visitNodeSubsuming) : undefined, statement.modifiers, visitNodeSubsuming(statement.expression)));
else if (ts.isImportDeclaration(statement)) {
const path = attemptImportMacro(ctx, statement);
if (path)
return { statement: ts.updateImportDeclaration(statement, statement.decorators, statement.modifiers, statement.importClause, path) };
return { statement };
}
else if (ts.isExportDeclaration(statement)) {
const path = attemptImportMacro(ctx, statement);
if (path)
return { statement: ts.updateExportDeclaration(statement, statement.decorators, statement.modifiers, statement.exportClause, path, statement.isTypeOnly) };
return { statement };
}
else if (ts.isEmptyStatement(statement) || ts.isMissingDeclaration(statement)
|| ts.isDebuggerStatement(statement) || ts.isBreakStatement(statement) || ts.isContinueStatement(statement)
|| ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)
|| ts.isNamespaceExportDeclaration(statement)
|| ts.isImportEqualsDeclaration(statement))
return { statement };
return undefined;
}
function flatVisitStatements(ctx, statements, context) {
let index = 0;
const finalStatements = [];
while (index < statements.length) {
const current = statements[index];
const blockResult = attemptBlockMacro(ctx, current, statements[index + 1], context);
if (blockResult) {
Array.prototype.push.apply(finalStatements, blockResult.default([]));
index += 2;
continue;
}
const decoratorsResult = attemptDecoratorMacros(ctx, current);
if (decoratorsResult) {
Array.prototype.push.apply(finalStatements, decoratorsResult.default([]));
index += 1;
continue;
}
const { prepend, statement, append } = visitStatement(ctx, current, context);
if (prepend)
Array.prototype.push.apply(finalStatements, prepend);
finalStatements.push(statement);
if (append)
Array.prototype.push.apply(finalStatements, append);
index++;
}
const nodeArray = ts.createNodeArray(finalStatements);
nodeArray.pos = statements.pos;
nodeArray.end = statements.end;
return nodeArray;
}
//# sourceMappingURL=transformer.js.map