Search code examples
javascriptregexparsingexpressionkeypaths

How to extract keypaths from a javascript expression


Does someone know a method to extract paths from a javascript expression for use in a PathObserver like in https://github.com/polymer/observe-js ?

Polymer defines a path by

an ECMAScript expression consisting only of identifiers (myVal), 
member accesses (foo.bar) and key lookup with literal values 
(arr[0] obj['str-value'].bar.baz).

My aim is to observe all the paths that might affect the result of the expression (low 'observe' the expression). I was hoping for a simple regex, but code using js parsers like Esprima or Shift is fine too.

So a practical example: input is

'if (count(body.arms)/numlegs==1) head[0]=eyes[symmetry]'

and output would be

["body.arms","numlegs","head[0]","eyes","symmetry"]

Fast is better than perfect; the PathObserver would eventually tell me if the path doesn't validate.


Solution

  • I'm using this now. It may not be complete, and it may not be optimally fast, but it does what I wanted: take an expression and return observable paths for Observe.js (https://github.com/polymer/observe-js) . It uses esprima (http://esprima.org) to parse the expression.

    Parser = {
    
        // requires esprima.
        // @see http://esprima.org/demo/parse.html
    
        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);
                    }
    
                }
            }
    
        }
    }
    

    Try it ..

    Parser.getObservablePaths('alert(life.and[42].the); universe=everything.else')
    
    [{"object":"life","path":"and[42]"},{"object":"everything","path":"else"}]
    
    Parser.getObservablePaths('if (count(body.arms)/numlegs==1) head[0]=eyes[symmetry]')
    
    [{"object":"body","path":"arms"},{"object":"window","path":"numlegs"},{"object":"eyes","path":"symmetry"}]
    

    Any suggestions are more than welcome !