Skip to content

Instantly share code, notes, and snippets.

@colxi
Created September 3, 2018 19:48
Show Gist options
  • Save colxi/c0bc24a3ef945a1248fe442c80fdb777 to your computer and use it in GitHub Desktop.
Save colxi/c0bc24a3ef945a1248fe442c80fdb777 to your computer and use it in GitHub Desktop.
Extract keypaths from expression AST (abstract syntax tree)
// works with AST generated by esprima or jsep
const Parser = {
// 'global' for Node, or any other value for custom scenaros
outerscope : 'window',
getObservablePaths : function(expression) {
console.log('Parser.getPaths',expression);
var ast = esprima.parse(expression);
if (ast) {
console.log('Parser.getPaths',ast);
var paths = new Array();
this.recurseObservablePaths(ast,paths);
return paths;
} else return false;
},
recurseObservablePaths : function(tree,paths,path) {
if (!tree || !paths) return false;
if (tree.type =='Identifier') {
// some sort of global
console.log('Parser.recurseObservablePaths','adding identifier '+tree.name);
paths.push({object:this.outerscope,path:tree.name});
} else if (tree.type =='MemberExpression') {
// member expression
if (tree.property.type=='Identifier' || tree.property.type=='Literal') {
// like foo[bar][24].quz ; the property is 'quz'
// dabble down the object to get the path
if (tree.property.type=='Identifier') {
path = (path)?'.'+tree.property.name+path:'.'+tree.property.name;
} else {
path = (path)?'['+tree.property.raw+']'+path:'['+tree.property.raw+']';
}
if (tree.object.type=='Identifier') {
// like foo.bar ; were done with this path - push !
console.log('Parser.recurseObservablePaths','adding path '+tree.object.name+path);
if (path.indexOf('.')===0) {
paths.push({object:tree.object.name,path:path.substring(1)});
} else {
paths.push({object:this.outerscope,path:tree.object.name+path});
}
} else {
if (tree.object.type=='MemberExpression') {
// like foo.bar.quz ; recurse the object
console.log('Parser.recurseObservablePaths','recursing member expression ..');
this.recurseObservablePaths(tree.object,paths,path);
} else {
// like foo(bar).quz ; the object is something weird.
// ignore the property .. but recurse the object
this.recurseObservablePaths(tree.object,paths);
}
}
} else {
// the property is some sort of thing itself:
if (tree.object.type=='Identifier') {
// like foo[bar.quz] - push the object, recurse the property
console.log('Parser.recurseObservablePaths','adding identifier '+tree.object.name);
paths.push({object:this.outerscope,path:tree.object.name});
this.recurseObservablePaths(tree.property);
} else {
// like foo.bar[quz(raz)] ; recurse both
console.log('Parser.recurseObservablePaths','recursing member expression ..');
this.recurseObservablePaths(tree.object,paths);
this.recurseObservablePaths(tree.property,paths);
}
}
} else if (tree.type=="CallExpression") {
// like foo.bar(quz.baz) ; we only want the arguments
this.recurseObservablePaths(tree.arguments,paths);
} else if (tree.type=="AssignmentExpression") {
// like foo.bar=baz*quz ; we only want the right hand
this.recurseObservablePaths(tree.right,paths);
} else {
// unknown garbage. dig deeper.
var props = Object.getOwnPropertyNames(tree);
for (var pc=0; pc<props.length; pc++) {
var key = props[pc];
if (typeof tree[key] == 'object') {
if (Array.isArray(tree[key])) {
for (var kc=0;kc<tree[key].length;kc++) {
console.log('Parser.recurseObservablePaths','recursing '+key+':'+kc);
this.recurseObservablePaths(tree[key][kc],paths);
}
} else {
console.log('Parser.recurseObservablePaths','recursing '+key);
this.recurseObservablePaths(tree[key],paths);
}
} else {
console.log('Parser.recurseObservablePaths','ignoring '+key);
}
}
}
}
}
// test:
Parser.getObservablePaths('myObj.myNested.arr[ myObject.myProp]')
/*
returns : [
{object:'myObj', keys:'myNested.arr'},
{object:'myObject', keys:'myProp'}
]
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment