1
0
mirror of https://github.com/danog/tl-schema.git synced 2024-12-11 17:09:46 +01:00
tl-schema/utils/lintdiff.py
2017-08-16 13:28:26 +03:00

448 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
import re
import struct
import binascii
class TLTokenParam:
def __init__(self, name, type, flags=None):
self.name = name
self.type = type
self.flags = flags or ''
self.typerefs = list()
class TLToken:
def __init__(self, name, crc, params, result, is_method, tl_info, no_crc_verify=True):
# name
self.name = str(name)
# aka id, should be already "converted" to int8 (struct.unpack'd for example)
self.crc = int(crc)
self.params = list(params)
# type
self.result = str(result)
self.is_method = is_method
for param in self.params:
param.typerefs = tl_info.registerParamType(param.type, self, is_method)
self.typerefs = tl_info.registerReturnType(self.result, self, is_method)
if not no_crc_verify:
self.verify_crc()
def __str__(self):
return 'TLToken: ' + self.name
def verify_crc(self):
# From TDesktop's generate.py
cleanline = self.dump(self)
cleanline = re.sub(r' [a-zA-Z0-9_]+\:flags\.[0-9]+\?true', '', cleanline)
cleanline = cleanline.replace('<', ' ').replace('>', ' ').replace(' ', ' ')
cleanline = cleanline.replace(':bytes ', ':string ')
cleanline = cleanline.replace('?bytes ', '?string ')
cleanline = cleanline.replace('{', '').replace('}', '')
cleanline = cleanline.strip()
if self.crc == binascii.crc32(binascii.a2b_qp(cleanline)):
print('WARNING: CRC MISMATCH -- expected {} in: '.format(self.crc2hex(self.crc)))
print(self.dump(self), end='\n\n')
@staticmethod
def crc2hex(crc: int):
return str(binascii.b2a_hex(struct.pack('>i', crc)), encoding='ascii').lstrip('0')
TL_line_regex = re.compile(r"([a-zA-Z\.0-9_]+)#([0-9a-f]+)([^=]*)=\s*([a-zA-Z\.<>0-9_]+);")
TL_param_regex = re.compile(r"\s([^:]+):([^\s?:]+\?)?(\S+)")
@classmethod
def from_tl_line(cls, tl_line, is_method, tl_info, no_crc_verify=False):
line = cls.TL_line_regex.match(tl_line)
crc = struct.unpack('>i',
binascii.a2b_hex(
'{:>08}'.format(line.group(2))
)
)[0]
params = [
TLTokenParam(name=name, type=type, flags=flags)
for name, flags, type in cls.TL_param_regex.findall(line.group(3))
]
return cls(
name=line.group(1),
crc=crc,
params=params,
result=line.group(4),
is_method=is_method,
tl_info=tl_info,
no_crc_verify=no_crc_verify,
)
@staticmethod
def dump(inst):
return '{name}#{crc} {params} = {result};'.format(
name=inst.name,
crc=inst.crc2hex(inst.crc),
params=' '.join('{name}:{flags}{type}'.format(**param.__dict__) for param in inst.params),
result=inst.result
).replace(' ', ' ')
def __repr__(self):
return self.dump(self)
def __eq__(self, item):
if type(item) is type(self):
return self.dump(self) == self.dump(item)
else:
return NotImplemented
# TODO: handle {X:Type} instead of hardcoding X and !X
BUILTIN_TYPES = ('int', 'long', 'bytes', 'string', 'double', 'Vector', 'vector', '#', 'true', 'X', '!X')
ALLOW_UNUSED = ('Error', 'Null', 'Updates', 'True')
class TLType:
def __init__(self, name):
self.name = name
self.constructors = list() # constructors which construct this type
self.compositors = list() # constructors which depend on this type
self.users = list() # functions which depend on this type
self.returners = list() # functions which return this type
self._linted = 0 # non-python way, yes
self._block = False # for simple blocking recursion guard
if name in BUILTIN_TYPES:
self.constructors.append(None)
if name in ALLOW_UNUSED:
self.users.append(None)
def lint(self, force=False, invalidate_builtin=False):
if invalidate_builtin and self.name in BUILTIN_TYPES:
return -4
if self._block:
return -5
self._block = True
if self._linted == 0 or force:
if len(self.constructors) == 0:
self._linted = -1
elif len(self.users) != 0 or len(self.returners) != 0:
self._linted = 1
elif len(self.compositors) != 0:
checkref = lambda r: any(ref.lint(force, True)>0 for ref in r.typerefs)
checkcomps = lambda comps: any(checkref(ref) for ref in comps)
if checkcomps(self.compositors):
self._linted = 2
else:
self._linted = -2
else:
self._linted = -3
self._block = False
return self._linted
@staticmethod
def lintstr(li):
LINTSTR = {
-5: 'ERR: Blocked (recursion?)',
-4: 'WTF: Is a builtin',
-3: 'WARN: Never used',
-2: 'ERR: Composited in other types but all of them are invalid',
-1: 'ERR: Has no constructor',
0: 'UNKNOWN',
1: 'OK: Used in fuctions',
2: 'OK: Used in some valid types'
}
return LINTSTR[li]
class TLInfo:
def __init__(self):
self.types = dict()
TEMPLATE_REGEX = re.compile(r'([a-zA-Z\.0-9_]+)<([a-zA-Z\.<>0-9_]+)>')
def _typesFactory(self, type):
match = self.TEMPLATE_REGEX.match(type)
if match is not None:
#print(type)
return self._typesFactory(match.group(1)) + self._typesFactory(match.group(2))
else:
if type not in self.types:
self.types[type] = TLType(type)
return [self.types[type]]
def registerParamType(self, type, token, is_method):
tl_types = self._typesFactory(type)
if is_method:
for tl_type in tl_types:
tl_type.users.append(token)
else:
for tl_type in tl_types:
tl_type.compositors.append(token)
return tl_types
def registerReturnType(self, type, token, is_method):
tl_types = self._typesFactory(type)
if is_method:
for tl_type in tl_types:
tl_type.returners.append(token)
else:
for tl_type in tl_types:
tl_type.constructors.append(token)
return tl_types
class TLFile:
def __init__(self):
self.constructors = list()
self.methods = list()
self.tl_info = TLInfo()
@classmethod
def from_file(cls, file, no_verify=False, diff_removed_file=None):
ret = cls()
VECTOR = 'vector#1cb5c415'
def repack(line):
return re.sub(r'#0+', '#', ' '.join(re.split(r' *', line)))
def verify(line, token):
tl_line = repr(token)
return repack(line) == repack(tl_line)
def process_line(l, tl_file):
try:
token = TLToken.from_tl_line(l, methods_now, tl_file.tl_info, no_verify)
if no_verify or verify(l, token):
if methods_now:
tl_file.methods.append(token)
else:
tl_file.constructors.append(token)
else:
print('MISMATCH:')
print(l)
print(repr(token), end='\n\n')
except Exception as e:
print('ERROR:')
print(e)
print(l, end='\n\n')
methods_now = False
for l in file:
l = l.strip()
if not l:
# empty
continue
elif l.startswith('///'):
continue # comment in diff
elif l.startswith('//'):
if diff_removed_file is None:
# comment
continue
else:
l = l.lstrip('//').lstrip()
process_line(l, diff_removed_file)
# removed
elif l == '---functions---':
methods_now = True
continue
elif l == '---types---':
methods_now = False
continue
elif l.startswith(VECTOR):
# vector's line isn't supported, and has to be skipped
continue
else:
process_line(l, ret)
return ret
@classmethod
def from_filename(cls, fname, no_verify=False, diff_removed_file=None):
with open(fname) as file:
return cls.from_file(file, no_verify, diff_removed_file)
def lint(self):
for _, tl_type in self.tl_info.types.items():
try:
lr = tl_type.lint()
if lr <= 0:
print('{} -- "{}"'.format(tl_type.lintstr(lr), tl_type.name))
except RecursionError:
print('RECURSION ERROR: ')
print(tl_type, end='\n\n')
for dupe_method in (x for n, x in enumerate(self.methods) if x in self.methods[:n]):
print('ERR: DUPE METHOD -- {}#{}'.format(dupe_method.name, dupe_method.crc2hex(dupe_method.crc)))
for dupe_constructor in (x for n, x in enumerate(self.constructors) if x in self.constructors[:n]):
print('ERR: DUPE CONSTR -- {}#{}'.format(dupe_constructor.name, dupe_constructor.crc2hex(dupe_constructor.crc)))
def diff(self, old, just_added=False, no_changed=False):
ret = { 'added': TLFile() }
if not just_added:
ret['removed'] = TLFile()
if not no_changed:
ret['changed'] = TLFile()
def getdiff(old, new):
ret = { 'added': list() }
if not just_added:
ret['removed'] = list()
if not no_changed:
ret['changed'] = list()
old_names = {m.name: m for m in old}
for m in new:
if m not in old:
if m.name not in old_names or just_added or no_changed:
ret['added'].append(m)
else:
ret['changed'].append({
'new': m,
'old': old_names[m.name]
})
old.remove(old_names[m.name])
else:
old.remove(m)
if not just_added:
ret['removed'] = old
return ret
methods = getdiff(old.methods, self.methods)
ret['added'].methods = methods['added']
if not just_added:
ret['removed'].methods = methods['removed']
if not no_changed:
ret['changed'].methods = methods['changed']
constructors = getdiff(old.constructors, self.constructors)
ret['added'].constructors = constructors['added']
if not just_added:
ret['removed'].constructors = constructors['removed']
if not no_changed:
ret['changed'].constructors = constructors['changed']
return ret
def write_to_file(self, file):
file.write('---types---\n')
for token in self.constructors:
file.write(repr(token))
file.write('\n')
file.write('---functions---\n')
for token in self.methods:
file.write(repr(token))
file.write('\n')
@staticmethod
def writediff(diff, file):
file.write('---types---\n')
for token in diff['removed'].constructors:
file.write('// ')
file.write(repr(token))
file.write('\n')
for token in diff['changed'].constructors:
file.write('// ')
file.write(repr(token['old']))
file.write('\n')
file.write(repr(token['new']))
file.write('\n')
for token in diff['added'].constructors:
file.write(repr(token))
file.write('\n')
file.write('---functions---\n')
for token in diff['removed'].methods:
file.write('// ')
file.write(repr(token))
file.write('\n')
for token in diff['changed'].methods:
file.write('// ')
file.write(repr(token['old']))
file.write('\n')
file.write(repr(token['new']))
file.write('\n')
for token in diff['added'].methods:
file.write(repr(token))
file.write('\n')
@staticmethod
def readdiff(file):
ret = dict()
ret['removed'] = TLFile()
ret['added'] = TLFile.from_file(file, False, ret['removed'])
return ret
@staticmethod
def applydiff(diff, old_filename, comment_out=False):
tl_file = TLFile.from_filename(old_filename)
with open(old_filename, 'w') as file:
def helper(olst, rlst, alst):
removed_names = set(t.name for t in rlst)
added_names = set(t.name for t in alst)
changed_names = removed_names & added_names
clst = dict()
new_alst = list()
for t in alst:
if t.name in changed_names:
clst[t.name] = t
else:
new_alst.append(t)
for token in olst:
if token not in rlst:
file.write(repr(token))
else:
if comment_out:
file.write('// ')
file.write(repr(token))
file.write('\n')
if token.name in changed_names:
file.write(repr(clst[token.name]))
else:
continue # Just removed
file.write('\n')
for token in new_alst:
file.write(repr(token))
file.write('\n')
file.write('---types---\n')
helper(tl_file.constructors, diff['removed'].constructors, diff['added'].constructors)
file.write('---functions---\n')
helper(tl_file.methods, diff['removed'].methods, diff['added'].methods)
if __name__ == '__main__':
from sys import argv, stdout
if len(argv) <= 1 or argv[1] == 'help':
print("""
lint file -- lints file
diff old new -- prints diff
update old new -- updates old to new without reordering (gets diff and then applies it)
apply old diff -- applies diff to old
""")
else:
if argv[1] == 'lint':
# linter mode
TLFile.from_filename(argv[2]).lint()
elif argv[1] == 'diff':
# diff mode
tl_diff = TLFile.from_filename(argv[2]).diff(TLFile.from_filename(argv[3]))
TLFile.writediff(tl_diff, stdout)
elif argv[1] == 'update':
# get diff
tl_diff = TLFile.from_filename(argv[2]).diff(TLFile.from_filename(argv[3]), no_changed=True)
# and apply it
TLFile.applydiff(tl_diff, argv[2])
elif argv[1] == 'apply':
with open(argv[3]) as df:
tl_diff = TLFile.readdiff(df)
TLFile.applydiff(tl_diff, argv[2])
else:
print('invalid argunents, try `help`')