diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..a69c2a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,21 @@ +name: Bug Report +description: We are no longer making fixes ourselves to robotpy-cppheaderparser, but we will accept pull requests with appropriately tested fixes. We recommend all users migrate to cxxheaderparser. +title: "[BUG]: " +body: + - type: textarea + id: description + attributes: + label: Problem description + placeholder: >- + Provide a short description, state the expected behavior and what + actually happens. + validations: + required: true + + - type: textarea + id: code + attributes: + label: Reproducible example code + placeholder: >- + Minimal code to reproduce this issue + render: text \ No newline at end of file diff --git a/.github/workflows/dist.yml b/.github/workflows/dist.yml index d6801dd..95df148 100644 --- a/.github/workflows/dist.yml +++ b/.github/workflows/dist.yml @@ -7,19 +7,19 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - uses: psf/black@stable check-doc: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: 3.8 - name: Sphinx @@ -32,22 +32,22 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest, macos-latest, ubuntu-18.04] - python_version: [2.7, 3.5, 3.6, 3.7, 3.8] - architecture: [x86, x64] + os: [windows-latest, macos-13, ubuntu-20.04] + python_version: [3.8] + architecture: [x64] exclude: - - os: macos-latest + - os: macos-13 architecture: x86 - - os: ubuntu-18.04 + - os: ubuntu-20.04 architecture: x86 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python_version }} architecture: ${{ matrix.architecture }} @@ -70,14 +70,16 @@ jobs: runs-on: ubuntu-latest needs: [check, check-doc, test] if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + permissions: + id-token: write steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: 3.8 - run: pip --disable-pip-version-check install wheel @@ -85,7 +87,4 @@ jobs: - name: Build packages run: python setup.py sdist bdist_wheel - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.PYPI_PASSWORD }} + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CppHeaderParser/CppHeaderParser.py b/CppHeaderParser/CppHeaderParser.py index 9796cea..c6061e6 100644 --- a/CppHeaderParser/CppHeaderParser.py +++ b/CppHeaderParser/CppHeaderParser.py @@ -111,9 +111,7 @@ def debug_print(fmt, *args): args = (inspect.currentframe().f_back.f_lineno,) + args print(fmt % args) - else: - debug_caller_lineno = None def debug_print(fmt, *args): @@ -128,7 +126,6 @@ def trace_print(*args): sys.stdout.write(" %s" % a) sys.stdout.write("\n") - else: def trace_print(*args): @@ -141,6 +138,10 @@ def trace_print(*args): #: Symbols to ignore, usually special macros ignoreSymbols = ["Q_OBJECT"] +_BRACE_REASON_OTHER = 0 +_BRACE_REASON_NS = 1 +_BRACE_REASON_EXTERN = 2 +_BRACE_REASON_VARIABLE = 3 # Track what was added in what order and at what depth parseHistory = [] @@ -239,10 +240,13 @@ def is_property_namestack(nameStack): r = False if "(" not in nameStack and ")" not in nameStack: r = True - elif ( - "(" in nameStack - and "=" in nameStack - and nameStack.index("=") < nameStack.index("(") + elif "(" in nameStack and ( + ( # = initialization + "=" in nameStack and nameStack.index("=") < nameStack.index("(") + ) + or ( # {} initialization + "{" in nameStack and nameStack.index("{") < nameStack.index("(") + ) ): r = True # See if we are a function pointer @@ -1204,11 +1208,12 @@ class CppVariable(_CppVariable): * ``extern`` - True if its an extern, False if not * ``parent`` - If not None, either the class this is a property of, or the method this variable is a parameter in + * ``access`` - Anything in supportedAccessSpecifier """ Vars = [] - def __init__(self, nameStack, doxygen, location, **kwargs): + def __init__(self, nameStack, doxygen, location, is_var=True, **kwargs): debug_print("var trace %s", nameStack) if len(nameStack) and nameStack[0] == "extern": self["extern"] = True @@ -1216,29 +1221,48 @@ def __init__(self, nameStack, doxygen, location, **kwargs): else: self["extern"] = False + if "=" in nameStack: + self["type"] = " ".join(nameStack[: nameStack.index("=") - 1]) + self["name"] = nameStack[nameStack.index("=") - 1] + default = " ".join(nameStack[nameStack.index("=") + 1 :]) + nameStack = nameStack[: nameStack.index("=")] + default = self._filter_name(default) + self["default"] = default + # backwards compat; deprecate camelCase in dicts + self["defaultValue"] = default + elif "{" in nameStack and "}" in nameStack: + posBracket = nameStack.index("{") + self["type"] = " ".join(nameStack[: posBracket - 1]) + self["name"] = nameStack[posBracket - 1] + default = " ".join(nameStack[posBracket + 1 : -1]) + nameStack = nameStack[:posBracket] + default = self._filter_name(default) + self["default"] = default + # backwards compat; deprecate camelCase in dicts + self["defaultValue"] = default + _stack_ = nameStack - if "[" in nameStack: # strip off array informatin - arrayStack = nameStack[nameStack.index("[") :] - if nameStack.count("[") > 1: + self["array"] = 0 + while "]" in nameStack[-1]: # strip off array information + arrayPos = len(nameStack) - 1 - nameStack[::-1].index("[") + arrayStack = nameStack[arrayPos:] + if self["array"] == 1: debug_print("Multi dimensional array") debug_print("arrayStack=%s", arrayStack) - nums = [x for x in arrayStack if x.isdigit()] - # Calculate size by multiplying all dimensions - p = 1 - for n in nums: - p *= int(n) + if len(arrayStack) == 3: + n = arrayStack[1] # Multi dimensional array - self["array_size"] = p + if not "multi_dimensional_array_size" in self: + self["multi_dimensional_array_size"] = self["array_size"] + self["multi_dimensional_array_size"] += "x" + n + self["array_size"] = str(int(self["array_size"]) * int(n)) self["multi_dimensional_array"] = 1 - self["multi_dimensional_array_size"] = "x".join(nums) else: debug_print("Array") if len(arrayStack) == 3: self["array_size"] = arrayStack[1] - nameStack = nameStack[: nameStack.index("[")] + nameStack = nameStack[:arrayPos] self["array"] = 1 - else: - self["array"] = 0 nameStack = self._name_stack_helper(nameStack) if doxygen: @@ -1267,15 +1291,6 @@ def __init__(self, nameStack, doxygen, location, **kwargs): ) self["function_pointer"] = 1 - elif "=" in nameStack: - self["type"] = " ".join(nameStack[: nameStack.index("=") - 1]) - self["name"] = nameStack[nameStack.index("=") - 1] - default = " ".join(nameStack[nameStack.index("=") + 1 :]) - default = self._filter_name(default) - self["default"] = default - # backwards compat; deprecate camelCase in dicts - self["defaultValue"] = default - elif ( is_fundamental(nameStack[-1]) or nameStack[-1] in [">", "<", ":", "."] @@ -1298,7 +1313,8 @@ def __init__(self, nameStack, doxygen, location, **kwargs): pass self.init() - CppVariable.Vars.append(self) # save and resolve later + if is_var: + CppVariable.Vars.append(self) # save and resolve later def _filter_name(self, name): name = name.replace(" :", ":").replace(": ", ":") @@ -1320,6 +1336,7 @@ def __str__(self): "desc", "line_number", "extern", + "access", ] cpy = dict((k, v) for (k, v) in list(self.items()) if k in keys_white_list) if "array_size" in self: @@ -1349,7 +1366,8 @@ def resolve_enum_values(self, values): # Remove single quotes from single quoted chars (unless part of some expression if len(a) == 3 and a[0] == "'" and a[2] == "'": a = v["value"] = a[1] - if a.lower().startswith("0x"): + a = i = ord(a) + elif a.lower().startswith("0x"): try: i = a = int(a, 16) except: @@ -1425,6 +1443,10 @@ def __str__(self): return self["value"] +class CppExternTemplate(dict): + pass + + # Implementation is shared between CppPragma, CppDefine, CppInclude but they are # distinct so that they may diverge if required without interface-breaking # changes @@ -1468,10 +1490,9 @@ class Resolver(object): C_MODIFIERS = "* & const constexpr static mutable".split() C_MODIFIERS = set(C_MODIFIERS) - C_KEYWORDS = "extern virtual static explicit inline friend".split() + C_KEYWORDS = "extern virtual static explicit inline friend constexpr".split() C_KEYWORDS = set(C_KEYWORDS) - SubTypedefs = {} # TODO deprecate? NAMESPACES = [] CLASSES = {} @@ -1500,6 +1521,11 @@ def cur_namespace(self, add_double_colon=False): i += 1 return rtn + def cur_linkage(self): + if len(self.linkage_stack): + return self.linkage_stack[-1] + return "" + def guess_ctypes_type(self, string): pointers = string.count("*") string = string.replace("*", "") @@ -1864,27 +1890,12 @@ def finalize_vars(self): var["ctypes_type"] = "ctypes.c_void_p" var["unresolved"] = True - elif tag in self.SubTypedefs: # TODO remove SubTypedefs - if ( - "property_of_class" in var - or "property_of_struct" in var - ): - trace_print( - "class:", self.SubTypedefs[tag], "tag:", tag - ) - var["typedef"] = self.SubTypedefs[tag] # class name - var["ctypes_type"] = "ctypes.c_void_p" - else: - trace_print("WARN-this should almost never happen!") - trace_print(var) - var["unresolved"] = True - elif tag in self._template_typenames: var["typename"] = tag var["ctypes_type"] = "ctypes.c_void_p" - var[ - "unresolved" - ] = True # TODO, how to deal with templates? + var["unresolved"] = ( + True # TODO, how to deal with templates? + ) elif tag.startswith( "_" @@ -2055,10 +2066,6 @@ def finalize(self): trace_print("meth returns class:", meth["returns"]) meth["returns_class"] = True - elif meth["returns"] in self.SubTypedefs: - meth["returns_class"] = True - meth["returns_nested"] = self.SubTypedefs[meth["returns"]] - elif meth["returns"] in cls._public_enums: enum = cls._public_enums[meth["returns"]] meth["returns_enum"] = enum.get("type") @@ -2202,11 +2209,21 @@ def parse_method_type(self, stack): else: assert 0 - if len(stack) > 3 and stack[-1] == ";" and stack[-3] == "=": - if stack[-2] == "0": - info["pure_virtual"] = True - elif stack[-2] == "delete": - info["deleted"] = True + refqual = "" + if len(stack) > 3: + if stack[-1] == ";" and stack[-3] == "=": + if stack[-2] == "0": + info["pure_virtual"] = True + elif stack[-2] == "delete": + info["deleted"] = True + + for e in reversed(stack): + if e == ")": + break + elif e == "&": + refqual += "&" + + info["ref_qualifiers"] = refqual r = header.split() name = None @@ -2356,6 +2373,7 @@ def _evaluate_method_stack(self): self._get_location(self.nameStack), ) newMethod["parent"] = None + newMethod["linkage"] = self.cur_linkage() self.functions.append(newMethod) # Reset template once it has been used @@ -2456,7 +2474,6 @@ def _evaluate_property_stack(self, clearStack=True, addToVar=None): klass["typedefs"][self.curAccessSpecifier].append(name) if self.curAccessSpecifier == "public": klass._public_typedefs[name] = typedef["type"] - Resolver.SubTypedefs[name] = self.curClass else: assert 0 elif self.curClass: @@ -2508,6 +2525,7 @@ def _evaluate_property_stack(self, clearStack=True, addToVar=None): self._get_location(self.nameStack), ) newVar["namespace"] = self.current_namespace() + newVar["linkage"] = self.cur_linkage() if self.curClass: klass = self.classes[self.curClass] klass["properties"][self.curAccessSpecifier].append(newVar) @@ -2526,6 +2544,7 @@ def _evaluate_property_stack(self, clearStack=True, addToVar=None): self._get_location(self.nameStack), ) newVar["namespace"] = self.cur_namespace(False) + newVar["linkage"] = self.cur_linkage() if addToVar: newVar.update(addToVar) self.variables.append(newVar) @@ -2584,6 +2603,7 @@ def _evaluate_class_stack(self): ) self.curTemplate = None newClass["declaration_method"] = self.nameStack[0] + newClass["linkage"] = self.cur_linkage() self.classes_order.append(newClass) # good idea to save ordering self.stack = [] # fixes if class declared with ';' in closing brace self.stmtTokens = [] @@ -2686,7 +2706,14 @@ def show(self): for className in list(self.classes.keys()): self.classes[className].show() - def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): + def __init__( + self, + headerFileName, + argType="file", + encoding=None, + preprocessed=False, + **kwargs + ): """Create the parsed C++ header file parse tree headerFileName - Name of the file to parse OR actual file contents (depends on argType) @@ -2747,6 +2774,8 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): #: List of enums in this header as :class:`.CppEnum` self.enums = [] + self.extern_templates = [] + #: List of variables in this header as :class:`.CppVariable` self.variables = [] self.global_enums = {} @@ -2757,6 +2786,7 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): self.curAccessSpecifier = "private" # private is default self.curTemplate = None self.accessSpecifierStack = [] + self.linkage_stack = [] debug_print( "curAccessSpecifier changed/defaulted to %s", self.curAccessSpecifier ) @@ -2784,31 +2814,24 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): "[ ]+", " ", supportedAccessSpecifier[i] ).strip() - # Change multi line #defines and expressions to single lines maintaining line nubmers - # Based from http://stackoverflow.com/questions/2424458/regular-expression-to-match-cs-multiline-preprocessor-statements - matches = re.findall(r"(?m)^(?:.*\\\r?\n)+.*$", headerFileStr) - is_define = re.compile(r"[ \t\v]*#[Dd][Ee][Ff][Ii][Nn][Ee]") - for m in matches: - # Keep the newlines so that linecount doesnt break - num_newlines = len([a for a in m if a == "\n"]) - if is_define.match(m): - new_m = m.replace("\n", "\\n") - else: - # Just expression taking up multiple lines, make it take 1 line for easier parsing - new_m = m.replace("\\\n", " ") - if num_newlines > 0: - new_m += "\n" * (num_newlines) - headerFileStr = headerFileStr.replace(m, new_m) - - # Filter out Extern "C" statements. These are order dependent - matches = re.findall( - re.compile(r'extern[\t ]+"[Cc]"[\t \n\r]*{', re.DOTALL), headerFileStr - ) - for m in matches: - # Keep the newlines so that linecount doesnt break - num_newlines = len([a for a in m if a == "\n"]) - headerFileStr = headerFileStr.replace(m, "\n" * num_newlines) - headerFileStr = re.sub(r'extern[ ]+"[Cc]"[ ]*', "", headerFileStr) + if not preprocessed: + # Change multi line #defines and expressions to single lines maintaining line nubmers + # Based from http://stackoverflow.com/questions/2424458/regular-expression-to-match-cs-multiline-preprocessor-statements + matches = re.findall(r"(?m)^(?:.*\\\r?\n)+.*$", headerFileStr) + is_define = re.compile(r"[ \t\v]*#[Dd][Ee][Ff][Ii][Nn][Ee]") + for m in matches: + # Keep the newlines so that linecount doesnt break + num_newlines = len([a for a in m if a == "\n"]) + if is_define.match(m): + new_m = m.replace( + "\n", "\\n" + ) + else: + # Just expression taking up multiple lines, make it take 1 line for easier parsing + new_m = m.replace("\\\n", " ") + if num_newlines > 0: + new_m += "\n" * (num_newlines) + headerFileStr = headerFileStr.replace(m, new_m) # Filter out any ignore symbols that end with "()" to account for #define magic functions for ignore in ignoreSymbols: @@ -2848,6 +2871,8 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): ) self.braceDepth = 0 + self.braceReason = [] + self.lastBraceReason = _BRACE_REASON_OTHER lex = Lexer(self.headerFileName) lex.input(headerFileStr) @@ -2879,7 +2904,7 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): break tok.value = TagStr(tok.value, location=tok.location) - # debug_print("TOK: %s", tok) + debug_print("TOK: %s", tok) if tok.type == "NAME": if tok.value in self.IGNORE_NAMES: continue @@ -2888,9 +2913,11 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): self._parse_template() continue elif tok.value == "alignas": + self._doxygen_cache = self.lex.get_doxygen() self._parse_attribute_specifier_seq(tok) continue elif tok.value == "__attribute__": + self._doxygen_cache = self.lex.get_doxygen() self._parse_gcc_attribute() continue elif not self.stack and tok.value == "static_assert": @@ -2899,6 +2926,7 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): continue elif tok.type == "DBL_LBRACKET": + self._doxygen_cache = self.lex.get_doxygen() self._parse_attribute_specifier_seq(tok) continue @@ -2917,23 +2945,36 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): continue if parenDepth == 0 and tok.type == "{": + if self.nameStack[0] in ( + "class", + "struct", + "union", + "namespace", + "enum", + "extern", + "typedef", + ) or (is_method_namestack(self.stack) or (not self.curClass)): + self.lastBraceReason = _BRACE_REASON_OTHER + else: + # Case : type variable {init}; + self.lastBraceReason = _BRACE_REASON_VARIABLE + self.braceDepth += 1 + self.braceReason.append(self.lastBraceReason) + self.nameStack.append(tok.value) + continue if len(self.nameStack) >= 2 and is_namespace( self.nameStack ): # namespace {} with no name used in boost, this sets default? - if ( - self.nameStack[1] - == "__IGNORED_NAMESPACE__CppHeaderParser__" - ): # Used in filtering extern "C" - self.nameStack[1] = "" self.nameSpaces.append("".join(self.nameStack[1:])) ns = self.cur_namespace() self.stack = [] self.stmtTokens = [] if ns not in self.namespaces: self.namespaces.append(ns) + self.lastBraceReason = _BRACE_REASON_NS # Detect special condition of macro magic before class declaration so we # can filter it out - if "class" in self.nameStack and self.nameStack[0] != "class": + elif "class" in self.nameStack and self.nameStack[0] != "class": classLocationNS = self.nameStack.index("class") classLocationS = self.stack.index("class") if ( @@ -2966,14 +3007,24 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): self.stmtTokens = [] if not self.braceHandled: self.braceDepth += 1 + self.braceReason.append(self.lastBraceReason) elif parenDepth == 0 and tok.type == "}": if self.braceDepth == 0: continue - if self.braceDepth == len(self.nameSpaces): - tmp = self.nameSpaces.pop() + reason = self.braceReason.pop() + if reason == _BRACE_REASON_NS: + self.nameSpaces.pop() self.stack = [] # clear stack when namespace ends? self.stmtTokens = [] + elif reason == _BRACE_REASON_EXTERN: + self.linkage_stack.pop() + self.stack = [] # clear stack when linkage ends? + self.stmtTokens = [] + # Case : type variable {init}; + elif reason == _BRACE_REASON_VARIABLE: + self.nameStack.append(tok.value) + continue else: self._evaluate_stack() self.braceDepth -= 1 @@ -3069,7 +3120,7 @@ def __init__(self, headerFileName, argType="file", encoding=None, **kwargs): newNsLen = len(self.nameStack) if nslen != newNsLen and newNsLen == 1: - if not self.curTemplate: + if not self._doxygen_cache: self._doxygen_cache = self.lex.get_doxygen() except Exception as e: @@ -3174,7 +3225,6 @@ def _next_token_must_be(self, *tokenTypes): } def _consume_balanced_tokens(self, *init_tokens): - _balanced_token_map = self._balanced_token_map consumed = list(init_tokens) @@ -3337,8 +3387,10 @@ def _evaluate_stack(self, token=None): pass elif len(self.nameStack) == 2 and self.nameStack[0] == "extern": debug_print("trace extern") + self.linkage_stack.append(self.nameStack[1].strip('"')) self.stack = [] self.stmtTokens = [] + self.lastBraceReason = _BRACE_REASON_EXTERN elif ( len(self.nameStack) == 2 and self.nameStack[0] == "friend" ): # friend class declaration @@ -3353,7 +3405,10 @@ def _evaluate_stack(self, token=None): alias = self.nameStack[1] ns, stack = _split_namespace(self.nameStack[3:]) atype = CppVariable( - stack, self._get_stmt_doxygen(), self._get_location(stack) + stack, + self._get_stmt_doxygen(), + self._get_location(stack), + is_var=False, ) # namespace refers to the embedded type @@ -3368,7 +3423,10 @@ def _evaluate_stack(self, token=None): # from a base class ns, stack = _split_namespace(self.nameStack[1:]) atype = CppVariable( - stack, self._get_stmt_doxygen(), self._get_location(stack) + stack, + self._get_stmt_doxygen(), + self._get_location(stack), + is_var=False, ) alias = atype["type"] atype["using_type"] = "declaration" @@ -3377,11 +3435,15 @@ def _evaluate_stack(self, token=None): else: atype["namespace"] = ns + atype["template"] = self.curTemplate + self.curTemplate = None + if atype["type"].startswith("typename "): atype["raw_type"] = "typename " + ns + atype["type"][9:] else: atype["raw_type"] = ns + atype["type"] + atype["access"] = self.curAccessSpecifier if self.curClass: klass = self.classes[self.curClass] klass["using"][alias] = atype @@ -3454,10 +3516,25 @@ def _evaluate_stack(self, token=None): # its a little confusing to have some if/else above return and others not, and then clearning the nameStack down here self.nameStack = [] - self.lex.doxygenCommentCache = "" + self._doxygen_cache = None self.curTemplate = None def _parse_template(self): + # check for 'extern template' + extern_template = False + if len(self.stmtTokens) == 1 and self.stmtTokens[0].value == "extern": + extern_template = True + tok = self._next_token_must_be("NAME") + if not tok.value == "class": + raise self._parse_error((tok,), "class") + + tok = self._next_token_must_be("NAME") + if tok.value == "__attribute__": + self._parse_gcc_attribute() + tok = self._next_token_must_be("NAME") + + extern_template_name = tok.value + tok = self._next_token_must_be("<") consumed = self._consume_balanced_tokens(tok) tmpl = " ".join(tok.value for tok in consumed) @@ -3470,7 +3547,20 @@ def _parse_template(self): .replace(" , ", ", ") .replace(" = ", "=") ) - self.curTemplate = "template" + tmpl + + if extern_template: + self.extern_templates.append( + CppExternTemplate( + name=extern_template_name, + params=tmpl, + namespace=self.cur_namespace(), + ) + ) + self.stack = [] + self.nameStack = [] + self.stmtTokens = [] + else: + self.curTemplate = "template" + tmpl def _parse_gcc_attribute(self): tok1 = self._next_token_must_be("(") @@ -3608,12 +3698,14 @@ def _parse_enum(self): def _install_enum(self, newEnum, instancesData): if len(self.curClass): newEnum["namespace"] = self.cur_namespace(False) + newEnum["linkage"] = self.cur_linkage() klass = self.classes[self.curClass] klass["enums"][self.curAccessSpecifier].append(newEnum) if self.curAccessSpecifier == "public" and "name" in newEnum: klass._public_enums[newEnum["name"]] = newEnum else: newEnum["namespace"] = self.cur_namespace(True) + newEnum["linkage"] = self.cur_linkage() self.enums.append(newEnum) if "name" in newEnum and newEnum["name"]: self.global_enums[newEnum["name"]] = newEnum diff --git a/CppHeaderParser/lexer.py b/CppHeaderParser/lexer.py index fa62a1a..b63501f 100644 --- a/CppHeaderParser/lexer.py +++ b/CppHeaderParser/lexer.py @@ -6,7 +6,6 @@ class Lexer(object): - tokens = [ "NUMBER", "FLOAT_NUMBER", diff --git a/README.rst b/README.rst index 77e2eb6..627f849 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,17 @@ robotpy-cppheaderparser ======================= -|Build Status| +**robotpy-cppheaderparser is DEPRECATED, and we are no longer updating it**. +We will accept pull requests that have fixes and appropriate tests, but we +will no longer be making any fixes ourselves. + +We highly recommend all current and future users to migrate to cxxheaderparser, +which supports all the syntax that CppHeaderParser does and much much more. The +parser output is very different, but it is strictly typed and hopefully easier +to work with. You can find it at https://github.com/robotpy/cxxheaderparser, +or try the live interactive demo at https://robotpy.github.io/cxxheaderparser/ + +--------- CppHeaderParser is a pure python C++ header parser that parses C++ headers and creates a data structure that you can use to do many types @@ -62,6 +72,3 @@ Past contributors include: .. _CppHeaderParser: https://bitbucket.org/senex/cppheaderparser .. _pcpp: https://github.com/ned14/pcpp - -.. |Build Status| image:: https://travis-ci.org/robotpy/robotpy-cppheaderparser.svg?branch=master - :target: https://travis-ci.org/robotpy/robotpy-cppheaderparser \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 696192f..cc167b2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,8 +31,8 @@ master_doc = "index" # General information about the project. -project = u"robotpy-cppheaderparser" -copyright = u"2019 RobotPy Development Team" +project = "robotpy-cppheaderparser" +copyright = "2019 RobotPy Development Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -200,8 +200,8 @@ ( "index", "sphinx.tex", - u"robotpy-cppheaderparser Documentation", - u"Author", + "robotpy-cppheaderparser Documentation", + "Author", "manual", ) ] @@ -232,7 +232,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ("index", "sphinx", u"robotpy-cppheaderparser Documentation", [u"Author"], 1) + ("index", "sphinx", "robotpy-cppheaderparser Documentation", ["Author"], 1) ] # If true, show URL addresses after external links. @@ -248,8 +248,8 @@ ( "index", "sphinx", - u"robotpy-cppheaderparser Documentation", - u"Author", + "robotpy-cppheaderparser Documentation", + "Author", "sphinx", "One line description of project.", "Miscellaneous", @@ -272,10 +272,10 @@ # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. -epub_title = u"robotpy-cppheaderparser" -epub_author = u"RobotPy Development Team" -epub_publisher = u"RobotPy Development Team" -epub_copyright = u"2019 RobotPy Development Team" +epub_title = "robotpy-cppheaderparser" +epub_author = "RobotPy Development Team" +epub_publisher = "RobotPy Development Team" +epub_copyright = "2019 RobotPy Development Team" # The basename for the epub file. It defaults to the project name. # epub_basename = u'..' diff --git a/test/TestSampleClass.h b/test/TestSampleClass.h index 439635c..45efef7 100644 --- a/test/TestSampleClass.h +++ b/test/TestSampleClass.h @@ -73,6 +73,8 @@ namespace Alpha Z_B = 0x2B, Z_C = 'j', Z_D, + Z_E = '9', + Z_F = 9, } Zebra; }; diff --git a/test/test_CppHeaderParser.py b/test/test_CppHeaderParser.py index 514cc48..4f949f5 100644 --- a/test/test_CppHeaderParser.py +++ b/test/test_CppHeaderParser.py @@ -418,6 +418,8 @@ def test_values(self): {"name": "Z_B", "raw_value": "0x2B", "value": 43}, {"name": "Z_C", "raw_value": "j", "value": 106}, {"name": "Z_D", "value": 107}, + {"name": "Z_E", "raw_value": "9", "value": 57}, + {"name": "Z_F", "value": 9}, ], ) @@ -1160,14 +1162,9 @@ def test_method_params(self): [], ) - def test_class_template(self): - self.assertEqual( - self.cppHeader.classes["CarrotClass"]["template"], "template" - ) - # Bug 3517289 -class CarrotClass_TestCase(unittest.TestCase): +class ExternClass_TestCase(unittest.TestCase): def setUp(self): self.cppHeader = CppHeaderParser.CppHeader("TestSampleClass.h") @@ -2362,7 +2359,7 @@ def setUp(self): def test_array_size(self): self.assertEqual( self.cppHeader.classes["Picture"]["properties"]["public"][1]["array_size"], - 16384, + "16384", ) def test_multi_dimensional_array_size(self): @@ -2999,6 +2996,21 @@ def test_fn(self): self.assertEqual(p["defaultValue"], "0.02") +class ConstExprFn_TestCase(unittest.TestCase): + def setUp(self): + self.cppHeader = CppHeaderParser.CppHeader( + """ +constexpr int overloaded_constexpr(int a, int b, int c) { return a + b + c; } +""", + "string", + ) + + def test_fn(self): + m = self.cppHeader.functions[0] + self.assertEqual(m["constexpr"], True) + self.assertEqual(m["rtnType"], "int") + + class DefaultEnum_TestCase(unittest.TestCase): def setUp(self): self.cppHeader = CppHeaderParser.CppHeader( @@ -3313,7 +3325,6 @@ def setUp(self): ) def test_existance(self): - self.assertIn("S", self.cppHeader.classes) self.assertIn("PS", self.cppHeader.typedefs) self.assertEqual("x", self.cppHeader.variables[0]["name"]) @@ -3399,6 +3410,10 @@ def setUp(self): class C2 {}; +/// hasattr comment +[[nodiscard]] +int hasattr(); + """, "string", ) @@ -3428,6 +3443,11 @@ def test_cls2(self): self.assertEqual("C2", c["name"]) self.assertEqual("/// template comment", c["doxygen"]) + def test_hasattr(self): + fn = self.cppHeader.functions[1] + self.assertEqual("hasattr", fn["name"]) + self.assertEqual("/// hasattr comment", fn["doxygen"]) + class EnumParameter_TestCase(unittest.TestCase): def setUp(self): @@ -4015,6 +4035,7 @@ def setUp(self): self.cppHeader = CppHeaderParser.CppHeader( """ template class P { +using A = typename f::TP::A; public: using State = typename f::TP::S; P(State st); @@ -4026,9 +4047,15 @@ def setUp(self): def test_fn(self): c = self.cppHeader.classes["P"] self.assertEqual("P", c["name"]) + self.assertEqual(len(c["using"]), 2) state = c["using"]["State"] self.assertEqual(state["raw_type"], "typename f::TP::S") self.assertEqual(state["type"], "typename TP::S") + self.assertEqual(state["access"], "public") + private = c["using"]["A"] + self.assertEqual(private["raw_type"], "typename f::TP::A") + self.assertEqual(private["type"], "typename TP::A") + self.assertEqual(private["access"], "private") m = c["methods"]["public"][0] self.assertEqual(m["name"], "P") @@ -4056,5 +4083,227 @@ def test_fn(self): self.assertEqual(c.typedefs["mmmmp"], "typedef int ( * ) ( int , int )") +class ExternTemplateTest(unittest.TestCase): + def setUp(self): + self.cppHeader = CppHeaderParser.CppHeader( + """ +extern template class MyClass<1,2>; +extern template class __attribute__(("something")) MyClass<3,4>; + +namespace foo { +extern template class MyClass<5,6>; +}; + +""", + "string", + ) + + def test_fn(self): + c = self.cppHeader + et0, et1, et2 = c.extern_templates + + self.assertEqual(et0["name"], "MyClass") + self.assertEqual(et0["namespace"], "") + self.assertEqual(et0["params"], "<1, 2>") + + self.assertEqual(et1["name"], "MyClass") + self.assertEqual(et1["namespace"], "") + self.assertEqual(et1["params"], "<3, 4>") + + self.assertEqual(et2["name"], "MyClass") + self.assertEqual(et2["namespace"], "foo") + self.assertEqual(et2["params"], "<5, 6>") + + +class UsingTemplateTest(unittest.TestCase): + def setUp(self): + self.cppHeader = CppHeaderParser.CppHeader( + """ +class X { + template + using TT = U; +}; + +""", + "string", + ) + + def test_fn(self): + u = self.cppHeader.classes["X"]["using"] + self.assertEqual(len(u), 1) + tt = u["TT"] + + self.assertEqual(tt["access"], "private") + self.assertEqual(tt["raw_type"], "U") + self.assertEqual(tt["type"], "U") + self.assertEqual(tt["typealias"], "TT") + self.assertEqual(tt["template"], "template") + + +class RefQualifierTest(unittest.TestCase): + def setUp(self): + self.cppHeader = CppHeaderParser.CppHeader( + """ +struct X { + void fn0(); + void fn1() &; + void fn2() &&; + void fn3() && = 0; +}; + +""", + "string", + ) + + def test_fn0(self): + fn = self.cppHeader.classes["X"]["methods"]["public"][0] + self.assertEqual(fn["name"], "fn0") + self.assertEqual(fn["ref_qualifiers"], "") + + def test_fn1(self): + fn = self.cppHeader.classes["X"]["methods"]["public"][1] + self.assertEqual(fn["name"], "fn1") + self.assertEqual(fn["ref_qualifiers"], "&") + + def test_fn1(self): + fn = self.cppHeader.classes["X"]["methods"]["public"][2] + self.assertEqual(fn["name"], "fn2") + self.assertEqual(fn["ref_qualifiers"], "&&") + + def test_fn3(self): + fn = self.cppHeader.classes["X"]["methods"]["public"][3] + self.assertEqual(fn["name"], "fn3") + self.assertEqual(fn["ref_qualifiers"], "&&") + self.assertEqual(fn["pure_virtual"], True) + + +class ExternCQuirk(unittest.TestCase): + # bug where extern "C" reset the namespace + def setUp(self): + self.cppHeader = CppHeaderParser.CppHeader( + """ +namespace cs { +extern "C" { + struct InCSAndExtern {}; + void FnInCSAndExtern(InCSAndExtern *n); +} + +class InCS {}; + +} + +void FnNotInCSOrExtern(); + +""", + "string", + ) + + def test_fn(self): + # NotCS should be in namespace cs, extern C + c = self.cppHeader.classes["InCSAndExtern"] + self.assertEqual(c["namespace"], "cs") + self.assertEqual(c["linkage"], "C") + + # FnNotCS should be in namespace cs, extern C + fn = self.cppHeader.functions[0] + self.assertEqual(fn["name"], "FnInCSAndExtern") + self.assertEqual(fn["namespace"], "cs::") + self.assertEqual(fn["linkage"], "C") + + # InCS should be in namespace cs + c = self.cppHeader.classes["InCS"] + self.assertEqual(c["namespace"], "cs") + self.assertEqual(c["linkage"], "") + + # FnNotCS should not in namespace cs nor extern C + fn = self.cppHeader.functions[1] + self.assertEqual(fn["name"], "FnNotInCSOrExtern") + self.assertEqual(fn["namespace"], "") + self.assertEqual(fn["linkage"], "") + + +# Github PR 85 +class ContainerOfArray_TestCase(unittest.TestCase): + def setUp(self): + self.cppHeader = CppHeaderParser.CppHeader( + """ +class ContainerOfArray { +public: + std::unique_ptr variable; + std::unique_ptr function(std::unique_ptr param1); +}; +""", + "string", + ) + + def test_rtntype(self): + self.assertEqual( + self.cppHeader.classes["ContainerOfArray"]["methods"]["public"][0][ + "rtnType" + ], + "std::unique_ptr", + ) + + def test_parameters(self): + self.assertEqual( + filter_pameters( + self.cppHeader.classes["ContainerOfArray"]["methods"]["public"][0][ + "parameters" + ] + ), + [{"name": "param1", "desc": None, "type": "std::unique_ptr"}], + ) + + def test_member(self): + self.assertEqual( + self.cppHeader.classes["ContainerOfArray"]["properties"]["public"][0][ + "name" + ], + "variable", + ) + self.assertEqual( + self.cppHeader.classes["ContainerOfArray"]["properties"]["public"][0][ + "type" + ], + "std::unique_ptr", + ) + + +class InitBracket_TestCase(unittest.TestCase): + def setUp(self): + self.cppHeader = CppHeaderParser.CppHeader( + """ +class InitBracket { +public: + int variable{10}; + std::shared_ptr variable2 {std::make_shared(150)}; +}; +""", + "string", + ) + + def test_member(self): + self.assertEqual( + self.cppHeader.classes["InitBracket"]["properties"]["public"][0]["name"], + "variable", + ) + self.assertEqual( + self.cppHeader.classes["InitBracket"]["properties"]["public"][0][ + "defaultValue" + ], + "10", + ) + self.assertEqual( + self.cppHeader.classes["InitBracket"]["properties"]["public"][1]["name"], + "variable2", + ) + self.assertEqual( + self.cppHeader.classes["InitBracket"]["properties"]["public"][1][ + "defaultValue" + ], + "std::make_shared ( 150 )", + ) + + if __name__ == "__main__": unittest.main()