Repository: sphinx-doc/sphinx

Issue:
C/CPP domains: support for bit fields
Context: [breathe #358](https://github.com/michaeljones/breathe/issues/358)

It looks like Sphinx declaration validator doesn't support bit fields since a declaration like
`uint8_t foo : 3` triggers fail in [`sphinx/domains/cpp.py`](https://github.com/sphinx-doc/sphinx/blob/master/sphinx/domains/cpp.py#L4646). With my limited insight in Sphinx's internals, my guess would be that [`_parse_declarator`](https://github.com/sphinx-doc/sphinx/blob/master/sphinx/domains/cpp.py#L5766) might need some modifications.

Ping @jakobandersen

Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index 7a5c28d46..beb9d5749 100644
--- a/CHANGES
+++ b/CHANGES
@@ -16,6 +16,8 @@ Deprecated
 Features added
 --------------
 
+* #6707: C++, support bit-fields.
+
 Bugs fixed
 ----------
 
diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py
index 417bdb2b6..4a8a73380 100644
--- a/sphinx/domains/cpp.py
+++ b/sphinx/domains/cpp.py
@@ -3191,6 +3191,52 @@ def describe_signature(self, signode, mode, env, symbol):
             self.paramQual.describe_signature(signode, mode, env, symbol)
 
 
+class ASTDeclaratorNameBitField(ASTBase):
+    def __init__(self, declId, size):
+        self.declId = declId
+        self.size = size
+
+    @property
+    def name(self):
+        # type: () -> ASTNestedName
+        return self.declId
+
+    def get_param_id(self, version):  # only the parameters (if any)
+        # type: (int) -> str
+        return ''
+
+    def get_ptr_suffix_id(self, version):  # only the array specifiers
+        # type: (int) -> str
+        return ''
+
+    # ------------------------------------------------------------------------
+
+    def require_space_after_declSpecs(self):
+        # type: () -> bool
+        return self.declId is not None
+
+    def is_function_type(self):
+        # type: () -> bool
+        return False
+
+    def _stringify(self, transform):
+        # type: (Callable[[Any], str]) -> str
+        res = []
+        if self.declId:
+            res.append(transform(self.declId))
+        res.append(" : ")
+        res.append(transform(self.size))
+        return ''.join(res)
+
+    def describe_signature(self, signode, mode, env, symbol):
+        # type: (addnodes.desc_signature, str, BuildEnvironment, Symbol) -> None
+        _verify_description_mode(mode)
+        if self.declId:
+            self.declId.describe_signature(signode, mode, env, symbol)
+        signode.append(nodes.Text(' : ', ' : '))
+        self.size.describe_signature(signode, mode, env, symbol)
+
+
 class ASTParenExprList(ASTBase):
     def __init__(self, exprs):
         # type: (List[Any]) -> None
@@ -5716,8 +5762,12 @@ def _parse_decl_specs(self, outer, typed=True):
             trailing = None
         return ASTDeclSpecs(outer, leftSpecs, rightSpecs, trailing)
 
-    def _parse_declarator_name_param_qual(self, named, paramMode, typed):
-        # type: (Union[bool, str], str, bool) -> ASTDeclaratorNameParamQual
+    def _parse_declarator_name_suffix(self,
+                                      named,      # type: Union[bool, str]
+                                      paramMode,  # type: str
+                                      typed       # type: bool
+                                      ):
+        # type: (...) -> Union[ASTDeclaratorNameParamQual, ASTDeclaratorNameBitField]
         # now we should parse the name, and then suffixes
         if named == 'maybe':
             pos = self.pos
@@ -5760,6 +5810,13 @@ def parser():
             else:
                 break
         paramQual = self._parse_parameters_and_qualifiers(paramMode)
+        if paramQual is None and len(arrayOps) == 0:
+            # perhaps a bit-field
+            if named and paramMode == 'type' and typed:
+                self.skip_ws()
+                if self.skip_string(':'):
+                    size = self._parse_constant_expression(inTemplate=False)
+                    return ASTDeclaratorNameBitField(declId=declId, size=size)
         return ASTDeclaratorNameParamQual(declId=declId, arrayOps=arrayOps,
                                           paramQual=paramQual)
 
@@ -5842,8 +5899,8 @@ def _parse_declarator(self, named, paramMode, typed=True):
             pos = self.pos
             try:
                 # assume this is params and quals
-                res = self._parse_declarator_name_param_qual(named, paramMode,
-                                                             typed)
+                res = self._parse_declarator_name_suffix(named, paramMode,
+                                                         typed)
                 return res
             except DefinitionError as exParamQual:
                 prevErrors.append((exParamQual, "If declId, parameters, and qualifiers"))
@@ -5868,7 +5925,7 @@ def _parse_declarator(self, named, paramMode, typed=True):
                     raise self._make_multi_error(prevErrors, header)
         pos = self.pos
         try:
-            return self._parse_declarator_name_param_qual(named, paramMode, typed)
+            return self._parse_declarator_name_suffix(named, paramMode, typed)
         except DefinitionError as e:
             self.pos = pos
             prevErrors.append((e, "If declarator-id"))


Test patch:
diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py
index bbb4cc55e..df1e1bae1 100644
--- a/tests/test_domain_cpp.py
+++ b/tests/test_domain_cpp.py
@@ -334,6 +334,16 @@ def test_member_definitions():
     check('member', 'thread_local extern int myInt', {1: 'myInt__i', 2: '5myInt'},
           'extern thread_local int myInt')
 
+    # tests based on https://en.cppreference.com/w/cpp/language/bit_field
+    check('member', 'int b : 3', {1: 'b__i', 2: '1b'})
+    check('member', 'int b : 8 = 42', {1: 'b__i', 2: '1b'})
+    check('member', 'int b : 8{42}', {1: 'b__i', 2: '1b'})
+    # TODO: enable once the ternary operator is supported
+    #check('member', 'int b : true ? 8 : a = 42', {1: 'b__i', 2: '1b'})
+    # TODO: enable once the ternary operator is supported
+    #check('member', 'int b : (true ? 8 : a) = 42', {1: 'b__i', 2: '1b'})
+    check('member', 'int b : 1 || new int{0}', {1: 'b__i', 2: '1b'})
+
 
 def test_function_definitions():
     check('function', 'void f(volatile int)', {1: "f__iV", 2: "1fVi"})