diff --git a/cxxheaderparser/parser.py b/cxxheaderparser/parser.py index 56a893f..f6ecdbc 100644 --- a/cxxheaderparser/parser.py +++ b/cxxheaderparser/parser.py @@ -35,6 +35,7 @@ from .types import ( Method, MoveReference, NameSpecifier, + NamespaceAlias, NamespaceDecl, Operator, PQNameSegment, @@ -404,13 +405,26 @@ class CxxParser: names = [] location = tok.location + ns_alias: typing.Union[typing.Literal[False], LexToken] = False tok = self._next_token_must_be("NAME", "{") if tok.type != "{": + endtok = "{" + # Check for namespace alias here + etok = self.lex.token_if("=") + if etok: + ns_alias = tok + endtok = ";" + # They can start with :: + maybe_tok = self.lex.token_if("DBL_COLON") + if maybe_tok: + names.append(maybe_tok.value) + tok = self._next_token_must_be("NAME") + while True: names.append(tok.value) - tok = self._next_token_must_be("DBL_COLON", "{") - if tok.type == "{": + tok = self._next_token_must_be("DBL_COLON", endtok) + if tok.type == endtok: break tok = self._next_token_must_be("NAME") @@ -418,7 +432,10 @@ class CxxParser: if inline and len(names) > 1: raise CxxParseError("a nested namespace definition cannot be inline") - # TODO: namespace_alias_definition + if ns_alias: + alias = NamespaceAlias(ns_alias.value, names) + self.visitor.on_namespace_alias(self.state, alias) + return ns = NamespaceDecl(names, inline, doxygen) state = self._push_state(NamespaceBlockState, ns) diff --git a/cxxheaderparser/simple.py b/cxxheaderparser/simple.py index ea34c66..65cf04a 100644 --- a/cxxheaderparser/simple.py +++ b/cxxheaderparser/simple.py @@ -39,6 +39,7 @@ from .types import ( FriendDecl, Function, Method, + NamespaceAlias, TemplateInst, Typedef, UsingAlias, @@ -110,6 +111,7 @@ class NamespaceScope: using: typing.List[UsingDecl] = field(default_factory=list) using_ns: typing.List["UsingNamespace"] = field(default_factory=list) using_alias: typing.List[UsingAlias] = field(default_factory=list) + ns_alias: typing.List[NamespaceAlias] = field(default_factory=list) #: Explicit template instantiations template_insts: typing.List[TemplateInst] = field(default_factory=list) @@ -261,6 +263,10 @@ class SimpleCxxVisitor: self.block = self.block_stack.pop() self.namespace = self.ns_stack.pop() + def on_namespace_alias(self, state: State, alias: NamespaceAlias) -> None: + assert isinstance(self.block, NamespaceScope) + self.block.ns_alias.append(alias) + def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: self.block.forward_decls.append(fdecl) diff --git a/cxxheaderparser/types.py b/cxxheaderparser/types.py index f0bcc5e..79f6d45 100644 --- a/cxxheaderparser/types.py +++ b/cxxheaderparser/types.py @@ -38,6 +38,26 @@ class Value: tokens: typing.List[Token] +@dataclass +class NamespaceAlias: + """ + A namespace alias + + .. code-block:: c++ + + namespace ANS = my::ns; + ~~~ ~~~~~~ + + """ + + alias: str + + #: These are the names (split by ::) for the namespace that this alias + #: refers to, but does not include any parent namespace names. It may + #: include a leading "::", but does not include a following :: string. + names: typing.List[str] + + @dataclass class NamespaceDecl: """ diff --git a/cxxheaderparser/visitor.py b/cxxheaderparser/visitor.py index c6d0d81..6f5708c 100644 --- a/cxxheaderparser/visitor.py +++ b/cxxheaderparser/visitor.py @@ -14,6 +14,7 @@ from .types import ( FriendDecl, Function, Method, + NamespaceAlias, TemplateInst, Typedef, UsingAlias, @@ -94,6 +95,11 @@ class CxxVisitor(Protocol): Called at the end of a ``namespace`` block """ + def on_namespace_alias(self, state: State, alias: NamespaceAlias) -> None: + """ + Called when a ``namespace`` alias is encountered + """ + def on_forward_decl(self, state: State, fdecl: ForwardDecl) -> None: """ Called when a forward declaration is encountered diff --git a/tests/test_namespaces.py b/tests/test_namespaces.py index 7535889..0b6892f 100644 --- a/tests/test_namespaces.py +++ b/tests/test_namespaces.py @@ -4,6 +4,7 @@ from cxxheaderparser.errors import CxxParseError from cxxheaderparser.types import ( ForwardDecl, FundamentalSpecifier, + NamespaceAlias, NameSpecifier, PQName, Token, @@ -168,3 +169,29 @@ def test_invalid_inline_namespace() -> None: err = ":1: parse error evaluating 'inline': a nested namespace definition cannot be inline" with pytest.raises(CxxParseError, match=re.escape(err)): parse_string(content, cleandoc=True) + + +def test_ns_alias() -> None: + content = """ + namespace ANS = my::ns; + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + ns_alias=[NamespaceAlias(alias="ANS", names=["my", "ns"])] + ) + ) + + +def test_ns_alias_global() -> None: + content = """ + namespace ANS = ::my::ns; + """ + data = parse_string(content, cleandoc=True) + + assert data == ParsedData( + namespace=NamespaceScope( + ns_alias=[NamespaceAlias(alias="ANS", names=["::", "my", "ns"])] + ) + )