Skip to content

Commit ea9871d

Browse files
authored
1.5.0 Optimization and Async (#22)
* Refactor generation of flat AST and optimize scoping and attaching references. Fix issue with scope blocks appearing as their own scope. Function now appear in their upper scope, rather than in themselves. Create generateFlastASTAsync. It's a bit slower than the non-async version, but at least it doesn't block. * Add missing properties * 1.5.0
1 parent 8235b5a commit ea9871d

File tree

4 files changed

+138
-53
lines changed

4 files changed

+138
-53
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "flast",
3-
"version": "1.4.0",
3+
"version": "1.5.0",
44
"description": "Flatten JS AST",
55
"main": "src/index.js",
66
"scripts": {

src/flast.js

Lines changed: 131 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ function generateFlatAST(inputCode, opts = {}) {
7272
opts = { ...generateFlatASTDefaultOptions, ...opts };
7373
const rootNode = generateRootNode(inputCode, opts);
7474
const tree = extractNodesFromRoot(rootNode, opts);
75-
const sm = initScopeManager(rootNode);
7675
if (opts.detailed) {
77-
for (let i = 0; i < tree.length; i++) injectScopeToNode(tree[i], sm);
76+
const scopes = getAllScopes(rootNode);
77+
for (let i = 0; i < tree.length; i++) injectScopeToNode(tree[i], scopes);
7878
}
7979
return tree;
8080
}
@@ -119,6 +119,7 @@ function extractNodesFromRoot(rootNode, opts) {
119119
const tree = [];
120120
let nodeId = 0;
121121

122+
// noinspection JSUnusedGlobalSymbols
122123
estraverse.traverse(rootNode, {
123124
/**
124125
* @param {ASTNode} node
@@ -129,8 +130,12 @@ function extractNodesFromRoot(rootNode, opts) {
129130
node.nodeId = nodeId++;
130131
node.childNodes = [];
131132
node.parentNode = parentNode;
132-
// Keep track of the node's lineage
133133
node.parentKey = parentNode ? getParentKey(node) : '';
134+
node.lineage = [...parentNode?.lineage || []];
135+
if (parentNode) {
136+
node.lineage.push(parentNode.nodeId);
137+
parentNode.childNodes.push(node);
138+
}
134139
if (opts.includeSrc) Object.defineProperty(node, 'src', {
135140
get() { return rootNode.srcClosure(node.range[0], node.range[1]);},
136141
});
@@ -139,71 +144,147 @@ function extractNodesFromRoot(rootNode, opts) {
139144
return tree;
140145
}
141146

142-
function initScopeManager(rootNode) {
143-
// noinspection JSCheckFunctionSignatures
144-
return analyze(rootNode, {
145-
optimistic: true,
146-
ecmaVersion,
147-
sourceType});
148-
}
149-
150147
/**
151-
*
152148
* @param {ASTNode} node
153-
* @param {ScopeManager} sm
149+
* @param {ASTScope[]} scopes
154150
*/
155-
function injectScopeToNode(node, sm) {
151+
function injectScopeToNode(node, scopes) {
156152
let parentNode = node.parentNode;
157153
// Acquire scope
158-
node.scope = sm.acquire(node);
159-
if (!node.scope) node.scope = node.parentNode.scope;
160-
else if (node.scope.type.includes('-name') && node.scope?.childScopes?.length === 1) node.scope = node.scope.childScopes[0];
161-
if (node.scope.scopeId === undefined) node.scope.scopeId = node.scope.block.nodeId;
162-
if (parentNode) {
163-
node.lineage = [...parentNode?.lineage || [], parentNode.nodeId];
164-
parentNode.childNodes.push(node);
165-
}
166-
if (node.type === 'Identifier') {
154+
node.scope = matchScopeToNode(node, scopes);
155+
if (node.type === 'Identifier' && !(!parentNode.computed && ['property', 'key'].includes(node.parentKey))) {
167156
// Track references and declarations
168157
// Prevent assigning declNode to member expression properties or object keys
169-
if (!(['property', 'key'].includes(node.parentKey) && !parentNode.computed)) {
170-
const variables = node.scope.variables.filter(n => n.name === node.name);
171-
const isDeclaration = variables?.length && variables[0].identifiers.filter(n => n.nodeId === node.nodeId).length;
172-
if (isDeclaration) node.references = node.references || [];
173-
else if (!(node.parentKey === 'id' && node.parentNode.type === 'FunctionDeclaration')) {
174-
// Find declaration by finding the closest declaration of the same name.
175-
let decls = [];
176-
if (variables?.length) decls = variables.filter(n => n.name === node.name)[0].identifiers;
177-
else {
178-
const scopeReferences = node.scope.references.filter(n => n.identifier.name === node.name);
179-
if (scopeReferences.length) decls = scopeReferences[0].resolved?.identifiers || [];
180-
}
181-
let declNode = decls[0];
182-
if (decls.length > 1) { // TODO: Defer setting declaration and references
183-
let commonAncestors = node.lineage.reduce((t, c) => declNode.lineage?.includes(c) ? ++t : t, 0);
184-
decls.slice(1).forEach(n => {
185-
const ca = node.lineage.reduce((t, c) => n.lineage?.includes(c) ? ++t : t, 0);
186-
if (ca > commonAncestors) {
187-
commonAncestors = ca;
188-
declNode = n;
189-
}
190-
});
191-
}
192-
if (declNode) {
193-
if (!declNode.references) declNode.references = [];
194-
declNode.references.push(node);
195-
node.declNode = declNode;
158+
const variables = node.scope.variables.filter(n => n.name === node.name);
159+
if (node.parentKey === 'id' || (variables?.length && variables[0].identifiers.some(n => n === node))) {
160+
node.references = node.references || [];
161+
} else {
162+
// Find declaration by finding the closest declaration of the same name.
163+
let decls = [];
164+
if (variables?.length) {
165+
decls = variables.find(n => n.name === node.name)?.identifiers;
166+
}
167+
else {
168+
const scopeReference = node.scope.references.find(n => n.identifier.name === node.name);
169+
if (scopeReference) decls = scopeReference.resolved?.identifiers || [];
170+
}
171+
let declNode = decls[0];
172+
if (decls.length > 1) {
173+
let commonAncestors = maxSharedLength(declNode.lineage, node.lineage);
174+
for (let i = 1; i < decls.length; i++) {
175+
const ca = maxSharedLength(decls[i].lineage, node.lineage);
176+
if (ca > commonAncestors) {
177+
commonAncestors = ca;
178+
declNode = decls[i];
179+
}
196180
}
197181
}
182+
if (declNode) {
183+
declNode.references = declNode.references || [];
184+
declNode.references.push(node);
185+
node.declNode = declNode;
186+
}
187+
}
188+
}
189+
}
190+
191+
/**
192+
* @param {number[]} targetArr
193+
* @param {number[]} containedArr
194+
* @return {number} Return the maximum length of shared numbers
195+
*/
196+
function maxSharedLength(targetArr, containedArr) {
197+
let count = 0;
198+
for (let i = 0; i < containedArr.length; i++) {
199+
if (targetArr[i] !== containedArr[i]) break;
200+
++count;
201+
}
202+
return count;
203+
}
204+
205+
/**
206+
* @param {ASTNode} node
207+
* @param {ASTScope[]} scopes
208+
* @return {Promise}
209+
*/
210+
async function injectScopeToNodeAsync(node, scopes) {
211+
return new Promise((resolve, reject) => {
212+
try {
213+
injectScopeToNode(node, scopes);
214+
resolve();
215+
} catch (e) {
216+
reject(e);
198217
}
218+
});
219+
}
220+
221+
function getAllScopes(rootNode) {
222+
const globalScope = analyze(rootNode, {
223+
optimistic: true,
224+
ecmaVersion,
225+
sourceType}).acquireAll(rootNode)[0];
226+
const allScopes = {};
227+
const stack = [globalScope];
228+
while (stack.length) {
229+
let scope = stack.pop();
230+
const scopeId = scope.block.nodeId;
231+
scope.block.isScopeBlock = true;
232+
if (!allScopes[scopeId]) {
233+
allScopes[scopeId] = scope;
234+
stack.push(...scope.childScopes);
235+
}
236+
}
237+
rootNode.allScopes = allScopes;
238+
return allScopes;
239+
}
240+
241+
/**
242+
* @param {ASTNode} node
243+
* @param {ASTScope[]} allScopes
244+
* @return {ASTScope}
245+
*/
246+
function matchScopeToNode(node, allScopes) {
247+
if (node.lineage?.length) {
248+
for (const nid of [...node.lineage].reverse()) {
249+
if (allScopes[nid]) {
250+
let scope = allScopes[nid];
251+
if (scope.type.includes('-name') && scope?.childScopes?.length === 1) scope = scope.childScopes[0];
252+
return scope;
253+
}
254+
}
255+
}
256+
return allScopes[0]; // Global scope - this should never be reached
257+
}
258+
259+
/**
260+
*
261+
* @param {string} inputCode
262+
* @param {object} opts
263+
* @return {Promise<ASTNode[]>}
264+
*/
265+
async function generateFlatASTAsync(inputCode, opts = {}) {
266+
opts = { ...generateFlatASTDefaultOptions, ...opts };
267+
const rootNode = generateRootNode(inputCode, opts);
268+
const tree = extractNodesFromRoot(rootNode, opts);
269+
const promises = [];
270+
if (opts.detailed) {
271+
const scopes = getAllScopes(rootNode);
272+
for (let i = 0; i < tree.length; i++) {
273+
promises.push(injectScopeToNodeAsync(tree[i], scopes));
274+
}
275+
199276
}
277+
return Promise.all(promises).then(() => tree);
200278
}
201279

202280
module.exports = {
203281
estraverse,
204282
extractNodesFromRoot,
205283
generateCode,
206284
generateFlatAST,
285+
generateFlatASTAsync,
207286
generateRootNode,
287+
injectScopeToNode,
288+
injectScopeToNodeAsync,
208289
parseCode,
209290
};

src/types.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {Scope} = require('eslint-scope');
99
* @property {boolean} [async]
1010
* @property {ASTNode|ASTNode[]} [body]
1111
* @property {ASTNode} [callee]
12+
* @property {ASTNode[]} [cases]
1213
* @property {ASTNode[]} [childNodes]
1314
* @property {boolean} [computed]
1415
* @property {ASTNode} [consequent]
@@ -17,6 +18,7 @@ const {Scope} = require('eslint-scope');
1718
* @property {ASTNode[]} [declarations]
1819
* @property {ASTNode} [declNode]
1920
* @property {boolean} [delegate]
21+
* @property {ASTNode} [discriminant]
2022
* @property {ASTNode[]} [elements]
2123
* @property {number} [end]
2224
* @property {ASTNode} [exported]
@@ -28,6 +30,7 @@ const {Scope} = require('eslint-scope');
2830
* @property {ASTNode} [imported]
2931
* @property {ASTNode} [init]
3032
* @property {boolean} [isMarked]
33+
* @property {boolean} [isScopeBlock]
3134
* @property {ASTNode} [key]
3235
* @property {string} [kind]
3336
* @property {ASTNode} [label]
@@ -75,6 +78,7 @@ class ASTNode {}
7578
* @property {ASTNode} block
7679
* @property {ASTScope[]} childScopes
7780
* @property {number} scopeId
81+
* @property {string} type
7882
*/
7983
class ASTScope extends Scope {}
8084

0 commit comments

Comments
 (0)