diff --git a/packages/typewiz-core/src/transformer.spec.ts b/packages/typewiz-core/src/transformer.spec.ts new file mode 100644 index 0000000..785b8d0 --- /dev/null +++ b/packages/typewiz-core/src/transformer.spec.ts @@ -0,0 +1,23 @@ +import * as ts from 'typescript'; +import { transformSourceCode } from './transformer'; + +function astPrettyPrint(sourceText: string) { + const printer: ts.Printer = ts.createPrinter(); + return printer.printFile(ts.createSourceFile('test.ts', sourceText, ts.ScriptTarget.Latest)); +} + +describe('transformer', () => { + it('should instrument function parameters without types', () => { + const input = `function (a) { return 5; }`; + expect(transformSourceCode(input, 'test.ts')).toMatch( + astPrettyPrint(`function (a) { $_$twiz("a", a, 11, "test.ts", {}); return 5; }`), + ); + }); + + it('should instrument class method parameters', () => { + const input = `class Foo { bar(a) { return 5; } }`; + expect(transformSourceCode(input, 'test.ts')).toMatch( + astPrettyPrint(`class Foo { bar(a) { $_$twiz("a", a, 17, "test.ts", {}); return 5; } }`), + ); + }); +}); diff --git a/packages/typewiz-core/src/transformer.ts b/packages/typewiz-core/src/transformer.ts new file mode 100644 index 0000000..f75a10c --- /dev/null +++ b/packages/typewiz-core/src/transformer.ts @@ -0,0 +1,92 @@ +import * as ts from 'typescript'; + +function updateFunction(node: ts.FunctionDeclaration, instrumentStatements: ReadonlyArray) { + return ts.updateFunctionDeclaration( + node, + node.decorators, + node.modifiers, + node.asteriskToken, + node.name, + node.typeParameters, + node.parameters, + node.type, + ts.createBlock([...instrumentStatements, ...(node.body ? node.body.statements : [])]), + ); +} + +function updateMethod(node: ts.MethodDeclaration, instrumentStatements: ReadonlyArray) { + return ts.updateMethod( + node, + node.decorators, + node.modifiers, + node.asteriskToken, + node.name, + node.questionToken, + node.typeParameters, + node.parameters, + node.type, + ts.createBlock([...instrumentStatements, ...(node.body ? node.body.statements : [])]), + ); +} + +export function visitorFactory(ctx: ts.TransformationContext, source: ts.SourceFile) { + const visitor: ts.Visitor = (node: ts.Node): ts.Node => { + if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) { + const instrumentStatements: ts.Statement[] = []; + for (const param of node.parameters) { + if (!param.type && !param.initializer && node.body) { + const typeInsertionPos = param.name.getEnd() + (param.questionToken ? 1 : 0); + // const opts: IExtraOptions = {}; + // if (isArrow) { + // opts.arrow = true; + // } + // if (!hasParensAroundArguments(node)) { + // opts.parens = [node.parameters[0].getStart(), node.parameters[0].getEnd()]; + // } + instrumentStatements.push( + ts.createStatement( + ts.createCall( + ts.createIdentifier('$_$twiz'), + [], + [ + ts.createLiteral(param.name.getText()), + ts.createIdentifier(param.name.getText()), + ts.createNumericLiteral(typeInsertionPos.toString()), + ts.createLiteral(source.fileName), + ts.createObjectLiteral(), // TODO: opts + ], + ), + ), + ); + } + if (ts.isFunctionDeclaration(node)) { + return updateFunction(node, instrumentStatements); + } + if (ts.isMethodDeclaration(node)) { + return updateMethod(node, instrumentStatements); + } + } + } + + return ts.visitEachChild(node, visitor, ctx); + }; + + return visitor; +} + +export function transformer() { + return (ctx: ts.TransformationContext): ts.Transformer => { + return (source: ts.SourceFile) => ts.visitNode(source, visitorFactory(ctx, source)); + }; +} + +export function transformSourceFile(sourceFile: ts.SourceFile) { + return ts.transform(sourceFile, [transformer()]).transformed[0]; +} + +export function transformSourceCode(sourceText: string, fileName: string) { + const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true); + const transformed = transformSourceFile(sourceFile); + const printer: ts.Printer = ts.createPrinter(); + return printer.printFile(transformed); +}