|
import { walk } from 'estree-walker'; |
|
import is_reference from 'is-reference'; |
|
|
|
|
|
export function analyze(expression) { |
|
|
|
|
|
|
|
const map = new WeakMap(); |
|
|
|
|
|
const globals = new Map(); |
|
|
|
const scope = new Scope(null, false); |
|
|
|
|
|
const references = []; |
|
|
|
let current_scope = scope; |
|
|
|
walk(expression, { |
|
enter(node, parent) { |
|
switch (node.type) { |
|
case 'Identifier': |
|
if (parent && is_reference(node, parent)) { |
|
references.push([current_scope, node]); |
|
} |
|
break; |
|
|
|
case 'ImportDeclaration': |
|
node.specifiers.forEach((specifier) => { |
|
current_scope.declarations.set(specifier.local.name, specifier); |
|
}); |
|
break; |
|
|
|
case 'FunctionExpression': |
|
case 'FunctionDeclaration': |
|
case 'ArrowFunctionExpression': |
|
if (node.type === 'FunctionDeclaration') { |
|
if (node.id) { |
|
current_scope.declarations.set(node.id.name, node); |
|
} |
|
|
|
map.set(node, current_scope = new Scope(current_scope, false)); |
|
} else { |
|
map.set(node, current_scope = new Scope(current_scope, false)); |
|
|
|
if (node.type === 'FunctionExpression' && node.id) { |
|
current_scope.declarations.set(node.id.name, node); |
|
} |
|
} |
|
|
|
node.params.forEach(param => { |
|
extract_names(param).forEach(name => { |
|
current_scope.declarations.set(name, node); |
|
}); |
|
}); |
|
break; |
|
|
|
case 'ForStatement': |
|
case 'ForInStatement': |
|
case 'ForOfStatement': |
|
map.set(node, current_scope = new Scope(current_scope, true)); |
|
break; |
|
|
|
case 'BlockStatement': |
|
map.set(node, current_scope = new Scope(current_scope, true)); |
|
break; |
|
|
|
case 'ClassDeclaration': |
|
case 'VariableDeclaration': |
|
current_scope.add_declaration(node); |
|
break; |
|
|
|
case 'CatchClause': |
|
map.set(node, current_scope = new Scope(current_scope, true)); |
|
|
|
if (node.param) { |
|
extract_names(node.param).forEach(name => { |
|
if (node.param) { |
|
current_scope.declarations.set(name, node.param); |
|
} |
|
}); |
|
} |
|
break; |
|
} |
|
}, |
|
|
|
leave(node) { |
|
if (map.has(node) && current_scope !== null && current_scope.parent) { |
|
current_scope = current_scope.parent; |
|
} |
|
} |
|
}); |
|
|
|
for (let i = references.length - 1; i >= 0; --i) { |
|
const [scope, reference] = references[i]; |
|
|
|
if (!scope.references.has(reference.name)) { |
|
add_reference(scope, reference.name); |
|
} |
|
if (!scope.find_owner(reference.name)) { |
|
globals.set(reference.name, reference); |
|
} |
|
} |
|
|
|
return { map, scope, globals }; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function add_reference(scope, name) { |
|
scope.references.add(name); |
|
if (scope.parent) add_reference(scope.parent, name); |
|
} |
|
|
|
export class Scope { |
|
|
|
|
|
|
|
|
|
constructor(parent, block) { |
|
|
|
this.parent = parent; |
|
|
|
|
|
this.block = block; |
|
|
|
|
|
this.declarations = new Map(); |
|
|
|
|
|
this.initialised_declarations = new Set(); |
|
|
|
|
|
this.references = new Set(); |
|
} |
|
|
|
|
|
|
|
|
|
add_declaration(node) { |
|
if (node.type === 'VariableDeclaration') { |
|
if (node.kind === 'var' && this.block && this.parent) { |
|
this.parent.add_declaration(node); |
|
} else { |
|
|
|
const handle_declarator = (declarator) => { |
|
extract_names(declarator.id).forEach(name => { |
|
this.declarations.set(name, node); |
|
if (declarator.init) this.initialised_declarations.add(name); |
|
});; |
|
} |
|
|
|
node.declarations.forEach(handle_declarator); |
|
} |
|
} else if (node.id) { |
|
this.declarations.set(node.id.name, node); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
find_owner(name) { |
|
if (this.declarations.has(name)) return this; |
|
return this.parent && this.parent.find_owner(name); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
has(name) { |
|
return ( |
|
this.declarations.has(name) || (!!this.parent && this.parent.has(name)) |
|
); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function extract_names(param) { |
|
return extract_identifiers(param).map(node => node.name); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function extract_identifiers(param, nodes = []) { |
|
switch (param.type) { |
|
case 'Identifier': |
|
nodes.push(param); |
|
break; |
|
|
|
case 'MemberExpression': |
|
let object = param; |
|
while (object.type === 'MemberExpression') { |
|
object = (object.object); |
|
} |
|
nodes.push( (object)); |
|
break; |
|
|
|
case 'ObjectPattern': |
|
|
|
const handle_prop = (prop) => { |
|
if (prop.type === 'RestElement') { |
|
extract_identifiers(prop.argument, nodes); |
|
} else { |
|
extract_identifiers(prop.value, nodes); |
|
} |
|
}; |
|
|
|
param.properties.forEach(handle_prop); |
|
break; |
|
|
|
case 'ArrayPattern': |
|
|
|
const handle_element = (element) => { |
|
if (element) extract_identifiers(element, nodes); |
|
}; |
|
|
|
param.elements.forEach((element) => { |
|
if (element) { |
|
handle_element(element) |
|
} |
|
}); |
|
break; |
|
|
|
case 'RestElement': |
|
extract_identifiers(param.argument, nodes); |
|
break; |
|
|
|
case 'AssignmentPattern': |
|
extract_identifiers(param.left, nodes); |
|
break; |
|
} |
|
|
|
return nodes; |
|
} |
|
|