#!/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')