@ -17,6 +17,8 @@ from .utils import (
)
)
_NAME_RE = r ' [a-zA-Z_$][ \ w$]* '
_NAME_RE = r ' [a-zA-Z_$][ \ w$]* '
# Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
_OPERATORS = { # None => Defined in JSInterpreter._operator
_OPERATORS = { # None => Defined in JSInterpreter._operator
' ? ' : None ,
' ? ' : None ,
@ -26,23 +28,31 @@ _OPERATORS = { # None => Defined in JSInterpreter._operator
' | ' : operator . or_ ,
' | ' : operator . or_ ,
' ^ ' : operator . xor ,
' ^ ' : operator . xor ,
# FIXME: This should actually be below comparision
' === ' : operator . is_ ,
' >> ' : operator . rshift ,
' !== ' : operator . is_not ,
' << ' : operator . lshift ,
' == ' : operator . eq ,
' != ' : operator . ne ,
' <= ' : operator . le ,
' <= ' : operator . le ,
' >= ' : operator . ge ,
' >= ' : operator . ge ,
' < ' : operator . lt ,
' < ' : operator . lt ,
' > ' : operator . gt ,
' > ' : operator . gt ,
' >> ' : operator . rshift ,
' << ' : operator . lshift ,
' + ' : operator . add ,
' + ' : operator . add ,
' - ' : operator . sub ,
' - ' : operator . sub ,
' * ' : operator . mul ,
' * ' : operator . mul ,
' / ' : operator . truediv ,
' / ' : operator . truediv ,
' % ' : operator . mod ,
' % ' : operator . mod ,
' ** ' : operator . pow ,
}
}
_COMP_OPERATORS = { ' === ' , ' !== ' , ' == ' , ' != ' , ' <= ' , ' >= ' , ' < ' , ' > ' }
_MATCHING_PARENS = dict ( zip ( ' ( { [ ' , ' )}] ' ) )
_MATCHING_PARENS = dict ( zip ( ' ( { [ ' , ' )}] ' ) )
_QUOTES = ' \' " '
_QUOTES = ' \' " '
@ -81,7 +91,7 @@ class LocalNameSpace(collections.ChainMap):
class Debugger :
class Debugger :
import sys
import sys
ENABLED = ' pytest ' in sys . modules
ENABLED = False and ' pytest ' in sys . modules
@staticmethod
@staticmethod
def write ( * args , level = 100 ) :
def write ( * args , level = 100 ) :
@ -200,7 +210,7 @@ class JSInterpreter:
if should_return :
if should_return :
return ret , should_return
return ret , should_return
m = re . match ( r ' (?P<var> var\ s)|return(?: \ s+|$) ' , stmt )
m = re . match ( r ' (?P<var> (?: var|const|let) \ s)|return(?: \ s+|$) ' , stmt )
if m :
if m :
expr = stmt [ len ( m . group ( 0 ) ) : ] . strip ( )
expr = stmt [ len ( m . group ( 0 ) ) : ] . strip ( )
should_return = not m . group ( ' var ' )
should_return = not m . group ( ' var ' )
@ -218,13 +228,18 @@ class JSInterpreter:
obj = expr [ 4 : ]
obj = expr [ 4 : ]
if obj . startswith ( ' Date( ' ) :
if obj . startswith ( ' Date( ' ) :
left , right = self . _separate_at_paren ( obj [ 4 : ] , ' ) ' )
left , right = self . _separate_at_paren ( obj [ 4 : ] , ' ) ' )
expr = unified_timestamp ( left [ 1 : - 1 ] , False )
expr = unified_timestamp (
self . interpret_expression ( left , local_vars , allow_recursion ) , False )
if not expr :
if not expr :
raise self . Exception ( f ' Failed to parse date { left !r} ' , expr )
raise self . Exception ( f ' Failed to parse date { left !r} ' , expr )
expr = self . _dump ( int ( expr * 1000 ) , local_vars ) + right
expr = self . _dump ( int ( expr * 1000 ) , local_vars ) + right
else :
else :
raise self . Exception ( f ' Unsupported object { obj } ' , expr )
raise self . Exception ( f ' Unsupported object { obj } ' , expr )
if expr . startswith ( ' void ' ) :
left = self . interpret_expression ( expr [ 5 : ] , local_vars , allow_recursion )
return None , should_return
if expr . startswith ( ' { ' ) :
if expr . startswith ( ' { ' ) :
inner , outer = self . _separate_at_paren ( expr , ' } ' )
inner , outer = self . _separate_at_paren ( expr , ' } ' )
inner , should_abort = self . interpret_statement ( inner , local_vars , allow_recursion )
inner , should_abort = self . interpret_statement ( inner , local_vars , allow_recursion )
@ -307,7 +322,8 @@ class JSInterpreter:
if default :
if default :
matched = matched or case == ' default '
matched = matched or case == ' default '
elif not matched :
elif not matched :
matched = case != ' default ' and switch_val == self . interpret_expression ( case , local_vars , allow_recursion )
matched = ( case != ' default '
and switch_val == self . interpret_expression ( case , local_vars , allow_recursion ) )
if not matched :
if not matched :
continue
continue
try :
try :
@ -347,7 +363,7 @@ class JSInterpreter:
m = re . match ( fr ''' (?x)
m = re . match ( fr ''' (?x)
( ? P < assign >
( ? P < assign >
( ? P < out > { _NAME_RE } ) ( ? : \[ ( ? P < index > [ ^ \] ] + ? ) \] ) ? \s *
( ? P < out > { _NAME_RE } ) ( ? : \[ ( ? P < index > [ ^ \] ] + ? ) \] ) ? \s *
( ? P < op > { " | " . join ( map ( re . escape , _OPERATORS) ) } ) ?
( ? P < op > { " | " . join ( map ( re . escape , set ( _OPERATORS) - _COMP _OPERATORS) ) } ) ?
= ( ? P < expr > . * ) $
= ( ? P < expr > . * ) $
) | ( ? P < return >
) | ( ? P < return >
( ? ! if | return | true | false | null | undefined ) ( ? P < name > { _NAME_RE } ) $
( ? ! if | return | true | false | null | undefined ) ( ? P < name > { _NAME_RE } ) $
@ -397,12 +413,14 @@ class JSInterpreter:
for op in _OPERATORS :
for op in _OPERATORS :
separated = list ( self . _separate ( expr , op ) )
separated = list ( self . _separate ( expr , op ) )
if len ( separated ) < 2 :
continue
right_expr = separated . pop ( )
right_expr = separated . pop ( )
while op == ' - ' and len ( separated ) > 1 and not separated [ - 1 ] . strip ( ) :
while op in ' <>*- ' and len ( separated ) > 1 and not separated [ - 1 ] . strip ( ) :
right_expr = f ' - { right_expr } '
separated . pop ( )
separated . pop ( )
right_expr = f ' { op } { right_expr } '
if op != ' - ' :
right_expr = f ' { separated . pop ( ) } { op } { right_expr } '
if not separated :
continue
left_val = self . interpret_expression ( op . join ( separated ) , local_vars , allow_recursion )
left_val = self . interpret_expression ( op . join ( separated ) , local_vars , allow_recursion )
return self . _operator ( op , 0 if left_val is None else left_val ,
return self . _operator ( op , 0 if left_val is None else left_val ,
right_expr , expr , local_vars , allow_recursion ) , should_return
right_expr , expr , local_vars , allow_recursion ) , should_return
@ -564,8 +582,8 @@ class JSInterpreter:
# Currently, it only supports function definitions
# Currently, it only supports function definitions
fields_m = re . finditer (
fields_m = re . finditer (
r ''' (?x)
r ''' (?x)
( ? P < key > % s ) \s * : \s * function \s * \( ( ? P < args > [a - z , ] + ) \) { ( ? P < code > [ ^ } ] + ) }
( ? P < key > % s ) \s * : \s * function \s * \( ( ? P < args > (? : % s | , ) * ) \) { ( ? P < code > [ ^ } ] + ) }
''' % _FUNC_NAME_RE,
''' % ( _FUNC_NAME_RE, _NAME_RE) ,
fields )
fields )
for f in fields_m :
for f in fields_m :
argnames = f . group ( ' args ' ) . split ( ' , ' )
argnames = f . group ( ' args ' ) . split ( ' , ' )
@ -580,7 +598,7 @@ class JSInterpreter:
( ? :
( ? :
function \s + % ( name ) s |
function \s + % ( name ) s |
[ { ; , ] \s * % ( name ) s \s * = \s * function |
[ { ; , ] \s * % ( name ) s \s * = \s * function |
var \s + % ( name ) s \s * = \s * function
( ? : var | const | let ) \s + % ( name ) s \s * = \s * function
) \s *
) \s *
\( ( ? P < args > [ ^ ) ] * ) \) \s *
\( ( ? P < args > [ ^ ) ] * ) \) \s *
( ? P < code > { . + } ) ''' % { ' name ' : re.escape(funcname)},
( ? P < code > { . + } ) ''' % { ' name ' : re.escape(funcname)},
@ -615,10 +633,8 @@ class JSInterpreter:
argnames = tuple ( argnames )
argnames = tuple ( argnames )
def resf ( args , kwargs = { } , allow_recursion = 100 ) :
def resf ( args , kwargs = { } , allow_recursion = 100 ) :
global_stack [ 0 ] . update ( {
global_stack [ 0 ] . update ( itertools . zip_longest ( argnames , args , fillvalue = None ) )
* * dict ( itertools . zip_longest ( argnames , args , fillvalue = None ) ) ,
global_stack [ 0 ] . update ( kwargs )
* * kwargs
} )
var_stack = LocalNameSpace ( * global_stack )
var_stack = LocalNameSpace ( * global_stack )
ret , should_abort = self . interpret_statement ( code . replace ( ' \n ' , ' ' ) , var_stack , allow_recursion - 1 )
ret , should_abort = self . interpret_statement ( code . replace ( ' \n ' , ' ' ) , var_stack , allow_recursion - 1 )
if should_abort :
if should_abort :