diff --git a/gen_definitions.py b/gen_definitions.py new file mode 100755 index 0000000..1acf095 --- /dev/null +++ b/gen_definitions.py @@ -0,0 +1,442 @@ +#!/bin/python3 +""" +A python script to generate toString functions for all enumerations in a cpp file. +Copyright © 2022 Matthias Quintern. +This software comes with no warranty. +This software is licensed under the GPL3 +""" +from os import path, listdir, chdir +from sys import argv, exit +import re + + +# 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" + +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 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 + + +class Member: + def __init__(self, name:str, m_temp:list=[], prefixes:list=[], ret_type:str="", args:str="", init:str="", requires:str="", const=False, defined=False, is_value=False, namespace:str=""): + """ + :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 requires: constraints on template types + :param const: True if the method does not change the object + :param defined: method defined + :param is_value: wether the member is a value or a method + + 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.init = "" + if init: self.init = f"\n\t : {init} " + self.requires = requires + self.const = "" + if const: self.const = " const" + self.defined = defined + self.comment = "" + # 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.const}" + 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: + 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.const}{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 __repr__(self): + return self.get_source_name([], "") + + +class Class: + def __init__(self, name: str, c_temp: list[str]): + self.member_functions: list[Member] = [] + self.member_variables: list[Member] = [] + self.name = name + self.c_temp: list[str] = c_temp + + +# +# 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 (\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("") + +# 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 + if match is None: + match = re.search(r"^((?:\w+ )+)(?:\w+)\(((?:\w+ )*(?:\w+))?\) *(const)? * *{", 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[0].strip(" ").split(" "): + if prefix in ["const", "constexpr", "inline", "static"]: + prefixes.append(prefix) + else: + ret_type += prefix + " " + ret_type = ret_type.strip(" ") + if g[2]: args = g[2] + else: args = "" + if g[3]: const = True + else: const = False + + + ret_type = "" + return True, Member(g[1], template_str2list(template_str), prefixes=prefixes, ret_type=ret_type, args=args, defined=defined, const=const, 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 = "" + + 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 + + match, name = namespace_open(line) + if match: + scopes.append(("namespace", name, indent, i)) + namespace += "::" + name + namespace = namespace.strip("::") + + 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)) + match, member = function_declaration(line, template_str, namespace) + if match: + # print("function in", i, "-", member.get_source_name([], "")) + if in_class: + declarations[-1].member_functions.append(member) + else: + declarations.append(member) + template_str = "" + match, member = variable_declaration(line, template_str, namespace) + if match: + # print("variable in", i, "-", member.get_source_name([], "")) + if in_class: + declarations[-1].member_variables.append(member) + else: + declarations.append(member) + template_str = "" + + 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_name: str): + member = None + for dec in declarations: + if type(dec) == Class: + for mem in dec.member_functions: + if mem.name in target_name: + return mem.source(dec.c_temp, dec.name, len(mem.namespace.split("::"))) + else: + if dec.name in target_name: + return dec.source([], "", len(dec.namespace.split("::"))) + + return "" + + +def print_help(): + print(""" +Synposis: gen_definitions.py ... --file --name +-h --help help +--getter +--setter +--getter-and-setter +--def +--name +--file +--no-docs turn off docstring generation +""") + +# def missing_arg(arg): +# print("Missing argument for", arg) +# exit(1) + +if __name__ == "__main__": + docs = True + getter = False + setter = False + definition = False + file = "" + target_name = "" + i = 1 + while i in range(1, len(argv)): + if argv[i] == "--help" or argv[i] == "-h": + print_help() + exit(0) + elif argv[i] == "--getter-and-setter": + getter = True + setter = True + elif argv[i] == "--getter": + getter = True + elif argv[i] == "--setter": + setter = True + elif argv[i] == "--def": + definition = True + interactive = True + elif argv[i] == "--no-docs": + docs = False + elif argv[i-1] == "--file": + file = argv[i] + elif argv[i-1] == "--name": + target_name = argv[i] + i += 1 + + ret = "" + declarations = [] + if file: + if path.isfile(file): + declarations = parse_header(file) + else: + # print("Not a file:", file) + exit(1) + if getter: + pass + if setter: + pass + if definition: + print(get_definition(declarations, target_name)) +