From 464e434626081c7cf0f4bebff8f87036271d426b Mon Sep 17 00:00:00 2001 From: "matthias@arch" Date: Sat, 19 Nov 2022 14:34:41 +0100 Subject: [PATCH] Initial commit --- gen_definition.py | 744 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 744 insertions(+) create mode 100755 gen_definition.py diff --git a/gen_definition.py b/gen_definition.py new file mode 100755 index 0000000..2aa7e5e --- /dev/null +++ b/gen_definition.py @@ -0,0 +1,744 @@ +#!/bin/python3 +""" +A python script to generate empty definitions from a header file having declarations +Copyright © 2022 Matthias Quintern. +This software comes with no warranty. +This software is licensed under the GPL3 +""" + +from typing import Callable +from os import path, listdir, chdir +from sys import argv, exit +import re + +print_debug = False + +# search in header files for enums +header_filetypes = [".hpp"] +# definition are written to source file having the same name as the header +source_filetype = ".cpp" + +def error(s): + print(s) + exit(1) + +constexpr = "constexpr" +inline = "inline" +static = "static" + +ACC_PUBLIC = 0 +ACC_PRIVATE = 1 +ACC_PROTECTED = 2 +acc_to_uml = [ '+', '-', '#' ] +acc_to_str = [ 'public', 'private', 'protected' ] +uml_style_autoresize = True + +def template_str2list(s:str): + if s: + templates = s.removeprefix("template<").removesuffix(">").split(", ") + templates = [ t.split(" ") for t in templates ] + else: + templates = [] + return templates + +def template_list2str(t:list[tuple[str, str]], req:str=""): + if len(t) == 0: + return "" + s = "template<" + for temp in t: + s += temp[0] + " " + temp[1] + ", " + s = s.removesuffix(", ") + ">\n" + if req: + s += "\t" + "requires " + req + return s + +def append_comma_separated(s: str, l: list[str]): + for e in l: + s += e + ", " + return s.removesuffix(", ") +def apply_and_append_comma_separated(s: str, l: list[str], f: Callable[[str], str]): + for e in l: + s += f(e) + ", " + return s.removesuffix(", ") + +def transformString(s: str, depth: int): + """Add tabs after all but the last line break and before the first char. + Do not add tabs to lines that start with a label""" + new_s = "" + for line in s.split("\n"): + if re.fullmatch(r"\w+:[^:].*", line) is None: + new_s += depth * "\t"; + new_s += line + "\n" + new_s = new_s.removesuffix("\n").removesuffix(depth*"\t") + # return depth * tab + s.replace("\n", "\n" + depth * tab, s.count("\n") - 1) + return new_s + +def firstLetterUppercase(s: str) -> str: + return s[0].capitalize() + s[1:] + +type_to_uml_alias = { + "unsigned int": "uint", + "glm::vec2": "vec2", + "glm::vec3": "vec3", +} +re_templated_type = r"([\w:]+)<(.*)>" + +def type_to_uml(t: str) -> str: + """ + std::vector -> T[] + std::array -> T[N] + std::map -> { K : T } + std::reference_wrapper -> T& + """ + if t in type_to_uml_alias.keys(): + return type_to_uml_alias[t] + # if templated + match = re.search(re_templated_type, t) + if match: + # remove namespaces + # name of outer template + t = match.groups()[0].split(":")[-1] + # the template arguments + temps = [ "" ] + scope = 0 + for c in match.groups()[1]: + if c == "<": scope += 1 + elif c == ">": scope -= 1 + elif c == ",": + if scope == 0: + temps.append("") + continue + temps[-1] += c + if print_debug: print("type_to_uml", t, temps) + + if len(temps) == 0: + pass + elif len(temps) == 1: + if t in [ "vector" ]: + return type_to_uml(temps[0]) + "[]" + elif t in [ "reference_wrapper" ]: + return type_to_uml(temps[0]) + "&" + elif len(temps) == 2: + if "map" in t: + return "{ " + type_to_uml(temps[0]) + " : " + type_to_uml(temps[1]) + " }" + elif "pair" in t: + return "{ " + type_to_uml(temps[0]) + ", " + type_to_uml(temps[1]) + " }" + elif "array" in t: + return f"{type_to_uml(temps[0])}[{type_to_uml(temps[1])}]" + else: + if t in "tuple": + return "{ " + apply_and_append_comma_separated(t, temps, type_to_uml) + " }" + + return apply_and_append_comma_separated(t + "<", temps, type_to_uml) + ">" + return t.strip(" ") + + +class Member: + def __init__(self, name:str, m_temp:list=[], prefixes:list=[], ret_type:str="", args:str="", attributes="", init:str="", requires:str="", suffixes="", defined=False, is_value=False, namespace:str="", accessibility=ACC_PUBLIC): + """ + :param name: member name + :param m_temp: list of templates of the member + :param prefixes: things like constexpr, inline... + :param ret_type: return type of the member + :param args: arguments of the member + :param init: initializion of the member + :param attributes: c++11 attributes [[ ... ]] + :param requires: constraints on template types + :param suffixes: const noexcept + :param defined: method defined + :param is_value: wether the member is a value or a method + :param accessibility: public, private or protected + + template + class class { + comment + template + prefixes ret_type name(args) const : init { + body + } + } + """ + self.name = name + self.prefixes = prefixes + self.m_temp = m_temp + self.ret_type = "" + if ret_type: self.ret_type = ret_type + " " + self.args = args + self.attributes = attributes + self.init = "" + if init: self.init = f"\n\t : {init} " + self.requires = requires + self.suffixes = suffixes # have a ' ' in front of them + self.defined = defined + self.comment = "" + self.accessibility = accessibility + # if comment: + # if type(comment) == str: + # self.comment = "/// " + comment + "\n" + # elif type(comment) == dict: + # self.comment = comment_from_dict(comment) + self.is_value = is_value + self.namespace = namespace + + def getArgsAsList(self): + args = [] + const = False + arglist = self.args.split(", ") + for i in range(len(arglist)): + if "const" in arglist[i]: + arglist[i] = arglist[i].replace("const ", "") + const = True + args.append(arglist[i].split(" ")) + if const: + args[-1][0] = "const " + args[-1][0] + return args + + def get_source_name(self, c_temp=[], class_=""): + s = "" + if class_: + if len(c_temp) > 0: + class_ = class_ + "<" + for t in c_temp: + class_ += f"{t[1]}, " + class_ = class_.removesuffix(", ") + ">" + class_ += "::" + + s += template_list2str(self.m_temp, self.requires) + for p in self.prefixes: + s += p + " " + + if self.is_value: + if static in self.prefixes and not constexpr in self.prefixes: + s += f"{self.ret_type}{class_}{self.name};\n" + return s + else: + s += f"{self.ret_type}{class_}{self.name};\n" + return s + s += f"{self.ret_type}{class_}{self.name}({self.args}){self.suffixes}" + return s + + def source(self, c_temp=[], class_="", depth:int=0): + """ + Return the empty defition + declarations for a source file + """ + s = "" + if class_: + s += template_list2str(c_temp) + if len(c_temp) > 0: + class_ = class_ + "<" + for t in c_temp: + class_ += f"{t[1]}, " + class_ = class_.removesuffix(", ") + ">" + class_ += "::" + + s += template_list2str(self.m_temp, self.requires) + for p in self.prefixes: + if p not in [ "static" ]: + s += p + " " + + if self.is_value: + if static in self.prefixes and not constexpr in self.prefixes: + s += f"{self.ret_type}{class_}{self.name};\n" + return s + else: + return "" + + s += f"{self.ret_type}{class_}{self.name}({self.args}){self.suffixes.replace(' override', '')}{self.init}" + " {\n\t\n}\n" + # s += " {\n" + # body = "" + # for l in self.body: body += l + "\n" + # s += transformString(body, depth) + # s += "}\n" + # else: + # s += "{}" + return transformString(s, depth) + + def uml(self): + """ + Return uml representation of the member + accessibility + """ + if not self.ret_type: return "" # if constructor, ret_type is empty + s = acc_to_uml[self.accessibility] + " " + s += self.name + # if function + if not self.is_value: + s += "(" + for arg in self.args.split(','): + s += type_to_uml(arg.replace("const", "").strip(' ').split(' ')[0]) + ", " + s = s.strip(", ") + ")" + s += ": " + type_to_uml(self.ret_type) + return s + "\n" + + def getter(self, depth:int=0): + """ + Return a getter + """ + return transformString(f"inline const {self.ret_type.strip(' ')}& get{firstLetterUppercase(self.name)}() const " + "{ " + f"return {self.name};" + " }\n", depth) + + def lv_setter(self, depth:int=0): + """ + Return a lvalue setter + """ + return transformString(f"inline void set{firstLetterUppercase(self.name)}(const {self.ret_type.strip(' ')}& v) " + "{ " + f"{self.name} = v;" + " }\n", depth) + + def rv_setter(self, depth:int=0): + """ + Return a rvalue setter + """ + return transformString(f"inline void set{firstLetterUppercase(self.name)}({self.ret_type.strip(' ')}&& v) " + "{ " + f"{self.name} = std::move(v);" + " }\n", depth) + + def __eq__(self, other) -> bool: + return self.name == other.name and self.args == other.args and self.ret_type == other.ret_type + + def __repr__(self): + return self.get_source_name([], "") + + +class Class: + def __init__(self, name: str, c_temp: list[tuple[str, str]]): + self.member_functions: list[Member] = [] + self.member_variables: list[Member] = [] + self.name = name + self.c_temp: list[tuple[str, str]] = c_temp + + def __repr__(self) -> str: + return f"<{self.name}: {self.member_variables}, {self.member_functions}>" + + def uml(self): + """ + Beginning of uml class + """ + s = "" + if self.c_temp: + s += "template=" + for temp, t in self.c_temp: + if temp == "typename": + s += t + ", " + else: + s += t + ": " + temp + ", " + s = s.strip(", ") + "\n" + s += self.name + "\n--\n" + for member in self.member_variables: + s += member.uml() + if self.member_variables: s += "--\n" + for member in self.member_functions: + s += member.uml() + if uml_style_autoresize: s += "style=autoresize" + return s + "\n" + + +# +# regex +# +# s must be a string where all indentation was removed +# COMMENTS +def starts_with_normal_comment(s: str) -> bool: + if re.search(r"^(//|/?\*)[^*/]", s): return True + else: return False + +def starts_with_doxygen_comment(s: str) -> bool: + if re.search(r"^(///|/\*\*)", s): return True + else: return False + +def starts_with_comment(s: str) -> bool: + return starts_with_normal_comment(s) or starts_with_doxygen_comment(s); + +def starts_with_comment_end(s: str) -> bool: + if re.search(r"^(\*/)", s): return True + else: return False + +# NAMESPACE +def namespace_open(s: str) -> tuple[bool, str]: + match = re.search(r"^namespace (\w+) *{", s) + if match: + return True, match.groups()[0] + return False, "" + + +# TEMPLATE +def template_declaration(s: str) -> bool: + if re.search(r"^template<(.+)>", s): + return True + return False + +# CLASS DECLARATION +def class_declaration(s: str) -> tuple[bool, str]: + match = re.search(r"^(?:(?:class)|(?:struct)) (\w+)", s) + if match: + return True, match.groups()[0] + return False, "" + +# VARIABLE/MEMBER DECLARATION +def variable_declaration(s: str, template_str:str = "", namespace:str="") -> tuple[bool, Member]: + match = re.search(r"^((?:[\w<>:,]+ )+)(\w+)( *= *.+)? *;", s) + # match = re.search(r"^((?:\w+ )+)(\w+) *(const)? *;", s) # declration + if match: + g = match.groups() + prefixes = [] + ret_type = "" + if g[2]: defined = True + else: defined = False + # get ret_type and prefixes + for prefix in g[0].strip(" ").split(" "): + if prefix in ["const", "constexpr", "inline", "static"]: + prefixes.append(prefix) + else: + ret_type += prefix + " " + return True, Member(g[1], m_temp=template_str2list(template_str), prefixes=prefixes, ret_type=ret_type, is_value=True, defined=defined, namespace=namespace) + return False, Member("") + +re_spaces = r"[ \t]*" # no or more spaces +re_spaces1 = r"[ \t]+" # at least one space +# each of these regexes captures something in one group +re_ret_type = r"((?:[\w<>:,&*()]+ )+)" # return type +re_attributes = r"((?:\[\[.+\]\])?)" # eg [[ nodiscard ]] +re_function_name = r"(\w+(?:(?:\[\])|(?:\(\))|[+\-/%=*&]{0,2})?)" # function name: eg operator[], operator%=, do_smth +re_function_args = r"\(((?:[\w<>:,&*= ]+,? )*(?:[\w<>:,&*= ]+))?\)" # function parameters (capture without ()), eg (std::map map, const Args&&... args) +re_suffixes = r"(" # eg const noexcept +for suffix in [ "const", "noexcept", "override" ]: + re_suffixes += f"(?:{re_spaces1}{suffix})?" +re_suffixes += ")" + +re_constructor = "^" + re_function_name + re_spaces + re_function_args + re_spaces + re_suffixes +re_member_f = "^" + re_attributes + re_spaces + re_ret_type + re_spaces + re_function_name + re_spaces + re_function_args + re_suffixes +re_member_f_declaration = re_member_f + re_spaces + ";" +re_member_f_dec_with_def = re_member_f + re_spaces + r"\{" + +if print_debug: print("re_member_f_declaration:", re_member_f_declaration) +if print_debug: print("re_member_f_dec_with_def:", re_member_f_dec_with_def) +if print_debug: print("re_constructor:", re_constructor) + +# FUNCTION DECLARATION +def function_declaration(s: str, template_str:str = "", namespace: str="") -> tuple[bool, Member]: + # match = re.search(r"^((?:[\w<>:,&*]+ )+)(\w+)\(((?:[\w<>:,&*]+,? )*(?:[\w<>:,&*=]+))?\) *((const)?) *;", s) # declration + # groups are: attributes, ret_type, name, args, suffixes + # if print_debug: print("function_declaration:", s.strip()) + match = re.search(re_member_f_declaration, s) # declaration + if match is None: + # match = re.search(r"^((?:[\w<>:,&*]+ )+)(\w+)\(((?:[\w<>:,&*]+,? )*(?:[\w<>:,&*=]+))?\) *((?:const)?) * *{", s) # declaration with definition + match = re.search(re_member_f_dec_with_def, s) # declaration with definition + defined = True + else: + defined = False + if match: + g = match.groups() + prefixes = [] + ret_type = "" + # get ret_type and prefixes + for prefix in g[1].strip(" ").split(" "): + if prefix in ["const", "constexpr", "inline", "static"]: + prefixes.append(prefix) + else: + ret_type += prefix + " " + ret_type = ret_type.strip(" ") + args = "" + if g[3]: + for arg in g[3].split(","): + if "=" in arg: + args += arg[:arg.find("=")].strip(" ") + ", " + else: + args += arg.strip(" ") + ", " + args = args[:-2] + else: args = "" + if print_debug: print("function_declaration:", match.groups()) + + return True, Member(g[2], template_str2list(template_str), prefixes=prefixes, ret_type=ret_type, args=args, attributes=g[0], defined=defined, suffixes=g[4], namespace=namespace) + return False, Member("") + +# constructor +def constructor_declaration(s: str, template_str:str = "", namespace: str="") -> tuple[bool, Member]: + # match = re.search(r"^((?:[\w<>:,&*]+ )+)(\w+)\(((?:[\w<>:,&*]+,? )*(?:[\w<>:,&*=]+))?\) *((const)?) *;", s) # declration + # groups are: name, args, suffixes + match = re.search(re_constructor, s) # declaration + if match: + g = match.groups() + args = "" + if g[1]: + for arg in g[1].split(","): + if "=" in arg: + args += arg[:arg.find("=")].strip(" ") + ", " + else: + args += arg.strip(" ") + ", " + args = args[:-2] + else: args = "" + if print_debug: print("constructor_declaration:", match.groups()) + + return True, Member(g[0], template_str2list(template_str), prefixes=[], ret_type="", args=args, defined=False, suffixes=g[2], namespace=namespace) + return False, Member("") + + +# +# INDENTATION +# +def get_indentation(s_indentation: str) -> int: + """ + returns number of spaces + """ + return s_indentation.replace("\t", " ").count(" ") + +def get_string_and_indent(s: str) -> tuple[str, int]: + """ + returns the string without indentation and the number of spaces in the indentation + """ + match = re.search("^([ \t]*)", s) + if match: + return s[len(match.groups()[0]):], get_indentation(match.groups()[0]) + return s, 0 + + +# SCOPE END +def scope_end(s: str) -> bool: + if re.search(r"^}", s): + return True + return False + +def parse_header(header_file:str): + with open(header_file, "r") as file: + header = file.readlines() + + declarations: list[Class | Member] = [] + + template_str = "" + scopes : list[tuple[str, str, int, int]] = [] # type (eg class, namespace...), name, indentation, line + in_class = "" + namespace = "" + accessibility = ACC_PUBLIC + + for i in range(len(header)): + line, indent = get_string_and_indent(header[i]) + if starts_with_normal_comment(line): continue + if starts_with_doxygen_comment(line): + pass + if starts_with_comment_end(line): + pass + + if template_declaration(line): + template_str = line.strip("\n") + + # check begin namespace + match, name = namespace_open(line) + if match: + scopes.append(("namespace", name, indent, i)) + namespace += "::" + name + namespace = namespace.strip("::") + + # check accessibility + if in_class: + for i in range(len(acc_to_str)): + if acc_to_str[i] in line: + accessibility = i + else: + accessibility = ACC_PUBLIC + + # check class begin + match, name = class_declaration(line) + if match: + # print("class in", i, "-", name) + scopes.append(("class", name, indent, i)) + declarations.append(Class(name, c_temp=template_str2list(template_str))) + in_class = name + # declarations[-1].members.append(member_from_str(line, template_str)) + template_str = "" + # check constructor + match, member = constructor_declaration(line, template_str, namespace) + if match: + # print("function in", i, "-", member.get_source_name([], "")) + member.accessibility = accessibility + if in_class: + declarations[-1].member_functions.append(member) + else: + declarations.append(member) + template_str = "" + # check function dec + match, member = function_declaration(line, template_str, namespace) + if match: + # print("function in", i, "-", member.get_source_name([], "")) + member.accessibility = accessibility + if in_class: + declarations[-1].member_functions.append(member) + else: + declarations.append(member) + template_str = "" + # check variable dec + match, member = variable_declaration(line, template_str, namespace) + if match: + # print("variable in", i, "-", member.get_source_name([], "")) + member.accessibility = accessibility + if in_class: + declarations[-1].member_variables.append(member) + else: + declarations.append(member) + template_str = "" + + # check scope end + if scope_end(line): + if len(scopes) > 0 and scopes[-1][2] == indent: + if (scopes[-1][0] == "namespace"): + for i in range(len(namespace)): + if namespace[-i-1] == ":": + namespace = namespace[:-i-2] + break; + scopes.pop() + else: + # print("WARNING: Found scope end but no scope was opened at this indentaiton. line:", i) + pass + return declarations + + +def get_definition(declarations: list[Class | Member], target: (str | Member)): + for dec in declarations: + if type(dec) == Class: + for mem in dec.member_functions + dec.member_variables: + # print("mem", mem) + if (type(target) == str and target in mem.name) or (type(target) == Member and target == mem): + return mem.source(dec.c_temp, dec.name, len(mem.namespace.split("::"))) + elif type(dec) == Member: + if (type(target) == str and target in dec.name) or (type(target) == Member and target == dec): + return dec.source([], "", len(dec.namespace.split("::"))) + + return "" + +def apply_on_target_member(f: Callable[[Member, int], str], declarations: list[Class | Member], target_name: (str | Member)): + for dec in declarations: + if type(dec) == Class: + for mem in dec.member_variables + dec.member_functions: + # print("mem", mem) + if (type(target_name) == str and target_name in mem.name) or (type(target_name) == Member and target_name == mem): + return f(mem, len(mem.namespace.split("::"))+2) + + return "" + +def apply_on_every_class_member(f: Callable[[Member], str], declarations: list[Class | Member], class_name: str): + s = "" + for dec in declarations: + if type(dec) == Class and dec.name == class_name: + for mem in dec.member_functions + dec.member_variables: + s += f(mem) + # print("mem", mem) + + return s + +def apply_on_class(f: Callable[[Class], str], declarations: list[Class | Member], class_name: str): + for dec in declarations: + if type(dec) == Class and dec.name == class_name: + return f(dec) + return "" + +def apply_on_all_classes(f: Callable[[Class], str], declarations: list[Class | Member]): + s = "" + for dec in declarations: + if type(dec) == Class: + s += f(dec) + return s + +def print_help(): + print(""" +Synposis: gen_definitions.py ... --file --name +General: + -h --help help + --no-docs turn off docstring generation + --file +Target: + --name + --line +Output: + --getter + --lv_setter + --rv_setter + --setters + --getter-and-setter + --def + --uml +""") + +# def missing_arg(arg): +# print("Missing argument for", arg) +# exit(1) + +if __name__ == "__main__": + docs = True + getter = False + rv_setter = False + lv_setter = False + definition = False + uml = False + file = "" + target_name = "" + target_line_nr = -1 + i = 1 + while i in range(1, len(argv)): + if argv[i] == "--help" or argv[i] == "-h": + print_help() + exit(0) + elif argv[i] == "--no-docs": + docs = False + elif argv[i-1] == "--file": + file = argv[i] + elif argv[i] == "--getter-and-setter": + getter = True + lv_setter = True + rv_setter = True + elif argv[i] == "--lv_setter": + lv_setter = True + elif argv[i] == "--rv_setter": + rv_setter = True + elif argv[i] == "--setters": + lv_setter = True + rv_setter = True + elif argv[i] == "--getter": + getter = True + elif argv[i] == "--def": + definition = True + interactive = True + elif argv[i] == "--uml": + uml = True + elif argv[i-1] == "--name": + target_name = argv[i] + elif argv[i-1] == "--line": + target_line_nr = int(argv[i]) + i += 1 + + ret = "" + declarations = [] + if file: + if path.isfile(file): + declarations = parse_header(file) + if print_debug: print("found declartions:", declarations) + else: + print("Not a file:", file) + exit(1) + if target_line_nr > 0: + with open(file) as file_: + line, indent = get_string_and_indent(file_.readlines()[target_line_nr-1]) + if print_debug: print("line", target_line_nr, line) + match, member = constructor_declaration(line, "", "") + if match: + target_name = member + match, member = function_declaration(line, "", "") + if match: + target_name = member + match, member = variable_declaration(line, "", "") + if match: + target_name = member + if not target_name: + error(f"Could not find target in line {target_line_nr}") + + s = "" + if print_debug: print("target:", target_name) + if getter: + s += apply_on_target_member(Member.getter, declarations, target_name) + if lv_setter: + s += apply_on_target_member(Member.lv_setter, declarations, target_name) + if rv_setter: + s += apply_on_target_member(Member.rv_setter, declarations, target_name) + if definition: + s = get_definition(declarations, target_name).strip("\n") + if uml: + if target_name: + s += apply_on_class(Class.uml, declarations, target_name) + else: + s += apply_on_all_classes(Class.uml, declarations) + # s += apply_on_every_class_member(Member.uml, declarations, target_name) + + print(s, end='\n')