diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 497eaee..f5a90fb 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -596,6 +596,9 @@ class CxxParser: with self.lex.set_group_of_tokens(raw_toks) as remainder: try: parsed_type, mods = self._parse_type(None) + if parsed_type is None: + raise self._parse_error(None) + mods.validate(var_ok=False, meth_ok=False, msg="") dtype = self._parse_cv_ptr(parsed_type, nonptr_fn=True) self._next_token_must_be(PhonyEnding.type) @@ -762,6 +765,9 @@ class CxxParser: """ parsed_type, mods = self._parse_type(None) + if parsed_type is None: + raise self._parse_error(None) + mods.validate(var_ok=False, meth_ok=False, msg="parsing typealias") dtype = self._parse_cv_ptr(parsed_type) @@ -1475,6 +1481,9 @@ class CxxParser: # required typename + decorators parsed_type, mods = self._parse_type(tok) + if parsed_type is None: + raise self._parse_error(None) + mods.validate(var_ok=False, meth_ok=False, msg="parsing parameter") dtype = self._parse_cv_ptr(parsed_type) @@ -1564,6 +1573,9 @@ class CxxParser: ) parsed_type, mods = self._parse_type(None) + if parsed_type is None: + raise self._parse_error(None) + mods.validate(var_ok=False, meth_ok=False, msg="parsing trailing return type") dtype = self._parse_cv_ptr(parsed_type) @@ -1726,9 +1738,10 @@ class CxxParser: friend = FriendDecl(fn=method) self.visitor.on_class_friend(state, friend) else: - # method must not have multiple segments - if len(pqname.segments) != 1: - raise self._parse_error(None) + # method must not have multiple segments except for operator + if len(pqname.segments) > 1: + if not pqname.segments[0].name == "operator": + raise self._parse_error(None) self.visitor.on_class_method(state, method) @@ -1897,7 +1910,8 @@ class CxxParser: def _parse_type( self, tok: typing.Optional[LexToken], - ) -> typing.Tuple[Type, ParsedTypeModifiers]: + operator_ok: bool = False, + ) -> typing.Tuple[typing.Optional[Type], ParsedTypeModifiers]: """ This parses a typename and stops parsing when it hits something that it doesn't understand. The caller uses the results to figure @@ -1905,6 +1919,9 @@ class CxxParser: This only parses the base type, does not parse pointers, references, or additional const/volatile qualifiers + + The returned type will only be None if operator_ok is True and an + operator is encountered. """ const = False @@ -1924,6 +1941,7 @@ class CxxParser: tok = get_token() pqname: typing.Optional[PQName] = None + pqname_optional = False _pqname_start_tokens = self._pqname_start_tokens _attribute_start = self._attribute_start_tokens @@ -1935,6 +1953,10 @@ class CxxParser: if pqname is not None: # found second set of names, done here break + if operator_ok and tok_type == "operator": + # special case: conversion operators such as operator bool + pqname_optional = True + break pqname, _ = self._parse_pqname( tok, compound_ok=True, fn_ok=False, fund_ok=True ) @@ -1963,14 +1985,17 @@ class CxxParser: tok = get_token() if pqname is None: - raise self._parse_error(tok) + if not pqname_optional: + raise self._parse_error(tok) + parsed_type = None + else: + # Construct a type from the parsed name + parsed_type = Type(pqname, const, volatile) self.lex.return_token(tok) - # Construct a type from the parsed name - parsed_type = Type(pqname, const, volatile) + # Always return the modifiers mods = ParsedTypeModifiers(vars, both, meths) - return parsed_type, mods def _parse_decl( @@ -2095,6 +2120,55 @@ class CxxParser: self._parse_field(mods, dtype, pqname, template, doxygen, location, is_typedef) return False + def _parse_operator_conversion( + self, + mods: ParsedTypeModifiers, + location: Location, + doxygen: typing.Optional[str], + template: typing.Optional[TemplateDecl], + is_typedef: bool, + is_friend: bool, + ): + tok = self._next_token_must_be("operator") + + if is_typedef: + raise self._parse_error(tok, "operator not permitted in typedef") + + # next piece must be the conversion type + ctype, cmods = self._parse_type(None) + if ctype is None: + raise self._parse_error(None) + + cmods.validate(var_ok=False, meth_ok=False, msg="parsing conversion operator") + + # then this must be a method + self._next_token_must_be("(") + + # make our own pqname/op here + segments = [NameSpecifier("operator")] + pqname = PQName(segments) + op = "conversion" + + if self._parse_function( + mods, + ctype, + pqname, + op, + template, + doxygen, + location, + False, + False, + is_friend, + False, + None, + ): + # has function body and handled it + return + + # if this is just a declaration, next token should be ; + self._next_token_must_be(";") + _class_enum_stage2 = {":", "final", "explicit", "{"} def _parse_declarations( @@ -2114,12 +2188,16 @@ class CxxParser: location = tok.location - # Always starts out with some kind of type name - parsed_type, mods = self._parse_type(tok) + # Almost always starts out with some kind of type name or a modifier + parsed_type, mods = self._parse_type(tok, operator_ok=True) # Check to see if this might be a class/enum declaration - if parsed_type.typename.classkey and self._maybe_parse_class_enum_decl( - parsed_type, mods, doxygen, template, is_typedef, is_friend, location + if ( + parsed_type is not None + and parsed_type.typename.classkey + and self._maybe_parse_class_enum_decl( + parsed_type, mods, doxygen, template, is_typedef, is_friend, location + ) ): return @@ -2138,6 +2216,13 @@ class CxxParser: mods.validate(var_ok=var_ok, meth_ok=meth_ok, msg=msg) + if parsed_type is None: + # this means an operator was encountered, deal with the special case + self._parse_operator_conversion( + mods, location, doxygen, template, is_typedef, is_friend + ) + return + # Ok, dealing with a variable or function/method while True: if self._parse_decl( diff --git a/cxxheaderparser/types.py b/cxxheaderparser/types.py index f9a65cb..8354427 100644 --- a/cxxheaderparser/types.py +++ b/cxxheaderparser/types.py @@ -542,6 +542,14 @@ class Method(Function): @dataclass class Operator(Method): + """ + Represents an operator method + """ + + #: The operator type (+, +=, etc). + #: + #: In the case of a conversion operator (such as 'operator bool'), this + #: is the string "conversion" and the full Type is found in return_type operator: str = "" diff --git a/tests/test_operators.py b/tests/test_operators.py index 7104caf..ae060ce 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -552,3 +552,68 @@ def test_class_operators(): ] ) ) + + +def test_conversion_operators(): + content = """ + + class Foo + { + public: + operator Type1() const { return SomeMethod(); } + explicit operator Type2() const; + virtual operator bool() const; + }; + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + classes=[ + ClassScope( + class_decl=ClassDecl( + typename=PQName( + segments=[NameSpecifier(name="Foo")], classkey="class" + ) + ), + methods=[ + Operator( + return_type=Type( + typename=PQName(segments=[NameSpecifier(name="Type1")]) + ), + name=PQName(segments=[NameSpecifier(name="operator")]), + parameters=[], + has_body=True, + access="public", + const=True, + operator="conversion", + ), + Operator( + return_type=Type( + typename=PQName(segments=[NameSpecifier(name="Type2")]) + ), + name=PQName(segments=[NameSpecifier(name="operator")]), + parameters=[], + access="public", + const=True, + explicit=True, + operator="conversion", + ), + Operator( + return_type=Type( + typename=PQName( + segments=[FundamentalSpecifier(name="bool")] + ) + ), + name=PQName(segments=[NameSpecifier(name="operator")]), + parameters=[], + access="public", + const=True, + virtual=True, + operator="conversion", + ), + ], + ) + ] + ) + )