gz-cpp-util/gen_definitions.py

443 lines
14 KiB
Python
Raw Permalink Normal View History

#!/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<c_temp>
class class {
comment
template<m_temp>
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 <Options>... --file <file> --name <name>
-h --help help
--getter
--setter
--getter-and-setter
--def
--name <name>
--file <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))