gz-cpp-parser/gen_definition.py

745 lines
26 KiB
Python
Raw Permalink Normal View History

2022-11-19 14:34:41 +01:00
#!/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> -> T[]
std::array<T, N> -> T[N]
std::map<K, T> -> { K : T }
std::reference_wrapper<T> -> 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<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.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<std::string, int> 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 <Options>... --file <file> --name <name>
General:
-h --help help
--no-docs turn off docstring generation
--file <file>
Target:
--name <name>
--line <number>
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')