From 0881438c0f0ae5db4d352c4eeef4bfe038795fdf Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 19 Nov 2019 00:32:21 -0500 Subject: [PATCH] Rewrite class declaration/base parser - Fixes #27 --- CppHeaderParser/CppHeaderParser.py | 420 ++++++++++++------- CppHeaderParser/test/test_CppHeaderParser.py | 212 +++++++++- docs/api.rst | 4 +- 3 files changed, 485 insertions(+), 151 deletions(-) diff --git a/CppHeaderParser/CppHeaderParser.py b/CppHeaderParser/CppHeaderParser.py index ee2d40a..a3fd087 100644 --- a/CppHeaderParser/CppHeaderParser.py +++ b/CppHeaderParser/CppHeaderParser.py @@ -334,6 +334,262 @@ class CppParseError(Exception): pass +class CppTemplateParam(dict): + """ + Dictionary that contains the following: + + - ``decltype`` - If True, this is a decltype + - ``param`` - Parameter value or typename + - ``params`` - Only present if a template specialization. Is a list of + :class:`.CppTemplateParam` + - ``...`` - If True, indicates a parameter pack + """ + + def __init__(self): + self["param"] = "" + self["decltype"] = False + self["..."] = False + + def __str__(self): + s = [] + if self["decltype"]: + s.append("decltype") + + s.append(self["param"]) + + params = self.get("params") + if params is not None: + s.append("<%s>" % ",".join(str(p) for p in params)) + + if self["..."]: + if s: + s[-1] = s[-1] + "..." + else: + s.append("...") + + return "".join(s) + + +class CppBaseDecl(dict): + """ + Dictionary that contains the following + + - ``access`` - Anything in supportedAccessSpecifier + - ``class`` - Name of the type, along with template specializations + - ``decl_name`` - Name of the type only + - ``decl_params`` - Only present if a template specialization (Foo). + Is a list of :class:`.CppTemplateParam`. + - ``decltype`` - True/False indicates a decltype, the contents are in + ``decl_name`` + - ``virtual`` - True/False indicates virtual inheritance + - ``...`` - True/False indicates a parameter pack + + """ + + def __init__(self): + self["access"] = "private" + self["class"] = "" + self["decl_name"] = "" + self["decltype"] = False + self["virtual"] = False + self["..."] = False + + def _fix_classname(self): + # set class to the full decl for legacy reasons + params = self.get("decl_params") + + if self["decltype"]: + s = "decltype" + else: + s = "" + + s += self["decl_name"] + if params: + s += "<%s>" % (",".join(str(p) for p in params)) + + if self["..."]: + s += "..." + + self["class"] = s + + +def _consume_parens(stack): + i = 0 + sl = len(stack) + nested = 1 + while i < sl: + t = stack[i] + i += 1 + if t == ")": + nested -= 1 + if nested == 0: + return i + elif t == "(": + nested += 1 + + raise CppParseError("Unmatched (") + + +def _parse_template_decl(stack): + debug_print("_parse_template_decl: %s" % stack) + params = [] + param = CppTemplateParam() + i = 0 + sl = len(stack) + init = True + require_ending = False + while i < sl: + t = stack[i] + i += 1 + if init: + init = False + if t == "decltype": + param["decltype"] = True + continue + + if t == ",": + params.append(param) + init = True + require_ending = False + param = CppTemplateParam() + + continue + elif t == ">": + params.append(param) + return params, i - 1 + + if require_ending: + raise CppParseError("expected comma, found %s" % t) + + if t == "...": + param["..."] = True + require_ending = True + elif t == "(": + s = stack[i:] + n = _consume_parens(s) + i += n + param["param"] = param["param"] + "".join(s[:n]) + else: + if t and t[0] == "<": + param["params"], n = _parse_template_decl([t[1:]] + stack[i:]) + i += n + else: + param["param"] = param["param"] + t + + raise CppParseError("Unmatched <") + + +def _parse_cppclass_name(c, stack): + # ignore first thing + i = 1 + sl = len(stack) + name = "" + require_ending = False + while i < sl: + t = stack[i] + i += 1 + + if t == ":": + if i >= sl: + raise CppParseError("class decl ended with ':'") + t = stack[i] + if t != ":": + # reached the base declaration + break + + i += 1 + name += "::" + continue + elif t == "final": + c["final"] = True + continue + + if require_ending: + raise CppParseError("Unexpected '%s' in class" % ("".join(stack[i - 1 :]))) + + if t and t[0] == "<": + c["class_params"], n = _parse_template_decl([t[1:]] + stack[i:]) + i += n + require_ending = True + else: + name += t + + c["namespace"] = "" + + # backwards compat + if name.startswith("anon-struct-"): + name = "<" + name + ">" + c["name"] = name + c["bare_name"] = name + + # backwards compat + classParams = c.get("class_params") + if classParams is not None: + c["name"] = c["name"] + "<%s>" % ",".join(str(p) for p in classParams) + + return i + + +def _parse_cpp_base(stack): + debug_print("Parsing base: %s" % stack) + inherits = [] + i = 0 + sl = len(stack) + init = True + base = CppBaseDecl() + require_ending = False + while i < sl: + t = stack[i] + i += 1 + + if init: + if t in supportedAccessSpecifier: + base["access"] = t + continue + elif t == "virtual": + base["virtual"] = True + continue + + init = False + + if t == "decltype": + base["decltype"] = True + continue + + if t == ",": + inherits.append(base) + base = CppBaseDecl() + init = True + require_ending = False + continue + + if require_ending: + raise CppParseError("expected comma, found '%s'" % t) + + if t == "(": + s = stack[i:] + n = _consume_parens(s) + i += n + base["decl_name"] = base["decl_name"] + "".join(s[:n]) + elif t == "...": + base["..."] = True + require_ending = True + else: + if t[0] == "<": + base["decl_params"], n = _parse_template_decl([t[1:]] + stack[i:]) + i += n + require_ending = True + else: + base["decl_name"] = base["decl_name"] + t + + # backwards compat + inherits.append(base) + for base in inherits: + base._fix_classname() + + return inherits + + class CppClass(dict): """ Dictionary that contains at least the following keys: @@ -341,11 +597,7 @@ class CppClass(dict): * ``name`` - Name of the class * ``doxygen`` - Doxygen comments associated with the class if they exist * ``inherits`` - List of Classes that this one inherits. Values are - dictionaries with the following key/values: - - - ``access`` - Anything in supportedAccessSpecifier - - ``class`` - Name of the class - + :class:`.CppBaseDecl` * ``methods`` - Dictionary where keys are from supportedAccessSpecifier and values are a lists of :class:`.CppMethod` * ``namespace`` - Namespace of class @@ -413,10 +665,10 @@ def get_pure_virtual_methods(self, type="public"): return r def __init__(self, nameStack, curTemplate, doxygen, location): - #: hm self["nested_classes"] = [] self["parent"] = None self["abstract"] = False + self["final"] = False self._public_enums = {} self._public_structs = {} self._public_typedefs = {} @@ -431,145 +683,18 @@ def __init__(self, nameStack, curTemplate, doxygen, location): if doxygen: self["doxygen"] = doxygen - if "::" in "".join(nameStack): - # Re-Join class paths (ex ['class', 'Bar', ':', ':', 'Foo'] -> ['class', 'Bar::Foo'] - try: - new_nameStack = [] - for name in nameStack: - if len(new_nameStack) == 0: - new_nameStack.append(name) - elif name == ":" and new_nameStack[-1].endswith(":"): - new_nameStack[-1] += name - elif new_nameStack[-1].endswith("::"): - new_nameStack[-2] += new_nameStack[-1] + name - del new_nameStack[-1] - else: - new_nameStack.append(name) - trace_print( - "Convert from namestack\n %s\nto\n%s" % (nameStack, new_nameStack) - ) - nameStack = new_nameStack - except: - pass + # consume name of class, with any namespaces or templates + n = _parse_cppclass_name(self, nameStack) - # Handle final specifier - self["final"] = False - try: - final_index = nameStack.index("final") - # Dont trip up the rest of the logic - del nameStack[final_index] - self["final"] = True - trace_print("final") - except: - pass + # consume bases + baseStack = nameStack[n:] + if baseStack: + self["inherits"] = _parse_cpp_base(baseStack) + else: + self["inherits"] = [] - self["name"] = nameStack[1] set_location_info(self, location) - # Handle template classes - if len(nameStack) > 3 and nameStack[2].startswith("<"): - open_template_count = 0 - param_separator = 0 - found_first = False - i = 0 - for elm in nameStack: - if "<" in elm: - open_template_count += 1 - found_first = True - elif ">" in elm: - open_template_count -= 1 - if found_first and open_template_count == 0: - self["name"] = "".join(nameStack[1 : i + 1]) - break - i += 1 - elif ":" in nameStack: - self["name"] = nameStack[nameStack.index(":") - 1] - - inheritList = [] - - if nameStack.count(":") == 1: - nameStack = nameStack[nameStack.index(":") + 1 :] - while len(nameStack): - tmpStack = [] - tmpInheritClass = {"access": "private", "virtual": False} - if "," in nameStack: - tmpStack = nameStack[: nameStack.index(",")] - nameStack = nameStack[nameStack.index(",") + 1 :] - else: - tmpStack = nameStack - nameStack = [] - - # Convert template classes to one name in the last index - for i in range(0, len(tmpStack)): - if "<" in tmpStack[i]: - tmpStack2 = tmpStack[: i - 1] - tmpStack2.append("".join(tmpStack[i - 1 :])) - tmpStack = tmpStack2 - break - if len(tmpStack) == 0: - break - elif len(tmpStack) == 1: - tmpInheritClass["class"] = tmpStack[0] - elif len(tmpStack) == 2: - tmpInheritClass["access"] = tmpStack[0] - tmpInheritClass["class"] = tmpStack[1] - elif len(tmpStack) == 3 and "virtual" in tmpStack: - tmpInheritClass["access"] = ( - tmpStack[1] if tmpStack[1] != "virtual" else tmpStack[0] - ) - tmpInheritClass["class"] = tmpStack[2] - tmpInheritClass["virtual"] = True - else: - warning_print( - "Warning: can not parse inheriting class %s" - % (" ".join(tmpStack)) - ) - if ">" in tmpStack: - pass # allow skip templates for now - else: - raise NotImplementedError - - if "class" in tmpInheritClass: - inheritList.append(tmpInheritClass) - - elif nameStack.count(":") == 2: - self["parent"] = self["name"] - self["name"] = nameStack[-1] - - elif nameStack.count(":") > 2 and nameStack[0] in ("class", "struct"): - tmpStack = nameStack[nameStack.index(":") + 1 :] - - superTmpStack = [[]] - for tok in tmpStack: - if tok == ",": - superTmpStack.append([]) - else: - superTmpStack[-1].append(tok) - - for tmpStack in superTmpStack: - tmpInheritClass = {"access": "private"} - - if len(tmpStack) and tmpStack[0] in supportedAccessSpecifier: - tmpInheritClass["access"] = tmpStack[0] - tmpStack = tmpStack[1:] - - inheritNSStack = [] - while len(tmpStack) > 3: - if tmpStack[0] == ":": - break - if tmpStack[1] != ":": - break - if tmpStack[2] != ":": - break - inheritNSStack.append(tmpStack[0]) - tmpStack = tmpStack[3:] - if len(tmpStack) == 1 and tmpStack[0] != ":": - inheritNSStack.append(tmpStack[0]) - tmpInheritClass["class"] = "::".join(inheritNSStack) - inheritList.append(tmpInheritClass) - - self["inherits"] = inheritList - if curTemplate: self["template"] = curTemplate trace_print("Setting template to '%s'" % self["template"]) @@ -2375,7 +2500,7 @@ def _evaluate_class_stack(self): if len(self.nameStack) == 1: self.anon_struct_counter += 1 # We cant handle more than 1 anonymous struct, so name them uniquely - self.nameStack.append("" % self.anon_struct_counter) + self.nameStack.append("anon-struct-%d" % self.anon_struct_counter) if self.nameStack[0] == "class": self.curAccessSpecifier = "private" @@ -2406,12 +2531,14 @@ def _evaluate_class_stack(self): newClass["declaration_method"] = self.nameStack[0] self.classes_order.append(newClass) # good idea to save ordering self.stack = [] # fixes if class declared with ';' in closing brace + classKey = newClass["name"] + if parent: newClass["namespace"] = self.classes[parent]["namespace"] + "::" + parent newClass["parent"] = parent self.classes[parent]["nested_classes"].append(newClass) ## supports nested classes with the same name ## - self.curClass = key = parent + "::" + newClass["name"] + self.curClass = key = parent + "::" + classKey self._classes_brace_level[key] = self.braceDepth elif newClass["parent"]: # nested class defined outside of parent. A::B {...} @@ -2419,14 +2546,13 @@ def _evaluate_class_stack(self): newClass["namespace"] = self.classes[parent]["namespace"] + "::" + parent self.classes[parent]["nested_classes"].append(newClass) ## supports nested classes with the same name ## - self.curClass = key = parent + "::" + newClass["name"] + self.curClass = key = parent + "::" + classKey self._classes_brace_level[key] = self.braceDepth else: newClass["namespace"] = self.cur_namespace() - key = newClass["name"] - self.curClass = newClass["name"] - self._classes_brace_level[newClass["name"]] = self.braceDepth + self.curClass = key = classKey + self._classes_brace_level[classKey] = self.braceDepth if not key.endswith("::") and not key.endswith(" ") and len(key) != 0: if key in self.classes: diff --git a/CppHeaderParser/test/test_CppHeaderParser.py b/CppHeaderParser/test/test_CppHeaderParser.py index 9ab0bbf..b316555 100644 --- a/CppHeaderParser/test/test_CppHeaderParser.py +++ b/CppHeaderParser/test/test_CppHeaderParser.py @@ -573,18 +573,37 @@ def test_BloodOrange_inherits(self): def test_Bananna_inherits(self): self.assertEqual( self.cppHeader.classes["Bananna"]["inherits"], - [{"access": "public", "class": "Citrus::BloodOrange", "virtual": False}], + [ + { + "access": "public", + "class": "Citrus::BloodOrange", + "decl_name": "Citrus::BloodOrange", + "decltype": False, + "virtual": False, + "...": False, + } + ], ) def test_ExcellentCake_inherits(self): self.assertEqual( self.cppHeader.classes["ExcellentCake"]["inherits"], [ - {"access": "private", "class": "Citrus::BloodOrange", "virtual": False}, + { + "access": "private", + "class": "Citrus::BloodOrange", + "decl_name": "Citrus::BloodOrange", + "decltype": False, + "virtual": False, + "...": False, + }, { "access": "private", "class": "Convoluted::Nested::Mixin", + "decl_name": "Convoluted::Nested::Mixin", + "decltype": False, "virtual": False, + "...": False, }, ], ) @@ -2955,5 +2974,194 @@ def test_fn(self): self.assertLocation(child, "child.h", 9) +class TemplateMadness_TestCase(unittest.TestCase): + def setUp(self): + self.cppHeader = CppHeaderParser.CppHeader( + """ +template +class XYZ : public MyBaseClass +{ + public: + XYZ(); +}; + +template +class concat_iterator + : public iterator_facade_base, + std::forward_iterator_tag, ValueT> { +}; + +template +struct build_index_impl : build_index_impl {}; + +template +struct build_index_impl<0, I...> : index_sequence {}; + +//template +//struct is_callable, +// void_t()).*std::declval())(std::declval()...))>> +// : std::true_type {}; + +template +struct S : public T... {}; + +""", + "string", + ) + + def testXYZ(self): + c = self.cppHeader.classes["XYZ"] + self.assertEqual("XYZ", c["name"]) + self.assertEqual( + [ + { + "access": "public", + "class": "MyBaseClass", + "decltype": False, + "decl_name": "MyBaseClass", + "decl_params": [ + {"param": "Type", "...": False, "decltype": False}, + {"param": "int", "...": False, "decltype": False}, + ], + "virtual": False, + "...": False, + } + ], + c["inherits"], + ) + + def testConcatIterator(self): + c = self.cppHeader.classes["concat_iterator"] + self.assertEqual("concat_iterator", c["name"]) + self.assertEqual( + [ + { + "access": "public", + "class": "iterator_facade_base,std::forward_iterator_tag,ValueT>", + "decltype": False, + "decl_name": "iterator_facade_base", + "decl_params": [ + { + "decltype": False, + "param": "concat_iterator", + "params": [ + {"param": "ValueT", "...": False, "decltype": False}, + {"param": "IterTs", "...": True, "decltype": False}, + ], + "...": False, + }, + { + "decltype": False, + "param": "std::forward_iterator_tag", + "...": False, + }, + {"decltype": False, "param": "ValueT", "...": False}, + ], + "virtual": False, + "...": False, + } + ], + c["inherits"], + ) + + def testBuildIndexImpl1(self): + c = self.cppHeader.classes["build_index_impl"] + self.assertEqual("build_index_impl", c["name"]) + self.assertEqual( + [ + { + "access": "private", + "class": "build_index_impl", + "decltype": False, + "decl_name": "build_index_impl", + "decl_params": [ + {"param": "N-1", "...": False, "decltype": False}, + {"param": "N-1", "...": False, "decltype": False}, + {"param": "I", "...": True, "decltype": False}, + ], + "virtual": False, + "...": False, + } + ], + c["inherits"], + ) + + def testBuildIndexImpl2(self): + c = self.cppHeader.classes["build_index_impl<0,I...>"] + self.assertEqual("build_index_impl", c["bare_name"]) + self.assertEqual("build_index_impl<0,I...>", c["name"]) + self.assertEqual( + [ + {"decltype": False, "param": "0", "...": False}, + {"decltype": False, "param": "I", "...": True}, + ], + c["class_params"], + ) + self.assertEqual( + [ + { + "access": "private", + "class": "index_sequence", + "decltype": False, + "decl_name": "index_sequence", + "decl_params": [{"decltype": False, "param": "I", "...": True}], + "virtual": False, + "...": False, + } + ], + c["inherits"], + ) + + # def testIsCallable(self): + # c = self.cppHeader.classes["is_callable"] + # self.assertEqual("is_callable", c["name"]) + # self.assertEqual( + # [ + # {"param": "F", "...": False, "decltype": False}, + # {"param": "P", "...": False, "decltype": False}, + # { + # "param": "typelist", + # "...": False, + # "decltype": False, + # "params": [{"param": "T", "...": True, "decltype": False},], + # }, + # { + # "param": "void_t", + # "...": False, + # "decltype": False, + # "params": [ + # { + # "param": "(((*std::declval

()).*std::declval())(std::declval()...))", + # "...": False, + # "decltype": True, + # }, + # ], + # }, + # ], + # c["class_params"], + # ) + # self.assertEqual( + # [{"access": "private", "class": "std::true_type", "virtual": False,}], + # c["inherits"], + # ) + + def testS(self): + c = self.cppHeader.classes["S"] + self.assertEqual("S", c["name"]) + self.assertEqual( + [ + { + "access": "public", + "class": "T...", + "decl_name": "T", + "virtual": False, + "...": True, + "decltype": False, + } + ], + c["inherits"], + ) + + if __name__ == "__main__": unittest.main() diff --git a/docs/api.rst b/docs/api.rst index c689857..50a10b8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -24,8 +24,8 @@ CppHeaderParser --------------- .. automodule:: CppHeaderParser.CppHeaderParser - :members: CppClass, CppEnum, CppHeader, CppMethod, CppParseError, - CppStruct, CppUnion, CppVariable, TagStr, + :members: CppBaseDecl, CppClass, CppEnum, CppHeader, CppMethod, CppParseError, + CppStruct, CppTemplateParam, CppUnion, CppVariable, TagStr, ignoreSymbols :undoc-members: :show-inheritance: