Skip to content

Instantly share code, notes, and snippets.

@bmeck
Created December 18, 2015 00:29
Show Gist options
  • Save bmeck/9c0a65ba5937f5470dda to your computer and use it in GitHub Desktop.
Save bmeck/9c0a65ba5937f5470dda to your computer and use it in GitHub Desktop.
finding globals in an ES6 world
#!/usr/bin/env node
// usage: node globals.js $FILE_TO_FIND_GLOBALS
var fs = require('fs');
var src = fs.readFileSync(process.argv[2]).toString();
//src = 'function foo() {foo()}'
var esprima = require('esprima');
var globals = new Set();
function eswalk(node, pre, post, path) {
if (path === undefined) path = [{node:node}];
pre(node,path);
// literals are special since they could
// look like an AST since they are raw
if (node.type !== 'Literal') {
for (var key of Object.keys(node)) {
var val = node[key];
flat_arr_each(val, val => {
if (val && typeof val.type === 'string') {
path.push({key:key,node:val});
eswalk(val, pre, post, path);
path.pop();
}
})
}
}
post(node,path);
}
function flat_arr_each(node,fn) {
if (Array.isArray(node)) {
node.forEach(n => flat_arr_each(n, fn));
}
else {
fn(node);
}
}
// where declarations are scoped
var ENV_RECORD = {
'Program': {},
'ArrowFunctionExpression': {params:'params'},
'FunctionExpression': {variables:['this','arguments']},
'FunctionDeclaration': {variables:['this','arguments']},
'BlockStatement': {type:'block'},
'CatchClause': {type:'catch',params:'param'}
}
var IGNORE_PATTERNS = [
[{node:{type:'LabeledStatement'}},{key:'label'}],
[{node:{type:'ContinueStatement'}},{key:'label'}],
[{node:{type:'BreakStatement'}},{key:'label'}],
]
// declarations can include expressions
// we need to know what identifiers are declaration vs access
// we always start in an access scope
var MEMBER_PATTERNS = [
[{node:{type:'Property',computed:false}},{key:'key'}],
[{node:{type:'MemberExpression',computed:false}},{key:'property'}],
];
var ACCESS_PATTERNS = [
[{node:{type:'Program'}}],
[{node:{type:'Property'}},{key:'value'}],
[{node:{type:'VariableDeclarator'}},{key:'init'}],
[{node:{type:'AssignmentPattern'}},{key:'right'}],
// snowflake since it can be an expression
[{node:{type:'ArrowFunctionExpression'}},{key:'body'}],
[{node:{type:'FunctionExpression'}},{key:'body'}],
[{node:{type:'FunctionDeclaration'}},{key:'body'}],
[{node:{type:'BlockStatement'}}]
]
var DECL_PATTERNS = [
[{node:{type:'VariableDeclaration'}}],
[{node:{type:'ArrowFunctionExpression'}}],
[{node:{type:'FunctionExpression'}}],
[{node:{type:'FunctionDeclaration'}}],
[{node:{type:'CatchClause'}}],
]
function matchesPatterns(path, patterns) {
check_pattern:
for (var pattern of patterns) {
if (pattern.length > path.length) continue;
var to_check = path.slice(-pattern.length);
for (var i = 0; i < to_check.length; i++) {
if (!match(to_check[i], pattern[i])) continue check_pattern;
}
return true;
}
return false;
}
function match(obj,criteria) {
if (typeof obj !== typeof criteria) return false;
for (var key of Object.keys(criteria)) {
var crit_val = criteria[key];
var obj_val = obj[key];
if (typeof obj_val === 'object') {
if (!match(obj_val, crit_val)) {
return false;
}
}
else if (obj_val !== crit_val) {
return false;
}
}
return true;
}
var env_records = [];
var identifier_scopes = [];
eswalk(esprima.parse(src), (node,path) => {
//console.log(node)
// snowflake, hoisted out and within
if (node.type === 'FunctionDeclaration') {
var env = env_records.slice(-1)[0];
// must be a block or function scope
env.variables.add(node.id.name);
}
var env_desc = ENV_RECORD[node.type];
if (env_desc) {
var scope = {
type: env_desc.type||'function',
variables: new Set(env_desc.variables||[]),
lookups: new Set()
}
env_records.push(scope);
}
if (matchesPatterns(path, IGNORE_PATTERNS)) {
identifier_scopes.push({type:'ignore'});
}
else if (matchesPatterns(path, MEMBER_PATTERNS)) {
identifier_scopes.push({type:'member'});
}
else if (matchesPatterns(path, ACCESS_PATTERNS)) {
identifier_scopes.push({type:'access'});
}
else if (matchesPatterns(path, DECL_PATTERNS)) {
var decl_scope = null;
if (node.type === 'VariableDeclaration') {
var i = env_records.length - 1;
for (; i >= 0; i--) {
if (node.kind === 'const' || node.kind === 'let') {
if (env_records[i].type !== 'catch') {
break;
}
}
else if (node.kind === 'var') {
if (env_records[i].type === 'function') {
break;
}
}
}
if (i >= 0) {
decl_scope = env_records[i];
}
}
else {
decl_scope = env_records[env_records.length - 1];
}
identifier_scopes.push({type:'declare',env:decl_scope});
}
if (node.type === 'Identifier') {
var id_scope = identifier_scopes.slice(-1)[0];
if (id_scope.type === 'ignore') {}
else if (id_scope.type === 'declare') {
id_scope.env.variables.add(node.name);
}
else if (id_scope.type === 'access') {
env_records[env_records.length-1].lookups.add(node.name);
}
else if (id_scope.type === 'member') {
// TODO show how what members are associated with each access
// HOW
// - add a new scope type 'obj_scope'
// - when lookup occurs, or a member occurs
// - attach it to the obj_scope
// - create a new sub 'obj_scope'
}
else {
throw new Error('unknown id scope type:' + id_scope);
}
}
}, (node,path) => {
if (matchesPatterns(path, IGNORE_PATTERNS)) {
identifier_scopes.pop();
}
else if (matchesPatterns(path, MEMBER_PATTERNS)) {
identifier_scopes.pop();
}
else if (matchesPatterns(path, ACCESS_PATTERNS)) {
identifier_scopes.pop();
}
else if (matchesPatterns(path, DECL_PATTERNS)) {
identifier_scopes.pop();
}
if (ENV_RECORD[node.type]) {
var env = env_records.pop();
for (var name of env.lookups) {
if (!env.variables.has(name)) {
if (env_records.length) {
env_records[env_records.length-1].lookups.add(name);
}
else {
globals.add(name);
}
}
}
}
});
console.log('globals', globals)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment