Skip to content
This repository was archived by the owner on Oct 5, 2021. It is now read-only.

Commit 835f4a5

Browse files
committed
feat: Implement instrumentation as a TypeScript Compiler TransformerInitial implementation of #41
1 parent bf3239d commit 835f4a5

File tree

2 files changed

+150
-0
lines changed

2 files changed

+150
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as ts from 'typescript';
2+
import { transformSourceCode } from './transformer';
3+
4+
function astPrettyPrint(sourceText: string) {
5+
const printer: ts.Printer = ts.createPrinter();
6+
return printer.printFile(ts.createSourceFile('test.ts', sourceText, ts.ScriptTarget.Latest));
7+
}
8+
9+
describe('transformer', () => {
10+
it('should instrument function parameters without types', () => {
11+
const input = `function (a) { return 5; }`;
12+
expect(transformSourceCode(input, 'test.ts')).toContain(
13+
astPrettyPrint(`function (a) { $_$twiz("a", a, 11, "test.ts", {}); return 5; }`),
14+
);
15+
});
16+
17+
it('should instrument function with two parameters', () => {
18+
const input = `function (a, b) { return 5; }`;
19+
expect(transformSourceCode(input, 'test.ts')).toContain(
20+
astPrettyPrint(`$_$twiz("b", b, 14, "test.ts", {});`).trim(),
21+
);
22+
});
23+
24+
it('should instrument class method parameters', () => {
25+
const input = `class Foo { bar(a) { return 5; } }`;
26+
expect(transformSourceCode(input, 'test.ts')).toContain(
27+
astPrettyPrint(`class Foo { bar(a) { $_$twiz("a", a, 17, "test.ts", {}); return 5; } }`),
28+
);
29+
});
30+
31+
it('should add typewiz declarations', () => {
32+
const input = `function (a) { return 5; }`;
33+
expect(transformSourceCode(input, 'test.ts')).toContain(
34+
astPrettyPrint(
35+
`declare function $_$twiz(name: string, value: any, pos: number, filename: string, opts: any): void`,
36+
),
37+
);
38+
});
39+
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as ts from 'typescript';
2+
3+
const declaration = `
4+
declare function $_$twiz(name: string, value: any, pos: number, filename: string, opts: any): void;
5+
declare namespace $_$twiz {
6+
function track<T>(value: T, filename: string, offset: number): T;
7+
function track(value: any, filename: string, offset: number): any;
8+
}
9+
`;
10+
11+
function getDeclarationStatements() {
12+
const sourceFile = ts.createSourceFile('twiz-declarations.ts', declaration, ts.ScriptTarget.Latest);
13+
return sourceFile.statements;
14+
}
15+
16+
function updateFunction(node: ts.FunctionDeclaration, instrumentStatements: ReadonlyArray<ts.Statement>) {
17+
return ts.updateFunctionDeclaration(
18+
node,
19+
node.decorators,
20+
node.modifiers,
21+
node.asteriskToken,
22+
node.name,
23+
node.typeParameters,
24+
node.parameters,
25+
node.type,
26+
ts.createBlock([...instrumentStatements, ...(node.body ? node.body.statements : [])]),
27+
);
28+
}
29+
30+
function updateMethod(node: ts.MethodDeclaration, instrumentStatements: ReadonlyArray<ts.Statement>) {
31+
return ts.updateMethod(
32+
node,
33+
node.decorators,
34+
node.modifiers,
35+
node.asteriskToken,
36+
node.name,
37+
node.questionToken,
38+
node.typeParameters,
39+
node.parameters,
40+
node.type,
41+
ts.createBlock([...instrumentStatements, ...(node.body ? node.body.statements : [])]),
42+
);
43+
}
44+
45+
export function visitorFactory(ctx: ts.TransformationContext, source: ts.SourceFile) {
46+
const visitor: ts.Visitor = (node: ts.Node): ts.Node => {
47+
if (ts.isSourceFile(node)) {
48+
return ts.updateSourceFileNode(node, [
49+
...getDeclarationStatements(),
50+
...ts.visitEachChild(node, visitor, ctx).statements,
51+
]);
52+
}
53+
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
54+
const instrumentStatements: ts.Statement[] = [];
55+
for (const param of node.parameters) {
56+
if (!param.type && !param.initializer && node.body) {
57+
const typeInsertionPos = param.name.getEnd() + (param.questionToken ? 1 : 0);
58+
// const opts: IExtraOptions = {};
59+
// if (isArrow) {
60+
// opts.arrow = true;
61+
// }
62+
// if (!hasParensAroundArguments(node)) {
63+
// opts.parens = [node.parameters[0].getStart(), node.parameters[0].getEnd()];
64+
// }
65+
instrumentStatements.push(
66+
ts.createStatement(
67+
ts.createCall(
68+
ts.createIdentifier('$_$twiz'),
69+
[],
70+
[
71+
ts.createLiteral(param.name.getText()),
72+
ts.createIdentifier(param.name.getText()),
73+
ts.createNumericLiteral(typeInsertionPos.toString()),
74+
ts.createLiteral(source.fileName),
75+
ts.createObjectLiteral(), // TODO: opts
76+
],
77+
),
78+
),
79+
);
80+
}
81+
}
82+
if (ts.isFunctionDeclaration(node)) {
83+
return ts.visitEachChild(updateFunction(node, instrumentStatements), visitor, ctx);
84+
}
85+
if (ts.isMethodDeclaration(node)) {
86+
return ts.visitEachChild(updateMethod(node, instrumentStatements), visitor, ctx);
87+
}
88+
}
89+
90+
return ts.visitEachChild(node, visitor, ctx);
91+
};
92+
93+
return visitor;
94+
}
95+
96+
export function transformer() {
97+
return (ctx: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
98+
return (source: ts.SourceFile) => ts.visitNode(source, visitorFactory(ctx, source));
99+
};
100+
}
101+
102+
export function transformSourceFile(sourceFile: ts.SourceFile) {
103+
return ts.transform(sourceFile, [transformer()]).transformed[0];
104+
}
105+
106+
export function transformSourceCode(sourceText: string, fileName: string) {
107+
const sourceFile = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true);
108+
const transformed = transformSourceFile(sourceFile);
109+
const printer: ts.Printer = ts.createPrinter();
110+
return printer.printFile(transformed);
111+
}

0 commit comments

Comments
 (0)