chiark / gitweb /
doxygen: support friend functions.
authorVladimír Vondruš <mosra@centrum.cz>
Tue, 14 Aug 2018 21:20:28 +0000 (23:20 +0200)
committerVladimír Vondruš <mosra@centrum.cz>
Thu, 13 Sep 2018 14:48:37 +0000 (16:48 +0200)
Not friend classes/structs/unions, in that case Doxygen provides way too
little info to be at all useful.

doc/doxygen.rst
doxygen/dox2html5.py
doxygen/templates/base-class-reference.html
doxygen/test/cpp_friends/Doxyfile [new file with mode: 0644]
doxygen/test/cpp_friends/File.h [new file with mode: 0644]
doxygen/test/cpp_friends/classClass.html [new file with mode: 0644]
doxygen/test/cpp_friends/classTemplate.html [new file with mode: 0644]
doxygen/test/test_cpp.py

index 7789110b5a4d627fe71d1a8f2b1004a6cedf7625..da428dcf4375ed517b631faf8f8012bbb220d1b6 100644 (file)
@@ -238,6 +238,11 @@ amount of generated content for no added value.
 -   Clickable symbols in code snippets. Doxygen has quite a lot of false
     positives while a lot of symbols stay unmatched. I need to find a way
     around that.
+-   Documented friend classes, structs and unions. Doxygen is unable to
+    cross-link the declarations with the definitions.
+-   Proper scoping for friend and related functions/classes/variables etc.
+    Doxygen doesn't provide any namespace scoping for these and at the moment
+    I have no way to deduct that information.
 
 `Configuration`_
 ================
@@ -1267,6 +1272,9 @@ Property                                Description
 :py:`compound.private_funcs`            List of documented private virtual
                                         functions. Set only for classes. See
                                         `Function properties`_ for details.
+:py:`compound.friend_funcs`             List of documented friend functions.
+                                        Set only for classes. See
+                                        `Function properties`_ for details.
 :py:`compound.related`                  List of related non-member symbols. Set
                                         only for classes. See
                                         `Related properties`_ for details.
@@ -1492,9 +1500,9 @@ Property                    Description
 
 The :py:`commpound.funcs`, :py:`compound.public_static_funcs`,
 :py:`compound.public_funcs`, :py:`compound.protected_static_funcs`,
-:py:`compound.protected_funcs`, :py:`compound.private_funcs` and
-:py:`compound.related_funcs` properties contain a list of functions, where
-every item has the following properties:
+:py:`compound.protected_funcs`, :py:`compound.private_funcs`,
+:py:`compound.friend_funcs` and :py:`compound.related_funcs` properties contain
+a list of functions, where every item has the following properties:
 
 .. class:: m-table m-fullwidth
 
index 1bc8a3818aabcacaab916d20657c70b3728f5b7f..c3d68f500922ab48c9fb6f94b923590d7fe08711 100755 (executable)
@@ -1570,7 +1570,7 @@ def parse_typedef(state: State, element: ET.Element):
     return None
 
 def parse_func(state: State, element: ET.Element):
-    assert element.tag == 'memberdef' and element.attrib['kind'] == 'function'
+    assert element.tag == 'memberdef' and element.attrib['kind'] in ['function', 'friend']
 
     func = Empty()
     state.current_definition_url_base, func.base_url, func.id = parse_id(element)
@@ -1579,6 +1579,10 @@ def parse_func(state: State, element: ET.Element):
     func.brief = parse_desc(state, element.find('briefdescription'))
     func.description, templates, params, func.return_value, func.return_values, func.exceptions, search_keywords, func.is_deprecated = parse_func_desc(state, element)
 
+    # Friend functions have friend as type. That's just awful. COME ON.
+    if func.type.startswith('friend '):
+        func.type = func.type[7:]
+
     # Extract function signature to prefix, suffix and various flags. Important
     # things affecting caller such as static or const (and rvalue overloads)
     # are put into signature prefix/suffix, other things to various is_*
@@ -1619,8 +1623,10 @@ def parse_func(state: State, element: ET.Element):
         func.is_pure_virtual = False
     func.suffix = html.escape(signature[signature.rindex(')') + 1:].strip())
     if func.suffix: func.suffix = ' ' + func.suffix
-    func.is_protected = element.attrib['prot'] == 'protected'
-    func.is_private = element.attrib['prot'] == 'private'
+    # Protected / private makes no sense for friend functions
+    if element.attrib['kind'] != 'friend':
+        func.is_protected = element.attrib['prot'] == 'protected'
+        func.is_private = element.attrib['prot'] == 'private'
 
     func.has_template_details, func.templates = parse_template_params(state, element.find('templateparamlist'), templates)
 
@@ -2085,6 +2091,7 @@ def parse_xml(state: State, xml: str):
     compound.protected_vars = []
     compound.private_funcs = []
     compound.related = []
+    compound.friend_funcs = []
     compound.groups = []
     compound.has_enum_details = False
     compound.has_typedef_details = False
@@ -2434,6 +2441,22 @@ def parse_xml(state: State, xml: str):
                     else: # pragma: no cover
                         logging.warning("{}: unknown related <memberdef> kind {}".format(state.current, memberdef.attrib['kind']))
 
+            elif compounddef_child.attrib['kind'] == 'friend':
+                for memberdef in compounddef_child:
+                    # Ignore friend classes. This does not ignore friend
+                    # classes written as `friend Foo;`, those are parsed as
+                    # variables (ugh).
+                    if memberdef.find('type').text in ['friend class', 'friend struct', 'friend union']:
+                        # Print a warning in case these are documented
+                        if (''.join(memberdef.find('briefdescription').itertext()).strip() or ''.join(memberdef.find('detaileddescription').itertext()).strip()):
+                            logging.warning("{}: doxygen is unable to cross-link {}, ignoring, sorry".format(state.current, memberdef.find('definition').text))
+                    # Only friend functions left, hopefully, parse as a func
+                    else:
+                        func = parse_func(state, memberdef)
+                        if func:
+                            compound.friend_funcs += [func]
+                            if func.has_details: compound.has_func_details = True
+
             elif compounddef_child.attrib['kind'] == 'user-defined':
                 list = []
 
@@ -2464,6 +2487,18 @@ def parse_xml(state: State, xml: str):
                         if define:
                             list += [('define', define)]
                             if define.has_details: compound.has_define_details = True
+                    elif memberdef.attrib['kind'] == 'friend':
+                        # Ignore friend classes. This does not ignore friend
+                        # classes written as `friend Foo;`, those are parsed as
+                        # variables (ugh).
+                        if memberdef.find('type').text in ['friend class', 'friend struct', 'friend union'] and (memberdef.find('briefdescription').text or memberdef.find('detaileddescription').text):
+                            logging.warning("{}: doxygen is unable to cross-link {}, ignoring, sorry".format(state.current, memberdef.find('definition').text))
+                        # Only friend functions left, hopefully, parse as a func
+                        else:
+                            func = parse_func(state, memberdef)
+                            if func:
+                                list += [('func', func)]
+                                if func.has_details: compound.has_func_details = True
                     else: # pragma: no cover
                         logging.warning("{}: unknown user-defined <memberdef> kind {}".format(state.current, memberdef.attrib['kind']))
 
index c94ba64b86853208eeaf79d0d12ae7a086d31fec..f80af0cba68681c6d53e45f4cbdd09125dbaf0f6 100644 (file)
@@ -41,7 +41,7 @@
           </tbody>
         </table>
         {% endif %}
-        {% if compound.sections or compound.public_types or compound.public_static_funcs or compound.typeless_funcs or compound.public_funcs or compound.public_static_vars or compound.public_vars or compound.protected_types or compound.protected_static_funcs or compound.protected_funcs or compound.protected_static_vars or compound.protected_vars or compound.private_funcs or compound.groups or compound.relate %}
+        {% if compound.sections or compound.public_types or compound.public_static_funcs or compound.typeless_funcs or compound.public_funcs or compound.public_static_vars or compound.public_vars or compound.protected_types or compound.protected_static_funcs or compound.protected_funcs or compound.protected_static_vars or compound.protected_vars or compound.private_funcs or compound.groups or compound.friend_funcs or compound.related %}
         <div class="m-block m-default">
           <h3>Contents</h3>
           <ul>
                 {% for group in compound.groups %}
                 <li><a href="#{{ group.id }}">{{ group.name }}</a></li>
                 {% endfor %}
+                {% if compound.friend_funcs %}
+                <li><a href="#friends">Friends</a></li>
+                {% endif %}
                 {% if compound.related %}
                 <li><a href="#related">Related</a></li>
                 {% endif %}
           </dl>
         </section>
         {% endfor %}
+        {% if compound.friend_funcs %}
+        <section id="friends">
+          <h2><a href="#friends">Friends</a></h2>
+          <dl class="m-dox">
+            {% for func in compound.friend_funcs %}
+{{ entry_func(func) }}
+            {% endfor %}
+          </dl>
+        </section>
+        {% endif %}
         {% if compound.related %}
         <section id="related">
           <h2><a href="#related">Related</a></h2>
           {% endif %}
           {% endfor %}
           {% endfor %}
+          {% for func in compound.friend_funcs %}
+          {% if func.has_details %}
+{{ details_func(func, '') }}
+          {% endif %}
+          {% endfor %}
           {% for kind, member in compound.related %}
           {% if kind == 'func' and member.has_details %}
 {{ details_func(member, '') }}
diff --git a/doxygen/test/cpp_friends/Doxyfile b/doxygen/test/cpp_friends/Doxyfile
new file mode 100644 (file)
index 0000000..162364c
--- /dev/null
@@ -0,0 +1,14 @@
+INPUT                   = File.h
+AUTOLINK_SUPPORT        = NO
+QUIET                   = YES
+GENERATE_HTML           = NO
+GENERATE_LATEX          = NO
+GENERATE_XML            = YES
+XML_PROGRAMLISTING      = NO
+
+##! M_PAGE_FINE_PRINT   =
+##! M_THEME_COLOR       =
+##! M_FAVICON           =
+##! M_LINKS_NAVBAR1     =
+##! M_LINKS_NAVBAR2     =
+##! M_SEARCH_DISABLED   = YES
diff --git a/doxygen/test/cpp_friends/File.h b/doxygen/test/cpp_friends/File.h
new file mode 100644 (file)
index 0000000..775d580
--- /dev/null
@@ -0,0 +1,57 @@
+/** @file
+ * @brief A file
+ */
+
+/**
+@brief A friend class
+
+Not displayed among @ref Class friends becase Doxygen doesn't provide any
+useful info for it.
+*/
+class FriendClassWarning {};
+
+/**
+@brief A friend class
+
+Not displayed among @ref Class friends becase Doxygen doesn't provide any
+useful info for it.
+*/
+class GroupedFriendClassWarning {};
+
+/** @brief A class */
+class Class {
+    public:
+        /* Ignored */
+        friend class FriendClass;
+        friend struct FriendStruct;
+        friend union FriendUnion;
+
+        /** @brief Ignored friend class with a warning because it has docs */
+        friend class FriendClassWarning;
+
+        /** @brief A friend function */
+        friend void friendFunction(int a, void* b);
+
+        /** @{ @name Group with friend functions */
+
+        /** @brief Ignored friend class with a warning because it has docs */
+        friend class GroupedFriendClassWarning;
+
+        /** @brief A friend grouped function */
+        friend void friendGroupedFunction();
+
+        /*@}*/
+};
+
+/** @brief Class with template parameters */
+template<class T, class U = void, class = int> class Template {
+    protected: /* Shouldn't matter */
+        /**
+         * @brief Friend function
+         * @tparam V This is a V
+         *
+         * This is broken in doxygen itself as it doesn't include any scope
+         * from the class.
+         */
+        template<class V> friend void foobar();
+};
diff --git a/doxygen/test/cpp_friends/classClass.html b/doxygen/test/cpp_friends/classClass.html
new file mode 100644 (file)
index 0000000..86211ef
--- /dev/null
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>Class class | My Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+doxygen.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          Class <span class="m-thin">class</span>
+        </h1>
+        <p>A class.</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#group-with-friend-functions">Group with friend functions</a></li>
+                <li><a href="#friends">Friends</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="group-with-friend-functions">
+          <h2><a href="#group-with-friend-functions">Group with friend functions</a></h2>
+          <dl class="m-dox">
+            <dt>
+              <span class="m-dox-wrap-bumper">void <a href="#ab0183d8a208fd1bc6842dcb37da48eff" class="m-dox-self" name="ab0183d8a208fd1bc6842dcb37da48eff">friendGroupedFunction</a>(</span><span class="m-dox-wrap">)</span>
+            </dt>
+            <dd>A friend grouped function.</dd>
+          </dl>
+        </section>
+        <section id="friends">
+          <h2><a href="#friends">Friends</a></h2>
+          <dl class="m-dox">
+            <dt>
+              <span class="m-dox-wrap-bumper">void <a href="#a70135398344d1e7003e0877018f73c4b" class="m-dox-self" name="a70135398344d1e7003e0877018f73c4b">friendFunction</a>(</span><span class="m-dox-wrap">int a,
+              void* b)</span>
+            </dt>
+            <dd>A friend function.</dd>
+          </dl>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
diff --git a/doxygen/test/cpp_friends/classTemplate.html b/doxygen/test/cpp_friends/classTemplate.html
new file mode 100644 (file)
index 0000000..596cbb7
--- /dev/null
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <title>Template class | My Project</title>
+  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400i,600,600i%7CSource+Code+Pro:400,400i,600" />
+  <link rel="stylesheet" href="m-dark+doxygen.compiled.css" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+</head>
+<body>
+<header><nav id="navigation">
+  <div class="m-container">
+    <div class="m-row">
+      <a href="index.html" id="m-navbar-brand" class="m-col-t-8 m-col-m-none m-left-m">My Project</a>
+    </div>
+  </div>
+</nav></header>
+<main><article>
+  <div class="m-container m-container-inflatable">
+    <div class="m-row">
+      <div class="m-col-l-10 m-push-l-1">
+        <h1>
+          <div class="m-dox-template">template&lt;class T, class U = void, class = int&gt;</div>
+          Template <span class="m-thin">class</span>
+        </h1>
+        <p>Class with template parameters.</p>
+        <div class="m-block m-default">
+          <h3>Contents</h3>
+          <ul>
+            <li>
+              Reference
+              <ul>
+                <li><a href="#friends">Friends</a></li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+        <section id="friends">
+          <h2><a href="#friends">Friends</a></h2>
+          <dl class="m-dox">
+            <dt>
+              <div class="m-dox-template">template&lt;class V&gt;</div>
+              <span class="m-dox-wrap-bumper">void <a href="#abd44b6f85f143be1e8b81e5ee25ef62c" class="m-dox">foobar</a>(</span><span class="m-dox-wrap">)</span>
+            </dt>
+            <dd>Friend function.</dd>
+          </dl>
+        </section>
+        <section>
+          <h2>Function documentation</h2>
+          <section class="m-dox-details" id="abd44b6f85f143be1e8b81e5ee25ef62c"><div>
+            <h3>
+              <div class="m-dox-template">
+                template&lt;class T, class U, class _3&gt;
+                template&lt;class V&gt;
+              </div>
+              <span class="m-dox-wrap-bumper">void </span><span class="m-dox-wrap"><span class="m-dox-wrap-bumper"><a href="#abd44b6f85f143be1e8b81e5ee25ef62c" class="m-dox-self">foobar</a>(</span><span class="m-dox-wrap">)</span></span>
+            </h3>
+            <p>Friend function.</p>
+            <table class="m-table m-fullwidth m-flat">
+              <thead>
+                <tr><th colspan="2">Template parameters</th></tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <td style="width: 1%">V</td>
+                  <td>This is a V</td>
+                </tr>
+              </tbody>
+            </table>
+<p>This is broken in doxygen itself as it doesn&#x27;t include any scope from the class.</p>
+          </div></section>
+        </section>
+      </div>
+    </div>
+  </div>
+</article></main>
+</body>
+</html>
index d88db18ff1d82a6cc5d988c3b65b6ae02c6f02df..44aca62d6a620444a9935fe1c0fef5f13fb2c6f5 100644 (file)
@@ -61,3 +61,14 @@ class Derived(IntegrationTestCase):
         self.assertEqual(*self.actual_expected_contents('classNamespace_1_1VirtualBase.html'))
         self.assertEqual(*self.actual_expected_contents('classBaseOutsideANamespace.html'))
         self.assertEqual(*self.actual_expected_contents('classDerivedOutsideANamespace.html'))
+
+class Friends(IntegrationTestCase):
+    def __init__(self, *args, **kwargs):
+        super().__init__(__file__, 'friends', *args, **kwargs)
+
+    @unittest.skipUnless(LooseVersion(doxygen_version()) > LooseVersion("1.8.13"),
+                         "1.8.13 produces invalid XML for friend declarations")
+    def test(self):
+        self.run_dox2html5(wildcard='class*.xml')
+        self.assertEqual(*self.actual_expected_contents('classClass.html'))
+        self.assertEqual(*self.actual_expected_contents('classTemplate.html'))