2015-01-22 15:21:02 +00:00
"use strict" ;
var INFER = '$' ;
var GIVEN = '#' ;
var EXPECTED _MAX _NODES _PER _NONOBFUSACATED _LINE = 25 ;
var MAX _RATIO _SHORT _NAMES = 0.45 ;
var NUM _NUMBERED _LOCALS = 5 ;
function isMinified ( toplevel , code , file ) {
var numLines = code . split ( /\r\n|\r|\n/ ) . length ;
var numStatements = 0 ;
var numNames = 0 ;
var numShortNames = 0 ;
var numNumberedNames = 0 ;
toplevel . walk ( new TreeWalker ( function ( node , descend ) {
numStatements ++ ;
if ( node instanceof AST _Symbol && ! ( node instanceof AST _This ) ) {
numNames ++ ;
if ( node . name . length <= 2 && node . name != "el" && node . name != "$" ) {
numShortNames ++ ;
}
if ( node . name . length >= 2 && node . name [ 0 ] == '_' ) {
var c2 = node . name [ 1 ] ;
if ( c2 >= '0' && c2 <= '9' ) ++ numNumberedNames ;
}
}
} ) ) ;
return ( EXPECTED _MAX _NODES _PER _NONOBFUSACATED _LINE * numLines <= numStatements ) ||
( numShortNames > numNames * MAX _RATIO _SHORT _NAMES ) ||
numNumberedNames == numNames ||
numNumberedNames >= NUM _NUMBERED _LOCALS ;
}
function replaceMangled ( code , file ) {
var toplevel ;
try {
toplevel = parseFile ( code , file ) ;
} catch ( e ) {
console . warn ( "Cannot parse file: '%s'" , file ) ;
return null ;
}
extendAst ( toplevel ) ;
var feature _outputter = new FeatureJsonOutputter ( ) ;
generateAstFeatures ( toplevel , feature _outputter ) ;
generateFnamesFeatures ( toplevel , feature _outputter ) ;
//feature_outputter.string_map defines what id is assigment to each node in the final output
//therefore to assign same ids, we need to first populate by running feature extraction
var stream = OutputStream ( { beautify : true , replace _mangled _map : feature _outputter . string _map } ) ;
toplevel . print ( stream ) ;
return stream . toString ( ) ;
}
function extractFeatures ( code , file , print _ast , features , skip _minified ) {
var toplevel ;
try {
toplevel = parseFile ( code , file ) ;
} catch ( e ) {
console . warn ( "Cannot parse file: '%s'" , file ) ;
return null ;
}
extendAst ( toplevel ) ;
if ( print _ast ) {
return printAst ( toplevel ) ;
}
if ( skip _minified && isMinified ( toplevel , code , file ) ) {
console . warn ( "Skipping minified file: '%s'" , file ) ;
return null ;
}
var feature _outputter = new FeatureJsonOutputter ( ) ;
feature _outputter . openElem ( ) ;
feature _outputter . openArray ( "query" ) ;
if ( features . indexOf ( "ASTREL" ) != - 1 ) {
generateAstFeatures ( toplevel , feature _outputter ) ;
}
if ( features . indexOf ( "FNAMES" ) != - 1 ) {
generateFnamesFeatures ( toplevel , feature _outputter ) ;
}
2015-01-24 23:11:19 +00:00
if ( features . indexOf ( "FSCOPE" ) != - 1 ) {
generateFscopeConstraints ( toplevel , feature _outputter ) ;
}
2015-01-22 15:21:02 +00:00
feature _outputter . closeArray ( ) ;
feature _outputter . dumpSymbols ( ) ;
feature _outputter . closeElem ( ) ;
return feature _outputter . output ;
}
/* -----[ functions ]----- */
function nodeToString ( node ) {
if ( node == null ) return null ;
if ( node instanceof AST _Symbol ) {
if ( node instanceof AST _This ) {
return GIVEN + node . name ;
}
// AST_Symbol::unmangleable() returns true if this symbol cannot be renamed (it's either global, undeclared, or defined in scope where eval or with are in use.
if ( node . unmangleable ( ) ) {
return GIVEN + node . name ;
}
return INFER + node . definition ( ) . id + "-" + node . name ;
} else if ( node instanceof AST _Constant ) {
2015-01-24 23:11:36 +00:00
return GIVEN + "!" + nodeType ( node ) + "!" + String ( node . value ) . slice ( 0 , 64 ) ;
2015-01-22 15:21:02 +00:00
} else if ( node instanceof AST _Sub ) {
//x[1], x -> expression, 1 -> property
return nodeToString ( node . expression ) + "[]" ;
} else if ( node instanceof AST _PropAccess ) {
return GIVEN + node . property ;
} else if ( node instanceof AST _Defun ) {
//function foo(...) { ... }
return nodeToString ( node . name ) ;
} else if ( node instanceof AST _VarDef ) {
// var x = function () { ... }
return nodeToString ( node . name ) ;
} else if ( node instanceof AST _Assign ) {
//x = function () { ... }
return nodeToString ( node . left ) ;
} else if ( node instanceof AST _ObjectProperty ) {
// { "x" : function () { ... } }
return GIVEN + node . key ;
} else if ( node instanceof AST _Call ) {
//x.foo( function () { ... } )
//foo( function () { ... } )
return nodeToString ( node . expression ) ;
} else if ( node instanceof AST _Lambda ) {
if ( node . parent instanceof AST _Call ) {
//'node.parent.expression != node' as lambda can call itself
return ( node . parent . expression == node ) ? null : nodeToString ( node . parent . expression ) + "(" + node . child _id + ")" ;
}
return nodeToString ( node . parent ) ;
}
return null ;
}
function nodeType ( node ) {
if ( node instanceof AST _Binary || node instanceof AST _Unary ) {
return node . _ _proto _ _ . TYPE + node . operator ;
} else if ( node instanceof AST _Boolean ) {
return "Bool" ;
} else if ( node instanceof AST _Atom && ! ( node instanceof AST _Constant ) ) {
//atoms are special constant values as Nan, Undefined, Infinity,..
return "Atom" ;
}
return node . _ _proto _ _ . TYPE ;
}
function pathToStringFw ( path , start ) {
var res = "" ;
for ( var i = start ; i < path . length - 1 ; i ++ ) {
res += nodeType ( path [ i ] ) ;
res += "[" + path [ i + 1 ] . child _id + "]" ;
}
return res ;
}
function pathToStringBw ( path , start ) {
var res = "" ;
for ( var i = path . length - 2 ; i >= start ; i -- ) {
res += nodeType ( path [ i ] ) ;
res += "[" + path [ i + 1 ] . child _id + "]" ;
}
return res ;
}
function printAst ( toplevel ) {
var output = "" ;
var walker = new TreeWalker ( function ( node ) {
output += string _template ( " node{id} [label=\"{label}\"];\n" , {
id : node . id ,
label : nodeType ( node )
} ) ;
if ( walker . parent ( ) != null ) {
output += string _template ( " node{id1} -> node{id2} [weight=1];\n" , {
id1 : walker . parent ( ) . id ,
id2 : node . id
} ) ;
}
} ) ;
output += "digraph AST {\n" ;
toplevel . walk ( walker ) ;
output += "}\n" ;
return output ;
}
function generateAstFeatures ( toplevel , feature _outputter ) {
var walker = new TreeWalker ( function ( node ) {
// console.log(nodeType(node) + " - " + nodeToString(node));
var paths = this . node _finder . find ( node ) ;
for ( var i = 0 ; i < paths . length ; i ++ ) {
var path1 = paths [ i ] ;
var node1 = path1 [ path1 . length - 1 ] ;
for ( var j = i + 1 ; j < paths . length ; j ++ ) {
var common _prefix _len = 0 ;
var path2 = paths [ j ] ;
var node2 = path2 [ path2 . length - 1 ] ;
//determine common prefix to be skipped
while ( common _prefix _len < path1 . length && common _prefix _len < path2 . length
&& path1 [ common _prefix _len ] === path2 [ common _prefix _len ] ) {
common _prefix _len ++ ;
}
if ( common _prefix _len == 0 ) {
throw "common prefix not greater than 0!" ;
}
feature _outputter . addFeature (
nodeToString ( node1 ) ,
2015-01-24 23:11:59 +00:00
nodeToString ( node2 ) ,
//pathToStringBw(path1, common_prefix_len) + ":" + nodeType(path1[common_prefix_len - 1]) + ":" + pathToStringFw(path2, common_prefix_len)
( path2 . length != common _prefix _len )
? pathToStringBw ( path1 , common _prefix _len ) + ":" + pathToStringFw ( path2 , common _prefix _len - 1 )
: pathToStringBw ( path2 , common _prefix _len ) + ":" + pathToStringFw ( path1 , common _prefix _len - 1 )
2015-01-22 15:21:02 +00:00
) ;
2015-01-24 23:11:59 +00:00
2015-01-22 15:21:02 +00:00
}
}
} ) ;
walker . node _finder = new NodePathFinder ( 3 , function ( node ) {
return ( node instanceof AST _Symbol || node instanceof AST _Constant || node instanceof AST _PropAccess ) ;
} ) ;
toplevel . walk ( walker ) ;
}
function addFeatures ( lhss , lhs _label , rhs , rhs _label , feature _outputter ) {
var prefix = "" ;
for ( var i = lhss . length - 1 ; i >= 0 ; i -- ) {
prefix += lhs _label ;
feature _outputter . addFeature ( lhss [ i ] , rhs , prefix + rhs _label ) ;
}
}
2015-01-24 23:11:19 +00:00
function addScopeConstraints ( node , toplevel , feature _outputter ) {
feature _outputter . beginScope ( ) ;
var name = nodeToString ( node ) ;
if ( name != null )
feature _outputter . addToScope ( name ) ;
for ( var i = 0 ; i < node . enclosed . length ; i ++ ) {
feature _outputter . addToScope ( nodeToString ( node . enclosed [ i ] . orig [ 0 ] ) ) ;
}
node . variables . each ( function ( symbol ) {
feature _outputter . addToScope ( nodeToString ( symbol . orig [ 0 ] ) ) ;
} ) ;
toplevel . globals . each ( function ( symbol ) {
feature _outputter . addToScope ( nodeToString ( symbol . orig [ 0 ] ) ) ;
} ) ;
feature _outputter . endScope ( ) ;
}
function generateFscopeConstraints ( toplevel , feature _outputter ) {
addScopeConstraints ( toplevel , toplevel , feature _outputter ) ;
toplevel . walk ( new TreeWalker ( function ( node ) {
if ( node instanceof AST _Defun || node instanceof AST _Lambda ) {
addScopeConstraints ( node , toplevel , feature _outputter ) ;
}
} ) ) ;
}
2015-01-22 15:21:02 +00:00
function generateFnamesFeatures ( toplevel , feature _outputter ) {
var outer _funcs = [ ] ;
toplevel . walk ( new TreeWalker ( function ( node , descend ) {
if ( ( node instanceof AST _Defun || node instanceof AST _Lambda ) && nodeToString ( node ) != null ) {
var name = nodeToString ( node ) ;
for ( var i = 0 ; i < node . argnames . length ; i ++ ) {
addFeatures ( [ name ] , "FN" , nodeToString ( node . argnames [ i ] ) , "PAR" , feature _outputter ) ;
}
outer _funcs . push ( name ) ;
descend ( ) ; //traverse childs
outer _funcs . pop ( ) ;
return true ; //do not traverse childs again
}
if ( node instanceof AST _New ) {
addFeatures ( outer _funcs , "FN" , nodeToString ( node ) , "NEW" , feature _outputter ) ;
} else if ( node instanceof AST _Call ) {
addFeatures ( outer _funcs , "FN" , nodeToString ( node ) , "CALL" , feature _outputter ) ;
} else if ( node instanceof AST _Constant ) {
addFeatures ( outer _funcs , "FN" , nodeToString ( node ) , nodeType ( node ) . toUpperCase ( ) , feature _outputter ) ;
} else if ( node instanceof AST _VarDef ) {
addFeatures ( outer _funcs , "FN" , nodeToString ( node . name ) , "DECL" , feature _outputter ) ;
} else if ( node instanceof AST _Dot && ! ( node . parent instanceof AST _Call ) ) {
addFeatures ( outer _funcs , "FN" , nodeToString ( node ) , "PROP" , feature _outputter ) ;
} else if ( node instanceof AST _Return && nodeToString ( node . value ) != null ) {
addFeatures ( outer _funcs , "FN" , nodeToString ( node . value ) , "RETURN" , feature _outputter ) ;
}
} ) ) ;
}
/* -----[ NodePathFinder ]----- */
function NodePathFinder ( max _depth , filter ) {
this . max _depth = max _depth ;
this . paths = [ ] ;
this . filter = filter ;
}
NodePathFinder . prototype = new TreeWalker ( function ( node , descend ) {
if ( this . stack . length > this . max _depth || node instanceof AST _Defun ) {
return true ;
}
//enforce in-order traversal
//otherwise we get for "x.foo()" feature foo - x instead of x - foo as x is a parent of foo in the AST
descend ( ) ;
if ( this . filter ( node ) ) {
this . paths . push ( this . stack . slice ( 0 ) ) ;
}
return true ;
} ) ;
NodePathFinder . prototype . find = function ( node ) {
this . root = node ;
this . paths = [ ] ;
node . walk ( this ) ;
return this . paths ;
} ;
/* ---[ JsonOutputter ]--- */
function FeatureJsonOutputter ( ) {
this . string _map = new StringMap ( false ) ;
this . first _element = true ;
this . output = "" ;
this . depth = 0 ;
this . pairs = { } ;
2015-01-24 23:11:19 +00:00
this . cur _scope = { } ;
2015-01-22 15:21:02 +00:00
}
FeatureJsonOutputter . prototype . indent = function ( ) {
var res = "" ;
for ( var i = 0 ; i < this . depth ; i ++ ) {
res += " " ;
}
return res ;
} ;
FeatureJsonOutputter . prototype . openElem = function ( ) {
if ( ! this . first _element ) {
this . output += "," ;
}
this . output += "\n" + this . indent ( ) + "{" ;
this . first _element = true ;
this . depth ++ ;
} ;
FeatureJsonOutputter . prototype . closeElem = function ( ) {
this . depth -- ;
this . output += "}" ;
this . first _element = false ;
} ;
FeatureJsonOutputter . prototype . openArray = function ( name ) {
if ( ! this . first _element ) {
this . output += "," ;
}
this . output += "\n" + this . indent ( ) + "\"" + name + "\":[" ;
this . first _element = true ;
this . depth ++ ;
} ;
FeatureJsonOutputter . prototype . closeArray = function ( ) {
this . depth -- ;
this . output += "\n" + this . indent ( ) + "]" ;
this . first _element = false ;
} ;
FeatureJsonOutputter . prototype . visitFeature = function ( a _id , b _id , name ) {
if ( ! ( a _id in this . pairs ) ) {
this . pairs [ a _id ] = [ ] ;
}
var visited = this . pairs [ a _id ] ;
if ( visited . indexOf ( b _id + "-" + name ) >= 0 ) {
return true ;
}
visited . push ( b _id + "-" + name ) ;
return false ;
} ;
FeatureJsonOutputter . prototype . addFeature = function ( a , b , name ) {
if ( a == null || b == null ) {
return ;
}
//do not add features between two fixed nodes
if ( a [ 0 ] == GIVEN && b [ 0 ] == GIVEN ) {
return ;
}
var a _id = this . string _map . getId ( a ) ;
var b _id = this . string _map . getId ( b ) ;
if ( a _id == b _id || this . visitFeature ( a _id , b _id , name ) ) {
return ;
}
this . openElem ( ) ;
this . output += '"a": ' + a _id + "," ;
this . output += '\t"b": ' + b _id + "," ;
this . output += '\t"f2": "' + name + '"' ;
this . closeElem ( ) ;
} ;
FeatureJsonOutputter . prototype . addSymbol = function ( key ) {
this . openElem ( ) ;
this . output += '"v": ' + this . string _map . getId ( key ) + "," ;
if ( key [ 0 ] == INFER ) {
//${id}-{name}
this . output += '\t"inf": "' + escapeString ( key . split ( "-" ) [ 1 ] ) + '"' ;
} else {
//#{name}
this . output += '\t"giv": "' + escapeString ( key . slice ( 1 ) ) + '"' ;
}
this . closeElem ( ) ;
} ;
FeatureJsonOutputter . prototype . dumpSymbols = function ( ) {
this . openArray ( "assign" ) ;
// var keys = Object.keys( this.string_map.map );
var keys = this . string _map . keys ;
for ( var i = 0 , length = keys . length ; i < length ; i ++ ) {
this . addSymbol ( keys [ i ] ) ;
}
this . closeArray ( ) ;
} ;
2015-01-24 23:11:19 +00:00
FeatureJsonOutputter . prototype . beginScope = function ( ) {
this . cur _scope = { } ;
} ;
FeatureJsonOutputter . prototype . addToScope = function ( a ) {
var a _id = this . string _map . getId ( a ) ;
this . cur _scope [ a _id ] = true ;
} ;
FeatureJsonOutputter . prototype . endScope = function ( ) {
//{"cn":"!=","n":[14,366,370,372,108,40,356]}
var keys = Object . keys ( this . cur _scope ) ;
if ( keys . length <= 1 ) {
return ;
}
this . openElem ( ) ;
this . output += '"cn":"!=", "n":[' ;
this . output += keys [ 0 ] ;
for ( var i = 1 , length = keys . length ; i < length ; i ++ ) {
this . output += ',' ;
this . output += keys [ i ] ;
}
this . output += "]" ;
this . closeElem ( ) ;
} ;
2015-01-22 15:21:02 +00:00
/* -----[ StringMap ]----- */
function StringMap ( nice _names ) {
this . map = { } ;
this . current _id = 0 ;
this . nice _names = nice _names ;
this . keys = [ ] ;
}
StringMap . prototype . getId = function ( input ) {
if ( input == null ) {
throw new Error ( "error null" ) ;
}
if ( this . nice _names ) return input ;
//we add a special character in from to allow for keys such as "toString"
var escaped _input = "#" + input ;
if ( ! ( escaped _input in this . map ) ) {
this . map [ escaped _input ] = this . current _id ;
//keep ordered map of keys for iterating later
this . keys . push ( input ) ;
this . current _id ++ ;
}
return this . map [ escaped _input ] ;
} ;
/* ------------------------ */
function escapeString ( input ) {
return encodeURIComponent ( input ) ;
}
function parseFile ( code , file ) {
var toplevel = parse ( code , {
filename : file
} ) ;
toplevel . figure _out _scope ( ) ;
return toplevel ;
}
function FakeSymbolDef ( name , id ) {
this . name = name ;
this . id = id ;
} ;
function extendAst ( root ) {
var current _id = 0 ;
var walker = new TreeWalker ( function ( node ) {
if ( ! node . hasOwnProperty ( "id" ) ) {
node . id = current _id ;
current _id += 1 ;
}
if ( ! node . hasOwnProperty ( "parent" ) ) {
node . parent = walker . parent ( ) ;
}
node . num _childs = 0 ;
node . child _id = 0 ;
if ( walker . parent ( ) !== undefined ) {
node . child _id = walker . parent ( ) . num _childs ;
walker . parent ( ) . num _childs ++ ;
}
if ( node instanceof AST _Symbol ) {
// if (node.definition() == null && node instanceof AST_This){
// var scope = node;
// while (!(scope instanceof AST_Lambda) && scope.parent != null){
// scope = scope.parent;
// }
// var name = nodeToString(scope);
// node.thedef = new FakeSymbolDef(name + "_this", scope.id);
// } else {
if ( node . definition ( ) != null ) {
node . definition ( ) . id = current _id ;
current _id ++ ;
}
}
} ) ;
root . walk ( walker ) ;
}