Skip to content

Commit

Permalink
Merge pull request #43 from roypeled/refactor-code-parser-to-ast-from…
Browse files Browse the repository at this point in the history
…-regex

Refactor code parser to ast from regex
  • Loading branch information
roypeled authored Jul 11, 2024
2 parents bf15b02 + 9b5b08a commit bc819e4
Show file tree
Hide file tree
Showing 8 changed files with 6,232 additions and 4,243 deletions.
10,285 changes: 6,085 additions & 4,200 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@typestrong/ts-mockito",
"version": "2.6.6",
"version": "2.6.7",
"description": "Mocking library for TypeScript",
"main": "lib/ts-mockito.js",
"typings": "lib/ts-mockito",
Expand Down Expand Up @@ -65,6 +65,7 @@
"typescript": "^4.7.4"
},
"dependencies": {
"@babel/parser": "^7.24.7",
"lodash": "^4.17.5",
"safe-json-stringify": "^1.2.0"
},
Expand Down
11 changes: 5 additions & 6 deletions src/Mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,11 @@ export class Mocker {

private processFunctionsCode(object: any): void {
this.objectInspector.getObjectPrototypes(object).forEach((obj: any) => {
this.objectInspector.getObjectOwnPropertyNames(obj).forEach((propertyName: string) => {
const functionNames = this.mockableFunctionsFinder.find(this.objectPropertyCodeRetriever.get(obj, propertyName));
functionNames.forEach((functionName: string) => {
this.createMethodStub(functionName);
this.createInstanceActionListener(functionName, this.clazz.prototype);
});
const fullClass = this.objectPropertyCodeRetriever.getObject(obj);
const functionNames = this.mockableFunctionsFinder.find(fullClass);
functionNames.forEach((functionName: string) => {
this.createMethodStub(functionName);
this.createInstanceActionListener(functionName, this.clazz.prototype);
});
});
}
Expand Down
107 changes: 99 additions & 8 deletions src/utils/MockableFunctionsFinder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,96 @@
import {parse} from "@babel/parser";
import * as _babel_types from "@babel/types";

type FunctionNode = |
_babel_types.ObjectMethod |
_babel_types.ClassMethod |
_babel_types.ClassPrivateMethod |
_babel_types.FunctionDeclaration |
_babel_types.ClassProperty |
_babel_types.Expression |
_babel_types.FunctionExpression;

const methodTokenName = new Set([
"ObjectMethod",
"ClassMethod",
"ClassPrivateMethod",
"FunctionDeclaration",
"FunctionExpression"
]);

const isFunctionNode = (node: _babel_types.Statement | FunctionNode): node is FunctionNode => methodTokenName.has(node.type);


function getAssignmentName(node: _babel_types.LVal) {
if (node.type === "Identifier")
return node.name;

if (node.type === "MemberExpression") {
const prop = node.property;
if (prop.type === 'Identifier') return prop.name;
if (prop.type === 'PrivateName') return prop.id.name;
return null;
}

return null;
}

function handleClassProp(node: _babel_types.ClassProperty): string {
if (node.value.type !== 'ArrowFunctionExpression' && node.value.type !== 'FunctionExpression') return null;

if('name' in node.key) return node.key.name;
if('value' in node.key) return node.key.value.toString();
return null;
}

function handleExpression(node: _babel_types.Expression): string {
if ('expression' in node && typeof node.expression !== 'boolean') return handleExpression(node.expression);

if (node.type === 'AssignmentExpression') {
return getAssignmentName(node.left);
}
}

function handleVariable(node: _babel_types.VariableDeclaration): string[] {
return node.declarations.filter(n => {
if (n.init.type === 'ArrowFunctionExpression') return true;
if (n.init.type === 'FunctionExpression') return true;
return false;
}).map(n => getAssignmentName(n.id));
}

function extractFunctionNames(nodes: (_babel_types.Statement | FunctionNode)[]) {
let names = [] as string[];
nodes.forEach(node => {
if (isFunctionNode(node)) {
if ('key' in node) {
if ('name' in node.key)
names.push(node.key.name);
if ('value' in node.key)
names.push(node.key.value.toString());
}
if ('id' in node && node.id) names.push(node.id.name);
}
if ('body' in node) {
names = [...extractFunctionNames(Array.isArray(node.body) ? node.body as _babel_types.Statement[] : [node.body as _babel_types.Statement]), ...names];
}

if (node.type === "ExpressionStatement") {
names = [handleExpression(node.expression), ...names];
}

if (node.type === "VariableDeclaration") {
names = [...handleVariable(node), ...names];
}

if (node.type === "ClassProperty") {
names = [handleClassProp(node), ...extractFunctionNames([node.value]), ...names];
}
});

return names;
}

/**
* Looking for all function calls and declarations and provides an array of their names. The mechanism is greedy
* and tries to match as many function names as it can find and not only those of inspecting class.
Expand All @@ -9,18 +102,16 @@
* - [.]functionName = function otherName(
*/
export class MockableFunctionsFinder {
private functionNameRegex = /[.\s]([^.\s]+?)(?:\(|\s+=\s+(?:function\s*(?:[^.\s]+?\s*)?)?\()/g;
private cleanFunctionNameRegex = /^[.\s]([^.\s]+?)[\s(]/;
private excludedFunctionNames: string[] = ["hasOwnProperty", "function"];
private excludedFunctionNames = new Set(["hasOwnProperty", "function"]);

public find(code: string): string[] {
return (code.match(this.functionNameRegex) || [])
.map((match: string) => match.match(this.cleanFunctionNameRegex)[1])
.filter((functionName: string) => this.isMockable(functionName))
.map((functionName: string) => functionName.indexOf('=') > 0 ? functionName.substr(0, functionName.indexOf('=')) : functionName);
const ast = parse(code);
const names = extractFunctionNames(ast.program.body);
return names
.filter((functionName: string) => this.isMockable(functionName));
}

private isMockable(name: string): boolean {
return this.excludedFunctionNames.indexOf(name) < 0;
return !this.excludedFunctionNames.has(name);
}
}
1 change: 1 addition & 0 deletions src/utils/ObjectInspector.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as _ from "lodash";


export class ObjectInspector {
public getObjectPrototypes(prototype: any): any[] {
const prototypes: any[] = [];
Expand Down
41 changes: 28 additions & 13 deletions src/utils/ObjectPropertyCodeRetriever.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
export class ObjectPropertyCodeRetriever {
public get(object: any, propertyName: string): string {
const descriptor = Object.getOwnPropertyDescriptor(object, propertyName);
if (!descriptor) {
// property is defined in prototype but has no descriptor (it comes from abstract class and was not override)
return "";
public getObject(object: any) {
const props = Object.getOwnPropertyNames(object);
return `class Prototype {
${props.map(prop => {
let result = '';
const descriptor = Object.getOwnPropertyDescriptor(object, prop);
if (descriptor.get) {
result += `
${descriptor.get.toString()}
`;
}
if (descriptor.set) {
result += `
${descriptor.set.toString()}
`;
}
if (!descriptor.get && !descriptor.set && typeof object[prop] === 'function') {
const propName = prop === 'constructor' ? 'mock_constructor' : prop;
result += `
${propName} = ${String(object[prop])}
`;
}
return result;
}).join(`
`)}
}
const accessorsCodes = [];
if (descriptor.get) {
accessorsCodes.push(descriptor.get.toString());
}
if (descriptor.set) {
accessorsCodes.push(descriptor.set.toString());
}
return accessorsCodes.join(" ") || String(object[propertyName]);
`;
}

}
9 changes: 4 additions & 5 deletions test/utils/MockableFunctionsFinder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ describe("MockableFunctionsFinder", () => {
const result = new MockableFunctionsFinder().find(code);

// then
expect(result).toContain("log");
expect(result).toContain("toString");
expect(result).toContain("anonymousMethod");
expect(result).toContain("convertNumberToString");
});

it("should not find hasOwnProperty as it should not be mocked (because its used by mockito to evaluate properties)", () => {
Expand All @@ -30,15 +29,15 @@ describe("MockableFunctionsFinder", () => {

function getSampleCode(): string {
return `
export class Foo {
constructor (private temp:string) {
class Foo {
constructor (temp) {
this.anonymousMethod = function(arg) {
console.log(arg);
temp.hasOwnProperty("fakeProperty");
}
}
private convertNumberToString(value:number):string {
convertNumberToString(value) {
return value.toString();
}
}
Expand Down
18 changes: 8 additions & 10 deletions test/utils/ObjectPropertyCodeRetriever.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,20 @@ describe("ObjectPropertyCodeRetriever", () => {
};
});

it("Provides code of given existing property", () => {
expect(objectPropertyCodeRetriever.get(object, "undefinedProperty")).toBe("undefined");
expect(objectPropertyCodeRetriever.get(object, "nullProperty")).toBe("null");
expect(objectPropertyCodeRetriever.get(object, "nanProperty")).toBe("NaN");
expect(objectPropertyCodeRetriever.get(object, "stringProperty")).toBe("stringProperty");
expect(objectPropertyCodeRetriever.get(object, "booleanProperty")).toBe("true");
expect(objectPropertyCodeRetriever.get(object, "testMethod")).toMatch(/function \(\)/);
it("Provides code of given existing property method", () => {
const objStr = objectPropertyCodeRetriever.getObject(object);
expect(objStr).toContain('testMethod = function () { return true; }');
});

it("Provides code of given existing property accessors", () => {
expect(objectPropertyCodeRetriever.get(object, "someValue")).toMatch(/return "someValue"/);
expect(objectPropertyCodeRetriever.get(object, "someValue")).toMatch(/console\.info\("someValue set"\)/);
const objStr = objectPropertyCodeRetriever.getObject(object);
expect(objStr).toMatch(/get someValue\(\) \{\s*return "someValue";\s*}/);
expect(objStr).toMatch(/set someValue\(newValue\) \{\s*console.info\("someValue set"\);\s*}/);
});

it("Returns empty string when checking non existent property", () => {
expect(objectPropertyCodeRetriever.get(object, "nonExistentProperty")).toBe("");
const objStr = objectPropertyCodeRetriever.getObject(object);
expect(objStr).not.toContain("nonExistentProperty");
});
});
});

0 comments on commit bc819e4

Please sign in to comment.