mirror of
https://github.com/SickGear/SickGear.git
synced 2024-12-29 14:13:38 +00:00
483 lines
17 KiB
Python
483 lines
17 KiB
Python
|
"""This module translates JS flow into PY flow.
|
||
|
|
||
|
Translates:
|
||
|
IF ELSE
|
||
|
|
||
|
DO WHILE
|
||
|
WHILE
|
||
|
FOR 123
|
||
|
FOR iter
|
||
|
CONTINUE, BREAK, RETURN, LABEL, THROW, TRY, SWITCH
|
||
|
"""
|
||
|
from __future__ import print_function
|
||
|
|
||
|
from utils import *
|
||
|
from jsparser import *
|
||
|
from nodevisitor import exp_translator
|
||
|
import random
|
||
|
|
||
|
TO_REGISTER = []
|
||
|
CONTINUE_LABEL = 'JS_CONTINUE_LABEL_%s'
|
||
|
BREAK_LABEL = 'JS_BREAK_LABEL_%s'
|
||
|
|
||
|
PREPARE = '''HOLDER = var.own.get(NAME)\nvar.force_own_put(NAME, PyExceptionToJs(PyJsTempException))\n'''
|
||
|
RESTORE = '''if HOLDER is not None:\n var.own[NAME] = HOLDER\nelse:\n del var.own[NAME]\ndel HOLDER\n'''
|
||
|
TRY_CATCH = '''%stry:\nBLOCKfinally:\n%s''' % (PREPARE, indent(RESTORE))
|
||
|
|
||
|
|
||
|
def get_continue_label(label):
|
||
|
return CONTINUE_LABEL % label.encode('hex')
|
||
|
|
||
|
|
||
|
def get_break_label(label):
|
||
|
return BREAK_LABEL % label.encode('hex')
|
||
|
|
||
|
|
||
|
def pass_until(source, start, tokens=(';', )):
|
||
|
while start < len(source) and source[start] not in tokens:
|
||
|
start += 1
|
||
|
return start + 1
|
||
|
|
||
|
|
||
|
def do_bracket_exp(source, start, throw=True):
|
||
|
bra, cand = pass_bracket(source, start, '()')
|
||
|
if throw and not bra:
|
||
|
raise SyntaxError('Missing bracket expression')
|
||
|
bra = exp_translator(bra[1:-1])
|
||
|
if throw and not bra:
|
||
|
raise SyntaxError('Empty bracket condition')
|
||
|
return bra, cand if bra else start
|
||
|
|
||
|
|
||
|
def do_if(source, start):
|
||
|
start += 2 # pass this if
|
||
|
bra, start = do_bracket_exp(source, start, throw=True)
|
||
|
statement, start = do_statement(source, start)
|
||
|
if statement is None:
|
||
|
raise SyntaxError('Invalid if statement')
|
||
|
translated = 'if %s:\n' % bra + indent(statement)
|
||
|
|
||
|
elseif = except_keyword(source, start, 'else')
|
||
|
is_elseif = False
|
||
|
if elseif:
|
||
|
start = elseif
|
||
|
if except_keyword(source, start, 'if'):
|
||
|
is_elseif = True
|
||
|
elseif, start = do_statement(source, start)
|
||
|
if elseif is None:
|
||
|
raise SyntaxError('Invalid if statement)')
|
||
|
if is_elseif:
|
||
|
translated += 'el' + elseif
|
||
|
else:
|
||
|
translated += 'else:\n' + indent(elseif)
|
||
|
return translated, start
|
||
|
|
||
|
|
||
|
def do_statement(source, start):
|
||
|
"""returns none if not found other functions that begin with 'do_' raise
|
||
|
also this do_ type function passes white space"""
|
||
|
start = pass_white(source, start)
|
||
|
# start is the fist position after initial start that is not a white space or \n
|
||
|
if not start < len(source): #if finished parsing return None
|
||
|
return None, start
|
||
|
if any(startswith_keyword(source[start:], e) for e in {'case', 'default'}):
|
||
|
return None, start
|
||
|
rest = source[start:]
|
||
|
for key, meth in KEYWORD_METHODS.iteritems(
|
||
|
): # check for statements that are uniquely defined by their keywords
|
||
|
if rest.startswith(key):
|
||
|
# has to startwith this keyword and the next letter after keyword must be either EOF or not in IDENTIFIER_PART
|
||
|
if len(key) == len(rest) or rest[len(key)] not in IDENTIFIER_PART:
|
||
|
return meth(source, start)
|
||
|
if rest[0] == '{': #Block
|
||
|
return do_block(source, start)
|
||
|
# Now only label and expression left
|
||
|
cand = parse_identifier(source, start, False)
|
||
|
if cand is not None: # it can mean that its a label
|
||
|
label, cand_start = cand
|
||
|
cand_start = pass_white(source, cand_start)
|
||
|
if source[cand_start] == ':':
|
||
|
return do_label(source, start)
|
||
|
return do_expression(source, start)
|
||
|
|
||
|
|
||
|
def do_while(source, start):
|
||
|
start += 5 # pass while
|
||
|
bra, start = do_bracket_exp(source, start, throw=True)
|
||
|
statement, start = do_statement(source, start)
|
||
|
if statement is None:
|
||
|
raise SyntaxError('Missing statement to execute in while loop!')
|
||
|
return 'while %s:\n' % bra + indent(statement), start
|
||
|
|
||
|
|
||
|
def do_dowhile(source, start):
|
||
|
start += 2 # pass do
|
||
|
statement, start = do_statement(source, start)
|
||
|
if statement is None:
|
||
|
raise SyntaxError('Missing statement to execute in do while loop!')
|
||
|
start = except_keyword(source, start, 'while')
|
||
|
if not start:
|
||
|
raise SyntaxError('Missing while keyword in do-while loop')
|
||
|
bra, start = do_bracket_exp(source, start, throw=True)
|
||
|
statement += 'if not %s:\n' % bra + indent('break\n')
|
||
|
return 'while 1:\n' + indent(statement), start
|
||
|
|
||
|
|
||
|
def do_block(source, start):
|
||
|
bra, start = pass_bracket(source, start, '{}')
|
||
|
#print source[start:], bra
|
||
|
#return bra +'\n', start
|
||
|
if bra is None:
|
||
|
raise SyntaxError('Missing block ( {code} )')
|
||
|
code = ''
|
||
|
bra = bra[1:-1] + ';'
|
||
|
bra_pos = 0
|
||
|
while bra_pos < len(bra):
|
||
|
st, bra_pos = do_statement(bra, bra_pos)
|
||
|
if st is None:
|
||
|
break
|
||
|
code += st
|
||
|
bra_pos = pass_white(bra, bra_pos)
|
||
|
if bra_pos < len(bra):
|
||
|
raise SyntaxError('Block has more code that could not be parsed:\n' +
|
||
|
bra[bra_pos:])
|
||
|
return code, start
|
||
|
|
||
|
|
||
|
def do_empty(source, start):
|
||
|
return 'pass\n', start + 1
|
||
|
|
||
|
|
||
|
def do_expression(source, start):
|
||
|
start = pass_white(source, start)
|
||
|
end = pass_until(source, start, tokens=(';', ))
|
||
|
if end == start + 1: #empty statement
|
||
|
return 'pass\n', end
|
||
|
# AUTOMATIC SEMICOLON INSERTION FOLLOWS
|
||
|
# Without ASI this function would end with: return exp_translator(source[start:end].rstrip(';'))+'\n', end
|
||
|
# ASI makes things a bit more complicated:
|
||
|
# we will try to parse as much as possible, inserting ; in place of last new line in case of error
|
||
|
rev = False
|
||
|
rpos = 0
|
||
|
while True:
|
||
|
try:
|
||
|
code = source[start:end].rstrip(';')
|
||
|
cand = exp_translator(code) + '\n', end
|
||
|
just_to_test = compile(cand[0], '', 'exec')
|
||
|
return cand
|
||
|
except Exception as e:
|
||
|
if not rev:
|
||
|
rev = source[start:end][::-1]
|
||
|
lpos = rpos
|
||
|
while True:
|
||
|
rpos = pass_until(rev, rpos, LINE_TERMINATOR)
|
||
|
if rpos >= len(rev):
|
||
|
raise
|
||
|
if filter(lambda x: x not in SPACE, rev[lpos:rpos]):
|
||
|
break
|
||
|
end = start + len(rev) - rpos + 1
|
||
|
|
||
|
|
||
|
def do_var(source, start):
|
||
|
#todo auto ; insertion
|
||
|
start += 3 #pass var
|
||
|
end = pass_until(source, start, tokens=(';', ))
|
||
|
defs = argsplit(
|
||
|
source[start:end - 1]
|
||
|
) # defs is the list of defined vars with optional initializer
|
||
|
code = ''
|
||
|
for de in defs:
|
||
|
var, var_end = parse_identifier(de, 0, True)
|
||
|
TO_REGISTER.append(var)
|
||
|
var_end = pass_white(de, var_end)
|
||
|
if var_end < len(
|
||
|
de
|
||
|
): # we have something more to parse... It has to start with =
|
||
|
if de[var_end] != '=':
|
||
|
raise SyntaxError(
|
||
|
'Unexpected initializer in var statement. Expected "=", got "%s"'
|
||
|
% de[var_end])
|
||
|
code += exp_translator(de) + '\n'
|
||
|
if not code.strip():
|
||
|
code = 'pass\n'
|
||
|
return code, end
|
||
|
|
||
|
|
||
|
def do_label(source, start):
|
||
|
label, end = parse_identifier(source, start)
|
||
|
end = pass_white(source, end)
|
||
|
#now source[end] must be :
|
||
|
assert source[end] == ':'
|
||
|
end += 1
|
||
|
inside, end = do_statement(source, end)
|
||
|
if inside is None:
|
||
|
raise SyntaxError('Missing statement after label')
|
||
|
defs = ''
|
||
|
if inside.startswith('while ') or inside.startswith(
|
||
|
'for ') or inside.startswith('#for'):
|
||
|
# we have to add contine label as well...
|
||
|
# 3 or 1 since #for loop type has more lines before real for.
|
||
|
sep = 1 if not inside.startswith('#for') else 3
|
||
|
cont_label = get_continue_label(label)
|
||
|
temp = inside.split('\n')
|
||
|
injected = 'try:\n' + '\n'.join(temp[sep:])
|
||
|
injected += 'except %s:\n pass\n' % cont_label
|
||
|
inside = '\n'.join(temp[:sep]) + '\n' + indent(injected)
|
||
|
defs += 'class %s(Exception): pass\n' % cont_label
|
||
|
break_label = get_break_label(label)
|
||
|
inside = 'try:\n%sexcept %s:\n pass\n' % (indent(inside), break_label)
|
||
|
defs += 'class %s(Exception): pass\n' % break_label
|
||
|
return defs + inside, end
|
||
|
|
||
|
|
||
|
def do_for(source, start):
|
||
|
start += 3 # pass for
|
||
|
entered = start
|
||
|
bra, start = pass_bracket(source, start, '()')
|
||
|
inside, start = do_statement(source, start)
|
||
|
if inside is None:
|
||
|
raise SyntaxError('Missing statement after for')
|
||
|
bra = bra[1:-1]
|
||
|
if ';' in bra:
|
||
|
init = argsplit(bra, ';')
|
||
|
if len(init) != 3:
|
||
|
raise SyntaxError('Invalid for statement')
|
||
|
args = []
|
||
|
for i, item in enumerate(init):
|
||
|
end = pass_white(item, 0)
|
||
|
if end == len(item):
|
||
|
args.append('' if i != 1 else '1')
|
||
|
continue
|
||
|
if not i and except_keyword(item, end, 'var') is not None:
|
||
|
# var statement
|
||
|
args.append(do_var(item, end)[0])
|
||
|
continue
|
||
|
args.append(do_expression(item, end)[0])
|
||
|
return '#for JS loop\n%swhile %s:\n%s%s\n' % (
|
||
|
args[0], args[1].strip(), indent(inside), indent(args[2])), start
|
||
|
# iteration
|
||
|
end = pass_white(bra, 0)
|
||
|
register = False
|
||
|
if bra[end:].startswith('var '):
|
||
|
end += 3
|
||
|
end = pass_white(bra, end)
|
||
|
register = True
|
||
|
name, end = parse_identifier(bra, end)
|
||
|
if register:
|
||
|
TO_REGISTER.append(name)
|
||
|
end = pass_white(bra, end)
|
||
|
if bra[end:end + 2] != 'in' or bra[end + 2] in IDENTIFIER_PART:
|
||
|
#print source[entered-10:entered+50]
|
||
|
raise SyntaxError('Invalid "for x in y" statement')
|
||
|
end += 2 # pass in
|
||
|
exp = exp_translator(bra[end:])
|
||
|
res = 'for temp in %s:\n' % exp
|
||
|
res += indent('var.put(%s, temp)\n' % name.__repr__()) + indent(inside)
|
||
|
return res, start
|
||
|
|
||
|
|
||
|
# todo - IMPORTANT
|
||
|
def do_continue(source, start, name='continue'):
|
||
|
start += len(name) #pass continue
|
||
|
start = pass_white(source, start)
|
||
|
if start < len(source) and source[start] == ';':
|
||
|
return '%s\n' % name, start + 1
|
||
|
# labeled statement or error
|
||
|
label, start = parse_identifier(source, start)
|
||
|
start = pass_white(source, start)
|
||
|
if start < len(source) and source[start] != ';':
|
||
|
raise SyntaxError('Missing ; after label name in %s statement' % name)
|
||
|
return 'raise %s("%s")\n' % (get_continue_label(label)
|
||
|
if name == 'continue' else
|
||
|
get_break_label(label), name), start + 1
|
||
|
|
||
|
|
||
|
def do_break(source, start):
|
||
|
return do_continue(source, start, 'break')
|
||
|
|
||
|
|
||
|
def do_return(source, start):
|
||
|
start += 6 # pass return
|
||
|
end = source.find(';', start) + 1
|
||
|
if end == -1:
|
||
|
end = len(source)
|
||
|
trans = exp_translator(source[start:end].rstrip(';'))
|
||
|
return 'return %s\n' % (trans if trans else "var.get('undefined')"), end
|
||
|
|
||
|
|
||
|
# todo later?- Also important
|
||
|
def do_throw(source, start):
|
||
|
start += 5 # pass throw
|
||
|
end = source.find(';', start) + 1
|
||
|
if not end:
|
||
|
end = len(source)
|
||
|
trans = exp_translator(source[start:end].rstrip(';'))
|
||
|
if not trans:
|
||
|
raise SyntaxError('Invalid throw statement: nothing to throw')
|
||
|
res = 'PyJsTempException = JsToPyException(%s)\nraise PyJsTempException\n' % trans
|
||
|
return res, end
|
||
|
|
||
|
|
||
|
def do_try(source, start):
|
||
|
start += 3 # pass try
|
||
|
block, start = do_block(source, start)
|
||
|
result = 'try:\n%s' % indent(block)
|
||
|
catch = except_keyword(source, start, 'catch')
|
||
|
if catch:
|
||
|
bra, catch = pass_bracket(source, catch, '()')
|
||
|
bra = bra[1:-1]
|
||
|
identifier, bra_end = parse_identifier(bra, 0)
|
||
|
holder = 'PyJsHolder_%s_%d' % (identifier.encode('hex'),
|
||
|
random.randrange(1e8))
|
||
|
identifier = identifier.__repr__()
|
||
|
bra_end = pass_white(bra, bra_end)
|
||
|
if bra_end < len(bra):
|
||
|
raise SyntaxError('Invalid content of catch statement')
|
||
|
result += 'except PyJsException as PyJsTempException:\n'
|
||
|
block, catch = do_block(source, catch)
|
||
|
# fill in except ( catch ) block and remember to recover holder variable to its previous state
|
||
|
result += indent(
|
||
|
TRY_CATCH.replace('HOLDER', holder).replace('NAME',
|
||
|
identifier).replace(
|
||
|
'BLOCK',
|
||
|
indent(block)))
|
||
|
start = max(catch, start)
|
||
|
final = except_keyword(source, start, 'finally')
|
||
|
if not (final or catch):
|
||
|
raise SyntaxError(
|
||
|
'Try statement has to be followed by catch or finally')
|
||
|
if not final:
|
||
|
return result, start
|
||
|
# translate finally statement
|
||
|
block, start = do_block(source, final)
|
||
|
return result + 'finally:\n%s' % indent(block), start
|
||
|
|
||
|
|
||
|
def do_debugger(source, start):
|
||
|
start += 8 # pass debugger
|
||
|
end = pass_white(source, start)
|
||
|
if end < len(source) and source[end] == ';':
|
||
|
end += 1
|
||
|
return 'pass\n', end #ignore errors...
|
||
|
|
||
|
|
||
|
# todo automatic ; insertion. fuck this crappy feature
|
||
|
|
||
|
# Least important
|
||
|
|
||
|
|
||
|
def do_switch(source, start):
|
||
|
start += 6 # pass switch
|
||
|
code = 'while 1:\n' + indent('SWITCHED = False\nCONDITION = (%s)\n')
|
||
|
# parse value of check
|
||
|
val, start = pass_bracket(source, start, '()')
|
||
|
if val is None:
|
||
|
raise SyntaxError('Missing () after switch statement')
|
||
|
if not val.strip():
|
||
|
raise SyntaxError('Missing content inside () after switch statement')
|
||
|
code = code % exp_translator(val)
|
||
|
bra, start = pass_bracket(source, start, '{}')
|
||
|
if bra is None:
|
||
|
raise SyntaxError('Missing block {} after switch statement')
|
||
|
bra_pos = 0
|
||
|
bra = bra[1:-1] + ';'
|
||
|
while True:
|
||
|
case = except_keyword(bra, bra_pos, 'case')
|
||
|
default = except_keyword(bra, bra_pos, 'default')
|
||
|
assert not (case and default)
|
||
|
if case or default: # this ?: expression makes things much harder....
|
||
|
case_code = None
|
||
|
if case:
|
||
|
case_code = 'if SWITCHED or PyJsStrictEq(CONDITION, %s):\n'
|
||
|
# we are looking for a first : with count 1. ? gives -1 and : gives +1.
|
||
|
count = 0
|
||
|
for pos, e in enumerate(bra[case:], case):
|
||
|
if e == '?':
|
||
|
count -= 1
|
||
|
elif e == ':':
|
||
|
count += 1
|
||
|
if count == 1:
|
||
|
break
|
||
|
else:
|
||
|
raise SyntaxError(
|
||
|
'Missing : token after case in switch statement')
|
||
|
case_condition = exp_translator(
|
||
|
bra[case:pos]) # switch {case CONDITION: statements}
|
||
|
case_code = case_code % case_condition
|
||
|
case = pos + 1
|
||
|
if default:
|
||
|
case = except_token(bra, default, ':')
|
||
|
case_code = 'if True:\n'
|
||
|
# now parse case statements (things after ':' )
|
||
|
cand, case = do_statement(bra, case)
|
||
|
while cand:
|
||
|
case_code += indent(cand)
|
||
|
cand, case = do_statement(bra, case)
|
||
|
case_code += indent('SWITCHED = True\n')
|
||
|
code += indent(case_code)
|
||
|
bra_pos = case
|
||
|
else:
|
||
|
break
|
||
|
# prevent infinite loop :)
|
||
|
code += indent('break\n')
|
||
|
return code, start
|
||
|
|
||
|
|
||
|
def do_pyimport(source, start):
|
||
|
start += 8
|
||
|
lib, start = parse_identifier(source, start)
|
||
|
jlib = 'PyImport_%s' % lib
|
||
|
code = 'import %s as %s\n' % (lib, jlib)
|
||
|
#check whether valid lib name...
|
||
|
try:
|
||
|
compile(code, '', 'exec')
|
||
|
except:
|
||
|
raise SyntaxError(
|
||
|
'Invalid Python module name (%s) in pyimport statement' % lib)
|
||
|
# var.pyimport will handle module conversion to PyJs object
|
||
|
code += 'var.pyimport(%s, %s)\n' % (repr(lib), jlib)
|
||
|
return code, start
|
||
|
|
||
|
|
||
|
def do_with(source, start):
|
||
|
raise NotImplementedError('With statement is not implemented yet :(')
|
||
|
|
||
|
|
||
|
KEYWORD_METHODS = {
|
||
|
'do': do_dowhile,
|
||
|
'while': do_while,
|
||
|
'if': do_if,
|
||
|
'throw': do_throw,
|
||
|
'return': do_return,
|
||
|
'continue': do_continue,
|
||
|
'break': do_break,
|
||
|
'try': do_try,
|
||
|
'for': do_for,
|
||
|
'switch': do_switch,
|
||
|
'var': do_var,
|
||
|
'debugger': do_debugger, # this one does not do anything
|
||
|
'with': do_with,
|
||
|
'pyimport': do_pyimport
|
||
|
}
|
||
|
|
||
|
#Also not specific statements (harder to detect)
|
||
|
# Block {}
|
||
|
# Expression or Empty Statement
|
||
|
# Label
|
||
|
#
|
||
|
# Its easy to recognize block but harder to distinguish between label and expression statement
|
||
|
|
||
|
|
||
|
def translate_flow(source):
|
||
|
"""Source cant have arrays, object, constant or function literals.
|
||
|
Returns PySource and variables to register"""
|
||
|
global TO_REGISTER
|
||
|
TO_REGISTER = []
|
||
|
return do_block('{%s}' % source, 0)[0], TO_REGISTER
|
||
|
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
#print do_dowhile('do {} while(k+f)', 0)[0]
|
||
|
#print 'e: "%s"'%do_expression('++(c?g:h); mj', 0)[0]
|
||
|
print(translate_flow('a; yimport test')[0])
|