UNPKG

@blainehansen/macro-ts

Version:

An ergonomic typescript compiler that enables typesafe syntactic macros.

365 lines 20.7 kB
"use strict"; 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