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

Commit ed82071

Browse files
committed
feat(core): finish implementing instrumentation as TypeScript compiler transforer#41
1 parent e446c22 commit ed82071

File tree

7 files changed

+229
-229
lines changed

7 files changed

+229
-229
lines changed

packages/typewiz-core/src/instrument.spec.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,6 @@
11
import { instrument } from './instrument';
22

33
describe('instrument', () => {
4-
it('should instrument function parameters without types', () => {
5-
const input = `function (a) { return 5; }`;
6-
expect(instrument(input, 'test.ts')).toMatch(`function (a) {$_$twiz("a",a,11,"test.ts",{}); return 5; }`);
7-
});
8-
9-
it('should correctly instrument optional function parameters', () => {
10-
const input = `function (a?) { return 5; }`;
11-
expect(instrument(input, 'test.ts')).toMatch(`function (a?) {$_$twiz("a",a,12,"test.ts",{}); return 5; }`);
12-
});
13-
14-
it('should instrument class method parameters', () => {
15-
const input = `class Foo { bar(a) { return 5; } }`;
16-
expect(instrument(input, 'test.ts')).toMatch(
17-
`class Foo { bar(a) {$_$twiz("a",a,17,"test.ts",{}); return 5; } }`,
18-
);
19-
});
20-
214
it('should not instrument function parameters that already have a type', () => {
225
const input = `function (a: string) { return 5; }`;
236
expect(instrument(input, 'test.ts')).toMatch(`function (a: string) { return 5; }`);
@@ -36,7 +19,7 @@ describe('instrument', () => {
3619
instrumentCallExpressions: true,
3720
skipTwizDeclarations: true,
3821
}),
39-
).toMatch(`foo($_$twiz.track(bar,"test.ts",4))`);
22+
).toMatch(`foo($_$twiz.track(bar, "test.ts", 4))`);
4023
});
4124

4225
it('should not instrument numeric arguments in function calls', () => {
Lines changed: 5 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as ts from 'typescript';
2-
32
import { getProgram, ICompilerOptions } from './compiler-helper';
4-
import { applyReplacements, Replacement } from './replacement';
3+
import { transformSourceFile } from './transformer';
54

65
export interface IInstrumentOptions extends ICompilerOptions {
76
instrumentCallExpressions?: boolean;
@@ -16,184 +15,19 @@ export interface IExtraOptions {
1615
thisNeedsComma?: boolean;
1716
}
1817

19-
function hasParensAroundArguments(node: ts.FunctionLike) {
20-
if (ts.isArrowFunction(node)) {
21-
return (
22-
node.parameters.length !== 1 ||
23-
node
24-
.getText()
25-
.substr(0, node.equalsGreaterThanToken.getStart() - node.getStart())
26-
.includes('(')
27-
);
28-
} else {
29-
return true;
30-
}
31-
}
32-
33-
function visit(
34-
node: ts.Node,
35-
replacements: Replacement[],
36-
fileName: string,
37-
options: IInstrumentOptions,
38-
program?: ts.Program,
39-
semanticDiagnostics?: ReadonlyArray<ts.Diagnostic>,
40-
) {
41-
const isArrow = ts.isArrowFunction(node);
42-
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
43-
if (node.body) {
44-
const needsThisInstrumentation =
45-
options.instrumentImplicitThis &&
46-
program &&
47-
semanticDiagnostics &&
48-
semanticDiagnostics.find((diagnostic) => {
49-
if (
50-
diagnostic.code === 2683 &&
51-
diagnostic.file &&
52-
diagnostic.file.fileName === node.getSourceFile().fileName &&
53-
diagnostic.start
54-
) {
55-
if (node.body && ts.isBlock(node.body)) {
56-
const body = node.body as ts.FunctionBody;
57-
return (
58-
body.statements.find((statement) => {
59-
return (
60-
diagnostic.start !== undefined &&
61-
statement.pos <= diagnostic.start &&
62-
diagnostic.start <= statement.end
63-
);
64-
}) !== undefined
65-
);
66-
} else {
67-
const body = node.body as ts.Expression;
68-
return body.pos <= diagnostic.start && diagnostic.start <= body.end;
69-
}
70-
}
71-
return false;
72-
}) !== undefined;
73-
if (needsThisInstrumentation) {
74-
const opts: IExtraOptions = { thisType: true };
75-
if (node.parameters.length > 0) {
76-
opts.thisNeedsComma = true;
77-
}
78-
const params = [
79-
JSON.stringify('this'),
80-
'this',
81-
node.parameters.pos,
82-
JSON.stringify(fileName),
83-
JSON.stringify(opts),
84-
];
85-
const instrumentExpr = `$_$twiz(${params.join(',')})`;
86-
87-
replacements.push(Replacement.insert(node.body.getStart() + 1, `${instrumentExpr};`));
88-
}
89-
}
90-
91-
const isShortArrow = ts.isArrowFunction(node) && !ts.isBlock(node.body);
92-
for (const param of node.parameters) {
93-
if (!param.type && !param.initializer && node.body) {
94-
const typeInsertionPos = param.name.getEnd() + (param.questionToken ? 1 : 0);
95-
const opts: IExtraOptions = {};
96-
if (isArrow) {
97-
opts.arrow = true;
98-
}
99-
if (!hasParensAroundArguments(node)) {
100-
opts.parens = [node.parameters[0].getStart(), node.parameters[0].getEnd()];
101-
}
102-
const params = [
103-
JSON.stringify(param.name.getText()),
104-
param.name.getText(),
105-
typeInsertionPos,
106-
JSON.stringify(fileName),
107-
JSON.stringify(opts),
108-
];
109-
const instrumentExpr = `$_$twiz(${params.join(',')})`;
110-
if (isShortArrow) {
111-
replacements.push(Replacement.insert(node.body.getStart(), `(${instrumentExpr},`));
112-
replacements.push(Replacement.insert(node.body.getEnd(), `)`, 10));
113-
} else {
114-
replacements.push(Replacement.insert(node.body.getStart() + 1, `${instrumentExpr};`));
115-
}
116-
}
117-
}
118-
}
119-
120-
if (
121-
options.instrumentCallExpressions &&
122-
ts.isCallExpression(node) &&
123-
node.expression.getText() !== 'require.context'
124-
) {
125-
for (const arg of node.arguments) {
126-
if (!ts.isStringLiteral(arg) && !ts.isNumericLiteral(arg) && !ts.isSpreadElement(arg)) {
127-
replacements.push(Replacement.insert(arg.getStart(), '$_$twiz.track('));
128-
replacements.push(Replacement.insert(arg.getEnd(), `,${JSON.stringify(fileName)},${arg.getStart()})`));
129-
}
130-
}
131-
}
132-
133-
if (
134-
ts.isPropertyDeclaration(node) &&
135-
ts.isIdentifier(node.name) &&
136-
!node.type &&
137-
!node.initializer &&
138-
!node.decorators
139-
) {
140-
const name = node.name.getText();
141-
const params = [
142-
JSON.stringify(node.name.getText()),
143-
'value',
144-
node.name.getEnd() + (node.questionToken ? 1 : 0),
145-
JSON.stringify(fileName),
146-
JSON.stringify({}),
147-
];
148-
const instrumentExpr = `$_$twiz(${params.join(',')});`;
149-
const preamble = `
150-
get ${name}() { return this._twiz_private_${name}; }
151-
set ${name}(value: any) { ${instrumentExpr} this._twiz_private_${name} = value; }
152-
`;
153-
// we need to remove any readonly modifiers, otherwise typescript will not let us update
154-
// our _twiz_private_... variable inside the setter.
155-
for (const modifier of node.modifiers || []) {
156-
if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) {
157-
replacements.push(Replacement.delete(modifier.getStart(), modifier.getEnd()));
158-
}
159-
}
160-
if (node.getStart() === node.name.getStart()) {
161-
replacements.push(Replacement.insert(node.getStart(), `${preamble} _twiz_private_`));
162-
} else {
163-
replacements.push(Replacement.insert(node.name.getStart(), '_twiz_private_'));
164-
replacements.push(Replacement.insert(node.getStart(), `${preamble}`));
165-
}
166-
}
167-
168-
node.forEachChild((child) => visit(child, replacements, fileName, options, program, semanticDiagnostics));
169-
}
170-
171-
const declaration = `
172-
declare function $_$twiz(name: string, value: any, pos: number, filename: string, opts: any): void;
173-
declare namespace $_$twiz {
174-
function track<T>(value: T, filename: string, offset: number): T;
175-
function track(value: any, filename: string, offset: number): any;
176-
}
177-
`;
178-
17918
export function instrument(source: string, fileName: string, options?: IInstrumentOptions) {
18019
const instrumentOptions: IInstrumentOptions = {
18120
instrumentCallExpressions: false,
18221
instrumentImplicitThis: false,
18322
skipTwizDeclarations: false,
18423
...options,
18524
};
25+
18626
const program: ts.Program | undefined = getProgram(instrumentOptions);
18727
const sourceFile = program
18828
? program.getSourceFile(fileName)
18929
: ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
190-
const replacements = [] as Replacement[];
191-
if (sourceFile) {
192-
const semanticDiagnostics = program ? program.getSemanticDiagnostics(sourceFile) : undefined;
193-
visit(sourceFile, replacements, fileName, instrumentOptions, program, semanticDiagnostics);
194-
}
195-
if (replacements.length && !instrumentOptions.skipTwizDeclarations) {
196-
replacements.push(Replacement.insert(0, declaration));
197-
}
198-
return applyReplacements(source, replacements);
30+
31+
const transformed = transformSourceFile(sourceFile, options, program);
32+
return ts.createPrinter().printFile(transformed);
19933
}

packages/typewiz-core/src/integration.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function typeWiz(input: string, typeCheck = false, options?: IApplyTypesOptions)
5151

5252
if (mockFs.writeFileSync.mock.calls.length) {
5353
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(1);
54-
expect(mockFs.writeFileSync).toHaveBeenCalledWith('c:\\test.ts', expect.any(String));
54+
expect(mockFs.writeFileSync).toHaveBeenCalledWith('c:/test.ts', expect.any(String));
5555
return mockFs.writeFileSync.mock.calls[0][1];
5656
} else {
5757
return null;

packages/typewiz-core/src/transformer.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ describe('transformer', () => {
1010
it('should instrument function parameters without types', () => {
1111
const input = `function (a) { return 5; }`;
1212
expect(transformSourceCode(input, 'test.ts')).toContain(
13-
astPrettyPrint(`function (a) { $_$twiz("a", a, 11, "test.ts", {}); return 5; }`),
13+
astPrettyPrint(`function (a) { $_$twiz("a", a, 11, "test.ts", "{}"); return 5; }`),
1414
);
1515
});
1616

1717
it('should instrument function with two parameters', () => {
1818
const input = `function (a, b) { return 5; }`;
1919
expect(transformSourceCode(input, 'test.ts')).toContain(
20-
astPrettyPrint(`$_$twiz("b", b, 14, "test.ts", {});`).trim(),
20+
astPrettyPrint(`$_$twiz("b", b, 14, "test.ts", "{}");`).trim(),
2121
);
2222
});
2323

2424
it('should instrument class method parameters', () => {
2525
const input = `class Foo { bar(a) { return 5; } }`;
2626
expect(transformSourceCode(input, 'test.ts')).toContain(
27-
astPrettyPrint(`class Foo { bar(a) { $_$twiz("a", a, 17, "test.ts", {}); return 5; } }`),
27+
astPrettyPrint(`class Foo { bar(a) { $_$twiz("a", a, 17, "test.ts", "{}"); return 5; } }`),
2828
);
2929
});
3030

0 commit comments

Comments
 (0)