Merge pull request #7468 from jakobandersen/c_namespace

C, add scoping directives
This commit is contained in:
Jakob Lykke Andersen 2020-04-13 13:47:39 +02:00 committed by GitHub
commit 09f66c690b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 194 additions and 0 deletions

View File

@ -19,6 +19,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
----------

View File

@ -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

View File

@ -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(),

View File

@ -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

View File

@ -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):