New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

flast

Package Overview
Dependencies
Maintainers
0
Versions
28
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 2.1.1 to 2.2.0

5

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

39

src/arborist.js

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

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

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