diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 1703e00..0e3dbc5 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -1758,16 +1758,15 @@ class CxxParser: params, vararg = self._parse_parameters() - if is_class_block and not is_typedef: - assert isinstance(state, ClassBlockState) + # A method outside of a class has multiple name segments + multiple_name_segments = len(pqname.segments) > 1 + + if (is_class_block or multiple_name_segments) and not is_typedef: props.update(dict.fromkeys(mods.meths.keys(), True)) method: Method - current_access = self._current_access - assert current_access is not None - if op: method = Operator( return_type, @@ -1777,7 +1776,7 @@ class CxxParser: doxygen=doxygen, operator=op, template=template, - access=current_access, + access=self._current_access, **props, # type: ignore ) else: @@ -1790,22 +1789,28 @@ class CxxParser: constructor=constructor, destructor=destructor, template=template, - access=current_access, + access=self._current_access, **props, # type: ignore ) self._parse_method_end(method) - if is_friend: - friend = FriendDecl(fn=method) - self.visitor.on_class_friend(state, friend) - else: - # method must not have multiple segments except for operator - if len(pqname.segments) > 1: - if getattr(pqname.segments[0], "name", None) != "operator": - raise self._parse_error(None) + if is_class_block: + assert isinstance(state, ClassBlockState) + if is_friend: + friend = FriendDecl(fn=method) + self.visitor.on_class_friend(state, friend) + else: + # method must not have multiple segments except for operator + if len(pqname.segments) > 1: + if getattr(pqname.segments[0], "name", None) != "operator": + raise self._parse_error(None) - self.visitor.on_class_method(state, method) + self.visitor.on_class_method(state, method) + else: + if not method.has_body: + raise self._parse_error(None, expected="Method body") + self.visitor.on_method_impl(state, method) return method.has_body or method.has_trailing_return @@ -2126,12 +2131,22 @@ class CxxParser: tok = self.lex.token_if("(") if tok: - # Check to see if this is a constructor/destructor - if isinstance(state, ClassBlockState) and isinstance(dtype, Type): - + dsegments: typing.List[PQNameSegment] = [] + if isinstance(dtype, Type): dsegments = dtype.typename.segments - if not is_friend: + # Check to see if this is a constructor/destructor by matching + # the method name to the class name + is_class_block = isinstance(state, ClassBlockState) + if (is_class_block or len(dsegments) > 1) and isinstance(dtype, Type): + + if not is_class_block: + # must be an instance of a class + cls_name = getattr(dsegments[-2], "name", None) + + elif not is_friend: + assert isinstance(state, ClassBlockState) + # class name to match against is this class cls_name = getattr( state.class_decl.typename.segments[-1], "name", None diff --git a/cxxheaderparser/simple.py b/cxxheaderparser/simple.py index 35832c3..35b004d 100644 --- a/cxxheaderparser/simple.py +++ b/cxxheaderparser/simple.py @@ -94,7 +94,13 @@ class NamespaceScope: classes: typing.List["ClassScope"] = field(default_factory=list) enums: typing.List[EnumDecl] = field(default_factory=list) + + #: Function declarations (with or without body) functions: typing.List[Function] = field(default_factory=list) + + #: Method implementations outside of a class (must have a body) + method_impls: typing.List[Method] = field(default_factory=list) + typedefs: typing.List[Typedef] = field(default_factory=list) variables: typing.List[Variable] = field(default_factory=list) @@ -264,6 +270,10 @@ class SimpleCxxVisitor: assert isinstance(self.block, NamespaceScope) self.block.functions.append(fn) + def on_method_impl(self, state: State, method: Method) -> None: + assert isinstance(self.block, NamespaceScope) + self.block.method_impls.append(method) + def on_typedef(self, state: State, typedef: Typedef) -> None: self.block.typedefs.append(typedef) diff --git a/cxxheaderparser/types.py b/cxxheaderparser/types.py index 03e107b..72382be 100644 --- a/cxxheaderparser/types.py +++ b/cxxheaderparser/types.py @@ -529,7 +529,8 @@ class Method(Function): A method declaration, potentially with the method body """ - access: str = "" + #: If parsed within a class, the access level for this method + access: typing.Optional[str] = None const: bool = False volatile: bool = False diff --git a/cxxheaderparser/visitor.py b/cxxheaderparser/visitor.py index 99ff169..15d2100 100644 --- a/cxxheaderparser/visitor.py +++ b/cxxheaderparser/visitor.py @@ -106,6 +106,22 @@ class CxxVisitor(Protocol): def on_function(self, state: State, fn: Function) -> None: ... + def on_method_impl(self, state: State, method: Method) -> None: + """ + Called when a method implementation is encountered outside of a class + declaration. For example: + + .. code-block:: c++ + + void MyClass::fn() { + // does something + } + + .. note:: The above implementation is ambiguous, as it technically could + be a function in a namespace. We emit this instead as it's + more likely to be the case in common code. + """ + def on_typedef(self, state: State, typedef: Typedef) -> None: """ Called for each typedef instance encountered. For example: @@ -184,7 +200,7 @@ class CxxVisitor(Protocol): def on_class_method(self, state: ClassBlockState, method: Method) -> None: """ - Called when a method of a class is encountered + Called when a method of a class is encountered inside of a class """ def on_class_end(self, state: ClassBlockState) -> None: diff --git a/tests/test_class.py b/tests/test_class.py index ed60f26..c8ec97d 100644 --- a/tests/test_class.py +++ b/tests/test_class.py @@ -1909,8 +1909,8 @@ def test_class_fn_inline_impl() -> None: ], ) ], - functions=[ - Function( + method_impls=[ + Method( return_type=Type( typename=PQName(segments=[FundamentalSpecifier(name="void")]) ), @@ -2109,8 +2109,8 @@ def test_class_fn_return_class() -> None: ], ), ], - functions=[ - Function( + method_impls=[ + Method( return_type=Pointer( ptr_to=Type( typename=PQName( @@ -2198,8 +2198,8 @@ def test_class_fn_template_impl() -> None: ], ) ], - functions=[ - Function( + method_impls=[ + Method( return_type=Pointer( ptr_to=Type( typename=PQName(segments=[FundamentalSpecifier(name="int")]) @@ -2265,8 +2265,8 @@ def test_class_fn_inline_template_impl() -> None: ], ) ], - functions=[ - Function( + method_impls=[ + Method( return_type=Type( typename=PQName(segments=[NameSpecifier(name="T")]) ), @@ -3212,3 +3212,51 @@ def test_class_ref_qualifiers() -> None: ] ) ) + + +def test_method_outside_class() -> None: + content = """ + int foo::bar() { return 1; } + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + method_impls=[ + Method( + return_type=Type( + typename=PQName(segments=[FundamentalSpecifier(name="int")]) + ), + name=PQName( + segments=[NameSpecifier(name="foo"), NameSpecifier(name="bar")] + ), + parameters=[], + has_body=True, + ) + ] + ) + ) + + +def test_constructor_outside_class() -> None: + content = """ + inline foo::foo() {} + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + method_impls=[ + Method( + return_type=None, + name=PQName( + segments=[NameSpecifier(name="foo"), NameSpecifier(name="foo")] + ), + parameters=[], + inline=True, + has_body=True, + constructor=True, + ) + ] + ) + )