Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

flast

Package Overview
Dependencies
Maintainers
2
Versions
24
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

flast - npm Package Compare versions

Comparing version 1.3.4 to 1.4.0

10

package.json
{
"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

@@ -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,

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc