[gi-docgen/check-command] Add a "check" command




commit c1d272cd07217c600f0fdc71243d3ae4d5736484
Author: Emmanuele Bassi <ebassi gnome org>
Date:   Thu Jun 3 09:45:41 2021 +0100

    Add a "check" command
    
    We want to catch undocumented API before we merge it. The introspection
    scanner can fill in the defaults, and this might not cause warnings; but
    the documentation is another matter entirely.
    
    Right now, we check only for missing documentation. In the future, we
    should add more checks, like:
    
     - property and signal annotations
     - invalid links
     - legacy gtk-doc content
       - `|[ ... ]|` code blocks
       - `[][]` internal links
       - `%` and `#` sigils
    
    Fixes: #51

 docs/gi-docgen.1      |  18 ++++
 docs/tools/check.rst  |  32 ++++++
 docs/tools/index.rst  |   6 +-
 gidocgen/gdcheck.py   | 280 ++++++++++++++++++++++++++++++++++++++++++++++++++
 gidocgen/gidocmain.py |   6 +-
 5 files changed, 340 insertions(+), 2 deletions(-)
---
diff --git a/docs/gi-docgen.1 b/docs/gi-docgen.1
index 36fc14e..1d64cbd 100644
--- a/docs/gi-docgen.1
+++ b/docs/gi-docgen.1
@@ -120,6 +120,24 @@ be used multiple times to specify multiple directories
 .B \-\-dry\-run
 parse the \fIGIR_FILE\fR without generating the documentation
 
+.SH The check command
+.sp
+The \fBcheck\fR command runs a series of checks on the introspection
+file, to verify that public API is properly documented. It can be used
+as part of a test suite.
+
+.B gi-docgen check [
+.I OPTIONS
+.B ] [
+.I GIR_FILE
+.B ]
+
+.SS "options:"
+.TP
+.BI \-\-add\-include\-path\fB= PATH
+add a directory to the path which the scanner uses to find GIR files. Can
+be used multiple times to specify multiple directories
+
 .SH The help command
 .sp
 The \fBhelp\fR command prints out the command line help. If you don't
diff --git a/docs/tools/check.rst b/docs/tools/check.rst
new file mode 100644
index 0000000..f1e7bc4
--- /dev/null
+++ b/docs/tools/check.rst
@@ -0,0 +1,32 @@
+.. SPDX-FileCopyrightText: 2021 GNOME Foundation
+..
+.. SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later
+
+===============
+gi-docgen check
+===============
+
+Check the documentation in the introspection data
+-------------------------------------------------
+
+SYNOPSIS
+========
+
+**gi-docgen check** [OPTIONS...] [GIRFILE]
+
+DESCRIPTION
+===========
+
+The **check** command runs a series of checks on the introspection
+file, to verify that public API is properly documented. It can be used 
+as part of a test suite.
+
+OPTIONS
+=======
+
+``--add-include--path DIR``
+  Adds ``DIR`` to the list of paths used to find introspection data
+  files included in the given ``GIRFILE``. The default search path
+  for GIR files is ``$XDG_DATA_DIRS/gir-1.0`` and ``$XDG_DATA_HOME/gir-1.0``;
+  this option is typically used to include uninstalled GIR files, or
+  non-standard locations.
diff --git a/docs/tools/index.rst b/docs/tools/index.rst
index e52da63..5508dc2 100644
--- a/docs/tools/index.rst
+++ b/docs/tools/index.rst
@@ -13,6 +13,7 @@ Commands
   
     generate
     gen-index
+    check
 
 SYNOPSIS
 ========
@@ -30,7 +31,10 @@ COMMANDS
   
 :doc:`gen-index`
   Generates the symbol indices for search
-  
+
+:doc:`check`
+  Checks the documentation
+
 OPTIONS
 =======
 
diff --git a/gidocgen/gdcheck.py b/gidocgen/gdcheck.py
new file mode 100644
index 0000000..0a5fce7
--- /dev/null
+++ b/gidocgen/gdcheck.py
@@ -0,0 +1,280 @@
+# SPDX-FileCopyrightText: 2021 GNOME Foundation
+# SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later
+
+import argparse
+import sys
+
+from . import gir, log, utils
+
+
+HELP_MSG = "Checks introspection data for valid documentation"
+
+
+def _check_doc_element(path, symbol, results):
+    name = '.'.join(path + [symbol.name])
+
+    if symbol.source_position is not None:
+        filename = symbol.source_position[0]
+        line = symbol.source_position[1]
+    else:
+        filename = "<unknown>"
+        line = 0
+
+    if symbol.doc is None:
+        results.append(f"Symbol '{name}' at {filename}:{line} is not documented")
+
+
+def _check_arg_docs(path, arguments, results):
+    symbol = '.'.join(path)
+    for arg in arguments:
+        if arg.doc is None:
+            results.append(f"Parameter '{arg.name}' of symbol '{symbol}' is not documented")
+
+
+def _check_retval_docs(path, retval, results):
+    if retval is None:
+        return
+
+    if isinstance(retval.target, gir.VoidType):
+        return
+
+    symbol = '.'.join(path)
+    if retval.doc is None:
+        results.append(f"Return value for symbol '{symbol}' is not documented")
+
+
+def _check_aliases(repository, symbols, results):
+    for alias in symbols:
+        _check_doc_element([repository.namespace.name], alias, results)
+
+
+def _check_bitfields(repository, symbols, results):
+    for bitfield in symbols:
+        _check_doc_element([repository.namespace.name], bitfield, results)
+
+        for member in bitfield.members:
+            _check_doc_element([repository.namespace.name, bitfield.name], member, results)
+
+        for func in bitfield.functions:
+            _check_doc_element([repository.namespace.name, bitfield.name], func, results)
+            _check_arg_docs([repository.namespace.name, bitfield.name, func.name], func.parameters, results)
+            _check_retval_docs([repository.namespace.name, bitfield.name, func.name], func.return_value, 
results)
+
+
+def _check_callbacks(repository, symbols, results):
+    for cb in symbols:
+        _check_doc_element([repository.namespace.name], cb, results)
+        _check_arg_docs([repository.namespace.name, cb.name], cb.parameters, results)
+        _check_retval_docs([repository.namespace.name, cb.name], cb.return_value, results)
+
+
+def _check_classes(repository, symbols, results):
+    for cls in symbols:
+        _check_doc_element([repository.namespace.name], cls, results)
+
+        for ctor in cls.constructors:
+            _check_doc_element([repository.namespace.name, cls.name], ctor, results)
+            _check_arg_docs([repository.namespace.name, cls.name, ctor.name], ctor.parameters, results)
+            _check_retval_docs([repository.namespace.name, cls.name, ctor.name], ctor.return_value, results)
+
+        for method in cls.methods:
+            _check_doc_element([repository.namespace.name, cls.name], method, results)
+            _check_arg_docs([repository.namespace.name, cls.name, method.name], method.parameters, results)
+            _check_retval_docs([repository.namespace.name, cls.name, method.name], method.return_value, 
results)
+
+        for func in cls.functions:
+            _check_doc_element([repository.namespace.name, cls.name], func, results)
+            _check_arg_docs([repository.namespace.name, cls.name, func.name], func.parameters, results)
+            _check_retval_docs([repository.namespace.name, cls.name, func.name], func.return_value, results)
+
+        for prop in cls.properties.values():
+            _check_doc_element([repository.namespace.name, cls.name], prop, results)
+
+        for signal in cls.signals.values():
+            _check_doc_element([repository.namespace.name, cls.name], signal, results)
+            _check_arg_docs([repository.namespace.name, cls.name, signal.name], signal.parameters, results)
+            _check_retval_docs([repository.namespace.name, cls.name, signal.name], signal.return_value, 
results)
+
+
+def _check_constants(repository, symbols, results):
+    for constant in symbols:
+        _check_doc_element([repository.namespace.name], constant, results)
+
+
+def _check_domains(repository, symbols, results):
+    for domain in symbols:
+        _check_doc_element([repository.namespace.name], domain, results)
+
+        for member in domain.members:
+            _check_doc_element([repository.namespace.name, domain.name], member, results)
+
+        for func in domain.functions:
+            _check_doc_element([repository.namespace.name, domain.name], func, results)
+            _check_arg_docs([repository.namespace.name, domain.name, func.name], func.parameters, results)
+            _check_retval_docs([repository.namespace.name, domain.name, func.name], func.return_value, 
results)
+
+
+def _check_enums(repository, symbols, results):
+    for enum in symbols:
+        _check_doc_element([repository.namespace.name], enum, results)
+
+        for member in enum.members:
+            _check_doc_element([repository.namespace.name, enum.name], member, results)
+
+        for func in enum.functions:
+            _check_doc_element([repository.namespace.name, enum.name], func, results)
+            _check_arg_docs([repository.namespace.name, enum.name, func.name], func.parameters, results)
+            _check_retval_docs([repository.namespace.name, enum.name, func.name], func.return_value, results)
+
+
+def _check_functions(repository, symbols, results):
+    for func in symbols:
+        _check_doc_element([repository.namespace.name], func, results)
+        _check_arg_docs([repository.namespace.name, func.name], func.parameters, results)
+        _check_retval_docs([repository.namespace.name, func.name], func.return_value, results)
+
+
+def _check_function_macros(repository, symbols, results):
+    for func in symbols:
+        _check_doc_element([repository.namespace.name], func, results)
+        _check_arg_docs([repository.namespace.name, func.name], func.parameters, results)
+        _check_retval_docs([repository.namespace.name, func.name], func.return_value, results)
+
+
+def _check_interfaces(repository, symbols, results):
+    for iface in symbols:
+        _check_doc_element([repository.namespace.name], iface, results)
+
+        for method in iface.methods:
+            _check_doc_element([repository.namespace.name, iface.name], method, results)
+            _check_arg_docs([repository.namespace.name, iface.name, method.name], method.parameters, results)
+            _check_retval_docs([repository.namespace.name, iface.name, method.name], method.return_value, 
results)
+
+        for func in iface.functions:
+            _check_doc_element([repository.namespace.name, iface.name], func, results)
+            _check_arg_docs([repository.namespace.name, iface.name, func.name], func.parameters, results)
+            _check_retval_docs([repository.namespace.name, iface.name, func.name], func.return_value, 
results)
+
+        for prop in iface.properties.values():
+            _check_doc_element([repository.namespace.name, iface.name], prop, results)
+
+        for signal in iface.signals.values():
+            _check_doc_element([repository.namespace.name, iface.name], signal, results)
+            _check_arg_docs([repository.namespace.name, iface.name, signal.name], signal.parameters, results)
+            _check_retval_docs([repository.namespace.name, iface.name, signal.name], signal.return_value, 
results)
+
+
+def _check_records(repository, symbols, results):
+    for struct in symbols:
+        _check_doc_element([repository.namespace.name], struct, results)
+
+        for ctor in struct.constructors:
+            _check_doc_element([repository.namespace.name, struct.name], ctor, results)
+            _check_arg_docs([repository.namespace.name, struct.name, ctor.name], ctor.parameters, results)
+            _check_retval_docs([repository.namespace.name, struct.name, ctor.name], ctor.return_value, 
results)
+
+        for method in struct.methods:
+            _check_doc_element([repository.namespace.name, struct.name], method, results)
+            _check_arg_docs([repository.namespace.name, struct.name, method.name], method.parameters, 
results)
+            _check_retval_docs([repository.namespace.name, struct.name, method.name], method.return_value, 
results)
+
+        for func in struct.functions:
+            _check_doc_element([repository.namespace.name, struct.name], func, results)
+            _check_arg_docs([repository.namespace.name, struct.name, func.name], func.parameters, results)
+            _check_retval_docs([repository.namespace.name, struct.name, func.name], func.return_value, 
results)
+
+
+def _check_unions(repository, symbols, results):
+    for union in symbols:
+        _check_doc_element([repository.namespace.name], union, results)
+
+        for ctor in union.constructors:
+            _check_doc_element([repository.namespace.name, union.name], ctor, results)
+            _check_arg_docs([repository.namespace.name, union.name, ctor.name], ctor.parameters, results)
+            _check_retval_docs([repository.namespace.name, union.name, ctor.name], ctor.return_value, 
results)
+
+        for method in union.methods:
+            _check_doc_element([repository.namespace.name, union.name], method, results)
+            _check_arg_docs([repository.namespace.name, union.name, method.name], method.parameters, results)
+            _check_retval_docs([repository.namespace.name, union.name, method.name], method.return_value, 
results)
+
+        for func in union.functions:
+            _check_doc_element([repository.namespace.name, union.name], func, results)
+            _check_arg_docs([repository.namespace.name, union.name, func.name], func.parameters, results)
+            _check_retval_docs([repository.namespace.name, union.name, func.name], func.return_value, 
results)
+
+
+def check(repository):
+    namespace = repository.namespace
+
+    symbols = {
+        "aliases": sorted(namespace.get_aliases(), key=lambda alias: alias.name.lower()),
+        "bitfields": sorted(namespace.get_bitfields(), key=lambda bitfield: bitfield.name.lower()),
+        "callbacks": sorted(namespace.get_callbacks(), key=lambda callback: callback.name.lower()),
+        "classes": sorted(namespace.get_classes(), key=lambda cls: cls.name.lower()),
+        "constants": sorted(namespace.get_constants(), key=lambda const: const.name.lower()),
+        "domains": sorted(namespace.get_error_domains(), key=lambda domain: domain.name.lower()),
+        "enums": sorted(namespace.get_enumerations(), key=lambda enum: enum.name.lower()),
+        "functions": sorted(namespace.get_functions(), key=lambda func: func.name.lower()),
+        "function_macros": sorted(namespace.get_effective_function_macros(), key=lambda func: 
func.name.lower()),
+        "interfaces": sorted(namespace.get_interfaces(), key=lambda interface: interface.name.lower()),
+        "structs": sorted(namespace.get_effective_records(), key=lambda record: record.name.lower()),
+        "unions": sorted(namespace.get_unions(), key=lambda union: union.name.lower()),
+    }
+
+    all_indices = {
+        "aliases": _check_aliases,
+        "bitfields": _check_bitfields,
+        "callbacks": _check_callbacks,
+        "classes": _check_classes,
+        "constants": _check_constants,
+        "domains": _check_domains,
+        "enums": _check_enums,
+        "functions": _check_functions,
+        "function_macros": _check_function_macros,
+        "interfaces": _check_interfaces,
+        "structs": _check_records,
+        "unions": _check_unions,
+    }
+
+    results = []
+
+    # Each section is isolated, so we run it into a thread pool
+    for section in all_indices:
+        checker = all_indices.get(section, None)
+        if checker is None:
+            log.error(f"No checker for section {section}")
+            continue
+
+        s = symbols.get(section, None)
+        if s is None:
+            log.debug(f"No symbols for section {section}")
+            continue
+
+        log.debug(f"Checking symbols for section {section}")
+        checker(repository, s, results)
+
+    for res in results:
+        log.warning(res)
+
+    return len(results) == 0
+
+
+def add_args(parser):
+    parser.add_argument("--add-include-path", action="append", dest="include_paths", default=[],
+                        help="include paths for other GIR files")
+    parser.add_argument("infile", metavar="GIRFILE", type=argparse.FileType('r', encoding='UTF-8'),
+                        default=sys.stdin, help="the GIR file to parse")
+
+
+def run(options):
+    paths = []
+    paths.extend(options.include_paths)
+    paths.extend(utils.default_search_paths())
+    log.info(f"Search paths: {paths}")
+
+    parser = gir.GirParser(search_paths=paths)
+    parser.parse(options.infile)
+
+    log.checkpoint()
+    return check(parser.get_repository())
diff --git a/gidocgen/gidocmain.py b/gidocgen/gidocmain.py
index 679efa6..a3c97f2 100644
--- a/gidocgen/gidocmain.py
+++ b/gidocgen/gidocmain.py
@@ -7,7 +7,7 @@ import sys
 import traceback
 
 from . import core, log
-from . import gdindex, gdgenerate, gdgenindices, gdgendeps, gdsearch
+from . import gdindex, gdgenerate, gdgenindices, gdgendeps, gdsearch, gdcheck
 
 
 class GIDocGenApp:
@@ -50,6 +50,10 @@ class GIDocGenApp:
                          add_args_func=gdsearch.add_args,
                          run_func=gdsearch.run,
                          help_msg=gdsearch.HELP_MSG)
+        self.add_command('check',
+                         add_args_func=gdcheck.add_args,
+                         run_func=gdcheck.run,
+                         help_msg=gdcheck.HELP_MSG)
 
     def run(self, args):
         """


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]