Skip to content

Commit

Permalink
feat: work on NodeCounter class
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxken committed Feb 3, 2024
1 parent 2cbb59d commit 598338f
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 14 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@nodesecure/estree-ast-utils": "^1.3.1",
"@nodesecure/sec-literal": "^1.2.0",
"estree-walker": "^3.0.1",
"frequency-set": "^1.0.2",
"is-minified-code": "^2.0.0",
"meriyah": "^4.3.3",
"safe-regex": "^2.1.1"
Expand Down
70 changes: 56 additions & 14 deletions src/NodeCounter.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,74 @@
// Import Third-party Dependencies
import FrequencySet from "frequency-set";

// Import Internal Dependencies
import { isNode } from "./utils/index.js";

// eslint-disable-next-line func-style
const noop = (node) => true;

export class NodeCounter {
#count = 0;
lookup = null;

#matchingCount = 0;
#properties = null;
#filterFn = noop;
#matchFn = noop;

/**
* @param {!string} type
* @param {Object} [options]
* @param {(node: any) => boolean} [options.filter]
* @param {(node: any, nc: NodeCounter) => void} [options.match]
*
* @example
* new NodeCounter("FunctionDeclaration");
* new NodeCounter("VariableDeclaration[kind]");
*/
constructor(type, options = {}) {
if (typeof type !== "string") {
throw new TypeError("type must be a string");
}

constructor(searchNode, filterFn = null) {
if (typeof searchNode !== "string") {
throw new TypeError("searchNode must be a string");
const typeResult = /([A-Za-z]+)(\[[a-zA-Z]+\])?/g.exec(type);
if (typeResult === null) {
throw new Error("invalid type argument syntax");
}
this.type = typeResult[1];
this.lookup = typeResult[2]?.slice(1, -1) ?? null;
if (this.lookup) {
this.#properties = new FrequencySet();
}

const [type, property = null] = searchNode.split(".");
this.#filterFn = options.filter ?? noop;
this.#matchFn = options.match ?? noop;
}

this.type = type;
this.property = property;
this.filterFn = filterFn;
get count() {
return this.#matchingCount;
}

get nodes() {
return this.#count;
get properties() {
return Object.fromEntries(
this.#properties?.entries() ?? []
);
}

walk(node) {
if (!node || node.type !== this.type) {
if (!isNode(node) || node.type !== this.type) {
return;
}

if (this.filterFn && !this.filterFn()) {
if (!this.#filterFn(node)) {
return;
}

this.#count++;
this.#matchingCount++;
if (this.lookup === null) {
this.#matchFn(node, this);
}
else if (this.lookup in node) {
this.#properties.add(node[this.lookup]);
this.#matchFn(node, this);
}
}
}
1 change: 1 addition & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./isUnsafeCallee.js";
export * from "./notNullOrUndefined.js";
export * from "./rootLocation.js";
export * from "./toArrayLocation.js";
export * from "./isNode.js";
5 changes: 5 additions & 0 deletions src/utils/isNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function isNode(value) {
return (
value !== null && typeof value === "object" && "type" in value && typeof value.type === "string"
);
}
101 changes: 101 additions & 0 deletions test/NodeCounter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Import Node.js Dependencies
import { describe, it } from "node:test";
import assert from "node:assert/strict";

// Import Third-party Dependencies
import { walk } from "estree-walker";

// Import Internal Dependencies
import { NodeCounter } from "../src/NodeCounter.js";
import { JsSourceParser } from "../src/JsSourceParser.js";
import { isNode } from "../src/utils/index.js";

describe("NodeCounter", () => {
it("should count one for a FunctionDeclaration with an identifier", () => {
const ids = [];
const nc = new NodeCounter(
"FunctionDeclaration",
{
match: (node) => ids.push(node.id.name)
}
);
assert.equal(nc.type, "FunctionDeclaration");
assert.equal(nc.lookup, null);

const body = new JsSourceParser().parse(
"function foo() {};"
);
walkAst(body, (node) => nc.walk(node));

assert.equal(nc.count, 1);
assert.deepEqual(nc.properties, {});
assert.deepEqual(ids, ["foo"]);
});

it("should count zero for a FunctionExpression with no identifier", () => {
const nc = new NodeCounter(
"FunctionExpression",
{
filter: (node) => isNode(node.id) && node.id.type === "Identifier"
}
);
assert.equal(nc.type, "FunctionExpression");
assert.equal(nc.lookup, null);

const body = new JsSourceParser().parse(
"const foo = function() {};"
);
walkAst(body, (node) => nc.walk(node));

assert.equal(nc.count, 0);
assert.deepEqual(nc.properties, {});
});

it("should count VariableDeclaration kinds property", () => {
const nc = new NodeCounter(
"VariableDeclaration[kind]"
);
assert.equal(nc.type, "VariableDeclaration");
assert.equal(nc.lookup, "kind");

const body = new JsSourceParser().parse(
`let foo, xd = 5;
const yo = 2;
const mdr = 5;`
);
walkAst(body, (node) => nc.walk(node));

assert.equal(nc.count, 3);
assert.deepEqual(nc.properties, {
let: 1,
const: 2
});
});

it("should count MemberExpression computed property", () => {
const nc = new NodeCounter(
"MemberExpression[computed]"
);

const body = new JsSourceParser().parse(
"yoo.xd[\"damn\"].oh;"
);
walkAst(body, (node) => nc.walk(node));

assert.equal(nc.count, 3);
assert.deepEqual(nc.properties, {
true: 1,
false: 2
});
});
});

function walkAst(body, callback = () => void 0) {
walk(body, {
enter(node) {
if (!Array.isArray(node)) {
callback(node);
}
}
});
}

0 comments on commit 598338f

Please sign in to comment.