Skip to content

Commit

Permalink
Fix hoisting
Browse files Browse the repository at this point in the history
  • Loading branch information
eoftedal committed Mar 8, 2024
1 parent dd76b23 commit 0a93045
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 68 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [1.0.0-beta.16] - 2024-03-08

### Bugfix

* Fixing binding issues for hoisting

## [1.0.0-beta.15] - 2024-02-24

### Fixing
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "astronomical",
"version": "1.0.0-beta.15",
"version": "1.0.0-beta.16",
"description": "offers a way to query a Javascript AST to find specific patterns using a syntax somewhat similar to XPath.",
"scripts": {
"lint": "eslint . --ext .ts --fix --ignore-path .gitignore",
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ function createQuerier() {
}

function beginHandle<T extends Record<string, QNode>>(queries: T, path: ASTNode) : Record<keyof T, Result[]> {
const rootPath: NodePath = createNodePath(path, undefined, undefined, undefined);
const rootPath: NodePath = createNodePath(path, undefined, undefined, undefined, undefined);
return travHandle(queries, rootPath);
}
return {
Expand Down
28 changes: 16 additions & 12 deletions src/nodeutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export function isPrimitive(value: unknown) : value is PrimitiveValue {
return typeof value == "string" || typeof value == "number" || typeof value == "boolean";
}

export function isUpdateExpression(value: unknown) : value is ESTree.UpdateExpression {
return isNode(value) && value.type === "UpdateExpression";
}

export function isAssignmentExpression(node: ESTree.Node): node is ESTree.AssignmentExpression {
return node.type === "AssignmentExpression";
}
Expand All @@ -40,7 +44,9 @@ export function isFunctionExpression(node: ESTree.Node): node is ESTree.Function
export function isVariableDeclarator(node: ESTree.Node): node is ESTree.VariableDeclarator {
return node.type === "VariableDeclarator";
}

export function isVariableDeclaration(node: ESTree.Node): node is ESTree.VariableDeclaration {
return node.type === "VariableDeclaration";
}
export function isBinding(node: ESTree.Node, parentNode: ESTree.Node, grandParentNode: ESTree.Node | undefined): boolean {
if (
grandParentNode &&
Expand All @@ -52,17 +58,15 @@ export function isBinding(node: ESTree.Node, parentNode: ESTree.Node, grandParen
}

const keys: string[] = bindingIdentifiersKeys[parentNode.type] ?? [];
if (keys) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const val =
// @ts-expect-error key must present in parent
parentNode[key];
if (Array.isArray(val)) {
if (val.indexOf(node) >= 0) return true;
} else {
if (val === node) return true;
}
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const val =
// @ts-expect-error key must present in parent
parentNode[key];
if (Array.isArray(val)) {
if (val.indexOf(node) >= 0) return true;
} else {
if (val === node) return true;
}
}

Expand Down
128 changes: 76 additions & 52 deletions src/traverse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VISITOR_KEYS, isAssignmentExpression, isBinding, isIdentifier, isMemberExpression, isNode, isPrimitive, isScopable, isScope } from "./nodeutils";
import { VISITOR_KEYS, isAssignmentExpression, isBinding, isExportSpecifier, isFunctionDeclaration, isFunctionExpression, isIdentifier, isMemberExpression, isNode, isPrimitive, isScopable, isScope, isUpdateExpression, isVariableDeclaration, isVariableDeclarator } from "./nodeutils";
import { ESTree } from "meriyah";
import { isDefined, toArray } from "./utils";
import { PrimitiveValue } from ".";
Expand All @@ -25,6 +25,7 @@ const scopes: Array<Scope | number> = new Array(100000);
export type ASTNode = ESTree.Node & {
extra?: {
scopeId?: number;
functionScopeId?: number;
nodePath?: NodePath;
}
};
Expand All @@ -35,6 +36,7 @@ export type NodePath = {
parentPath?: NodePath;
parentKey?: string;
scopeId: number;
functionScopeId: number;
};

type Visitor<T> = {
Expand All @@ -45,7 +47,7 @@ type Visitor<T> = {
export default function createTraverser() {
let scopeIdCounter = 0;
let removedScopes = 0;
//const nodePathsCreated: Record<string, number> = {}
const nodePathsCreated: Record<string, number> = {}

function createScope(parentScopeId?: number): number {
const id = scopeIdCounter++;
Expand Down Expand Up @@ -82,17 +84,15 @@ export default function createTraverser() {
scope.bindings[name] = binding;
}



let pathsCreated = 0;

function getChildren(key: string, path: NodePath) : NodePath[] {
if (key in path.node) {
const r = (path.node as unknown as Record<string, unknown>)[key];
if (Array.isArray(r)) {
return r.map((n, i) => createNodePath(n, i.toString(), key, path.scopeId, path));
return r.map((n, i) => createNodePath(n, i, key, path.scopeId, path.functionScopeId, path));
} else if (r != undefined) {
return [createNodePath(r as ASTNode, key, key, path.scopeId, path)];
return [createNodePath(r as ASTNode, key, key, path.scopeId, path.functionScopeId, path)];
}
}
return [];
Expand All @@ -111,97 +111,119 @@ export default function createTraverser() {
return r.map((n, i) =>
isPrimitive(n) ? n :
// isLiteral(n) ? n.value as PrimitiveValue :
createNodePath(n, i.toString(), key, path.scopeId, path));
createNodePath(n, i, key, path.scopeId, path.functionScopeId, path));
} else if (r != undefined) {
return [
isPrimitive(r) ? r :
// isLiteral(r) ? r.value as PrimitiveValue :
createNodePath(r as ASTNode, key, key, path.scopeId, path)
createNodePath(r as ASTNode, key, key, path.scopeId, path.functionScopeId, path)
];
}
}
return [];
}


function createNodePath(node: ASTNode, key: string | undefined, parentKey: string | undefined, scopeId: number | undefined, nodePath?: NodePath) : NodePath {
function createNodePath(node: ASTNode, key: string | undefined | number, parentKey: string | undefined, scopeId: number | undefined, functionScopeId: number | undefined, nodePath?: NodePath) : NodePath {
if (node.extra?.nodePath) {
const path = node.extra.nodePath;
path.key = key;
path.parentKey = parentKey;
path.parentPath = nodePath;
if (nodePath && isExportSpecifier(nodePath.node) && key == "exported" && path.key == "local") {
//Special handling for "export { someName }" as id is both local and exported
path.key = "exported";
path.parentPath = nodePath;
return path;
}
if (key != undefined) path.key = typeof(key) == "number" ? key.toString() : key;
if (parentKey != undefined) path.parentKey = parentKey;
if (nodePath != undefined) path.parentPath = nodePath;

return path;
}
const finalScope: number = ((node.extra && node.extra["scopeId"]) ? node.extra["scopeId"] as number : scopeId) ?? createScope();

const path = {

const finalScope: number = ((node.extra && node.extra.scopeId != undefined) ? node.extra.scopeId : scopeId) ?? createScope();
const finalFScope: number = ((node.extra && node.extra.functionScopeId != undefined) ? node.extra.functionScopeId : functionScopeId) ?? finalScope;
const path: NodePath = {
node,
scopeId: finalScope,
functionScopeId: finalFScope,
parentPath: nodePath,
key,
key: typeof(key) == "number" ? key.toString() : key,
parentKey
}
if (isNode(node)) {
node.extra = node.extra ?? {};
node.extra.nodePath = path;
Object.defineProperty(node.extra, "nodePath", { enumerable: false });
}
/*const x: string| undefined = node.type;
if (x == undefined) {
console.log("x", node, key, parentKey, nodePath?.node?.type);
}*/
//nodePathsCreated[node.type] = (nodePathsCreated[node.type] ?? 0) + 1;
nodePathsCreated[node.type] = (nodePathsCreated[node.type] ?? 0) + 1;
pathsCreated++;
return path;
}




function registerBinding(node: ASTNode, parentNode: ASTNode, grandParentNode: ASTNode | undefined, scopeId: number) {
function registerBinding(stack: ASTNode[], scopeId: number, functionScopeId: number, key: string | number, parentKey: string) {
//console.log("x registerBinding?", isIdentifier(node) ? node.name : node.type, parentNode.type, grandParentNode?.type, scopeId, isBinding(node, parentNode, grandParentNode));
if (isBinding(node, parentNode, grandParentNode) ) {
if (isIdentifier(node) && !isAssignmentExpression(parentNode) && !isMemberExpression(parentNode)) {
//console.log("x registerBinding!", node.name, parentNode.type, grandParentNode?.type, scopeId);
//A bit of a hack here as well. Needs some further investigation
if (isScope(node, parentNode)) {
setBinding(scopeId, node.name, { path: createNodePath(node, undefined, undefined, scopeId) });
} else {
setBinding(scopeId, node.name, { path: createNodePath(parentNode, undefined, undefined, scopeId) });
}
const node = stack[stack.length - 1];
if (!isIdentifier(node)) return;
const parentNode = stack[stack.length - 2];
if (isAssignmentExpression(parentNode) || isMemberExpression(parentNode) || isUpdateExpression(parentNode) || isExportSpecifier(parentNode)) return;
const grandParentNode = stack[stack.length - 3];
if (!isBinding(node, parentNode, grandParentNode)) return;

if (key == "id" && !isVariableDeclarator(parentNode)) {
setBinding(functionScopeId, node.name, { path: createNodePath(node, undefined, undefined, scopeId, functionScopeId) });
return;
}
if (isVariableDeclarator(parentNode) && isVariableDeclaration(grandParentNode)) {
if (grandParentNode.kind == "var") {
setBinding(functionScopeId, node.name, { path: createNodePath(parentNode, undefined, undefined, scopeId, functionScopeId) });
return;
} else {
setBinding(scopeId, node.name, { path: createNodePath(parentNode, undefined, undefined, scopeId, functionScopeId) });
return;
}
}

if (isScope(node, parentNode)) {
setBinding(scopeId, node.name, { path: createNodePath(node, key, parentKey, scopeId, functionScopeId) });
} /*else {
console.log(node.type, parentNode.type, grandParentNode?.type);
}*/
}



let bindingNodesVisited = 0;
function registerBindings(node: ASTNode, parentNode: ASTNode, grandParentNode: ASTNode | undefined, scopeId: number) {
function registerBindings(stack: ASTNode[], scopeId: number, functionScopeId: number) {
const node = stack[stack.length - 1];
if (!isNode(node)) return
if (node.extra?.scopeId != undefined) return;
node.extra = node.extra ?? {};
if (node.extra["scopeId"] != undefined) return;
node.extra["scopeId"] = scopeId;
bindingNodesVisited++;;
node.extra.scopeId = scopeId;
bindingNodesVisited++;
const keys = VISITOR_KEYS[node.type];
//console.log(keys, node);
if (keys.length == 0) return;

let childScopeId = scopeId;
// This is also buggy. Need to investigate what creates a new scope
if (isScopable(node)) {
childScopeId = createScope(scopeId);
}
for (const key of keys) {
const childNodes = node[key as keyof ASTNode];
const children = toArray(childNodes).filter(isDefined);
children.forEach((child) => {
if (isNode(child)) {
// This feels like a hack. Need to figure out how to make this work
// for other types of scopes as well (classes, etc.)
const s = key == "id" ? scopeId : childScopeId;
registerBinding(child, node, parentNode, s);
registerBindings(child, node, parentNode, s);
children.forEach((child, i) => {
if (!isNode(child)) return;
const f = key == "body" && (isFunctionDeclaration(node) || isFunctionExpression(node)) ? childScopeId : functionScopeId;
stack.push(child);
if (isIdentifier(child)) {
const k = Array.isArray(childNodes) ? i : key;
registerBinding(stack, childScopeId, f, k, key);
} else {
registerBindings(stack, childScopeId, f);
}
stack.pop();
});
}
if (childScopeId != scopeId && typeof scopes[childScopeId] == "number") { // Scope has not been populated
Expand All @@ -213,27 +235,28 @@ export default function createTraverser() {
function traverseInner<T>(
node: ASTNode,
visitor: Visitor<T>,
scopeId: number | undefined,
scopeId: number | undefined,
functionScopeId: number | undefined,
state: T,
path?: NodePath
) {
const nodePath = path ?? createNodePath(node, undefined, undefined, scopeId);
const nodePath = path ?? createNodePath(node, undefined, undefined, scopeId, functionScopeId);
const keys = VISITOR_KEYS[node.type] ?? [];

if (nodePath.parentPath) registerBindings(nodePath.node, nodePath.parentPath.node, nodePath.parentPath.parentPath?.node, nodePath.scopeId);
if (nodePath.parentPath) registerBindings([nodePath.parentPath.parentPath?.node, nodePath.parentPath.node, nodePath.node].filter(isDefined), nodePath.scopeId, nodePath.functionScopeId);

for (const key of keys) {
const childNodes = node[key as keyof ASTNode];
const children = Array.isArray(childNodes) ? childNodes : childNodes ? [childNodes] : [];
const nodePaths = children.map((child, i) => {
if (isNode(child)) {
return createNodePath(child, key, Array.isArray(childNodes) ? i.toString() : key, nodePath.scopeId, nodePath);
return createNodePath(child, Array.isArray(childNodes) ? i : key, key, nodePath.scopeId, nodePath.functionScopeId, nodePath);
}
return undefined;
}).filter(x => x != undefined) as NodePath[];
nodePaths.forEach((childPath) => {
visitor.enter(childPath, state);
traverseInner(childPath.node, visitor, nodePath.scopeId, state, childPath);
traverseInner(childPath.node, visitor, nodePath.scopeId, nodePath.functionScopeId, state, childPath);
visitor.exit(childPath, state);
});
}
Expand All @@ -246,12 +269,13 @@ export default function createTraverser() {
scopeId: number | undefined,
state: T,
path?: NodePath) {
traverseInner(node, visitor, scopeId, state, path);
const fscope = path?.functionScopeId ?? node.extra?.functionScopeId ?? scopeId;
traverseInner(node, visitor, scopeId, fscope, state, path);
if (!sOut.includes(scopeIdCounter)) {
log.debug("Scopes created", scopeIdCounter, " Scopes removed", removedScopes, "Paths created", pathsCreated, bindingNodesVisited);
sOut.push(scopeIdCounter);
//const k = Object.fromEntries(Object.entries(nodePathsCreated).sort((a, b) => b[1] - a[1]));
//console.log("Node paths created", k);
const k = Object.fromEntries(Object.entries(nodePathsCreated).sort((a, b) => a[1] - b[1]));
log.debug("Node paths created", k);
}


Expand Down
11 changes: 11 additions & 0 deletions tests/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,17 @@ describe('testing index file', () => {
expect(nodes).toEqual([1]);
});

test("should hoist variables to function scope", () => {
const code = `
function a() {
for(var x=0,u=22; x<10; x++) {
}
return u;
}
`
const nodes = query(code, "//ReturnStatement/$:argument/:init/:value");
expect(nodes).toEqual([22]);
})

});

Expand Down

0 comments on commit 0a93045

Please sign in to comment.