Comparing version 2.1.1 to 2.2.0
{ | ||
"name": "flast", | ||
"version": "2.1.1", | ||
"version": "2.2.0", | ||
"description": "Flatten JS AST", | ||
@@ -31,4 +31,3 @@ "main": "src/index.js", | ||
"eslint-scope": "^8.2.0", | ||
"espree": "^10.3.0", | ||
"estraverse": "^5.3.0" | ||
"espree": "^10.3.0" | ||
}, | ||
@@ -35,0 +34,0 @@ "devDependencies": { |
@@ -0,1 +1,2 @@ | ||
import {logger} from './utils/logger.js'; | ||
import {generateCode, generateFlatAST} from './flast.js'; | ||
@@ -6,11 +7,10 @@ | ||
* @param {string|ASTNode[]} scriptOrFlatAstArr - the target script or a flat AST array | ||
* @param {Function} logFunc - (optional) Logging function | ||
*/ | ||
constructor(scriptOrFlatAstArr, logFunc = null) { | ||
constructor(scriptOrFlatAstArr) { | ||
this.script = ''; | ||
this.ast = []; | ||
this.log = logFunc || (() => true); | ||
this.markedForDeletion = []; // Array of node ids. | ||
this.appliedCounter = 0; // Track the number of times changes were applied. | ||
this.replacements = []; | ||
this.logger = logger; | ||
if (typeof scriptOrFlatAstArr === 'string') { | ||
@@ -36,3 +36,3 @@ this.script = scriptOrFlatAstArr; | ||
(currentNode.parentNode.declarations.length === 1 || | ||
!currentNode.parentNode.declarations.filter(d => d !== currentNode && !d.isMarked).length) | ||
!currentNode.parentNode.declarations.some(d => d !== currentNode && !d.isMarked)) | ||
)) currentNode = currentNode.parentNode; | ||
@@ -44,3 +44,2 @@ if (relevantClauses.includes(currentNode.parentKey)) currentNode.isEmpty = true; | ||
/** | ||
* | ||
* @returns {number} The number of changes to be applied. | ||
@@ -55,4 +54,4 @@ */ | ||
* node is provided. | ||
* @param targetNode The node to replace or remove. | ||
* @param replacementNode If exists, replace the target node with this node. | ||
* @param {ASTNode} targetNode The node to replace or remove. | ||
* @param {object|ASTNode} replacementNode If exists, replace the target node with this node. | ||
*/ | ||
@@ -83,9 +82,8 @@ markNode(targetNode, replacementNode) { | ||
try { | ||
const that = this; | ||
if (this.getNumberOfChanges() > 0) { | ||
let rootNode = this.ast[0]; | ||
const rootNodeReplacement = this.replacements.find(n => n[0].nodeId === 0); | ||
if (rootNodeReplacement) { | ||
if (rootNode.isMarked) { | ||
const rootNodeReplacement = this.replacements.find(n => n[0].nodeId === 0); | ||
++changesCounter; | ||
this.log(`[+] Applying changes to the root node...`); | ||
this.logger.debug(`[+] Applying changes to the root node...`); | ||
const leadingComments = rootNode.leadingComments || []; | ||
@@ -97,3 +95,4 @@ const trailingComments = rootNode.trailingComments || []; | ||
} else { | ||
for (const targetNodeId of this.markedForDeletion) { | ||
for (let i = 0; i < this.markedForDeletion.length; i++) { | ||
const targetNodeId = this.markedForDeletion[i]; | ||
try { | ||
@@ -105,3 +104,3 @@ let targetNode = this.ast[targetNodeId]; | ||
if (parent[targetNode.parentKey] === targetNode) { | ||
parent[targetNode.parentKey] = undefined; | ||
delete parent[targetNode.parentKey]; | ||
const comments = (targetNode.leadingComments || []).concat(targetNode.trailingComments || []); | ||
@@ -112,4 +111,3 @@ if (comments.length) parent.trailingComments = (parent.trailingComments || []).concat(comments); | ||
const idx = parent[targetNode.parentKey].indexOf(targetNode); | ||
parent[targetNode.parentKey][idx] = undefined; | ||
parent[targetNode.parentKey] = parent[targetNode.parentKey].filter(n => n); | ||
parent[targetNode.parentKey].splice(idx, 1); | ||
const comments = (targetNode.leadingComments || []).concat(targetNode.trailingComments || []); | ||
@@ -124,6 +122,7 @@ if (comments.length) { | ||
} catch (e) { | ||
that.log(`[-] Unable to delete node: ${e}`); | ||
this.logger.debug(`[-] Unable to delete node: ${e}`); | ||
} | ||
} | ||
for (const [targetNode, replacementNode] of this.replacements) { | ||
for (let i = 0; i < this.replacements.length; i++) { | ||
const [targetNode, replacementNode] = this.replacements[i]; | ||
try { | ||
@@ -154,3 +153,3 @@ if (targetNode) { | ||
} catch (e) { | ||
that.log(`[-] Unable to replace node: ${e}`); | ||
this.logger.debug(`[-] Unable to replace node: ${e}`); | ||
} | ||
@@ -171,3 +170,3 @@ } | ||
else { | ||
this.log(`[-] Modified script is invalid. Reverting ${changesCounter} changes...`); | ||
this.logger.log(`[-] Modified script is invalid. Reverting ${changesCounter} changes...`); | ||
changesCounter = 0; | ||
@@ -178,3 +177,3 @@ } | ||
} catch (e) { | ||
this.log(`[-] Unable to apply changes to AST: ${e}`); | ||
this.logger.log(`[-] Unable to apply changes to AST: ${e}`); | ||
} | ||
@@ -181,0 +180,0 @@ ++this.appliedCounter; |
229
src/flast.js
import {parse} from 'espree'; | ||
import estraverse from 'estraverse'; | ||
import {analyze} from 'eslint-scope'; | ||
@@ -8,2 +7,3 @@ import {logger} from './utils/logger.js'; | ||
const ecmaVersion = 'latest'; | ||
const currentYear = (new Date()).getFullYear(); | ||
const sourceType = 'module'; | ||
@@ -23,27 +23,6 @@ | ||
const excludedParentKeys = [ | ||
'type', 'start', 'end', 'range', 'sourceType', 'comments', 'srcClosure', 'nodeId', | ||
'childNodes', 'parentNode', 'parentKey', 'scope', 'typeMap', 'lineage', 'allScopes', | ||
'type', 'start', 'end', 'range', 'sourceType', 'comments', 'srcClosure', 'nodeId', 'leadingComments', 'trailingComments', | ||
'childNodes', 'parentNode', 'parentKey', 'scope', 'typeMap', 'lineage', 'allScopes', 'tokens', | ||
]; | ||
/** | ||
* Return the key the child node is assigned in the parent node if applicable; null otherwise. | ||
* @param {ASTNode} node | ||
* @returns {string|null} | ||
*/ | ||
function getParentKey(node) { | ||
if (node.parentNode) { | ||
const keys = Object.keys(node.parentNode); | ||
for (let i = 0; i < keys.length; i++) { | ||
if (excludedParentKeys.includes(keys[i])) continue; | ||
if (node.parentNode[keys[i]] === node) return keys[i]; | ||
if (Array.isArray(node.parentNode[keys[i]])) { | ||
for (let j = 0; j < node.parentNode[keys[i]]?.length; j++) { | ||
if (node.parentNode[keys[i]][j] === node) return keys[i]; | ||
} | ||
} | ||
} | ||
} | ||
return null; | ||
} | ||
const generateFlatASTDefaultOptions = { | ||
@@ -79,3 +58,3 @@ // If false, do not include any scope details | ||
function generateFlatAST(inputCode, opts = {}) { | ||
opts = { ...generateFlatASTDefaultOptions, ...opts }; | ||
opts = {...generateFlatASTDefaultOptions, ...opts}; | ||
let tree = []; | ||
@@ -85,7 +64,2 @@ const rootNode = generateRootNode(inputCode, opts); | ||
tree = extractNodesFromRoot(rootNode, opts); | ||
if (opts.detailed) { | ||
const scopes = getAllScopes(rootNode); | ||
for (let i = 0; i < tree.length; i++) injectScopeToNode(tree[i], scopes); | ||
tree[0].allScopes = scopes; | ||
} | ||
} | ||
@@ -117,4 +91,9 @@ return tree; | ||
/** | ||
* @param {string} inputCode | ||
* @param {object} [opts] | ||
* @return {ASTNode} | ||
*/ | ||
function generateRootNode(inputCode, opts = {}) { | ||
opts = { ...generateFlatASTDefaultOptions, ...opts }; | ||
opts = {...generateFlatASTDefaultOptions, ...opts}; | ||
const parseOpts = opts.parseOpts || {}; | ||
@@ -132,34 +111,64 @@ let rootNode; | ||
/** | ||
* @param rootNode | ||
* @param opts | ||
* @return {ASTNode[]} | ||
*/ | ||
function extractNodesFromRoot(rootNode, opts) { | ||
opts = { ...generateFlatASTDefaultOptions, ...opts }; | ||
const tree = []; | ||
opts = {...generateFlatASTDefaultOptions, ...opts}; | ||
let nodeId = 0; | ||
const typeMap = {}; | ||
const allNodes = []; | ||
const scopes = opts.detailed ? getAllScopes(rootNode) : {}; | ||
// noinspection JSUnusedGlobalSymbols | ||
estraverse.traverse(rootNode, { | ||
/** | ||
* @param {ASTNode} node | ||
* @param {ASTNode} parentNode | ||
*/ | ||
enter(node, parentNode) { | ||
tree.push(node); | ||
node.nodeId = nodeId++; | ||
if (!typeMap[node.type]) typeMap[node.type] = [node]; | ||
else typeMap[node.type].push(node); | ||
node.childNodes = []; | ||
node.parentNode = parentNode; | ||
node.parentKey = parentNode ? getParentKey(node) : ''; | ||
node.lineage = [...parentNode?.lineage || []]; | ||
if (parentNode) { | ||
node.lineage.push(parentNode.nodeId); | ||
parentNode.childNodes.push(node); | ||
const stack = [rootNode]; | ||
while (stack.length) { | ||
const node = stack.shift(); | ||
if (node.nodeId) continue; | ||
node.childNodes = node.childNodes || []; | ||
const childrenLoc = {}; // Store the location of child nodes to sort them by order | ||
node.parentKey = node.parentKey || ''; // Make sure parentKey exists | ||
// Iterate over all keys of the node to find child nodes | ||
const keys = Object.keys(node); | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
if (excludedParentKeys.includes(key)) continue; | ||
const content = node[key]; | ||
if (content && typeof content === 'object') { | ||
// Sort each child node by its start position | ||
// and set the parentNode and parentKey attributes | ||
if (Array.isArray(content)) { | ||
for (let j = 0; j < content.length; j++) { | ||
const childNode = content[j]; | ||
childNode.parentNode = node; | ||
childNode.parentKey = key; | ||
childrenLoc[childNode.start] = childNode; | ||
} | ||
} else { | ||
content.parentNode = node; | ||
content.parentKey = key; | ||
childrenLoc[content.start] = content; | ||
} | ||
} | ||
if (opts.includeSrc && !node.src) Object.defineProperty(node, 'src', { | ||
get() { return rootNode.srcClosure(node.range[0], node.range[1]);}, | ||
}); | ||
} | ||
}); | ||
if (tree?.length) tree[0].typeMap = typeMap; | ||
return tree; | ||
// Add the child nodes to top of the stack and populate the node's childNodes array | ||
stack.unshift(...Object.values(childrenLoc)); | ||
node.childNodes.push(...Object.values(childrenLoc)); | ||
allNodes.push(node); | ||
node.nodeId = nodeId++; | ||
typeMap[node.type] = typeMap[node.type] || []; | ||
typeMap[node.type].push(node); | ||
node.lineage = [...node.parentNode?.lineage || []]; | ||
if (node.parentNode) { | ||
node.lineage.push(node.parentNode.start); | ||
} | ||
// Add a getter for the node's source code | ||
if (opts.includeSrc && !node.src) Object.defineProperty(node, 'src', { | ||
get() {return rootNode.srcClosure(node.start, node.end);}, | ||
}); | ||
if (opts.detailed) injectScopeToNode(node, scopes); | ||
} | ||
if (allNodes?.length) allNodes[0].typeMap = typeMap; | ||
return allNodes; | ||
} | ||
@@ -178,4 +187,7 @@ | ||
// Prevent assigning declNode to member expression properties or object keys | ||
const variables = node.scope.variables.filter(n => n.name === node.name); | ||
if (node.parentKey === 'id' || (variables?.length && variables[0].identifiers.some(n => n === node))) { | ||
const variables = []; | ||
for (let i = 0; i < node.scope.variables.length; i++) { | ||
if (node.scope.variables[i].name === node.name) variables.push(node.scope.variables[i]); | ||
} | ||
if (node.parentKey === 'id' || variables?.[0]?.identifiers?.includes(node)) { | ||
node.references = node.references || []; | ||
@@ -186,7 +198,16 @@ } else { | ||
if (variables?.length) { | ||
decls = variables.find(n => n.name === node.name)?.identifiers; | ||
for (let i = 0; i < variables.length; i++) { | ||
if (variables[i].name === node.name) { | ||
decls = variables[i].identifiers || []; | ||
break; | ||
} | ||
} | ||
} | ||
else { | ||
const scopeReference = node.scope.references.find(n => n.identifier.name === node.name); | ||
if (scopeReference) decls = scopeReference.resolved?.identifiers || []; | ||
for (let i = 0; i < node.scope.references.length; i++) { | ||
if (node.scope.references[i].identifier.name === node.name) { | ||
decls = node.scope.references[i].resolved?.identifiers || []; | ||
break; | ||
} | ||
} | ||
} | ||
@@ -228,38 +249,26 @@ let declNode = decls[0]; | ||
/** | ||
* @param {ASTNode} node | ||
* @param {ASTScope[]} scopes | ||
* @return {Promise} | ||
* @param {ASTNode} rootNode | ||
* @return {{number: ASTScope}} | ||
*/ | ||
async function injectScopeToNodeAsync(node, scopes) { | ||
return new Promise((resolve, reject) => { | ||
try { | ||
injectScopeToNode(node, scopes); | ||
resolve(); | ||
} catch (e) { | ||
reject(e); | ||
} | ||
}); | ||
} | ||
function getAllScopes(rootNode) { | ||
// noinspection JSCheckFunctionSignatures | ||
const globalScope = analyze(rootNode, { | ||
optimistic: true, | ||
ecmaVersion: (new Date()).getFullYear(), | ||
ecmaVersion: currentYear, | ||
sourceType}).acquireAll(rootNode)[0]; | ||
const allScopes = {}; | ||
const stack = [globalScope]; | ||
const seen = []; | ||
while (stack.length) { | ||
let scope = stack.pop(); | ||
if (seen.includes(scope)) continue; | ||
seen.push(scope); | ||
const scopeId = scope.block.nodeId; | ||
let scope = stack.shift(); | ||
const scopeId = scope.block.start; | ||
scope.block.isScopeBlock = true; | ||
if (!allScopes[scopeId]) { | ||
allScopes[scopeId] = scope; | ||
allScopes[scopeId] = allScopes[scopeId] || scope; | ||
stack.unshift(...scope.childScopes); | ||
// A single global scope is enough, so if there are variables in a module scope, add them to the global scope | ||
if (scope.type === 'module' && scope.upper === globalScope && scope.variables?.length) { | ||
for (let i = 0; i < scope.variables.length; i++) { | ||
const v = scope.variables[i]; | ||
if (!globalScope.variables.includes(v)) globalScope.variables.push(v); | ||
} | ||
} | ||
stack.push(...scope.childScopes); | ||
if (scope.type === 'module' && scope.upper?.type === 'global' && scope.variables?.length) { | ||
for (const v of scope.variables) if (!scope.upper.variables.includes(v)) scope.upper.variables.push(v); | ||
} | ||
} | ||
@@ -276,47 +285,21 @@ rootNode.allScopes = allScopes; | ||
function matchScopeToNode(node, allScopes) { | ||
if (node.lineage?.length) { | ||
for (const nid of [...node.lineage].reverse()) { | ||
if (allScopes[nid]) { | ||
let scope = allScopes[nid]; | ||
if (scope.type.includes('-name') && scope?.childScopes?.length === 1) scope = scope.childScopes[0]; | ||
return scope; | ||
} | ||
} | ||
let scopeBlock = node; | ||
while (scopeBlock && !scopeBlock.isScopeBlock) { | ||
scopeBlock = scopeBlock.parentNode; | ||
} | ||
return allScopes[0]; // Global scope - this should never be reached | ||
let scope; | ||
if (scopeBlock) { | ||
scope = allScopes[scopeBlock.start]; | ||
if (scope.type.includes('-name') && scope?.childScopes?.length === 1) scope = scope.childScopes[0]; | ||
} else scope = allScopes[0]; // Global scope - this should never be reached | ||
return scope; | ||
} | ||
/** | ||
* | ||
* @param {string} inputCode | ||
* @param {object} opts | ||
* @return {Promise<ASTNode[]>} | ||
*/ | ||
async function generateFlatASTAsync(inputCode, opts = {}) { | ||
opts = { ...generateFlatASTDefaultOptions, ...opts }; | ||
let tree = []; | ||
const promises = []; | ||
const rootNode = generateRootNode(inputCode, opts); | ||
if (rootNode) { | ||
tree = extractNodesFromRoot(rootNode, opts); | ||
if (opts.detailed) { | ||
const scopes = getAllScopes(rootNode); | ||
for (let i = 0; i < tree.length; i++) { | ||
promises.push(injectScopeToNodeAsync(tree[i], scopes)); | ||
} | ||
} | ||
} | ||
return Promise.all(promises).then(() => tree); | ||
} | ||
export { | ||
estraverse, | ||
extractNodesFromRoot, | ||
generateCode, | ||
generateFlatAST, | ||
generateFlatASTAsync, | ||
generateRootNode, | ||
injectScopeToNode, | ||
injectScopeToNodeAsync, | ||
parseCode, | ||
}; |
@@ -26,7 +26,8 @@ import {Arborist} from '../arborist.js'; | ||
scriptSnapshot = script; | ||
// Mark each node with the script hash to distinguish cache of different scripts. | ||
for (let i = 0; i < arborist.ast.length; i++) arborist.ast[i].scriptHash = scriptHash; | ||
// Mark the root node with the script hash to distinguish cache of different scripts. | ||
arborist.ast[0].scriptHash = scriptHash; | ||
for (let i = 0; i < funcs.length; i++) { | ||
const func = funcs[i]; | ||
const funcStartTime = +new Date(); | ||
const funcStartTime = Date.now(); | ||
try { | ||
@@ -44,3 +45,3 @@ logger.debug(`\t[!] Running ${func.name}...`); | ||
scriptHash = generateHash(script); | ||
for (let j = 0; j < arborist.ast.length; j++) arborist.ast[j].scriptHash = scriptHash; | ||
arborist.ast[0].scriptHash = scriptHash; | ||
} | ||
@@ -51,3 +52,3 @@ } catch (e) { | ||
logger.debug(`\t\t[!] Running ${func.name} completed in ` + | ||
`${((+new Date() - funcStartTime) / 1000).toFixed(3)} seconds`); | ||
`${((Date.now() - funcStartTime) / 1000).toFixed(3)} seconds`); | ||
} | ||
@@ -54,0 +55,0 @@ } |
@@ -24,3 +24,3 @@ import path from 'node:path'; | ||
for (const [k, v] of Object.entries(node)) { | ||
assert.equal(v, parsedNode[k], `Node #${parsedNode[k]} parsed wrong on key '${k}'`); | ||
assert.equal(v, parsedNode[k], `Node #${node.nodeId} parsed wrong on key '${k}'`); | ||
} | ||
@@ -34,3 +34,2 @@ }); | ||
'ASTScope', | ||
'estraverse', | ||
'generateCode', | ||
@@ -37,0 +36,0 @@ 'generateFlatAST', |
3
58419
1122
- Removedestraverse@^5.3.0