Просмотр исходного кода

Merge topic 'signature-refs'

cc21d0e478 Utilities/Sphinx: Make signatures linkable
37e015d4a6 Utilities/Sphinx: Refactor Sphinx reference recording

Acked-by: Kitware Robot <[email protected]>
Merge-request: !8305
Brad King 3 лет назад
Родитель
Сommit
9db40bec4e
2 измененных файлов с 71 добавлено и 31 удалено
  1. 3 3
      Help/dev/documentation.rst
  2. 68 28
      Utilities/Sphinx/cmake.py

+ 3 - 3
Help/dev/documentation.rst

@@ -270,8 +270,7 @@ The ``signature`` directive requires one argument, the signature summary:
   abbreviate it in the ``signature`` directive argument and specify the full
   abbreviate it in the ``signature`` directive argument and specify the full
   signature in a ``code-block`` in the description.
   signature in a ``code-block`` in the description.
 
 
-The ``signature`` directive generates a document-local hyperlink target
-for each signature:
+The ``signature`` directive generates a hyperlink target for each signature:
 
 
 * Default target names are automatically extracted from leading "keyword"
 * Default target names are automatically extracted from leading "keyword"
   arguments in the signatures, where a keyword is any sequence of
   arguments in the signatures, where a keyword is any sequence of
@@ -299,7 +298,8 @@ for each signature:
 
 
 * The targets may be referenced from within the same document using
 * The targets may be referenced from within the same document using
   ```REF`_`` or ```TEXT <REF_>`_`` syntax.  Like reStructuredText section
   ```REF`_`` or ```TEXT <REF_>`_`` syntax.  Like reStructuredText section
-  headers, the targets do not work with Sphinx ``:ref:`` syntax.
+  headers, the targets do not work with Sphinx ``:ref:`` syntax, however
+  they can be globally referenced using e.g. ``:command:`string(APPEND)```.
 
 
 The directive treats its content as the documentation of the signature(s).
 The directive treats its content as the documentation of the signature(s).
 Indent the signature documentation accordingly.
 Indent the signature documentation accordingly.

+ 68 - 28
Utilities/Sphinx/cmake.py

@@ -4,6 +4,9 @@
 import os
 import os
 import re
 import re
 
 
+from dataclasses import dataclass
+from typing import Any, cast
+
 # Override much of pygments' CMakeLexer.
 # Override much of pygments' CMakeLexer.
 # We need to parse CMake syntax definitions, not CMake code.
 # We need to parse CMake syntax definitions, not CMake code.
 
 
@@ -69,9 +72,11 @@ from sphinx.directives import ObjectDescription, nl_escape_re
 from sphinx.domains import Domain, ObjType
 from sphinx.domains import Domain, ObjType
 from sphinx.roles import XRefRole
 from sphinx.roles import XRefRole
 from sphinx.util.nodes import make_refnode
 from sphinx.util.nodes import make_refnode
-from sphinx.util import ws_re
+from sphinx.util import logging, ws_re
 from sphinx import addnodes
 from sphinx import addnodes
 
 
+logger = logging.getLogger(__name__)
+
 sphinx_before_1_4 = False
 sphinx_before_1_4 = False
 sphinx_before_1_7_2 = False
 sphinx_before_1_7_2 = False
 try:
 try:
@@ -104,6 +109,14 @@ if sphinx_before_1_7_2:
     return new_items
     return new_items
   QtHelpBuilder.build_keywords = new_build_keywords
   QtHelpBuilder.build_keywords = new_build_keywords
 
 
+@dataclass
+class ObjectEntry:
+    docname: str
+    objtype: str
+    node_id: str
+    name: str
+
+
 class CMakeModule(Directive):
 class CMakeModule(Directive):
     required_arguments = 1
     required_arguments = 1
     optional_arguments = 0
     optional_arguments = 0
@@ -205,14 +218,6 @@ _cmake_index_objs = {
     'variable':   _cmake_index_entry('variable'),
     'variable':   _cmake_index_entry('variable'),
     }
     }
 
 
-def _cmake_object_inventory(env, document, line, objtype, targetid):
-    inv = env.domaindata['cmake']['objects']
-    if targetid in inv:
-        document.reporter.warning(
-            'CMake object "%s" also described in "%s".' %
-            (targetid, env.doc2path(inv[targetid][0])), line=line)
-    inv[targetid] = (env.docname, objtype)
-
 class CMakeTransform(Transform):
 class CMakeTransform(Transform):
 
 
     # Run this transform early since we insert nodes we want
     # Run this transform early since we insert nodes we want
@@ -275,8 +280,10 @@ class CMakeTransform(Transform):
             indexnode = addnodes.index()
             indexnode = addnodes.index()
             indexnode['entries'] = [make_index_entry(title, targetid)]
             indexnode['entries'] = [make_index_entry(title, targetid)]
             self.document.insert(0, indexnode)
             self.document.insert(0, indexnode)
+
             # Add to cmake domain object inventory
             # Add to cmake domain object inventory
-            _cmake_object_inventory(env, self.document, 1, objtype, targetid)
+            domain = cast(CMakeDomain, env.get_domain('cmake'))
+            domain.note_object(objtype, targetname, targetid, targetid)
 
 
 class CMakeObject(ObjectDescription):
 class CMakeObject(ObjectDescription):
 
 
@@ -300,8 +307,10 @@ class CMakeObject(ObjectDescription):
             signode['ids'].append(targetid)
             signode['ids'].append(targetid)
             signode['first'] = (not self.names)
             signode['first'] = (not self.names)
             self.state.document.note_explicit_target(signode)
             self.state.document.note_explicit_target(signode)
-            _cmake_object_inventory(self.env, self.state.document,
-                                    self.lineno, self.objtype, targetid)
+
+            domain = cast(CMakeDomain, self.env.get_domain('cmake'))
+            domain.note_object(self.objtype, targetname, targetid, targetid,
+                               location=signode)
 
 
         make_index_entry = _cmake_index_objs.get(self.objtype)
         make_index_entry = _cmake_index_objs.get(self.objtype)
         if make_index_entry:
         if make_index_entry:
@@ -351,7 +360,7 @@ class CMakeSignatureObject(CMakeObject):
 
 
     def add_target_and_index(self, name, sig, signode):
     def add_target_and_index(self, name, sig, signode):
         if name in self.targetnames:
         if name in self.targetnames:
-            targetname = self.targetnames[name].lower()
+            sigargs = self.targetnames[name]
         else:
         else:
             def extract_keywords(params):
             def extract_keywords(params):
                 for p in params:
                 for p in params:
@@ -361,7 +370,8 @@ class CMakeSignatureObject(CMakeObject):
                         return
                         return
 
 
             keywords = extract_keywords(name.split('(')[1].split())
             keywords = extract_keywords(name.split('(')[1].split())
-            targetname = ' '.join(keywords).lower()
+            sigargs = ' '.join(keywords)
+        targetname = sigargs.lower()
         targetid = nodes.make_id(targetname)
         targetid = nodes.make_id(targetname)
 
 
         if targetid not in self.state.document.ids:
         if targetid not in self.state.document.ids:
@@ -370,6 +380,15 @@ class CMakeSignatureObject(CMakeObject):
             signode['first'] = (not self.names)
             signode['first'] = (not self.names)
             self.state.document.note_explicit_target(signode)
             self.state.document.note_explicit_target(signode)
 
 
+            # Register the signature as a command object.
+            command = name.split('(')[0].lower()
+            refname = f'{command}({sigargs})'
+            refid = f'command:{command}({targetname})'
+
+            domain = cast(CMakeDomain, self.env.get_domain('cmake'))
+            domain.note_object('command', name=refname, target_id=refid,
+                               node_id=targetid, location=signode)
+
     def run(self):
     def run(self):
         targets = self.options.get('target')
         targets = self.options.get('target')
         if targets is not None:
         if targets is not None:
@@ -384,19 +403,15 @@ class CMakeXRefRole(XRefRole):
 
 
     # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
     # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
     _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', re.DOTALL)
     _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', re.DOTALL)
-    _re_sub = re.compile(r'^([^()\s]+)\s*\(([^()]*)\)$', re.DOTALL)
+    _re_ref = re.compile(r'^.*\s<\w+([(][\w\s]+[)])?>$', re.DOTALL)
     _re_genex = re.compile(r'^\$<([^<>:]+)(:[^<>]+)?>$', re.DOTALL)
     _re_genex = re.compile(r'^\$<([^<>:]+)(:[^<>]+)?>$', re.DOTALL)
     _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL)
     _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL)
 
 
     def __call__(self, typ, rawtext, text, *args, **keys):
     def __call__(self, typ, rawtext, text, *args, **keys):
-        # Translate CMake command cross-references of the form:
-        #  `command_name(SUB_COMMAND)`
-        # to have an explicit target:
-        #  `command_name(SUB_COMMAND) <command_name>`
         if typ == 'cmake:command':
         if typ == 'cmake:command':
-            m = CMakeXRefRole._re_sub.match(text)
-            if m:
-                text = '%s <%s>' % (text, m.group(1))
+            m = CMakeXRefRole._re_ref.match(text)
+            if m is None:
+                text = f'{text} <{text}>'
         elif typ == 'cmake:genex':
         elif typ == 'cmake:genex':
             m = CMakeXRefRole._re_genex.match(text)
             m = CMakeXRefRole._re_genex.match(text)
             if m:
             if m:
@@ -452,6 +467,10 @@ class CMakeXRefTransform(Transform):
                 # Do not index cross-references to guide sections.
                 # Do not index cross-references to guide sections.
                 continue
                 continue
 
 
+            if objtype == 'command':
+                # Index signature references to their parent command.
+                objname = objname.split('(')[0].lower()
+
             targetnum = env.new_serialno('index-%s:%s' % (objtype, objname))
             targetnum = env.new_serialno('index-%s:%s' % (objtype, objname))
 
 
             targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname)
             targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname)
@@ -518,25 +537,46 @@ class CMakeDomain(Domain):
 
 
     def clear_doc(self, docname):
     def clear_doc(self, docname):
         to_clear = set()
         to_clear = set()
-        for fullname, (fn, _) in self.data['objects'].items():
-            if fn == docname:
+        for fullname, obj in self.data['objects'].items():
+            if obj.docname == docname:
                 to_clear.add(fullname)
                 to_clear.add(fullname)
         for fullname in to_clear:
         for fullname in to_clear:
             del self.data['objects'][fullname]
             del self.data['objects'][fullname]
 
 
     def resolve_xref(self, env, fromdocname, builder,
     def resolve_xref(self, env, fromdocname, builder,
                      typ, target, node, contnode):
                      typ, target, node, contnode):
-        targetid = '%s:%s' % (typ, target)
+        targetid = f'{typ}:{target}'
         obj = self.data['objects'].get(targetid)
         obj = self.data['objects'].get(targetid)
+
+        if obj is None and typ == 'command':
+            # If 'command(args)' wasn't found, try just 'command'.
+            # TODO: remove this fallback? warn?
+            # logger.warning(f'no match for {targetid}')
+            command = target.split('(')[0]
+            targetid = f'{typ}:{command}'
+            obj = self.data['objects'].get(targetid)
+
         if obj is None:
         if obj is None:
             # TODO: warn somehow?
             # TODO: warn somehow?
             return None
             return None
-        return make_refnode(builder, fromdocname, obj[0], targetid,
+
+        return make_refnode(builder, fromdocname, obj.docname, obj.node_id,
                             contnode, target)
                             contnode, target)
 
 
+    def note_object(self, objtype: str, name: str, target_id: str,
+                    node_id: str, location: Any = None):
+        if target_id in self.data['objects']:
+            other = self.data['objects'][target_id].docname
+            logger.warning(
+                f'CMake object {target_id!r} also described in {other!r}',
+                location=location)
+
+        self.data['objects'][target_id] = ObjectEntry(
+            self.env.docname, objtype, node_id, name)
+
     def get_objects(self):
     def get_objects(self):
-        for refname, (docname, type) in self.data['objects'].items():
-            yield (refname, refname, type, docname, refname, 1)
+        for refname, obj in self.data['objects'].items():
+            yield (refname, obj.name, obj.objtype, obj.docname, obj.node_id, 1)
 
 
 def setup(app):
 def setup(app):
     app.add_directive('cmake-module', CMakeModule)
     app.add_directive('cmake-module', CMakeModule)