diff --git a/src/index.ts b/src/index.ts index b716493..8f3246f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ -import traverse, { ASTNode, createNodePath, getBinding, getChildren, NodePath } from "./traverse"; +import createTraverser, { ASTNode, NodePath } from "./traverse"; import { FunctionCall, parse, QNode } from "./parseQuery"; -import { parseScript, ESTree } from "meriyah"; +import { parseScript } from "meriyah"; +import { isIdentifier, isPrimitive } from "./nodeutils"; const debugLogEnabled = false; @@ -52,10 +53,7 @@ export function isAvailableFunction(name: string) : name is AvailableFunction { return functionNames.includes(name); } -function beginHandle>(queries: T, path: ASTNode) : Record { - const rootPath: NodePath = createNodePath(path, undefined, undefined, undefined); - return travHandle(queries, rootPath); -} + type Result = ASTNode | string | number | boolean; @@ -102,347 +100,365 @@ function breadCrumb(path: NodePath) { } } -function createFilter(filter: QNode, filterResult: Array) : FilterNode { - if (filter.type == "and" || filter.type == "or" || filter.type == "equals") { - return { - type: filter.type, - left: createFilter(filter.left, []), - right: createFilter(filter.right, []) - }; - } else if (filter.type == "literal") { - const r = [ filter.value ]; +function createQuerier() { + + const traverser = createTraverser(); + const { getChildren, getBinding, createNodePath, traverse } = traverser; + + function createFilter(filter: QNode, filterResult: Array) : FilterNode { + if (filter.type == "and" || filter.type == "or" || filter.type == "equals") { + return { + type: filter.type, + left: createFilter(filter.left, []), + right: createFilter(filter.right, []) + }; + } else if (filter.type == "literal") { + const r = [ filter.value ]; + return { + node: filter, + result: r + }; + } + return createFNode(filter, filterResult); + } + + function createFNode(token: QNode, result: Array) : FNode { return { - node: filter, - result: r + node: token, + result: result }; } - return createFNode(filter, filterResult); -} -function createFNode(token: QNode, result: Array) : FNode { - return { - node: token, - result: result - }; -} - -function addFilterChildrenToState(filter: FilterNode, state: State) { - if ("type" in filter && (filter.type == "and" || filter.type == "or" || filter.type == "equals")) { - addFilterChildrenToState(filter.left, state); - addFilterChildrenToState(filter.right, state); - } else if ("node" in filter) { - if (filter.node.type == "child") { - log.debug("ADDING FILTER CHILD", filter.node); - state.child[state.depth+1].push(filter); - } - if (filter.node.type == "descendant") { - log.debug("ADDING FILTER DESCENDANT", filter.node); - state.descendant[state.depth+1].push(filter); + function addFilterChildrenToState(filter: FilterNode, state: State) { + if ("type" in filter && (filter.type == "and" || filter.type == "or" || filter.type == "equals")) { + addFilterChildrenToState(filter.left, state); + addFilterChildrenToState(filter.right, state); + } else if ("node" in filter) { + if (filter.node.type == "child") { + log.debug("ADDING FILTER CHILD", filter.node); + state.child[state.depth+1].push(filter); + } + if (filter.node.type == "descendant") { + log.debug("ADDING FILTER DESCENDANT", filter.node); + state.descendant[state.depth+1].push(filter); + } } } -} -function createFNodeAndAddToState(token: QNode, result: Array, state: State) : FNode { - log.debug("ADDING FNODE", token); - const fnode = createFNode(token, result); - if (token.type == "child") { - state.child[state.depth+1].push(fnode); - } else if (token.type == "descendant") { - state.descendant[state.depth+1].push(fnode); + function createFNodeAndAddToState(token: QNode, result: Array, state: State) : FNode { + log.debug("ADDING FNODE", token); + const fnode = createFNode(token, result); + if (token.type == "child") { + state.child[state.depth+1].push(fnode); + } else if (token.type == "descendant") { + state.descendant[state.depth+1].push(fnode); + } + return fnode; } - return fnode; -} -function isMatch(fnode: FNode, path: NodePath) : boolean { - if (fnode.node.attribute) { - const m = fnode.node.value == path.parentKey || fnode.node.value == path.key - if (m) log.debug("ATTR MATCH", fnode.node.value, breadCrumb(path)); + function isMatch(fnode: FNode, path: NodePath) : boolean { + if (fnode.node.attribute) { + const m = fnode.node.value == path.parentKey || fnode.node.value == path.key + if (m) log.debug("ATTR MATCH", fnode.node.value, breadCrumb(path)); + return m; + } + if (fnode.node.value == "*") { + return true; + } + const m = fnode.node.value == path.node.type + if (m) log.debug("NODE MATCH", fnode.node.value, breadCrumb(path)); return m; } - if (fnode.node.value == "*") { - return true; - } - const m = fnode.node.value == path.node.type - if (m) log.debug("NODE MATCH", fnode.node.value, breadCrumb(path)); - return m; -} -function addIfTokenMatch(fnode: FNode, path: NodePath, state: State) { - if (!isMatch(fnode, path)) return; - state.matches[state.depth].push([fnode, path]); - if (fnode.node.filter) { - const filter = createFilter(fnode.node.filter, []); - const filteredResult: Array = []; - state.filters[state.depth].push({ filter: filter, qNode: fnode.node, node: path.node, result: filteredResult }); - addFilterChildrenToState(filter, state); - const child = fnode.node.child; - if (child) { - if (child.type == "function") { + function addIfTokenMatch(fnode: FNode, path: NodePath, state: State) { + if (!isMatch(fnode, path)) return; + state.matches[state.depth].push([fnode, path]); + if (fnode.node.filter) { + const filter = createFilter(fnode.node.filter, []); + const filteredResult: Array = []; + state.filters[state.depth].push({ filter: filter, qNode: fnode.node, node: path.node, result: filteredResult }); + addFilterChildrenToState(filter, state); + const child = fnode.node.child; + if (child) { + if (child.type == "function") { + const fr = addFunction(fnode, child, path, state); + state.functionCalls[state.depth].push(fr); + } else { + createFNodeAndAddToState(child, filteredResult, state); + } + } + } else { + const child = fnode.node.child; + if (child?.type == "function") { const fr = addFunction(fnode, child, path, state); state.functionCalls[state.depth].push(fr); - } else { - createFNodeAndAddToState(child, filteredResult, state); - } + } else if (child && !fnode.node.binding && !fnode.node.resolve) { + createFNodeAndAddToState(child, fnode.result, state); + } } - } else { - const child = fnode.node.child; - if (child?.type == "function") { - const fr = addFunction(fnode, child, path, state); - state.functionCalls[state.depth].push(fr); - } else if (child && !fnode.node.binding && !fnode.node.resolve) { - createFNodeAndAddToState(child, fnode.result, state); - } } -} -function addFunction(rootNode: FNode, functionCall: FunctionCall, path: NodePath, state: State): FunctionCallResult { - const functionNode: FunctionCallResult = { node: rootNode.node, functionCall: functionCall, parameters: [], result: [] }; - for (const param of functionCall.parameters) { - if (param.type == "literal") { - functionNode.parameters.push({ node: param, result: [param.value] }); - } else { - if (param.type == "function") { - functionNode.parameters.push(addFunction(functionNode, param, path, state)); + function addFunction(rootNode: FNode, functionCall: FunctionCall, path: NodePath, state: State): FunctionCallResult { + const functionNode: FunctionCallResult = { node: rootNode.node, functionCall: functionCall, parameters: [], result: [] }; + for (const param of functionCall.parameters) { + if (param.type == "literal") { + functionNode.parameters.push({ node: param, result: [param.value] }); } else { - functionNode.parameters.push(createFNodeAndAddToState(param, [], state)); + if (param.type == "function") { + functionNode.parameters.push(addFunction(functionNode, param, path, state)); + } else { + functionNode.parameters.push(createFNodeAndAddToState(param, [], state)); + } } } + return functionNode; } - return functionNode; -} -function isPrimitive(value: unknown) : boolean { - return typeof value == "string" || typeof value == "number" || typeof value == "boolean"; -} - -function addPrimitiveAttributeIfMatch(fnode: FNode, path: NodePath) { - if (!fnode.node.attribute || !fnode.node.value) return; - if (fnode.node.child || fnode.node.filter) return; - if (!Object.hasOwn(path.node, fnode.node.value)) return; - const lookup = getChildren(fnode.node.value, path); - const nodes = (Array.isArray(lookup) ? lookup : [lookup]) - .filter(n => n.node != undefined) - .filter(n => isPrimitive(n.node)); - if (nodes.length == 0) return; - log.debug("PRIMITIVE", fnode.node.value, nodes.map(n => n.node)); - fnode.result.push(...nodes.map(n => n.node)); -} + function addPrimitiveAttributeIfMatch(fnode: FNode, path: NodePath) { + if (!fnode.node.attribute || !fnode.node.value) return; + if (fnode.node.child || fnode.node.filter) return; + if (!Object.hasOwn(path.node, fnode.node.value)) return; + const lookup = getChildren(fnode.node.value, path); + const nodes = toArray(lookup) + .filter(n => n.node != undefined) + .filter(n => isPrimitive(n.node)); + if (nodes.length == 0) return; + log.debug("PRIMITIVE", fnode.node.value, nodes.map(n => n.node)); + fnode.result.push(...nodes.map(n => n.node)); + } -function evaluateFilter(filter: FilterNode, path: NodePath) : Result[] { - log.debug("EVALUATING FILTER", filter, breadCrumb(path)); - if ("type" in filter) { - if (filter.type == "and") { - const left = evaluateFilter(filter.left, path); - if (left.length == 0) return []; - return evaluateFilter(filter.right, path); - } - if (filter.type == "or") { - const left = evaluateFilter(filter.left, path); - if (left.length > 0) return left; - return evaluateFilter(filter.right, path); + function evaluateFilter(filter: FilterNode, path: NodePath) : Result[] { + log.debug("EVALUATING FILTER", filter, breadCrumb(path)); + if ("type" in filter) { + if (filter.type == "and") { + const left = evaluateFilter(filter.left, path); + if (left.length == 0) return []; + return evaluateFilter(filter.right, path); + } + if (filter.type == "or") { + const left = evaluateFilter(filter.left, path); + if (left.length > 0) return left; + return evaluateFilter(filter.right, path); + } + if (filter.type == "equals") { + const left = evaluateFilter(filter.left, path); + const right = evaluateFilter(filter.right, path); + return left.filter(x => right.includes(x)); + } + throw new Error("Unknown filter type: " + filter.type); } - if (filter.type == "equals") { - const left = evaluateFilter(filter.left, path); - const right = evaluateFilter(filter.right, path); - return left.filter(x => right.includes(x)); + if (filter.node.type == "parent") { + return resolveFilterWithParent(filter.node, path); } - throw new Error("Unknown filter type: " + filter.type); - } - if (filter.node.type == "parent") { - return resolveFilterWithParent(filter.node, path); + return filter.result; } - return filter.result; -} -function isIdentifier(node: ASTNode) : node is ESTree.Identifier { - return node.type == "Identifier"; -} -function resolveBinding(path: NodePath) : NodePath | undefined { - if (!isIdentifier(path.node)) return undefined; - log.debug("RESOLVING BINDING FOR ", path.node); - const name = path.node.name; - if (name == undefined || typeof name != "string") return undefined; - //const binding = path.scope.getBinding(name); - const binding = getBinding(path.scopeId, name); - if (!binding) return undefined; - log.debug("THIS IS THE BINDING", binding); - return binding.path; -} - -function resolveFilterWithParent(node: QNode, path: NodePath) : Result[] { - let startNode: QNode = node; - let startPath = path; - while(startNode.type == "parent") { - if (!startNode.child) throw new Error("Parent filter must have child"); - if (!startPath.parentPath) return []; - log.debug("STEP OUT", startNode, breadCrumb(startPath)); - startNode = startNode.child; - startPath = startPath.parentPath; + function resolveBinding(path: NodePath) : NodePath | undefined { + if (!isIdentifier(path.node)) return undefined; + log.debug("RESOLVING BINDING FOR ", path.node); + const name = path.node.name; + if (name == undefined || typeof name != "string") return undefined; + //const binding = path.scope.getBinding(name); + const binding = getBinding(path.scopeId, name); + if (!binding) return undefined; + log.debug("THIS IS THE BINDING", binding); + return binding.path; } - return resolveDirectly(startNode, startPath); -} -const toArray = (value: T | T[]) : T[] => Array.isArray(value) ? value : [value]; -function isDefined(value: T | undefined | null) : value is T { - return value != undefined && value != null; -} -let subQueryCounter = 0; -function resolveDirectly(node: QNode, path: NodePath) : Result[] { - let startNode: QNode = node; - const startPath = path; - let paths = [startPath]; - while(startNode.attribute && startNode.type == "child") { - const lookup = startNode.value; - if (!lookup) throw new Error("Selector must have a value"); - log.debug("STEP IN ", lookup, paths.map(p => breadCrumb(p))); - const nodes = paths.map(n => getChildren(lookup, n)).map(toArray).flat().filter(n => n.node != undefined); - log.debug("LOOKUP", lookup, path.node.type, nodes.map(n => n.node), nodes.filter(n => n.node == undefined)); - if (nodes.length == 0) return []; - paths = nodes; - if (startNode.resolve) { - const resolved = paths.map(p => resolveBinding(p)).filter(isDefined).map(p => getChildren("init", p)).flatMap(toArray).filter(p => p.node != undefined).filter(isDefined); - if (resolved.length > 0) paths = resolved; - } else if (startNode.binding) { - paths = paths.map(p => resolveBinding(p)).filter(isDefined); - } - const filter = startNode.filter; - if (filter) { - paths = paths.filter(p => travHandle({subquery: filter}, p).subquery.length > 0); + function resolveFilterWithParent(node: QNode, path: NodePath) : Result[] { + let startNode: QNode = node; + let startPath = path; + while(startNode.type == "parent") { + if (!startNode.child) throw new Error("Parent filter must have child"); + if (!startPath.parentPath) return []; + log.debug("STEP OUT", startNode, breadCrumb(startPath)); + startNode = startNode.child; + startPath = startPath.parentPath; } - if (!startNode.child) { - return paths.map(p => p.node); + return resolveDirectly(startNode, startPath); + } + const toArray = (value: T | T[]) : T[] => Array.isArray(value) ? value : [value]; + + function isDefined(value: T | undefined | null) : value is T { + return value != undefined && value != null; + } + let subQueryCounter = 0; + function resolveDirectly(node: QNode, path: NodePath) : Result[] { + let startNode: QNode = node; + const startPath = path; + let paths = [startPath]; + while(startNode.attribute && startNode.type == "child") { + const lookup = startNode.value; + if (!lookup) throw new Error("Selector must have a value"); + log.debug("STEP IN ", lookup, paths.map(p => breadCrumb(p))); + const nodes = paths.map(n => getChildren(lookup, n)).map(toArray).flat().filter(n => n.node != undefined); + log.debug("LOOKUP", lookup, path.node.type, nodes.map(n => n.node), nodes.filter(n => n.node == undefined)); + if (nodes.length == 0) return []; + paths = nodes; + if (startNode.resolve) { + const resolved = paths.map(p => resolveBinding(p)).filter(isDefined).map(p => getChildren("init", p)).flatMap(toArray).filter(p => p.node != undefined).filter(isDefined); + if (resolved.length > 0) paths = resolved; + } else if (startNode.binding) { + paths = paths.map(p => resolveBinding(p)).filter(isDefined); + } + const filter = startNode.filter; + if (filter) { + paths = paths.filter(p => travHandle({subquery: filter}, p).subquery.length > 0); + } + if (!startNode.child) { + return paths.map(p => p.node); + } + startNode = startNode.child; } - startNode = startNode.child; + log.debug("DIRECT TRAV RESOLVE", startNode, paths.map(p => breadCrumb(p))); + const result = paths.flatMap(path => { + const subQueryKey = "subquery-" + subQueryCounter++; + return travHandle({[subQueryKey]:startNode}, path)[subQueryKey]; + }); + log.debug("DIRECT TRAV RESOLVE RESULT", result); + return result; } - log.debug("DIRECT TRAV RESOLVE", startNode, paths.map(p => breadCrumb(p))); - const result = paths.flatMap(path => { - const subQueryKey = "subquery-" + subQueryCounter++; - return travHandle({[subQueryKey]:startNode}, path)[subQueryKey]; - }); - log.debug("DIRECT TRAV RESOLVE RESULT", result); - return result; -} -function addResultIfTokenMatch(fnode: FNode, path: NodePath, state: State) { - const filters = state.filters[state.depth].filter(f => f.node == path.node && f.qNode == fnode.node); - const matchingFilters = filters.filter(f => evaluateFilter(f.filter, path).length > 0); - log.debug("RESULT MATCH", fnode.node.value, breadCrumb(path), filters.length, matchingFilters.length); - if (filters.length > 0 && matchingFilters.length == 0) return; + function addResultIfTokenMatch(fnode: FNode, path: NodePath, state: State) { + const filters = state.filters[state.depth].filter(f => f.node == path.node && f.qNode == fnode.node); + const matchingFilters = filters.filter(f => evaluateFilter(f.filter, path).length > 0); + log.debug("RESULT MATCH", fnode.node.value, breadCrumb(path), filters.length, matchingFilters.length); + if (filters.length > 0 && matchingFilters.length == 0) return; - if (fnode.node.resolve) { - const binding = resolveBinding(path); - const resolved = binding ? getChildren("init", binding)[0] : undefined; + if (fnode.node.resolve) { + const binding = resolveBinding(path); + const resolved = binding ? getChildren("init", binding)[0] : undefined; - if (fnode.node.child) { - const result = resolveDirectly(fnode.node.child, resolved ?? path); - fnode.result.push(...result); - } else { - fnode.result.push(path.node); - } - } else if (fnode.node.binding) { - const binding = resolveBinding(path); - if (binding) { if (fnode.node.child) { - const result = resolveDirectly(fnode.node.child, binding); + const result = resolveDirectly(fnode.node.child, resolved ?? path); fnode.result.push(...result); } else { - fnode.result.push(binding.node); + fnode.result.push(path.node); } + } else if (fnode.node.binding) { + const binding = resolveBinding(path); + if (binding) { + if (fnode.node.child) { + const result = resolveDirectly(fnode.node.child, binding); + fnode.result.push(...result); + } else { + fnode.result.push(binding.node); + } + } + } else if (!fnode.node.child) { + fnode.result.push(path.node); + } else if (fnode.node.child.type == "function") { + const functionCallResult = state.functionCalls[state.depth].find(f => f.node == fnode.node); + if (!functionCallResult) throw new Error("Did not find expected function call for " + fnode.node.child.function); + resolveFunctionCalls(fnode, functionCallResult, path, state); + } else if (matchingFilters.length > 0) { + log.debug("HAS MATCHING FILTER", fnode.result.length, matchingFilters.length, breadCrumb(path)); + fnode.result.push(...matchingFilters.flatMap(f => f.result)); } - } else if (!fnode.node.child) { - fnode.result.push(path.node); - } else if (fnode.node.child.type == "function") { - const functionCallResult = state.functionCalls[state.depth].find(f => f.node == fnode.node); - if (!functionCallResult) throw new Error("Did not find expected function call for " + fnode.node.child.function); - resolveFunctionCalls(fnode, functionCallResult, path, state); - } else if (matchingFilters.length > 0) { - log.debug("HAS MATCHING FILTER", fnode.result.length, matchingFilters.length, breadCrumb(path)); - fnode.result.push(...matchingFilters.flatMap(f => f.result)); - } -} + } -function resolveFunctionCalls(fnode: FNode, functionCallResult: FunctionCallResult, path: NodePath, state: State) { - const parameterResults: Result[][] = []; - for (const p of functionCallResult.parameters) { - if ("parameters" in p) { - resolveFunctionCalls(p, p, path, state); - parameterResults.push(p.result); - } else { - parameterResults.push(p.result); + function resolveFunctionCalls(fnode: FNode, functionCallResult: FunctionCallResult, path: NodePath, state: State) { + const parameterResults: Result[][] = []; + for (const p of functionCallResult.parameters) { + if ("parameters" in p) { + resolveFunctionCalls(p, p, path, state); + parameterResults.push(p.result); + } else { + parameterResults.push(p.result); + } } + const functionResult = functions[functionCallResult.functionCall.function].fn(parameterResults); + log.debug("PARAMETER RESULTS", functionCallResult.functionCall.function, parameterResults, functionResult); + fnode.result.push(...functionResult); } - const functionResult = functions[functionCallResult.functionCall.function].fn(parameterResults); - log.debug("PARAMETER RESULTS", functionCallResult.functionCall.function, parameterResults, functionResult); - fnode.result.push(...functionResult); -} + function travHandle>(queries: T, root: NodePath) : Record { + const results = Object.fromEntries(Object.keys(queries).map(name => [name, [] as Result[]])) as Record; + const state: State = { + depth: 0, + child: [[],[]], + descendant: [[],[]], + filters: [[],[]], + matches: [[]], + functionCalls: [[]] + }; + Object.entries(queries).forEach(([name, node]) => { + createFNodeAndAddToState(node, results[name], state); + }); + state.child[state.depth+1].forEach(fnode => addPrimitiveAttributeIfMatch(fnode, root)); + state.descendant.slice(0, state.depth+1).forEach(fnodes => fnodes.forEach(fnode => addPrimitiveAttributeIfMatch(fnode, root))); + + traverse(root.node, { + enter(path, state) { + log.debug("ENTER", breadCrumb(path)); + state.depth++; + state.child.push([]); + state.descendant.push([]); + state.filters.push([]); + state.matches.push([]); + state.functionCalls.push([]); + state.child[state.depth].forEach(fnode => addIfTokenMatch(fnode, path, state)); + state.descendant.slice(0, state.depth+1).forEach(fnodes => + fnodes.forEach(fnode => addIfTokenMatch(fnode, path, state)) + ); + }, + exit(path, state) { + log.debug("EXIT", breadCrumb(path)); + // Check for attributes as not all attributes are visited + state.child[state.depth +1].forEach(fnode => addPrimitiveAttributeIfMatch(fnode, path)); + state.descendant.forEach(fnodes => + fnodes.forEach(fnode => addPrimitiveAttributeIfMatch(fnode, path)) + ); + + state.matches[state.depth].forEach(([fNode, path]) => addResultIfTokenMatch(fNode, path, state)); + state.depth--; + state.child.pop(); + state.descendant.pop(); + state.filters.pop(); + state.matches.pop(); + state.functionCalls.pop(); + } + }, root.scopeId, state, root); + return results; + } -function travHandle>(queries: T, root: NodePath) : Record { - const results = Object.fromEntries(Object.keys(queries).map(name => [name, [] as Result[]])) as Record; - const state: State = { - depth: 0, - child: [[],[]], - descendant: [[],[]], - filters: [[],[]], - matches: [[]], - functionCalls: [[]] - }; - Object.entries(queries).forEach(([name, node]) => { - createFNodeAndAddToState(node, results[name], state); - }); - state.child[state.depth+1].forEach(fnode => addPrimitiveAttributeIfMatch(fnode, root)); - state.descendant.slice(0, state.depth+1).forEach(fnodes => fnodes.forEach(fnode => addPrimitiveAttributeIfMatch(fnode, root))); - - traverse(root.node, { - enter(path, state) { - log.debug("ENTER", breadCrumb(path)); - state.depth++; - state.child.push([]); - state.descendant.push([]); - state.filters.push([]); - state.matches.push([]); - state.functionCalls.push([]); - state.child[state.depth].forEach(fnode => addIfTokenMatch(fnode, path, state)); - state.descendant.slice(0, state.depth+1).forEach(fnodes => - fnodes.forEach(fnode => addIfTokenMatch(fnode, path, state)) - ); - }, - exit(path, state) { - log.debug("EXIT", breadCrumb(path)); - // Check for attributes as not all attributes are visited - state.child[state.depth +1].forEach(fnode => addPrimitiveAttributeIfMatch(fnode, path)); - state.descendant.forEach(fnodes => - fnodes.forEach(fnode => addPrimitiveAttributeIfMatch(fnode, path)) - ); - - state.matches[state.depth].forEach(([fNode, path]) => addResultIfTokenMatch(fNode, path, state)); - state.depth--; - state.child.pop(); - state.descendant.pop(); - state.filters.pop(); - state.matches.pop(); - state.functionCalls.pop(); - } - }, root.scopeId, state, root); - return results; + function beginHandle>(queries: T, path: ASTNode) : Record { + const rootPath: NodePath = createNodePath(path, undefined, undefined, undefined); + return travHandle(queries, rootPath); + } + return { + beginHandle + } } + + const defaultKey = "__default__"; -export function query(code: ASTNode | string, query: string) : Result[] { - return multiQuery(code, { [defaultKey]: query })[defaultKey]; +export function query(code: string | ASTNode, query: string, returnAST?: boolean) : Result[] & { __AST?: ASTNode } { + const result = multiQuery(code, { [defaultKey]: query }, returnAST); + if (returnAST) { + const r = result[defaultKey] as Result[] & { __AST?: ASTNode }; + r.__AST = result.__AST; + return r; + } + return result[defaultKey]; } -export function multiQuery>(code: ASTNode | string, namedQueries: T) : Record { +export function multiQuery>(code: string | ASTNode, namedQueries: T, returnAST?: boolean) : Record & { __AST?: ASTNode } { const start = Date.now(); const ast = typeof code == "string" ? parseSource(code) : code; if (ast == null) throw new Error("Could not pase code"); const queries = Object.fromEntries(Object.entries(namedQueries).map(([name, query]) => [name, parse(query)])) as Record; - const result = beginHandle(queries, ast); + const querier = createQuerier(); + const result = querier.beginHandle(queries, ast); log.debug("Query time: ", Date.now() - start); + if (returnAST) { + return { ...result, __AST: ast }; + } return result; } diff --git a/src/nodeutils.ts b/src/nodeutils.ts index 1c023d3..fc0a42e 100644 --- a/src/nodeutils.ts +++ b/src/nodeutils.ts @@ -5,6 +5,10 @@ export function isNode(candidate: unknown) : candidate is ASTNode { return typeof candidate === "object" && candidate != null && "type" in candidate; } +export function isPrimitive(value: unknown) : boolean { + return typeof value == "string" || typeof value == "number" || typeof value == "boolean"; +} + export function isAssignmentExpression(node: ESTree.Node): node is ESTree.AssignmentExpression { return node.type === "AssignmentExpression"; } diff --git a/src/traverse.ts b/src/traverse.ts index c19eead..3b00d08 100644 --- a/src/traverse.ts +++ b/src/traverse.ts @@ -1,5 +1,6 @@ import { VISITOR_KEYS, isAssignmentExpression, isBinding, isIdentifier, isMemberExpression, isNode, isScopable, isScope } from "./nodeutils"; import { ESTree } from "meriyah"; +import { isDefined, toArray } from "./utils"; const debugLogEnabled = false; @@ -40,181 +41,189 @@ type Visitor = { exit: (path: NodePath, state: T) => void; } +export default function createTraverser() { + let scopeIdCounter = 0; + let removedScopes = 0; -let scopeIdCounter = 0; -let removedScopes = 0; - -function createScope(parentScopeId?: number): number { - const id = scopeIdCounter++; - scopes[id] = parentScopeId ?? -1; - return id; -} - -export function getBinding(scopeId: number, name: string) { - const scope = scopes[scopeId]; - if (typeof scope == "number") { - if (scope == -1) return undefined; - return getBinding(scope, name); + function createScope(parentScopeId?: number): number { + const id = scopeIdCounter++; + scopes[id] = parentScopeId ?? -1; + return id; } - const s = scope.bindings[name]; - if (s) return s; - if (scope.parentScopeId != undefined && scope.parentScopeId >= 0) { - return getBinding(scope.parentScopeId, name); + + function getBinding(scopeId: number, name: string) { + const scope = scopes[scopeId]; + if (typeof scope == "number") { + if (scope == -1) return undefined; + return getBinding(scope, name); + } + const s = scope.bindings[name]; + if (s) return s; + if (scope.parentScopeId != undefined && scope.parentScopeId >= 0) { + return getBinding(scope.parentScopeId, name); + } + return undefined; } - return undefined; -} -function setBinding(scopeId: number, name: string, binding: Binding) { - let scope: Scope; - const s = scopes[scopeId]; - if (typeof s == "number") { - scope = { - bindings: {}, - id: scopeId, - parentScopeId: s == -1 ? undefined : s, - }; - scopes[scopeId] = scope; - } else { - scope = s; + function setBinding(scopeId: number, name: string, binding: Binding) { + let scope: Scope; + const s = scopes[scopeId]; + if (typeof s == "number") { + scope = { + bindings: {}, + id: scopeId, + parentScopeId: s == -1 ? undefined : s, + }; + scopes[scopeId] = scope; + } else { + scope = s; + } + scope.bindings[name] = binding; } - scope.bindings[name] = binding; -} -let pathsCreated = 0; + let pathsCreated = 0; -export function getChildren(key: string, path: NodePath) : NodePath[] { - if (key in path.node) { - const r = (path.node as unknown as Record)[key]; - if (Array.isArray(r)) { - return r.map((n, i) => createNodePath(n, i.toString(), key, path.scopeId, path)); - } else if (r != undefined) { - return [createNodePath(r as ASTNode, key, key, path.scopeId, path)]; + function getChildren(key: string, path: NodePath) : NodePath[] { + if (key in path.node) { + const r = (path.node as unknown as Record)[key]; + if (Array.isArray(r)) { + return r.map((n, i) => createNodePath(n, i.toString(), key, path.scopeId, path)); + } else if (r != undefined) { + return [createNodePath(r as ASTNode, key, key, path.scopeId, path)]; + } } - } - return []; -} + return []; + } -export function createNodePath(node: ASTNode, key: string | undefined, parentKey: string | undefined, scopeId: number | undefined, nodePath?: NodePath) : NodePath { - if (node.extra?.nodePath) { - const path = node.extra.nodePath; - path.key = key; - path.parentKey = parentKey; - path.parentPath = nodePath; + function createNodePath(node: ASTNode, key: string | undefined, parentKey: string | undefined, scopeId: number | undefined, nodePath?: NodePath) : NodePath { + if (node.extra?.nodePath) { + const path = node.extra.nodePath; + path.key = key; + path.parentKey = parentKey; + path.parentPath = nodePath; + return path; + } + const finalScope: number = ((node.extra && node.extra["scopeId"]) ? node.extra["scopeId"] as number : scopeId) ?? createScope(); + + const path = { + node, + scopeId: finalScope, + parentPath: nodePath, + key, + parentKey + } + if (isNode(node)) { + node.extra = node.extra ?? {}; + node.extra.nodePath = path; + Object.defineProperty(node.extra, "nodePath", { enumerable: false }); + } + pathsCreated++; return path; } - const finalScope: number = ((node.extra && node.extra["scopeId"]) ? node.extra["scopeId"] as number : scopeId) ?? createScope(); - - const path = { - node, - scopeId: finalScope, - parentPath: nodePath, - key, - parentKey - } - if (isNode(node)) { - node.extra = node.extra ?? {}; - node.extra.nodePath = path; - Object.defineProperty(node.extra, "nodePath", { enumerable: false }); - } - pathsCreated++; - return path; -} -function registerBinding(node: ASTNode, parentNode: ASTNode, grandParentNode: ASTNode | undefined, scopeId: number) { - //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) }); + function registerBinding(node: ASTNode, parentNode: ASTNode, grandParentNode: ASTNode | undefined, scopeId: number) { + //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) }); + } } } } -} - - -function registerBindings(node: ASTNode, parentNode: ASTNode, grandParentNode: ASTNode | undefined, scopeId: number) { - if (typeof node == "object" && node != null) { - node.extra = node.extra ?? {}; - if (node.extra["scopeId"]) return; - node.extra["scopeId"] = scopeId; - } - 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 = Array.isArray(childNodes) ? childNodes : childNodes ? [childNodes] : []; - 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); - } - }); - } - if (childScopeId != scopeId && typeof scopes[childScopeId] == "number") { // Scope has not been populated - scopes[childScopeId] = scopes[scopeId]; - removedScopes++; - } -} -function traverseInner( - node: ASTNode, - visitor: Visitor, - scopeId: number | undefined, - state: T, - path?: NodePath - ) { - const nodePath = path ?? createNodePath(node, undefined, undefined, scopeId); - const keys = VISITOR_KEYS[node.type] ?? []; - - if (nodePath.parentPath) registerBindings(nodePath.node, nodePath.parentPath.node, nodePath.parentPath.parentPath?.node, nodePath.scopeId); + function registerBindings(node: ASTNode, parentNode: ASTNode, grandParentNode: ASTNode | undefined, scopeId: number) { + if (typeof node == "object" && node != null) { + node.extra = node.extra ?? {}; + if (node.extra["scopeId"]) return; + node.extra["scopeId"] = scopeId; + } + 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 = Array.isArray(childNodes) ? childNodes : childNodes ? [childNodes] : []; - const nodePaths = children.map((child, i) => { + const children = toArray(childNodes).filter(isDefined); + children.forEach((child) => { if (isNode(child)) { - return createNodePath(child, key, Array.isArray(childNodes) ? i.toString() : key, nodePath.scopeId, nodePath); + // 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); } - return undefined; - }).filter(x => x != undefined) as NodePath[]; - nodePaths.forEach((childPath) => { - visitor.enter(childPath, state); - traverseInner(childPath.node, visitor, nodePath.scopeId, state, childPath); - visitor.exit(childPath, state); }); } -} + if (childScopeId != scopeId && typeof scopes[childScopeId] == "number") { // Scope has not been populated + scopes[childScopeId] = scopes[scopeId]; + removedScopes++; + } + } -const sOut: number[] = []; -export default function traverse( node: ASTNode, - visitor: Visitor, - scopeId: number | undefined, - state: T, - path?: NodePath) { - traverseInner(node, visitor, scopeId, state, path); - if (!sOut.includes(scopeIdCounter)) { - log.debug("Scopes created", scopeIdCounter, " Scopes removed", removedScopes, "Paths created", pathsCreated); - sOut.push(scopeIdCounter); + function traverseInner( + node: ASTNode, + visitor: Visitor, + scopeId: number | undefined, + state: T, + path?: NodePath + ) { + const nodePath = path ?? createNodePath(node, undefined, undefined, scopeId); + const keys = VISITOR_KEYS[node.type] ?? []; + + if (nodePath.parentPath) registerBindings(nodePath.node, nodePath.parentPath.node, nodePath.parentPath.parentPath?.node, nodePath.scopeId); + + 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 undefined; + }).filter(x => x != undefined) as NodePath[]; + nodePaths.forEach((childPath) => { + visitor.enter(childPath, state); + traverseInner(childPath.node, visitor, nodePath.scopeId, state, childPath); + visitor.exit(childPath, state); + }); + } + } + + const sOut: number[] = []; + + function traverse( node: ASTNode, + visitor: Visitor, + scopeId: number | undefined, + state: T, + path?: NodePath) { + traverseInner(node, visitor, scopeId, state, path); + if (!sOut.includes(scopeIdCounter)) { + log.debug("Scopes created", scopeIdCounter, " Scopes removed", removedScopes, "Paths created", pathsCreated); + sOut.push(scopeIdCounter); + } + } + return { + traverse, + createNodePath, + getChildren, + getBinding } } \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..ec4ef6a --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,7 @@ +export function toArray(value: T | T[]) : T[] { + return Array.isArray(value) ? value : [value]; +} + +export function isDefined(value: T | undefined | null) : value is T { + return value != undefined && value != null; +} \ No newline at end of file diff --git a/tests/query.test.ts b/tests/query.test.ts index e013353..d04b2d5 100644 --- a/tests/query.test.ts +++ b/tests/query.test.ts @@ -1,5 +1,4 @@ -import { query } from '../src/index'; -import { parseScript } from "meriyah"; +import { multiQuery, query } from '../src/index'; import { ESTree as t } from "meriyah"; @@ -12,19 +11,20 @@ describe('testing index file', () => { return b + c; }`; - const ast = parseScript(code); test('dummy', () => {}) test('Find FunctionExpression', () => { - const nodes = query(ast!, "/FunctionDeclaration"); - const expectedNode = ast!.body[0] as t.FunctionDeclaration; + const nodes = query(code, "/FunctionDeclaration", true); + const ast = nodes.__AST as t.Program; + const expectedNode = ast.body[0] as t.FunctionDeclaration; expect(nodes.length).toEqual(1); expect(nodes[0]).toBeDefined(); expect(nodes[0]).toEqual(expectedNode); }); test('Find FunctionExpressions identifier', () => { - const nodes = query(ast!, "/FunctionDeclaration/Identifier"); - const functionD = ast!.body[0] as t.FunctionDeclaration; + const nodes = query(code, "/FunctionDeclaration/Identifier", true); + const ast = nodes.__AST as t.Program; + const functionD = ast.body[0] as t.FunctionDeclaration; const expectedNode1 = functionD.id as t.Identifier; const expectedNode2 = functionD.params[0] as t.Identifier; expect(nodes.length).toEqual(2); @@ -35,12 +35,12 @@ describe('testing index file', () => { }); test('Find identifiers below FunctionExpression', () => { - const nodes = query(ast!, "/FunctionDeclaration//Identifier"); + const nodes = query(code, "/FunctionDeclaration//Identifier"); expect(nodes.length).toEqual(10); }); test('Find identifiers below FunctionExpression', () => { - const nodes = query(ast!, "/FunctionDeclaration/:id"); + const nodes = query(code, "/FunctionDeclaration/:id"); expect(nodes.length).toEqual(1); const identifier = nodes[0] as t.Identifier; expect(identifier.name).toEqual("a"); @@ -48,7 +48,7 @@ describe('testing index file', () => { test('Find identifiers below FunctionExpression', () => { - const nodes = query(ast!, "/FunctionDeclaration/:params/:name"); + const nodes = query(code, "/FunctionDeclaration/:params/:name"); expect(nodes.length).toEqual(1); expect(nodes[0]).toEqual("x"); }); @@ -56,44 +56,48 @@ describe('testing index file', () => { test('Find named FunctionExpression', () => { - const nodes = query(ast!, '/FunctionDeclaration[/:id/:name == "a"]'); + const nodes = query(code, '/FunctionDeclaration[/:id/:name == "a"]', true); + const ast = nodes.__AST as t.Program; const expectedNode = ast!.body[0] as t.FunctionDeclaration; expect(nodes[0]).toBeDefined(); expect(nodes[0]).toEqual(expectedNode); }); test('Dont find wrongly named FunctionExpression', () => { - const nodes = query(ast!, '/FunctionDeclaration[/:id/:name == "b"]'); + const nodes = query(code, '/FunctionDeclaration[/:id/:name == "b"]'); expect(nodes[0]).toEqual(undefined); }); test('Find named FunctionExpression double declaration', () => { - const nodes = query(ast!, '/FunctionDeclaration[/:id/:name == "b" || /:id/:name == "a"]'); + const nodes = query(code, '/FunctionDeclaration[/:id/:name == "b" || /:id/:name == "a"]', true); + const ast = nodes.__AST as t.Program; const expectedNode = ast!.body[0] as t.FunctionDeclaration; expect(nodes[0]).toBeDefined(); expect(nodes[0]).toEqual(expectedNode); }); test('Find named FunctionExpression triple declaration', () => { - const nodes = query(ast!, '/FunctionDeclaration[/:id/:name == "b" || /:id/:name == "a" || /:id/:name == "c"]'); + const nodes = query(code, '/FunctionDeclaration[/:id/:name == "b" || /:id/:name == "a" || /:id/:name == "c"]', true); + const ast = nodes.__AST as t.Program; const expectedNode = ast!.body[0] as t.FunctionDeclaration; expect(nodes[0]).toBeDefined(); expect(nodes[0]).toEqual(expectedNode); }); test('Find named FunctionExpression nested', () => { - const nodes = query(ast!, '/FunctionDeclaration[/:id[/:name == "a"]]'); + const nodes = query(code, '/FunctionDeclaration[/:id[/:name == "a"]]', true); + const ast = nodes.__AST as t.Program; const expectedNode = ast!.body[0] as t.FunctionDeclaration; expect(nodes[0]).toBeDefined(); expect(nodes[0]).toEqual(expectedNode); }); test('Dont find named FunctionExpression nested when wrong name', () => { - const nodes = query(ast!, '/FunctionDeclaration[/:id[/:name == "b"]]'); + const nodes = query(code, '/FunctionDeclaration[/:id[/:name == "b"]]'); expect(nodes.length).toEqual(0); }); test('Find named FunctionExpression as descendant', () => { - const nodes = query(ast!, "/FunctionDeclaration//AssignmentExpression"); + const nodes = query(code, "/FunctionDeclaration//AssignmentExpression"); expect(nodes.length).toEqual(2); const assignmentExpression = nodes[0] as t.AssignmentExpression; expect(assignmentExpression).toBeDefined(); @@ -102,18 +106,18 @@ describe('testing index file', () => { }); test('Find named FunctionExpression as descendant', () => { - const nodes = query(ast!, "//AssignmentExpression[/:left/:name == 'b']/:right/:name"); + const nodes = query(code, "//AssignmentExpression[/:left/:name == 'b']/:right/:name"); expect(nodes.length).toEqual(1); expect(nodes[0]).toEqual("c"); }); test('Find named decalartion as descendant', () => { - const nodes = query(ast!, "//VariableDeclarator[/:id/:name == 'c']/:init/:value"); + const nodes = query(code, "//VariableDeclarator[/:id/:name == 'c']/:init/:value"); expect(nodes[0]).toEqual(3); }); test('Find named decalartion as descendant', () => { - const nodes = query(ast!, "//VariableDeclarator[/:id/:name == 'k']/:init/:value"); + const nodes = query(code, "//VariableDeclarator[/:id/:name == 'k']/:init/:value"); expect(nodes.length).toEqual(0); }); @@ -121,13 +125,13 @@ describe('testing index file', () => { test("find assigment to parameter", () => { - const nodes = query(ast!, "//FunctionDeclaration[/:params/:name == //AssignmentExpression/:left/:object/:name]"); + const nodes = query(code, "//FunctionDeclaration[/:params/:name == //AssignmentExpression/:left/:object/:name]"); //const nodes = query(ast!, "//FunctionDeclaration//AssignmentExpression/:left/:object/:name"); expect(nodes.length).toEqual(1); }); test("find double function expression", () => { - const ast = parseScript(`function a() { function b() { let b = 2; } }`); - const nodes = query(ast!, "//FunctionDeclaration[//VariableDeclarator//Identifier/:name == 'b']"); + const code = `function a() { function b() { let b = 2; } }`; + const nodes = query(code, "//FunctionDeclaration[//VariableDeclarator//Identifier/:name == 'b']"); expect(nodes.length).toEqual(2); expect(nodes[0] == nodes[1]).toEqual(false); //@ts-expect-error should be right type @@ -136,7 +140,7 @@ describe('testing index file', () => { expect(nodes[1].type).toEqual("FunctionDeclaration"); }); test("find assigment to named parameter", () => { - const nodes = query(ast!, `//FunctionDeclaration[ + const nodes = query(code, `//FunctionDeclaration[ /:params/:name == //AssignmentExpression/:left/:object/:name && //AssignmentExpression/:left/:property/:name == 'y' ]`); @@ -145,7 +149,7 @@ describe('testing index file', () => { }); test("find assigment to named parameter", () => { - const nodes = query(ast!, + const nodes = query(code, `//FunctionDeclaration//AssignmentExpression[ ../../../:params/:name == /:left/:object/:name && /:left/:property/:name == 'y' @@ -155,7 +159,7 @@ describe('testing index file', () => { }); test("find assigment to named parameter and get the value", () => { - const nodes = query(ast!, `//FunctionDeclaration//AssignmentExpression[ + const nodes = query(code, `//FunctionDeclaration//AssignmentExpression[ ../../../:params/:name == /:left/:object/:name && /:left/:property/:name == 'y' ]/:right/:value`); @@ -163,13 +167,13 @@ describe('testing index file', () => { }); test("Should work with wildcards", () => { - const nodes = query(ast!, `//FunctionDeclaration//AssignmentExpression/* + const nodes = query(code, `//FunctionDeclaration//AssignmentExpression/* /Identifier/:name`); expect(nodes).toEqual(['x', 'y']); }); test("should find assigment property of object bound to function parameter", () => { - const nodes = query(ast!, `//FunctionDeclaration//AssignmentExpression[ + const nodes = query(code, `//FunctionDeclaration//AssignmentExpression[ /:left/$:object == ../../../:params ]/:right/:value`); expect(nodes).toEqual([25]); @@ -177,85 +181,77 @@ describe('testing index file', () => { test("should return binding", () => { - const nodes = query(ast!, `//FunctionDeclaration//AssignmentExpression/:left/$:object`); + const nodes = query(code, `//FunctionDeclaration//AssignmentExpression/:left/$:object`); expect(nodes[0]).toMatchObject({name: "x"}); }); test("should return binding value", () => { - const nodes = query(ast!, `//FunctionDeclaration//AssignmentExpression/$:right/:init/:value`); + const nodes = query(code, `//FunctionDeclaration//AssignmentExpression/$:right/:init/:value`); expect(nodes).toEqual([3]); }); test("should return named binding value", () => { - const nodes = query(ast!, `//FunctionDeclaration//AssignmentExpression[/:left/:name == 'b']/$:right/:init/:value`); + const nodes = query(code, `//FunctionDeclaration//AssignmentExpression[/:left/:name == 'b']/$:right/:init/:value`); expect(nodes).toEqual([3]); }); test("should NOT find assigment property of object bound to function parameter", () => { - const nodes = query(ast!, `//FunctionDeclaration//AssignmentExpression[ + const nodes = query(code, `//FunctionDeclaration//AssignmentExpression[ /:left/$:property == ../../../:params ]/:right/:value`); expect(nodes).toEqual([]); }); test("should only add double filtered nodes once", () => { - const ast = parseScript(`function a() { function b() { let c = 2; } }`); - const nodes = query(ast!, `//FunctionDeclaration[/:id/:name == 'a']//FunctionDeclaration[/:id/:name == 'b']//VariableDeclaration//Identifier/:name`); + const code = (`function a() { function b() { let c = 2; } }`); + const nodes = query(code, `//FunctionDeclaration[/:id/:name == 'a']//FunctionDeclaration[/:id/:name == 'b']//VariableDeclaration//Identifier/:name`); expect(nodes).toEqual(['c']); }) test("should resolve value", () => { const code = "let x = 1; let y = 2; x = y; y = 3"; - const ast = parseScript(code); - const nodes = query(ast!, "//AssignmentExpression/$$:right/:value"); + const nodes = query(code, "//AssignmentExpression/$$:right/:value"); expect(nodes).toEqual([2, 3]); }); test("should join values", () => { const code = "var a = { b: 1, c: 2 }"; - const ast = parseScript(code); - const nodes = query(ast!, "//ObjectExpression/fn:join(/:properties/:value/:value, '.')"); + const nodes = query(code, "//ObjectExpression/fn:join(/:properties/:value/:value, '.')"); expect(nodes).toEqual(["1.2"]); }); test("should find first", () => { const code = "var a = { b: 1, c: 2 }"; - const ast = parseScript(code); - const nodes = query(ast!, "//ObjectExpression/fn:first(/:properties/:value/:value)"); + const nodes = query(code, "//ObjectExpression/fn:first(/:properties/:value/:value)"); expect(nodes).toEqual([1]); }); test("should concat values", () => { const code = "var a = { b: 1, c: 2 }"; - const ast = parseScript(code); - const nodes = query(ast!, "//ObjectExpression/fn:concat(/:properties/:value/:value, 'ms')"); + const nodes = query(code, "//ObjectExpression/fn:concat(/:properties/:value/:value, 'ms')"); expect(nodes).toEqual(["12ms"]); }); test("should call function in function values", () => { const code = "var a = { b: 1, c: 2 }"; - const ast = parseScript(code); - const nodes = query(ast!, "//ObjectExpression/fn:concat(/fn:first(/:properties/:value/:value), 'ms')"); + const nodes = query(code, "//ObjectExpression/fn:concat(/fn:first(/:properties/:value/:value), 'ms')"); expect(nodes).toEqual(["1ms"]); }); test("should be able to filter", () => { const code = "var a = { b: 1, c: 2 }; var d = { x: 27}"; - const ast = parseScript(code); - const nodes = query(ast!, `//ObjectExpression[//:name == 'x']/fn:concat(/:properties/:value/:value, 'ms')`); + const nodes = query(code, `//ObjectExpression[//:name == 'x']/fn:concat(/:properties/:value/:value, 'ms')`); console.log(nodes); expect(nodes.length).toEqual(1); }); test("should pick nth child", () => { const code = "var a = { b: 1, c: 2 }"; - const ast = parseScript(code); - const nodes = query(ast!, `//ObjectExpression/fn:nthchild(/:properties/:value/:value, 1)`); + const nodes = query(code, `//ObjectExpression/fn:nthchild(/:properties/:value/:value, 1)`); console.log(nodes); expect(nodes.length).toEqual(1); expect(nodes[0]).toEqual(2); }); test("should pick nth child by key", () => { const code = "var a = { b: 1, c: 2 }"; - const ast = parseScript(code); - const nodes = query(ast!, `//ObjectExpression/:1/:value/:value`); + const nodes = query(code, `//ObjectExpression/:1/:value/:value`); console.log(nodes); expect(nodes.length).toEqual(1); expect(nodes[0]).toEqual(2); @@ -263,8 +259,7 @@ describe('testing index file', () => { test("object expression selection", () => { const code = "let k = 32; var a = { b: 1, c: 2 }; var d = { b: k, e: 3}"; - const ast = parseScript(code); - const nodes = query(ast!, `//ObjectExpression[ + const nodes = query(code, `//ObjectExpression[ /Property/:key/:name == 'e' ]/Property[/:key/:name == 'b']/$:value/:init/:value`); console.log(nodes); @@ -278,9 +273,13 @@ describe('testing index file', () => { let k = 1; b.c = k; }` - const ast = parseScript(code); - const nodes1 = query(ast!, "//MemberExpression/$:object"); - const nodes2 = query(ast!, "//FunctionDeclaration//FunctionDeclaration/:id"); + const queries = { + A : "//MemberExpression/$:object", + B : "//FunctionDeclaration//FunctionDeclaration/:id" + } + const result = multiQuery(code, queries); + const nodes1 = result.A; + const nodes2 = result.B; expect(nodes1).toEqual(nodes2); }); test("find correct binding when exported", () => { @@ -291,8 +290,7 @@ describe('testing index file', () => { export { a }` - const ast = parseScript(code, { module: true }); - const nodes = query(ast!, "//AssignmentExpression/$:right/:init/:value"); + const nodes = query(code, "//AssignmentExpression/$:right/:init/:value"); console.log(nodes); expect(nodes).toEqual([1]); });