From bbc7b6cc54f97f3ff8a10a28d3da38c77c88024a Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sun, 12 Apr 2020 14:13:54 +0200 Subject: [PATCH] C, add scoping directives --- CHANGES | 2 + doc/usage/restructuredtext/domains.rst | 66 +++++++++++++++++ sphinx/domains/c.py | 97 +++++++++++++++++++++++++ tests/roots/test-domain-c/namespace.rst | 21 ++++++ tests/test_domain_c.py | 8 ++ 5 files changed, 194 insertions(+) create mode 100644 tests/roots/test-domain-c/namespace.rst diff --git a/CHANGES b/CHANGES index 863a5ea24..ad5b6bd11 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,8 @@ Features added * LaTeX: Make the ``toplevel_sectioning`` setting optional in LaTeX theme * #7410: Allow to suppress "circular toctree references detected" warnings using :confval:`suppress_warnings` +* C, added scope control directives, :rst:dir:`c:namespace`, + :rst:dir:`c:namespace-push`, and :rst:dir:`c:namespace-pop`. Bugs fixed ---------- diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 7a987be70..5d6c9b656 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -706,6 +706,72 @@ Inline Expressions and Types .. versionadded:: 3.0 +Namespacing +~~~~~~~~~~~ + +.. versionadded:: 3.1 + +The C language it self does not support namespacing, but it can sometimes be +useful to emulate it in documentation, e.g., to show alternate declarations. +The feature may also be used to document members of structs/unions/enums +separate from their parent declaration. + +The current scope can be changed using three namespace directives. They manage +a stack declarations where ``c:namespace`` resets the stack and changes a given +scope. + +The ``c:namespace-push`` directive changes the scope to a given inner scope +of the current one. + +The ``c:namespace-pop`` directive undoes the most recent +``c:namespace-push`` directive. + +.. rst:directive:: .. c:namespace:: scope specification + + Changes the current scope for the subsequent objects to the given scope, and + resets the namespace directive stack. Note that nested scopes can be + specified by separating with a dot, e.g.:: + + .. c:namespace:: Namespace1.Namespace2.SomeStruct.AnInnerStruct + + All subsequent objects will be defined as if their name were declared with + the scope prepended. The subsequent cross-references will be searched for + starting in the current scope. + + Using ``NULL`` or ``0`` as the scope will change to global scope. + +.. rst:directive:: .. c:namespace-push:: scope specification + + Change the scope relatively to the current scope. For example, after:: + + .. c:namespace:: A.B + + .. c:namespace-push:: C.D + + the current scope will be ``A.B.C.D``. + +.. rst:directive:: .. c:namespace-pop:: + + Undo the previous ``c:namespace-push`` directive (*not* just pop a scope). + For example, after:: + + .. c:namespace:: A.B + + .. c:namespace-push:: C.D + + .. c:namespace-pop:: + + the current scope will be ``A.B`` (*not* ``A.B.C``). + + If no previous ``c:namespace-push`` directive has been used, but only a + ``c:namespace`` directive, then the current scope will be reset to global + scope. That is, ``.. c:namespace:: A.B`` is equivalent to:: + + .. c:namespace:: NULL + + .. c:namespace-push:: A.B + + .. _cpp-domain: The C++ Domain diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index cf815bd04..7eaeef5da 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -35,6 +35,7 @@ from sphinx.util.cfamily import ( char_literal_re ) from sphinx.util.docfields import Field, TypedField +from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import make_refnode logger = logging.getLogger(__name__) @@ -2928,6 +2929,9 @@ class DefinitionParser(BaseParser): assert False return ASTDeclaration(objectType, directiveType, declaration) + def parse_namespace_object(self) -> ASTNestedName: + return self._parse_nested_name() + def parse_xref_object(self) -> ASTNestedName: name = self._parse_nested_name() # if there are '()' left, just skip them @@ -3178,6 +3182,95 @@ class CTypeObject(CObject): object_type = 'type' +class CNamespaceObject(SphinxDirective): + """ + This directive is just to tell Sphinx that we're documenting stuff in + namespace foo. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} # type: Dict + + def run(self) -> List[Node]: + rootSymbol = self.env.domaindata['c']['root_symbol'] + if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + symbol = rootSymbol + stack = [] # type: List[Symbol] + else: + parser = DefinitionParser(self.arguments[0], + location=self.get_source_info()) + try: + name = parser.parse_namespace_object() + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=self.get_source_info()) + name = _make_phony_error_name() + symbol = rootSymbol.add_name(name) + stack = [symbol] + self.env.temp_data['c:parent_symbol'] = symbol + self.env.temp_data['c:namespace_stack'] = stack + self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() + return [] + + +class CNamespacePushObject(SphinxDirective): + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} # type: Dict + + def run(self) -> List[Node]: + if self.arguments[0].strip() in ('NULL', '0', 'nullptr'): + return [] + parser = DefinitionParser(self.arguments[0], + location=self.get_source_info()) + try: + name = parser.parse_namespace_object() + parser.assert_end() + except DefinitionError as e: + logger.warning(e, location=self.get_source_info()) + name = _make_phony_error_name() + oldParent = self.env.temp_data.get('c:parent_symbol', None) + if not oldParent: + oldParent = self.env.domaindata['c']['root_symbol'] + symbol = oldParent.add_name(name) + stack = self.env.temp_data.get('c:namespace_stack', []) + stack.append(symbol) + self.env.temp_data['c:parent_symbol'] = symbol + self.env.temp_data['c:namespace_stack'] = stack + self.env.ref_context['c:parent_key'] = symbol.get_lookup_key() + return [] + + +class CNamespacePopObject(SphinxDirective): + has_content = False + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} # type: Dict + + def run(self) -> List[Node]: + stack = self.env.temp_data.get('c:namespace_stack', None) + if not stack or len(stack) == 0: + logger.warning("C namespace pop on empty stack. Defaulting to gobal scope.", + location=self.get_source_info()) + stack = [] + else: + stack.pop() + if len(stack) > 0: + symbol = stack[-1] + else: + symbol = self.env.domaindata['c']['root_symbol'] + self.env.temp_data['c:parent_symbol'] = symbol + self.env.temp_data['c:namespace_stack'] = stack + self.env.ref_context['cp:parent_key'] = symbol.get_lookup_key() + return [] + + class CXRefRole(XRefRole): def process_link(self, env: BuildEnvironment, refnode: Element, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]: @@ -3256,6 +3349,10 @@ class CDomain(Domain): 'enum': CEnumObject, 'enumerator': CEnumeratorObject, 'type': CTypeObject, + # scope control + 'namespace': CNamespaceObject, + 'namespace-push': CNamespacePushObject, + 'namespace-pop': CNamespacePopObject, } roles = { 'member': CXRefRole(), diff --git a/tests/roots/test-domain-c/namespace.rst b/tests/roots/test-domain-c/namespace.rst new file mode 100644 index 000000000..c220d38e7 --- /dev/null +++ b/tests/roots/test-domain-c/namespace.rst @@ -0,0 +1,21 @@ +.. c:namespace:: NS + +.. c:var:: int NSVar + +.. c:namespace:: NULL + +.. c:var:: int NULLVar + +.. c:namespace:: NSDummy + +.. c:namespace:: 0 + +.. c:var:: int ZeroVar + +.. c:namespace-push:: NS2.NS3 + +.. c:var:: int NS2NS3Var + +.. c:namespace-pop:: + +.. c:var:: int PopVar diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index 3255efc55..012e5c314 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -473,6 +473,14 @@ def test_build_domain_c(app, status, warning): ws = filter_warnings(warning, "index") assert len(ws) == 0 +@pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) +def test_build_domain_c(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "namespace") + assert len(ws) == 0 + t = (app.outdir / "namespace.html").read_text() + for id_ in ('NS.NSVar', 'NULLVar', 'ZeroVar', 'NS2.NS3.NS2NS3Var', 'PopVar'): + assert 'id="c.{}"'.format(id_) in t @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) def test_build_domain_c_anon_dup_decl(app, status, warning):