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

Commit

Permalink
feat(core): implement instrumentation as a TypeScript Compiler Transf…
Browse files Browse the repository at this point in the history
…ormer

close #41
  • Loading branch information
urish authored Jun 21, 2018
1 parent bf3239d commit 87f243a
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 190 deletions.
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"packages/*"
],
"useWorkspaces": true,
"version": "1.0.3"
"version": "1.1.0-0"
}
2 changes: 1 addition & 1 deletion packages/typewiz-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "typewiz-core",
"version": "1.0.2",
"version": "1.1.0-0",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"repository": "https://github.com/urish/typewiz",
Expand Down
3 changes: 2 additions & 1 deletion packages/typewiz-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './apply-types';
export * from './configuration-parser';
export * from './compiler-helper';
export * from './configuration-parser';
export * from './instrument';
export * from './transformer';
export * from './type-collector';
export * from './type-coverage';
31 changes: 24 additions & 7 deletions packages/typewiz-core/src/instrument.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import * as ts from 'typescript';
import { instrument } from './instrument';

function astPrettyPrint(sourceText: string) {
const printer: ts.Printer = ts.createPrinter();
return printer.printFile(ts.createSourceFile('test.ts', sourceText, ts.ScriptTarget.Latest));
}

describe('instrument', () => {
it('should instrument function parameters without types', () => {
const input = `function (a) { return 5; }`;
expect(instrument(input, 'test.ts')).toMatch(`function (a) {$_$twiz("a",a,11,"test.ts",{}); return 5; }`);
expect(instrument(input, 'test.ts')).toContain(
astPrettyPrint(`function (a) { $_$twiz("a", a, 11, "test.ts", "{}"); return 5; }`),
);
});

it('should correctly instrument optional function parameters', () => {
const input = `function (a?) { return 5; }`;
expect(instrument(input, 'test.ts')).toMatch(`function (a?) {$_$twiz("a",a,12,"test.ts",{}); return 5; }`);
it('should instrument function with two parameters', () => {
const input = `function (a, b) { return 5; }`;
expect(instrument(input, 'test.ts')).toContain(astPrettyPrint(`$_$twiz("b", b, 14, "test.ts", "{}");`).trim());
});

it('should instrument class method parameters', () => {
const input = `class Foo { bar(a) { return 5; } }`;
expect(instrument(input, 'test.ts')).toMatch(
`class Foo { bar(a) {$_$twiz("a",a,17,"test.ts",{}); return 5; } }`,
expect(instrument(input, 'test.ts')).toContain(
astPrettyPrint(`class Foo { bar(a) { $_$twiz("a", a, 17, "test.ts", "{}"); return 5; } }`),
);
});

Expand All @@ -28,6 +36,15 @@ describe('instrument', () => {
expect(instrument(input, 'test.ts')).toMatch(`function (a = 12) { return a; }`);
});

it('should add typewiz declarations', () => {
const input = `function (a) { return 5; }`;
expect(instrument(input, 'test.ts')).toContain(
astPrettyPrint(
`declare function $_$twiz(name: string, value: any, pos: number, filename: string, opts: any): void`,
),
);
});

describe('instrumentCallExpressions', () => {
it('should instrument function calls', () => {
const input = `foo(bar)`;
Expand All @@ -36,7 +53,7 @@ describe('instrument', () => {
instrumentCallExpressions: true,
skipTwizDeclarations: true,
}),
).toMatch(`foo($_$twiz.track(bar,"test.ts",4))`);
).toMatch(`foo($_$twiz.track(bar, "test.ts", 4))`);
});

it('should not instrument numeric arguments in function calls', () => {
Expand Down
176 changes: 5 additions & 171 deletions packages/typewiz-core/src/instrument.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as ts from 'typescript';

import { getProgram, ICompilerOptions } from './compiler-helper';
import { applyReplacements, Replacement } from './replacement';
import { transformSourceFile } from './transformer';

export interface IInstrumentOptions extends ICompilerOptions {
instrumentCallExpressions?: boolean;
Expand All @@ -16,184 +15,19 @@ export interface IExtraOptions {
thisNeedsComma?: boolean;
}

function hasParensAroundArguments(node: ts.FunctionLike) {
if (ts.isArrowFunction(node)) {
return (
node.parameters.length !== 1 ||
node
.getText()
.substr(0, node.equalsGreaterThanToken.getStart() - node.getStart())
.includes('(')
);
} else {
return true;
}
}

function visit(
node: ts.Node,
replacements: Replacement[],
fileName: string,
options: IInstrumentOptions,
program?: ts.Program,
semanticDiagnostics?: ReadonlyArray<ts.Diagnostic>,
) {
const isArrow = ts.isArrowFunction(node);
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
if (node.body) {
const needsThisInstrumentation =
options.instrumentImplicitThis &&
program &&
semanticDiagnostics &&
semanticDiagnostics.find((diagnostic) => {
if (
diagnostic.code === 2683 &&
diagnostic.file &&
diagnostic.file.fileName === node.getSourceFile().fileName &&
diagnostic.start
) {
if (node.body && ts.isBlock(node.body)) {
const body = node.body as ts.FunctionBody;
return (
body.statements.find((statement) => {
return (
diagnostic.start !== undefined &&
statement.pos <= diagnostic.start &&
diagnostic.start <= statement.end
);
}) !== undefined
);
} else {
const body = node.body as ts.Expression;
return body.pos <= diagnostic.start && diagnostic.start <= body.end;
}
}
return false;
}) !== undefined;
if (needsThisInstrumentation) {
const opts: IExtraOptions = { thisType: true };
if (node.parameters.length > 0) {
opts.thisNeedsComma = true;
}
const params = [
JSON.stringify('this'),
'this',
node.parameters.pos,
JSON.stringify(fileName),
JSON.stringify(opts),
];
const instrumentExpr = `$_$twiz(${params.join(',')})`;

replacements.push(Replacement.insert(node.body.getStart() + 1, `${instrumentExpr};`));
}
}

const isShortArrow = ts.isArrowFunction(node) && !ts.isBlock(node.body);
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()];
}
const params = [
JSON.stringify(param.name.getText()),
param.name.getText(),
typeInsertionPos,
JSON.stringify(fileName),
JSON.stringify(opts),
];
const instrumentExpr = `$_$twiz(${params.join(',')})`;
if (isShortArrow) {
replacements.push(Replacement.insert(node.body.getStart(), `(${instrumentExpr},`));
replacements.push(Replacement.insert(node.body.getEnd(), `)`, 10));
} else {
replacements.push(Replacement.insert(node.body.getStart() + 1, `${instrumentExpr};`));
}
}
}
}

if (
options.instrumentCallExpressions &&
ts.isCallExpression(node) &&
node.expression.getText() !== 'require.context'
) {
for (const arg of node.arguments) {
if (!ts.isStringLiteral(arg) && !ts.isNumericLiteral(arg) && !ts.isSpreadElement(arg)) {
replacements.push(Replacement.insert(arg.getStart(), '$_$twiz.track('));
replacements.push(Replacement.insert(arg.getEnd(), `,${JSON.stringify(fileName)},${arg.getStart()})`));
}
}
}

if (
ts.isPropertyDeclaration(node) &&
ts.isIdentifier(node.name) &&
!node.type &&
!node.initializer &&
!node.decorators
) {
const name = node.name.getText();
const params = [
JSON.stringify(node.name.getText()),
'value',
node.name.getEnd() + (node.questionToken ? 1 : 0),
JSON.stringify(fileName),
JSON.stringify({}),
];
const instrumentExpr = `$_$twiz(${params.join(',')});`;
const preamble = `
get ${name}() { return this._twiz_private_${name}; }
set ${name}(value: any) { ${instrumentExpr} this._twiz_private_${name} = value; }
`;
// we need to remove any readonly modifiers, otherwise typescript will not let us update
// our _twiz_private_... variable inside the setter.
for (const modifier of node.modifiers || []) {
if (modifier.kind === ts.SyntaxKind.ReadonlyKeyword) {
replacements.push(Replacement.delete(modifier.getStart(), modifier.getEnd()));
}
}
if (node.getStart() === node.name.getStart()) {
replacements.push(Replacement.insert(node.getStart(), `${preamble} _twiz_private_`));
} else {
replacements.push(Replacement.insert(node.name.getStart(), '_twiz_private_'));
replacements.push(Replacement.insert(node.getStart(), `${preamble}`));
}
}

node.forEachChild((child) => visit(child, replacements, fileName, options, program, semanticDiagnostics));
}

const declaration = `
declare function $_$twiz(name: string, value: any, pos: number, filename: string, opts: any): void;
declare namespace $_$twiz {
function track<T>(value: T, filename: string, offset: number): T;
function track(value: any, filename: string, offset: number): any;
}
`;

export function instrument(source: string, fileName: string, options?: IInstrumentOptions) {
const instrumentOptions: IInstrumentOptions = {
instrumentCallExpressions: false,
instrumentImplicitThis: false,
skipTwizDeclarations: false,
...options,
};

const program: ts.Program | undefined = getProgram(instrumentOptions);
const sourceFile = program
? program.getSourceFile(fileName)
: ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
const replacements = [] as Replacement[];
if (sourceFile) {
const semanticDiagnostics = program ? program.getSemanticDiagnostics(sourceFile) : undefined;
visit(sourceFile, replacements, fileName, instrumentOptions, program, semanticDiagnostics);
}
if (replacements.length && !instrumentOptions.skipTwizDeclarations) {
replacements.push(Replacement.insert(0, declaration));
}
return applyReplacements(source, replacements);

const transformed = transformSourceFile(sourceFile, options, program);
return ts.createPrinter().printFile(transformed);
}
2 changes: 1 addition & 1 deletion packages/typewiz-core/src/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function typeWiz(input: string, typeCheck = false, options?: IApplyTypesOptions)

if (mockFs.writeFileSync.mock.calls.length) {
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(1);
expect(mockFs.writeFileSync).toHaveBeenCalledWith('c:\\test.ts', expect.any(String));
expect(mockFs.writeFileSync).toHaveBeenCalledWith('c:/test.ts', expect.any(String));
return mockFs.writeFileSync.mock.calls[0][1];
} else {
return null;
Expand Down
Loading

0 comments on commit 87f243a

Please sign in to comment.