Comparing version 1.3.4 to 1.4.0
{ | ||
"name": "flast", | ||
"version": "1.3.4", | ||
"version": "1.4.0", | ||
"description": "Flatten JS AST", | ||
@@ -28,10 +28,10 @@ "main": "src/index.js", | ||
"escodegen": "^2.0.0", | ||
"espree": "^9.0.0", | ||
"eslint-scope": "^6.0.0", | ||
"espree": "^9.5.2", | ||
"eslint-scope": "^7.2.0", | ||
"estraverse": "^5.3.0" | ||
}, | ||
"devDependencies": { | ||
"eslint": "^8.19.0", | ||
"husky": "^8.0.1" | ||
"eslint": "^8.43.0", | ||
"husky": "^8.0.3" | ||
} | ||
} |
@@ -1,2 +0,1 @@ | ||
const estraverse = require('estraverse'); | ||
const {generateCode, generateFlatAST,} = require(__dirname + '/flast'); | ||
@@ -13,6 +12,5 @@ | ||
this.log = logFunc || (() => true); | ||
this.maxLogLength = 60; // Max length of logged strings. | ||
this.markedForDeletion = []; // Array of node ids. | ||
this.markedForReplacement = {}; // nodeId: replacementNode pairs. | ||
this.appliedCounter = 0; // Track the number of times changes were applied. | ||
this.replacements = []; | ||
if (typeof scriptOrFlatAstArr === 'string') { | ||
@@ -43,17 +41,2 @@ this.script = scriptOrFlatAstArr; | ||
/** | ||
* @param {string} src | ||
* @param {boolean} padEnd Pad end with spaces to the maxLogLength if true. | ||
* @returns {string} A parsed string fit for a log. | ||
* @private | ||
*/ | ||
_parseSrcForLog(src, padEnd = false) { | ||
const output = src | ||
.replace(/\n/g, ' ') | ||
.substring(0, this.maxLogLength) | ||
.replace(/([\n\r])/g, ' ') | ||
.replace(/\s{2,}/g, ' '); | ||
return padEnd ? output.padEnd(this.maxLogLength, ' ') : output; | ||
} | ||
/** | ||
* | ||
@@ -63,3 +46,3 @@ * @returns {number} The number of changes to be applied. | ||
getNumberOfChanges() { | ||
return Object.keys(this.markedForReplacement).length + this.markedForDeletion.length; | ||
return this.replacements.length + this.markedForDeletion.length; | ||
} | ||
@@ -76,3 +59,3 @@ | ||
if (replacementNode) { // Mark for replacement | ||
this.markedForReplacement[targetNode.nodeId] = replacementNode; | ||
this.replacements.push([targetNode, replacementNode]); | ||
targetNode.isMarked = true; | ||
@@ -86,3 +69,2 @@ } else { // Mark for deletion | ||
} | ||
this.ast = this.ast.filter(n => n.nodeId !== targetNode.nodeId); | ||
} | ||
@@ -100,57 +82,49 @@ } | ||
const that = this; | ||
const replacementNodeIds = Object.keys(this.markedForReplacement).map(nid => parseInt(nid)); | ||
if (this.getNumberOfChanges() > 0) { | ||
let rootNode = this.ast[0]; | ||
if (replacementNodeIds.includes(0)) { | ||
const rootNodeReplacement = this.replacements.find(n => n[0].nodeId === 0); | ||
if (rootNodeReplacement) { | ||
++changesCounter; | ||
this.log(`[+] Applying changes to the root node...`); | ||
rootNode = this.markedForReplacement[0]; | ||
rootNode = rootNodeReplacement[1]; | ||
} else { | ||
const removalLogCache = []; // Prevents multiple printing of similar changes to the log | ||
const replacementLogCache = []; | ||
const badReplacements = []; | ||
estraverse.replace(rootNode, { | ||
enter(node) { | ||
try { | ||
if (replacementNodeIds.includes(node.nodeId) && node.isMarked) { | ||
if (node.src) { | ||
try { | ||
if (badReplacements.includes(node.src)) return; | ||
const nsrc = that._parseSrcForLog(node.src, true); | ||
if (!replacementLogCache.includes(nsrc)) { | ||
const tsrc = that._parseSrcForLog(generateCode(that.markedForReplacement[node.nodeId])); | ||
that.log(`\t\t[+] Replacing\t${nsrc}\t--with--\t${tsrc}`, 2); | ||
replacementLogCache.push(nsrc); | ||
} | ||
} catch { | ||
that.log(`\t\t[+] Replacing ${that._parseSrcForLog('N/A')}\t--with\t${that._parseSrcForLog('N/A')}`); | ||
} | ||
} | ||
for (const targetNodeId of this.markedForDeletion) { | ||
try { | ||
const targetNode = this.ast.find(n => n.nodeId === targetNodeId); | ||
if (targetNode) { | ||
const parent = targetNode.parentNode; | ||
if (parent[targetNode.parentKey] === targetNode) { | ||
parent[targetNode.parentKey] = undefined; | ||
++changesCounter; | ||
return that.markedForReplacement[node.nodeId]; | ||
} else if (that.markedForDeletion.includes(node.nodeId) && node.isMarked) { | ||
if (node.src) { | ||
try { | ||
const ns = that._parseSrcForLog(node.src); | ||
if (!removalLogCache.includes(ns)) { | ||
that.log(`\t\t[+] Removing\t${ns}`, 2); | ||
removalLogCache.push(ns); | ||
} | ||
} catch { | ||
that.log(`\t\t[+] Removing\tN/A`, 2); | ||
} | ||
} | ||
this.remove(); | ||
} else if (Array.isArray(parent[targetNode.parentKey])) { | ||
const idx = parent[targetNode.parentKey].indexOf(targetNode); | ||
parent[targetNode.parentKey][idx] = undefined; | ||
parent[targetNode.parentKey] = parent[targetNode.parentKey].filter(n => n); | ||
++changesCounter; | ||
return null; | ||
} | ||
} catch (e) { | ||
that.log(`[-] Unable to replace/delete node: ${e}`); | ||
badReplacements.push(node.src); | ||
} | ||
}, | ||
}); | ||
} catch (e) { | ||
that.log(`[-] Unable to delete node: ${e}`); | ||
} | ||
} | ||
for (const [targetNode, replacementNode] of this.replacements) { | ||
try { | ||
if (targetNode) { | ||
const parent = targetNode.parentNode; | ||
if (parent[targetNode.parentKey] === targetNode) { | ||
parent[targetNode.parentKey] = replacementNode; | ||
++changesCounter; | ||
} else if (Array.isArray(parent[targetNode.parentKey])) { | ||
const idx = parent[targetNode.parentKey].indexOf(targetNode); | ||
parent[targetNode.parentKey][idx] = replacementNode; | ||
++changesCounter; | ||
} | ||
} | ||
} catch (e) { | ||
that.log(`[-] Unable to replace node: ${e}`); | ||
} | ||
} | ||
} | ||
if (changesCounter) { | ||
this.markedForReplacement = {}; | ||
this.replacements.length = 0; | ||
this.markedForDeletion.length = 0; | ||
@@ -157,0 +131,0 @@ // If any of the changes made will break the script the next line will fail and the |
214
src/flast.js
@@ -18,15 +18,21 @@ const {parse} = require('espree'); | ||
const excludedParentKeys = [ | ||
'type', 'start', 'end', 'range', 'sourceType', 'comments', 'srcClosure', 'nodeId', | ||
'childNodes', 'parentNode', 'parentKey', 'scope', | ||
]; | ||
/** | ||
* Return the key the child node is assigned in the parent node if applicable; null otherwise. | ||
* @param {ASTNode} parent | ||
* @param {number} targetChildNodeId | ||
* @param {ASTNode} node | ||
* @returns {string|null} | ||
*/ | ||
function getParentKey(parent, targetChildNodeId) { | ||
if (parent) { | ||
for (const key of Object.keys(parent)) { | ||
if (parent[key]?.nodeId === targetChildNodeId) return key; | ||
else if (Array.isArray(parent[key])) { | ||
for (const item of (parent[key] || [])) { | ||
if (item?.nodeId === targetChildNodeId) return key; | ||
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]; | ||
} | ||
@@ -40,5 +46,5 @@ } | ||
const generateFlatASTDefaultOptions = { | ||
// If false, include only original node with nodeId, without any further details | ||
// If false, do not include any scope details | ||
detailed: true, | ||
// If false, do not include node src. Only available when `detailed` option is true | ||
// If false, do not include node src | ||
includeSrc: true, | ||
@@ -69,84 +75,8 @@ // Retry to parse the code with sourceType: 'script' if 'module' failed with 'strict' error message | ||
opts = { ...generateFlatASTDefaultOptions, ...opts }; | ||
const parseOpts = opts.parseOpts || {}; | ||
let rootNode; | ||
try { | ||
rootNode = parseCode(inputCode, parseOpts); | ||
} catch (e) { | ||
if (opts.alernateSourceTypeOnFailure && e.message.includes('in strict mode')) rootNode = parseCode(inputCode, {...parseOpts, sourceType: 'script'}); | ||
const rootNode = generateRootNode(inputCode, opts); | ||
const tree = extractNodesFromRoot(rootNode, opts); | ||
const sm = initScopeManager(rootNode); | ||
if (opts.detailed) { | ||
for (let i = 0; i < tree.length; i++) injectScopeToNode(tree[i], sm); | ||
} | ||
let scopeManager; | ||
let srcClosure; | ||
try { | ||
if (opts.detailed) { // noinspection JSCheckFunctionSignatures | ||
scopeManager = analyze(rootNode, { | ||
optimistic: true, | ||
ecmaVersion, | ||
sourceType}); | ||
if (opts.includeSrc) srcClosure = createSrcClosure(inputCode); | ||
} | ||
} catch {} | ||
const tree = []; | ||
let nodeId = 0; | ||
let scopeId = 0; | ||
estraverse.traverse(rootNode, { | ||
/** | ||
* @param {ASTNode} node | ||
* @param {ASTNode} parentNode | ||
*/ | ||
enter(node, parentNode) { | ||
node.nodeId = nodeId++; | ||
if (opts.detailed) { | ||
if (opts.includeSrc) Object.defineProperty(node, 'src', { | ||
get() { return srcClosure(node.range[0], node.range[1]);}, | ||
}); | ||
node.childNodes = []; | ||
node.parentNode = parentNode; | ||
node.parentKey = parentNode ? getParentKey(parentNode, node.nodeId) : ''; | ||
// Keep track of the node's lineage | ||
if (parentNode) node.lineage = [...parentNode?.lineage || [], parentNode.nodeId]; | ||
// Acquire scope | ||
node.scope = scopeManager.acquire(node); | ||
if (!node.scope) node.scope = node.parentNode.scope; | ||
else if (node.scope.type.includes('-name') && node.scope?.childScopes?.length === 1) node.scope = node.scope.childScopes[0]; | ||
if (node.scope.scopeId === undefined) node.scope.scopeId = scopeId++; | ||
if (parentNode) parentNode.childNodes.push(node); | ||
if (node.type === 'Identifier') { | ||
// Track references and declarations | ||
// Prevent assigning declNode to member expression properties or object keys | ||
if (!(['property', 'key'].includes(node.parentKey) && !parentNode.computed)) { | ||
const variables = node.scope.variables.filter(n => n.name === node.name); | ||
const isDeclaration = variables?.length && variables[0].identifiers.filter(n => n.nodeId === node.nodeId).length; | ||
if (isDeclaration) node.references = node.references || []; | ||
else if (!(node.parentKey === 'id' && node.parentNode.type === 'FunctionDeclaration')) { | ||
// Find declaration by finding the closest declaration of the same name. | ||
let decls = []; | ||
if (variables?.length) decls = variables.filter(n => n.name === node.name)[0].identifiers; | ||
else { | ||
const scopeReferences = node.scope.references.filter(n => n.identifier.name === node.name); | ||
if (scopeReferences.length) decls = scopeReferences[0].resolved?.identifiers || []; | ||
} | ||
let declNode = decls[0]; | ||
if (decls.length > 1) { // TODO: Defer setting declaration and references | ||
let commonAncestors = node.lineage.reduce((t, c) => declNode.lineage?.includes(c) ? ++t : t, 0); | ||
decls.slice(1).forEach(n => { | ||
const ca = node.lineage.reduce((t, c) => n.lineage?.includes(c) ? ++t : t, 0); | ||
if (ca > commonAncestors) { | ||
commonAncestors = ca; | ||
declNode = n; | ||
} | ||
}); | ||
} | ||
if (declNode) { | ||
if (!declNode.references) declNode.references = []; | ||
declNode.references.push(node); | ||
node.declNode = declNode; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
tree.push(node); | ||
}, | ||
}); | ||
return tree; | ||
@@ -177,7 +107,107 @@ } | ||
function generateRootNode(inputCode, opts = {}) { | ||
opts = { ...generateFlatASTDefaultOptions, ...opts }; | ||
const parseOpts = opts.parseOpts || {}; | ||
let rootNode; | ||
try { | ||
rootNode = parseCode(inputCode, parseOpts); | ||
if (opts.includeSrc) rootNode.srcClosure = createSrcClosure(inputCode); | ||
} catch (e) { | ||
if (opts.alernateSourceTypeOnFailure && e.message.includes('in strict mode')) rootNode = parseCode(inputCode, {...parseOpts, sourceType: 'script'}); | ||
} | ||
return rootNode; | ||
} | ||
function extractNodesFromRoot(rootNode, opts) { | ||
opts = { ...generateFlatASTDefaultOptions, ...opts }; | ||
const tree = []; | ||
let nodeId = 0; | ||
estraverse.traverse(rootNode, { | ||
/** | ||
* @param {ASTNode} node | ||
* @param {ASTNode} parentNode | ||
*/ | ||
enter(node, parentNode) { | ||
tree.push(node); | ||
node.nodeId = nodeId++; | ||
node.childNodes = []; | ||
node.parentNode = parentNode; | ||
// Keep track of the node's lineage | ||
node.parentKey = parentNode ? getParentKey(node) : ''; | ||
if (opts.includeSrc) Object.defineProperty(node, 'src', { | ||
get() { return rootNode.srcClosure(node.range[0], node.range[1]);}, | ||
}); | ||
} | ||
}); | ||
return tree; | ||
} | ||
function initScopeManager(rootNode) { | ||
// noinspection JSCheckFunctionSignatures | ||
return analyze(rootNode, { | ||
optimistic: true, | ||
ecmaVersion, | ||
sourceType}); | ||
} | ||
/** | ||
* | ||
* @param {ASTNode} node | ||
* @param {ScopeManager} sm | ||
*/ | ||
function injectScopeToNode(node, sm) { | ||
let parentNode = node.parentNode; | ||
// Acquire scope | ||
node.scope = sm.acquire(node); | ||
if (!node.scope) node.scope = node.parentNode.scope; | ||
else if (node.scope.type.includes('-name') && node.scope?.childScopes?.length === 1) node.scope = node.scope.childScopes[0]; | ||
if (node.scope.scopeId === undefined) node.scope.scopeId = node.scope.block.nodeId; | ||
if (parentNode) { | ||
node.lineage = [...parentNode?.lineage || [], parentNode.nodeId]; | ||
parentNode.childNodes.push(node); | ||
} | ||
if (node.type === 'Identifier') { | ||
// Track references and declarations | ||
// Prevent assigning declNode to member expression properties or object keys | ||
if (!(['property', 'key'].includes(node.parentKey) && !parentNode.computed)) { | ||
const variables = node.scope.variables.filter(n => n.name === node.name); | ||
const isDeclaration = variables?.length && variables[0].identifiers.filter(n => n.nodeId === node.nodeId).length; | ||
if (isDeclaration) node.references = node.references || []; | ||
else if (!(node.parentKey === 'id' && node.parentNode.type === 'FunctionDeclaration')) { | ||
// Find declaration by finding the closest declaration of the same name. | ||
let decls = []; | ||
if (variables?.length) decls = variables.filter(n => n.name === node.name)[0].identifiers; | ||
else { | ||
const scopeReferences = node.scope.references.filter(n => n.identifier.name === node.name); | ||
if (scopeReferences.length) decls = scopeReferences[0].resolved?.identifiers || []; | ||
} | ||
let declNode = decls[0]; | ||
if (decls.length > 1) { // TODO: Defer setting declaration and references | ||
let commonAncestors = node.lineage.reduce((t, c) => declNode.lineage?.includes(c) ? ++t : t, 0); | ||
decls.slice(1).forEach(n => { | ||
const ca = node.lineage.reduce((t, c) => n.lineage?.includes(c) ? ++t : t, 0); | ||
if (ca > commonAncestors) { | ||
commonAncestors = ca; | ||
declNode = n; | ||
} | ||
}); | ||
} | ||
if (declNode) { | ||
if (!declNode.references) declNode.references = []; | ||
declNode.references.push(node); | ||
node.declNode = declNode; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
module.exports = { | ||
estraverse, | ||
extractNodesFromRoot, | ||
generateCode, | ||
generateFlatAST, | ||
generateRootNode, | ||
parseCode, | ||
}; |
@@ -22,2 +22,3 @@ const {Scope} = require('eslint-scope'); | ||
* @property {number} [end] | ||
* @property {ASTNode} [exported] | ||
* @property {ASTNode|boolean} [expression] | ||
@@ -28,2 +29,3 @@ * @property {ASTNode[]} [expressions] | ||
* @property {ASTNode} [id] | ||
* @property {ASTNode} [imported] | ||
* @property {ASTNode} [init] | ||
@@ -36,2 +38,3 @@ * @property {boolean} [isMarked] | ||
* @property {number[]} [lineage] | ||
* @property {ASTNode} [local] | ||
* @property {boolean} [method] | ||
@@ -58,3 +61,5 @@ * @property {string} [name] | ||
* @property {boolean} [shorthand] | ||
* @property {ASTNode} [source] | ||
* @property {string} [sourceType] | ||
* @property {ASTNode[]} [specifiers] | ||
* @property {boolean} [static] | ||
@@ -74,4 +79,5 @@ * @property {number} [start] | ||
* @extends Scope | ||
* @property {ASTNode} block | ||
* @property {ASTScope[]} childScopes | ||
* @property {number} scopeId | ||
* @property {ASTScope[]} childScopes | ||
*/ | ||
@@ -78,0 +84,0 @@ class ASTScope extends Scope {} |
@@ -97,7 +97,11 @@ const assert = require('node:assert'); | ||
const code = `var a = [1]; a[0];`; | ||
const noDetailsAst = generateFlatAST(code, {detailed: false, includeSrc: true}); // includeSrc will be ignored | ||
const noDetailsAst = generateFlatAST(code, {detailed: false}); | ||
const [noDetailsVarDec, noDetailsVarRef] = noDetailsAst.filter(n => n.type === 'Identifier'); | ||
assert.equal(noDetailsVarDec.parentNode || noDetailsVarDec.childNodes || noDetailsVarDec.references || | ||
noDetailsVarRef.declNode || noDetailsVarRef.scope || noDetailsVarRef.src, undefined, | ||
`Flat AST generated with details despite 'detailed' option set to false.`); | ||
assert.equal(noDetailsVarDec.references || noDetailsVarRef.declNode || noDetailsVarRef.scope, undefined, | ||
`Flat AST generated with details despite 'detailed' option set to false.`); | ||
const noSrcAst = generateFlatAST(code, {includeSrc: false}); | ||
assert.equal(noSrcAst.find(n => n.src !== undefined), null, | ||
`Flat AST generated with src despite 'includeSrc' option set to false.`); | ||
const detailedAst = generateFlatAST(code, {detailed: true}); | ||
@@ -108,2 +112,3 @@ const [detailedVarDec, detailedVarRef] = detailedAst.filter(n => n.type === 'Identifier'); | ||
`Flat AST missing details despite 'detailed' option set to true.`); | ||
const detailedNoSrcAst = generateFlatAST(code, {detailed: true, includeSrc: false}); | ||
@@ -110,0 +115,0 @@ assert.equal(detailedNoSrcAst[0].src, undefined, |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
857
43810
+ Addedeslint-scope@7.2.2(transitive)
- Removedeslint-scope@6.0.0(transitive)
Updatedeslint-scope@^7.2.0
Updatedespree@^9.5.2