#!/usr/bin/env python3 ''' Reads two lua docs files and checks for differences python ./libraries/AP_Scripting/tests/docs_check.py "./libraries/AP_Scripting/docs/docs expected.lua" "./libraries/AP_Scripting/docs/docs.lua" AP_FLAKE8_CLEAN ''' import optparse, sys, re class method(object): def __init__(self, global_name, local_name, num_args, full_line, returns, params): self.global_name = global_name self.local_name = local_name self.num_args = num_args self.full_line = full_line self.returns = returns self.params = params self.manual = False for i in range(len(self.returns)): if self.returns[i][0] == 'UNKNOWN': self.manual = True for i in range(len(self.params)): if self.params[i][0] == 'UNKNOWN': self.manual = True def __str__(self): ret_str = "%s\n" % (self.full_line) if len(self.local_name): ret_str += "\tClass: %s\n" % (self.global_name) ret_str += "\tFunction: %s\n" % (self.local_name) else: ret_str += "\tGlobal: %s\n" % (self.global_name) ret_str += "\tNum Args: %s\n" % (self.num_args) ret_str += "\tParams:\n" for param_type in self.params: ret_str += "\t\t%s\n" % param_type ret_str += "\tReturns:\n" for return_type in self.returns: ret_str += "\t\t%s\n" % return_type ret_str +="\n" return ret_str def type_compare(self, A, B): if (((len(A) == 1) and (A[0] == 'UNKNOWN')) or ((len(B) == 1) and (B[0] == 'UNKNOWN'))): # UNKNOWN is a special case used for manual bindings return True if len(A) != len(B): return False for i in range(len(A)): if A[i] != B[i]: return False return True def types_compare(self, A, B): if len(A) != len(B): return False for i in range(len(A)): if not self.type_compare(A[i], B[i]): return False return True def check_types(self, other): if not self.types_compare(self.returns, other.returns): return False if not self.types_compare(self.params, other.params): return False return True def __eq__(self, other): return (self.global_name == other.global_name) and (self.local_name == other.local_name) and (self.num_args == other.num_args) def is_overload(self, other): # this allows multiple function definitions with different params white_list = [ "Parameter" ] allow_override = other.manual or (self.global_name in white_list) return allow_override and (self.global_name == other.global_name) and (self.local_name == other.local_name) and (self.num_args != other.num_args) def get_return_type(line): try: match = re.findall(r"^---@return (\w+(\|(\w+))*)", line) all_types = match[0][0] return all_types.split("|") except: raise Exception("Could not get return type in: %s" % line) def get_param_type(line): try: match = re.findall(r"^---@param (?:\w+\??|...) (\w+(\|(\w+))*)", line) all_types = match[0][0] return all_types.split("|") except: raise Exception("Could not get param type in: %s" % line) def parse_file(file_name): methods = [] returns = [] params = [] with open(file_name) as fp: while True: line = fp.readline() if not line: break # Acuminate return and params to associate with next function if line.startswith("---@return"): returns.append(get_return_type(line)) if line.startswith("---@param"): params.append(get_param_type(line)) # only consider functions if not line.startswith("function"): continue # remove any comment line = line.split('--', 1)[0] # remove any trailing whitespace line = line.strip() # remove "function" function_line = line.split(' ', 1)[1] # remove "end" function_line = function_line.rsplit(' ', 1)[0] # remove spaces function_line = function_line.replace(" ", "") # get arguments function_name, args = function_line.split("(",1) args = args[0:args.find(")")] if len(args) == 0: num_args = 0 else: num_args = args.count(",") + 1 if num_args != len(params): raise Exception("Missing \"---@param\" for function: %s", line) # get global/class name and function name local_name = "" if function_name.count(":") == 1: global_name, local_name = function_name.split(":",1) else: global_name = function_name methods.append(method(global_name, local_name, num_args, line, returns, params)) returns = [] params = [] return methods def compare(expected_file_name, got_file_name): # parse in methods expected_methods = parse_file(expected_file_name) got_methods = parse_file(got_file_name) pass_check = True # make sure each expected method is included once for meth in expected_methods: found = False for got in got_methods: if got == meth: if found: print("Multiple definitions of:") print(meth) pass_check = False elif not meth.check_types(got): print("Type error:") print("Want:") print(meth) print("Got:") print(got) pass_check = False found = True if not found: print("Missing definition of:") print(meth) pass_check = False # White list of classes that are allowed unexpected definitions white_list = [ # "virtual" class to bypass need for nil check when getting a parameter value, Parameter_ud is used internally, Parameter_ud_const exists only in the docs. "Parameter_ud_const" ] # make sure no unexpected methods are included for got in got_methods: if got.global_name in white_list: # Dont check if in the white list continue found = False for meth in expected_methods: if (got == meth) or got.is_overload(meth): found = True break if not found: print("Unexpected definition of:") print(got) pass_check = False if not pass_check: raise Exception("Docs do not match") else: print("Docs check passed") if __name__ == '__main__': parser = optparse.OptionParser(__file__) opts, args = parser.parse_args() if len(args) != 2: print('Usage: %s "expected docs path" "current docs path"' % parser.usage) sys.exit(0) compare(args[0], args[1])