Repository: sphinx-doc/sphinx

Issue:
C++: duplicate error with template function redefinition
I was redirected here from the Breathe issue tracker, specifically https://github.com/michaeljones/breathe/issues/406

I have two different implementations of a C++ template method that are selected using the [SFINAE pattern](https://en.cppreference.com/w/cpp/language/sfinae) (implementations omitted below):
```cpp

class BooleanBuilder : public ArrayBuilder {
 public:
  /// \brief Append a sequence of elements in one shot, with a specified nullmap
  /// \param[in] values_begin InputIterator to the beginning of the values
  /// \param[in] values_end InputIterator pointing to the end of the values
  /// \param[in] valid_begin InputIterator with elements indication valid(1)
  ///  or null(0) values
  /// \return Status
  template <typename ValuesIter, typename ValidIter>
  typename std::enable_if<!std::is_pointer<ValidIter>::value, Status>::type AppendValues(
      ValuesIter values_begin, ValuesIter values_end, ValidIter valid_begin) {
     ...
  }

  /// \brief Append a sequence of elements in one shot, with a specified nullmap
  /// \param[in] values_begin InputIterator to the beginning of the values
  /// \param[in] values_end InputIterator pointing to the end of the values
  /// \param[in] valid_begin uint8_t* indication valid(1) or null(0) values.
  ///  nullptr indicates all values are valid.
  /// \return Status
  template <typename ValuesIter, typename ValidIter>
  typename std::enable_if<std::is_pointer<ValidIter>::value, Status>::type AppendValues(
      ValuesIter values_begin, ValuesIter values_end, ValidIter valid_begin) {
     ...
  }
};
```

When trying to integrate Doxygen documentation for that class's members, I get the following error:
```
Warning, treated as error:
/home/antoine/arrow/docs/source/cpp/api/builder.rst:31:Duplicate declaration.
Makefile:71: recipe for target 'html' failed
make: *** [html] Error 2
```

Sphinx version is 1.8.2, Python version is 3.7.


Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index d0aa89d50..b34a19431 100644
--- a/CHANGES
+++ b/CHANGES
@@ -20,6 +20,10 @@ Bugs fixed
   be searched.
 * #5889: LaTeX: user ``numfig_format`` is stripped of spaces and may cause
   build failure
+* C++, fix hyperlinks for declarations involving east cv-qualifiers.
+* #5755: C++, fix duplicate declaration error on function templates with constraints
+  in the return type.
+* C++, parse unary right fold expressions and binary fold expressions.
 
 Testing
 --------
diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py
index 56ac5ed09..6740638ae 100644
--- a/sphinx/domains/cpp.py
+++ b/sphinx/domains/cpp.py
@@ -364,8 +364,8 @@
     'while', 'xor', 'xor_eq'
 ]
 
-_max_id = 3
-_id_prefix = [None, '', '_CPPv2', '_CPPv3']
+_max_id = 4
+_id_prefix = [None, '', '_CPPv2', '_CPPv3', '_CPPv4']
 
 # ------------------------------------------------------------------------------
 # Id v1 constants
@@ -872,6 +872,7 @@ def describe_signature(self, signode, mode, env, symbol):
 
 class ASTFoldExpr(ASTBase):
     def __init__(self, leftExpr, op, rightExpr):
+        # type: (Any, unicode, Any) -> None
         assert leftExpr is not None or rightExpr is not None
         self.leftExpr = leftExpr
         self.op = op
@@ -897,8 +898,22 @@ def get_id(self, version):
         assert version >= 3
         if version == 3:
             return text_type(self)
-        # TODO: find the right mangling scheme
-        assert False
+        # https://github.com/itanium-cxx-abi/cxx-abi/pull/67
+        res = []
+        if self.leftExpr is None:  # (... op expr)
+            res.append('fl')
+        elif self.rightExpr is None:  # (expr op ...)
+            res.append('fr')
+        else:  # (expr op ... op expr)
+            # we don't check where the parameter pack is,
+            # we just always call this a binary left fold
+            res.append('fL')
+        res.append(str(_id_operator_v2[self.op]))  # TODO: remove str when merging to 2.0
+        if self.leftExpr:
+            res.append(self.leftExpr.get_id(version))
+        if self.rightExpr:
+            res.append(self.rightExpr.get_id(version))
+        return ''.join(res)
 
     def describe_signature(self, signode, mode, env, symbol):
         signode.append(nodes.Text('('))
@@ -2537,11 +2552,12 @@ def get_id(self, version):
                 res.append('C')
             return u''.join(res)
         res = []
-        if self.leftSpecs.volatile or self.rightSpecs.volatile:
+        if self.allSpecs.volatile:
             res.append('V')
-        if self.leftSpecs.const or self.rightSpecs.volatile:
+        if self.allSpecs.const:
             res.append('K')
-        res.append(self.trailingTypeSpec.get_id(version))
+        if self.trailingTypeSpec is not None:
+            res.append(self.trailingTypeSpec.get_id(version))
         return u''.join(res)
 
     def _stringify(self, transform):
@@ -3177,6 +3193,14 @@ def get_id(self, version, objectType=None, symbol=None):
             if objectType == 'function':  # also modifiers
                 modifiers = self.decl.get_modifiers_id(version)
                 res.append(symbol.get_full_nested_name().get_id(version, modifiers))
+                if version >= 4:
+                    # with templates we need to mangle the return type in as well
+                    templ = symbol.declaration.templatePrefix
+                    if templ is not None:
+                        typeId = self.decl.get_ptr_suffix_id(version)
+                        returnTypeId = self.declSpecs.get_id(version)
+                        res.append(typeId)
+                        res.append(returnTypeId)
                 res.append(self.decl.get_param_id(version))
             elif objectType == 'type':  # just the name
                 res.append(symbol.get_full_nested_name().get_id(version))
@@ -4623,13 +4647,45 @@ def _parse_fold_or_paren_expression(self):
             if not self.skip_string(')'):
                 self.fail("Expected ')' in end of fold expression.")
             return ASTFoldExpr(None, op, rightExpr)
-        # TODO: actually try to parse fold expression
-        # fall back to a paren expression
-        res = self._parse_expression(inTemplate=False)
+        # try first parsing a unary right fold, or a binary fold
+        pos = self.pos
+        try:
+            self.skip_ws()
+            leftExpr = self._parse_cast_expression()
+            self.skip_ws()
+            if not self.match(_fold_operator_re):
+                self.fail("Expected fold operator after left expression in fold expression.")
+            op = self.matched_text
+            self.skip_ws()
+            if not self.skip_string_and_ws('...'):
+                self.fail("Expected '...' after fold operator in fold expression.")
+        except DefinitionError as eFold:
+            self.pos = pos
+            # fall back to a paren expression
+            try:
+                res = self._parse_expression(inTemplate=False)
+                self.skip_ws()
+                if not self.skip_string(')'):
+                    self.fail("Expected ')' in end of parenthesized expression.")
+            except DefinitionError as eExpr:
+                raise self._make_multi_error([
+                    (eFold, "If fold expression"),
+                    (eExpr, "If parenthesized expression")
+                ], "Error in fold expression or parenthesized expression.")
+            return ASTParenExpr(res)
+        # now it definitely is a fold expression
+        if self.skip_string(')'):
+            return ASTFoldExpr(leftExpr, op, None)
+        if not self.match(_fold_operator_re):
+            self.fail("Expected fold operator or ')' after '...' in fold expression.")
+        if op != self.matched_text:
+            self.fail("Operators are different in binary fold: '%s' and '%s'."
+                      % (op, self.matched_text))
+        rightExpr = self._parse_cast_expression()
         self.skip_ws()
         if not self.skip_string(')'):
-            self.fail("Expected ')' in end of fold expression or parenthesized expression.")
-        return ASTParenExpr(res)
+            self.fail("Expected ')' to end binary fold expression.")
+        return ASTFoldExpr(leftExpr, op, rightExpr)
 
     def _parse_primary_expression(self):
         # literal
@@ -4938,7 +4994,7 @@ def _parse_cast_expression(self):
             try:
                 typ = self._parse_type(False)
                 if not self.skip_string(')'):
-                    raise DefinitionError("Expected ')' in cast expression.")
+                    self.fail("Expected ')' in cast expression.")
                 expr = self._parse_cast_expression()
                 return ASTCastExpr(typ, expr)
             except DefinitionError as exCast:
@@ -6354,7 +6410,7 @@ def handle_signature(self, sig, signode):
             # Assume we are actually in the old symbol,
             # instead of the newly created duplicate.
             self.env.temp_data['cpp:last_symbol'] = e.symbol
-            self.warn("Duplicate declaration.")
+            self.warn("Duplicate declaration, %s" % sig)
 
         if ast.objectType == 'enumerator':
             self._add_enumerator_to_parent(ast)


Test patch:
diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py
index f059b32e5..0e35c0045 100644
--- a/tests/test_domain_cpp.py
+++ b/tests/test_domain_cpp.py
@@ -106,9 +106,12 @@ def makeIdV2():
 
 
 def test_expressions():
-    def exprCheck(expr, id):
+    def exprCheck(expr, id, id4=None):
         ids = 'IE1CIA%s_1aE'
-        check('class', 'template<> C<a[%s]>' % expr, {2: ids % expr, 3: ids % id})
+        idDict = {2: ids % expr, 3: ids % id}
+        if id4 is not None:
+            idDict[4] = ids % id4
+        check('class', 'template<> C<a[%s]>' % expr, idDict)
     # primary
     exprCheck('nullptr', 'LDnE')
     exprCheck('true', 'L1E')
@@ -155,7 +158,9 @@ def exprCheck(expr, id):
             exprCheck(p + "'\\U0001F34C'", t + "127820")
 
     # TODO: user-defined lit
-    exprCheck('(... + Ns)', '(... + Ns)')
+    exprCheck('(... + Ns)', '(... + Ns)', id4='flpl2Ns')
+    exprCheck('(Ns + ...)', '(Ns + ...)', id4='frpl2Ns')
+    exprCheck('(Ns + ... + 0)', '(Ns + ... + 0)', id4='fLpl2NsL0E')
     exprCheck('(5)', 'L5E')
     exprCheck('C', '1C')
     # postfix
@@ -244,7 +249,7 @@ def exprCheck(expr, id):
     # a < expression that starts with something that could be a template
     exprCheck('A < 42', 'lt1AL42E')
     check('function', 'template<> void f(A<B, 2> &v)',
-          {2: "IE1fR1AI1BX2EE", 3: "IE1fR1AI1BXL2EEE"})
+          {2: "IE1fR1AI1BX2EE", 3: "IE1fR1AI1BXL2EEE", 4: "IE1fvR1AI1BXL2EEE"})
     exprCheck('A<1>::value', 'N1AIXL1EEE5valueE')
     check('class', "template<int T = 42> A", {2: "I_iE1A"})
     check('enumerator', 'A = std::numeric_limits<unsigned long>::max()', {2: "1A"})
@@ -428,7 +433,7 @@ def test_function_definitions():
     check('function', 'virtual void f()', {1: "f", 2: "1fv"})
     # test for ::nestedName, from issue 1738
     check("function", "result(int val, ::std::error_category const &cat)",
-          {1: "result__i.std::error_categoryCR", 2: "6resultiRNSt14error_categoryE"})
+          {1: "result__i.std::error_categoryCR", 2: "6resultiRKNSt14error_categoryE"})
     check("function", "int *f()", {1: "f", 2: "1fv"})
     # tests derived from issue #1753 (skip to keep sanity)
     check("function", "f(int (&array)[10])", {2: "1fRA10_i", 3: "1fRAL10E_i"})
@@ -479,6 +484,10 @@ def test_function_definitions():
     check('function', 'int C::* f(int, double)', {2: '1fid'})
     check('function', 'void f(int C::* *)', {2: '1fPM1Ci'})
 
+    # exceptions from return type mangling
+    check('function', 'template<typename T> C()', {2: 'I0E1Cv'})
+    check('function', 'template<typename T> operator int()', {1: None, 2: 'I0Ecviv'})
+
 
 def test_operators():
     check('function', 'void operator new [  ] ()',
@@ -558,7 +567,7 @@ def test_templates():
     check('class', "A<T>", {2: "IE1AI1TE"}, output="template<> A<T>")
     # first just check which objects support templating
     check('class', "template<> A", {2: "IE1A"})
-    check('function', "template<> void A()", {2: "IE1Av"})
+    check('function', "template<> void A()", {2: "IE1Av", 4: "IE1Avv"})
     check('member', "template<> A a", {2: "IE1a"})
     check('type', "template<> a = A", {2: "IE1a"})
     with pytest.raises(DefinitionError):
@@ -596,6 +605,10 @@ def test_templates():
           "std::basic_ostream<Char, Traits> &os, "
           "const c_string_view_base<const Char, Traits> &str)",
           {2: "I00ElsRNSt13basic_ostreamI4Char6TraitsEE"
+              "RK18c_string_view_baseIK4Char6TraitsE",
+           4: "I00Els"
+              "RNSt13basic_ostreamI4Char6TraitsEE"
+              "RNSt13basic_ostreamI4Char6TraitsEE"
               "RK18c_string_view_baseIK4Char6TraitsE"})
 
     # template introductions
@@ -619,9 +632,11 @@ def test_templates():
     check('type', 'abc::ns::foo{id_0, id_1, ...id_2} xyz::bar = ghi::qux',
           {2: 'I00DpEXN3abc2ns3fooEI4id_04id_1sp4id_2EEN3xyz3barE'})
     check('function', 'abc::ns::foo{id_0, id_1, id_2} void xyz::bar()',
-          {2: 'I000EXN3abc2ns3fooEI4id_04id_14id_2EEN3xyz3barEv'})
+          {2: 'I000EXN3abc2ns3fooEI4id_04id_14id_2EEN3xyz3barEv',
+           4: 'I000EXN3abc2ns3fooEI4id_04id_14id_2EEN3xyz3barEvv'})
     check('function', 'abc::ns::foo{id_0, id_1, ...id_2} void xyz::bar()',
-          {2: 'I00DpEXN3abc2ns3fooEI4id_04id_1sp4id_2EEN3xyz3barEv'})
+          {2: 'I00DpEXN3abc2ns3fooEI4id_04id_1sp4id_2EEN3xyz3barEv',
+           4: 'I00DpEXN3abc2ns3fooEI4id_04id_1sp4id_2EEN3xyz3barEvv'})
     check('member', 'abc::ns::foo{id_0, id_1, id_2} ghi::qux xyz::bar',
           {2: 'I000EXN3abc2ns3fooEI4id_04id_14id_2EEN3xyz3barE'})
     check('member', 'abc::ns::foo{id_0, id_1, ...id_2} ghi::qux xyz::bar',
@@ -648,7 +663,8 @@ def test_template_args():
           "template<typename F> "
           "void allow(F *f, typename func<F, B, G != 1>::type tt)",
           {2: "I0E5allowP1FN4funcI1F1BXG != 1EE4typeE",
-           3: "I0E5allowP1FN4funcI1F1BXne1GL1EEE4typeE"})
+           3: "I0E5allowP1FN4funcI1F1BXne1GL1EEE4typeE",
+           4: "I0E5allowvP1FN4funcI1F1BXne1GL1EEE4typeE"})
     # from #3542
     check('type', "template<typename T> "
           "enable_if_not_array_t = std::enable_if_t<!is_array<T>::value, int>",