-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
164 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
}); | ||
} |