Better include dependency graph generator
authorTeemu Murtola <teemu.murtola@gmail.com>
Sat, 22 Mar 2014 20:16:51 +0000 (22:16 +0200)
committerGerrit Code Review <gerrit@gerrit.gromacs.org>
Sat, 29 Mar 2014 13:52:50 +0000 (14:52 +0100)
Rewrite the include dependency graph generator to use the scripts used
for the doc-check target.  Most of the graph generation code is still
the same as in the old includedeps.py (except for some renaming and
other cleanup), but it now uses the data structures in gmxtree.py as
input instead of custom parsing.  Only real differences in the output
for now:
 - The module graph colors nodes based on the group they are documented
   in.  All modules that don't have explicit documentation are now
   marked as "legacy".
 - The file graphs don't use clusters for subdirectories, but instead
   only color tests differently.  Not sure which one is clearer; the
   answer may even depend on the module.

However, the new code is much cleaner and easier to extend than the old
one was, so improving the graphs further should be much easier now.

The generated graph is also laid out differently, but that may just be
by chance; because of existing cyclic dependencies, dot doesn't produce
stable layouts, and the layout can depend on the order of things in the
generated graph.  Since the internal data structures are now mainly
dicts, stuff is likely in a different order than what it used to be.

doxygen.md now also has a bit more documentation for the graphs.

Change-Id: I0530c8e4278890fdb5b5d874bda6fef3521a704e

admin/includedeps.cmake [deleted file]
admin/includedeps.py [deleted file]
doxygen/CMakeLists.txt
doxygen/doxygen.md
doxygen/doxygenxml.py
doxygen/generateGraphs.cmake [new file with mode: 0644]
doxygen/gmxtree.py
doxygen/graphbuilder.py [new file with mode: 0755]
doxygen/reporter.py

diff --git a/admin/includedeps.cmake b/admin/includedeps.cmake
deleted file mode 100644 (file)
index e7a98a7..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-#
-# This file is part of the GROMACS molecular simulation package.
-#
-# Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
-# Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
-# and including many others, as listed in the AUTHORS file in the
-# top-level source directory and at http://www.gromacs.org.
-#
-# GROMACS is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public License
-# as published by the Free Software Foundation; either version 2.1
-# of the License, or (at your option) any later version.
-#
-# GROMACS is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with GROMACS; if not, see
-# http://www.gnu.org/licenses, or write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
-#
-# If you want to redistribute modifications to GROMACS, please
-# consider that scientific software is very special. Version
-# control is crucial - bugs must be traceable. We will be happy to
-# consider code for inclusion in the official distribution, but
-# derived work must not be called official GROMACS. Details are found
-# in the README & COPYING files - if they are missing, get the
-# official version at http://www.gromacs.org.
-#
-# To help us fund GROMACS development, we humbly ask that you cite
-# the research papers on the package. Check out http://www.gromacs.org.
-
-function (generate_module_file_list SRCDIR OUTFILE MODE)
-    set(_file_list)
-    file(GLOB_RECURSE _file_list
-        ${SRCDIR}/src/gromacs/*.cpp
-        ${SRCDIR}/src/gromacs/*.c
-        ${SRCDIR}/src/gromacs/*.cu
-        ${SRCDIR}/src/gromacs/*.h
-        ${SRCDIR}/src/gromacs/*.cuh
-        ${SRCDIR}/src/testutils/*.cpp
-        ${SRCDIR}/src/testutils/*.h
-        )
-    string(REPLACE ";" "\n" _file_list "${_file_list}")
-    file(WRITE ${OUTFILE} "${_file_list}")
-endfunction ()
-
-function (generate_installed_file_list SRCDIR BUILDDIR OUTFILE)
-    file(GLOB_RECURSE INSTALL_FILE_LIST "${BUILDDIR}/cmake_install.cmake")
-    set(MATCH_REGEX "${SRCDIR}/.*\\.h")
-    set(HEADER_LIST)
-    foreach (INSTALL_FILE ${INSTALL_FILE_LIST})
-        file(STRINGS ${INSTALL_FILE} HEADER_LINES REGEX "${MATCH_REGEX}")
-        foreach (HEADER_LINE ${HEADER_LINES})
-            string (REGEX MATCH "${MATCH_REGEX}" HEADER "${HEADER_LINE}")
-            list(APPEND HEADER_LIST "${HEADER}")
-        endforeach ()
-    endforeach ()
-    string(REPLACE ";" "\n" HEADER_LIST "${HEADER_LIST}")
-    file(WRITE ${OUTFILE} "${HEADER_LIST}")
-endfunction ()
-
-if (NOT DEFINED SRCDIR OR NOT DEFINED BUILDDIR OR NOT DEFINED OUTDIR)
-    message(FATAL_ERROR "Required input variable (SRCDIR, BUILDDIR, OUTDIR) not set")
-endif()
-
-if (NOT DEFINED PYTHON_EXECUTABLE)
-    set(PYTHON_EXECUTABLE python)
-endif()
-
-if (NOT DEFINED MODE)
-    set(MODE "CHECK")
-endif()
-
-if (MODE STREQUAL "CHECK")
-    set(GRAPHOPTIONS --check)
-elseif (MODE STREQUAL "CHECKDOC")
-    # TODO: Add --warn-undoc after most code has at least rudimentary comments.
-    set(GRAPHOPTIONS --check --check-doc)
-elseif (MODE STREQUAL "GRAPHS")
-    set(GRAPHOPTIONS
-        --module-graph module-deps.dot --module-file-graphs
-        -o ${OUTDIR})
-else()
-    message(FATAL_ERROR "Unknown mode ${MODE}")
-endif()
-
-file(MAKE_DIRECTORY ${OUTDIR})
-generate_module_file_list(${SRCDIR} ${OUTDIR}/module-files.txt ${MODE})
-generate_installed_file_list(${SRCDIR} ${BUILDDIR} ${OUTDIR}/installed-headers.txt)
-execute_process(COMMAND ${PYTHON_EXECUTABLE} ${SRCDIR}/admin/includedeps.py
-                        -f ${OUTDIR}/module-files.txt
-                        --installed ${OUTDIR}/installed-headers.txt
-                        -R ${SRCDIR}/src -R ${BUILDDIR}/src
-                        -I ${SRCDIR}/src/gromacs/legacyheaders
-                        -I ${BUILDDIR}/src/gromacs/utility
-                        ${GRAPHOPTIONS})
-
-if (MODE STREQUAL "GRAPHS" AND DOT_EXECUTABLE)
-    file(GLOB DOT_INPUT_FILES ${OUTDIR}/*.dot)
-    execute_process(COMMAND ${DOT_EXECUTABLE} -Tpng -O ${DOT_INPUT_FILES})
-endif()
diff --git a/admin/includedeps.py b/admin/includedeps.py
deleted file mode 100755 (executable)
index 0122aa1..0000000
+++ /dev/null
@@ -1,766 +0,0 @@
-#!/usr/bin/python
-#
-# This file is part of the GROMACS molecular simulation package.
-#
-# Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
-# Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
-# and including many others, as listed in the AUTHORS file in the
-# top-level source directory and at http://www.gromacs.org.
-#
-# GROMACS is free software; you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public License
-# as published by the Free Software Foundation; either version 2.1
-# of the License, or (at your option) any later version.
-#
-# GROMACS is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public
-# License along with GROMACS; if not, see
-# http://www.gnu.org/licenses, or write to the Free Software Foundation,
-# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
-#
-# If you want to redistribute modifications to GROMACS, please
-# consider that scientific software is very special. Version
-# control is crucial - bugs must be traceable. We will be happy to
-# consider code for inclusion in the official distribution, but
-# derived work must not be called official GROMACS. Details are found
-# in the README & COPYING files - if they are missing, get the
-# official version at http://www.gromacs.org.
-#
-# To help us fund GROMACS development, we humbly ask that you cite
-# the research papers on the package. Check out http://www.gromacs.org.
-
-"""Check and generate include dependency graphs for Gromacs.
-
-This script can do a few things related to include file dependencies:
- - Check that there are no broken dependencies between installed headers.
- - Check that documentated usage of a header matches its installation status
-   and usage from other modules.
- - Generate two types of include dependency graphs: per-file or per-module
-   (where module is equivalent to a subdirectory).
-It is intended to be run on a subset of files under the src/ directory.
-Output format for the graphs is suitable for processing with 'dot'.
-
-FILE GRAPHS
-
-The legend for per-file graph nodes:
-    gray:          source files
-    light blue:    public headers
-    dark blue:     library headers
-    no background: other files
-
-MODULE GRAPHS
-
-Module graph will contain one node for each top-level subdirectory under src/,
-except that the src/gromacs/ directory will be expanded one level further.
-Legacy modules have gray background.
-
-The legend for per-module graph links (a link with a certain color indicates
-that types above it in the list are not present):
-    red:          invalid dependency
-    grey:         legacy dependency (dependency on undocumented file, or to
-                  legacy directories)
-    solid black:  public header depends on the other module
-    solid blue:   library header depends on the other module
-    dashed blue:  source file depends on a library header in the other module
-    dashed black: source file depends on a public header in the other module
-    dashed green: test file depends on the other module
-"""
-
-import os.path
-import re
-import sys
-
-from optparse import OptionParser
-
-class ErrorReporter(object):
-    def input_warning(self, file_path, msg):
-        sys.stderr.write('warning: {0}: {1}\n'.format(file_path, msg))
-
-    def error(self, file_path, msg):
-        sys.stderr.write('error: {0}: {1}\n'.format(file_path, msg))
-
-class Link(object):
-
-    """Link between two node objects.
-
-    Signifies an include dependency between the two nodes, and manages types
-    associated with the dependencies.
-    """
-
-    _priorities = {
-            'undocumented': 1,
-            'legacy': 2,
-            'intramodule': 3,
-            'public': 4,
-            'library': 5,
-            'libimpl': 6,
-            'pubimpl': 7,
-            'test': 8}
-
-    def __init__(self, fromnode, tonode, link_type):
-        self.fromnode = fromnode
-        self.tonode = tonode
-        self.link_type = link_type
-        if link_type not in Link._priorities:
-            raise ValueError('Unknown link type {0}'.format(link_type))
-
-    def merge_link(self, other):
-        """Merge another link into this one and choose an appropriate type.
-
-        Updates the type of this link based on the types of the merged links.
-        """
-        if Link._priorities[other.link_type] < Link._priorities[self.link_type]:
-            self.link_type = other.link_type
-
-    def format(self):
-        """Format this link for 'dot'."""
-        if self.fromnode.is_file_node() and self.tonode.is_file_node():
-            properties = ''
-        elif self.link_type == 'intramodule':
-            properties = ''
-        elif self.link_type == 'test':
-            properties = 'color=".33 .8 .8", style=dashed'
-        elif self.link_type == 'libimpl':
-            properties = 'color=".66 .8 .8", style=dashed'
-        elif self.link_type == 'pubimpl':
-            properties = 'color=black, style=dashed'
-        elif self.link_type == 'library':
-            properties = 'color=".66 .8 .8"'
-        elif self.link_type == 'public':
-            properties = 'color=black'
-        elif self.link_type == 'legacy':
-            properties = 'color=grey75'
-        else: # undocumented
-            properties = 'color=red'
-        return '{0} -> {1} [{2}]'.format(self.fromnode.nodename,
-                                         self.tonode.nodename,
-                                         properties)
-
-class Node(object):
-    def __init__(self, nodename, label, properties, is_file):
-        self.nodename = nodename
-        self.label = label
-        self._properties = properties
-        self._is_file = is_file
-        self.children = []
-        self.root = False
-
-    def set_root(self):
-        self.root = True
-
-    def add_child(self, child):
-        self.children.append(child)
-
-    def remove_child(self, child):
-        self.children.remove(child)
-
-    def clear_children(self):
-        self.children = []
-
-    def is_file_node(self):
-        return self._is_file
-
-    def get_children(self, recursive=False):
-        if recursive:
-            result = list(self.children)
-            for child in self.children:
-                result.extend(child.get_children(recursive=True))
-            return result
-        else:
-            return self.children
-
-    def format(self):
-        """Format this node for 'dot'."""
-        result = ''
-        if self.children:
-            if not self.root:
-                result += '    subgraph cluster_{0} {{\n' \
-                              .format(self.nodename)
-                result += '        label = "{0}"\n'.format(self.label)
-            for child in self.children:
-                result += child.format()
-            if not self.root:
-                result += '    }\n'
-        else:
-            properties = 'label="{0}"'.format(self.label)
-            if self._properties:
-                properties += ', ' + self._properties
-            result += '    {0} [{1}]\n'.format(self.nodename, properties)
-        return result
-
-
-class Graph(object):
-    def __init__(self, nodes, links):
-        self.nodes = set(nodes)
-        self.links = links
-        self.left_to_right = False
-        self.concentrate = True
-
-    def set_options(self, left_to_right=None, concentrate=None):
-        if left_to_right != None:
-            self.left_to_right = left_to_right
-        if concentrate != None:
-            self.concentrate = concentrate
-
-    def prune_links(self):
-        nodes = set()
-        for node in self.nodes:
-            nodes.update(node.get_children(recursive=True))
-        newlinks = []
-        for link in self.links:
-            if link.fromnode in nodes and link.tonode in nodes:
-                newlinks.append(link)
-        self.links = newlinks
-
-    def merge_nodes(self, nodes, target):
-        nodes = set(nodes)
-        nodes.add(target)
-        newlinks = []
-        linksto = dict()
-        linksfrom = dict()
-        for link in self.links:
-            isfrom = (link.fromnode in nodes)
-            isto = (link.tonode in nodes)
-            if isfrom and isto:
-                pass
-            elif isfrom:
-                if not link.tonode in linksfrom:
-                    linksfrom[link.tonode] = \
-                            Link(target, link.tonode, link.link_type)
-                else:
-                    linksfrom[link.tonode].merge_link(link)
-            elif isto:
-                if not link.fromnode in linksto:
-                    linksto[link.fromnode] = \
-                            Link(link.fromnode, target, link.link_type)
-                else:
-                    linksto[link.fromnode].merge_link(link)
-            else:
-                newlinks.append(link)
-        newlinks.extend(linksfrom.values())
-        newlinks.extend(linksto.values())
-        self.links = newlinks
-
-    def collapse_node(self, node):
-        nodes = node.get_children(recursive=True)
-        self.merge_nodes(nodes, node)
-        node.clear_children()
-
-    def write(self, outfile):
-        outfile.write('digraph includedeps {\n')
-        if self.left_to_right:
-            outfile.write('    rankdir = LR\n')
-        if self.concentrate:
-            outfile.write('    concentrate = true\n')
-        outfile.write('    node [fontname="FreeSans",fontsize=10,height=.2,'
-                                 'shape=box]\n')
-        for link in self.links:
-            outfile.write('    ' + link.format() + '\n')
-        for node in self.nodes:
-            outfile.write(node.format())
-        outfile.write('}\n')
-
-
-def find_include_file(filename, includedirs):
-    """Find full path to filename, looking in a set of directories."""
-    for includedir in includedirs:
-        fullpath = os.path.abspath(os.path.join(includedir, filename))
-        if os.path.exists(fullpath):
-            return fullpath
-    return None
-
-
-class IncludedFile(object):
-    def __init__(self, included_file, included_path, is_relative, is_system):
-        self._included_file = included_file
-        self._included_path = included_path
-        #self._used_include_path = used_include_path
-        self._is_relative = is_relative
-        self._is_system = is_system
-
-
-class File(object):
-    def __init__(self, path, module):
-        self.path = path
-        self.name = os.path.basename(path)
-        self.module = module
-        if module.name == 'tests' or module.name == 'legacytests':
-            self.type = 'test'
-        elif re.search(r'\.c(pp|u)?$', self.name) != None:
-            self.type = 'source'
-        else:
-            self.type = 'header'
-        self.doctype = 'none'
-        #headername = re.sub(r'\.cpp$', '.h', self.name)
-        #implheadername = re.sub(r'\.cpp$', '-impl.h', self.name)
-        self._included = []
-        self.installed = False
-
-    def is_documented(self):
-        return self.doctype != 'none'
-
-    def is_installed(self):
-        return self.installed
-
-    def set_installed(self, reporter):
-        if self.type != 'header':
-            reporter.input_warning(self.path,
-                    'installing {0} file'.format(self.type))
-            return
-        self.installed = True
-
-    def get_included_files(self):
-        return self._included
-
-    def scan_include_file(self, line, allfiles, selfdir, includedirs,
-            ignorelist, reporter):
-        """Process #include directive during scan().
-
-        Searches for the included file in given directories, does some checks,
-        and adds the dependency link to the other file if applicable.
-        """
-        fullpath = None
-        includedpath = None
-        includedfile = None
-        is_system = False
-        is_relative = False
-        match = re.match(r'#include *<([^>]*)>', line)
-        if match:
-            includedpath = match.group(1)
-            is_system = True
-            fullpath = find_include_file(includedpath, includedirs)
-        else:
-            match = re.match(r'#include *"([^"]*)"', line)
-            if match:
-                includedpath = match.group(1)
-                fullpath = os.path.abspath(os.path.join(selfdir, includedpath))
-                #if os.path.abspath(fullpath) in ignorelist:
-                #    return
-                if os.path.exists(fullpath):
-                    is_relative = True
-                else:
-                    fullpath = find_include_file(includedpath, includedirs)
-                    if not fullpath:
-                        if not includedpath in ('corewrap.h', 'tmpi_config.h'):
-                            reporter.input_warning(self.path,
-                                    'included file "{0}" not found'
-                                        .format(includedpath))
-        if not includedpath:
-            reporter.input_warning(self.path, 'line "{0}" could not be parsed'
-                    .format(line))
-        else:
-            if fullpath and fullpath in allfiles:
-                includedfile = allfiles[fullpath]
-            #elif not dep in ignorelist:
-            #    depfile = File(dep, None)
-            #    files[dep] = depfile
-            #    file.add_dependency(depfile)
-            #    extrafiles.append(dep)
-            self._included.append(IncludedFile(includedfile, includedpath,
-                    is_relative, is_system))
-
-    def scan(self, filename, allfiles, includedirs, ignorelist, reporter):
-        selfdir = os.path.dirname(filename)
-        infileblock = False
-        foundfileblock = False
-        self.docmodule = None
-        with open(filename, 'r') as scanfile:
-            for line in scanfile:
-                if line.startswith('#include'):
-                    self.scan_include_file(line, allfiles, selfdir,
-                            includedirs, ignorelist, reporter)
-                    continue
-                if not foundfileblock:
-                    if infileblock:
-                        if r'*/' in line:
-                            infileblock = False
-                            foundfileblock = True
-                            continue
-                        if self.type == 'implheader':
-                            if line.startswith(r' * \inpublicapi'):
-                                self.type = 'publicheader'
-                            elif line.startswith(r' * \inlibraryapi'):
-                                self.type = 'libheader'
-                        match = re.match(r' \* \\ingroup module_([a-z_]*)', line)
-                        if match:
-                            if self.docmodule:
-                                reporter.error(self.path,
-                                        'file documented in multiple modules')
-                            self.docmodule = match.group(1)
-                    else:
-                        match = re.match(r'/\*! *(\\[a-z]*internal)? *\\file', line)
-                        if match:
-                            docspec = match.group(1)
-                            if not docspec:
-                                self.doctype = 'public'
-                            elif docspec == r'\libinternal':
-                                self.doctype = 'library'
-                            elif docspec == r'\internal':
-                                self.doctype = 'implementation'
-                            else:
-                                reporter.input_warning(self.path,
-                                        'unknown specifier "{0}"'.format(docspec))
-                                self.doctype = 'unknown'
-                            infileblock = True
-                            if self.type == 'header':
-                                # Default type if no other found
-                                self.type = 'implheader'
-
-
-class Module(object):
-    def __init__(self, name, parent = None):
-        self.parent = parent
-        self.name = name
-        if parent:
-            self.fullname = parent.fullname + '_' + name
-        else:
-            self.fullname = 'module'
-        self.files = []
-        self.children = dict()
-        self.is_top_level = (not parent or parent.name in ('', 'gromacs'))
-
-    def get_parent(self):
-        return self.parent
-
-    def is_child(self, module):
-        parent = module.parent
-        while parent:
-            if parent == self:
-                return True
-            parent = parent.parent
-        return False
-
-    def get_top_level_module(self):
-        if self.is_top_level or not self.parent:
-            return self
-        return self.parent.get_top_level_module()
-
-    def add_nested_file(self, modules, path):
-        if len(modules) == 1:
-            newfile = File(path, self)
-            self.files.append(newfile)
-        else:
-            if not modules[0] in self.children:
-                module = Module(modules[0], self)
-                self.children[modules[0]] = module
-            else:
-                module = self.children[modules[0]]
-            newfile = module.add_nested_file(modules[1:], path)
-        return newfile
-
-
-class Dependencies(object):
-    def __init__(self, rootdir, includedirs, installedfiles):
-        self.files = dict()
-        self.root = Module("")
-        self.rootpath = []
-        for root in rootdir:
-            self.rootpath.append(os.path.abspath(root))
-        if includedirs:
-            self.includedirs = self.rootpath + includedirs
-        else:
-            self.includedirs = self.rootpath
-        self.installedfiles = installedfiles
-
-    def add_file(self, filename, reporter):
-        fullpath = os.path.abspath(filename)
-        for root in self.rootpath:
-            if fullpath.startswith(root):
-                relpath = fullpath[len(root)+1:]
-                break
-        else:
-            reporter.input_warning(filename,
-                    'input file not under root path, skipped')
-            return
-        modules = relpath.split(os.sep)
-        newfile = self.root.add_nested_file(modules, relpath)
-        if fullpath in self.installedfiles:
-            newfile.set_installed(reporter)
-        self.files[os.path.abspath(filename)] = newfile
-
-    def scan_files(self, ignorelist, reporter):
-        for (filename, scanfile) in self.files.iteritems():
-            scanfile.scan(filename, self.files, self.includedirs, ignorelist,
-                    reporter)
-
-    def get_toplevel_modules(self):
-        result = []
-        for module in self.root.children.itervalues():
-            if module.name == 'gromacs':
-                result.extend(module.children.itervalues())
-            else:
-                result.append(module)
-        return result
-
-
-def _is_legacy_module(module):
-    if module.name in ('legacyheaders', 'gmxlib', 'mdlib', 'gmxana', 'gmxpreprocess'):
-        return True
-    if module.get_parent():
-        return _is_legacy_module(module.get_parent())
-    return False
-
-
-class GraphBuilder(object):
-    def __init__(self, deps):
-        self._deps = deps
-
-    def _create_file_node(self, fileobj, filenodes):
-        nodename = re.subn(r'[-./]', '_', fileobj.path)[0]
-        properties = []
-        style = []
-        properties.append('URL="\\ref {0}"'.format(fileobj.name))
-        if not fileobj.module:
-            style.append('bold')
-            properties.append('color=red')
-        if fileobj.type == 'source':
-            style.append('filled')
-            properties.append('fillcolor=grey75')
-        elif fileobj.type == 'publicheader':
-            style.append('filled')
-            properties.append('fillcolor=".66 .2 1"')
-        elif fileobj.type == 'libheader':
-            style.append('filled')
-            properties.append('fillcolor=".66 .5 1"')
-        if style:
-            properties.append('style="{0}"'.format(','.join(style)))
-        node = Node(nodename, fileobj.name, ', '.join(properties), is_file=True)
-        filenodes[fileobj] = node
-        return node
-
-    def _create_file_edge(self, fromfile, tofile, filenodes):
-        intramodule = \
-                (fromfile.module.get_top_level_module() == \
-                 tofile.module.get_top_level_module())
-        is_legacy = _is_legacy_module(tofile.module)
-        if tofile.type not in ('publicheader', 'libheader', 'header', 'test'):
-            if intramodule:
-                link_type = 'intramodule'
-            elif is_legacy:
-                link_type = 'legacy'
-            else:
-                link_type = 'undocumented'
-        elif fromfile.type == 'test':
-            link_type = 'test'
-        elif tofile.type == 'test':
-            link_type = 'undocumented'
-        elif fromfile.type in ('source', 'header', 'implheader') and \
-                not fromfile.is_installed():
-            if intramodule:
-                link_type = 'intramodule'
-            elif tofile.type == 'publicheader':
-                link_type = 'pubimpl'
-            elif tofile.type == 'libheader':
-                link_type = 'libimpl'
-            elif is_legacy:
-                link_type = 'legacy'
-            elif not tofile.is_documented():
-                link_type = 'legacy'
-            else:
-                raise ValueError('Unknown link type between {0} and {1}'
-                        .format(fromfile.path, tofile.path))
-        elif fromfile.type == 'libheader':
-            link_type = 'library'
-        elif fromfile.type == 'publicheader' or fromfile.is_installed():
-            if tofile.type == 'publicheader' or tofile.doctype == 'public' or \
-                    (tofile.is_installed() and not tofile.is_documented()):
-                link_type = 'public'
-            else:
-                link_type = 'undocumented'
-        else:
-            raise ValueError('Unknown link type between {0} and {1}'
-                    .format(fromfile.path, tofile.path))
-        return Link(filenodes[fromfile], filenodes[tofile], link_type)
-
-    def _create_file_edges(self, fileobj, filenodes):
-        links = []
-        if fileobj in filenodes:
-            for includedfile in fileobj.get_included_files():
-                otherfile = includedfile._included_file
-                if otherfile and otherfile in filenodes:
-                    link = self._create_file_edge(fileobj, otherfile, filenodes)
-                    links.append(link)
-        return links
-
-    def create_module_node(self, module, filenodes):
-        properties = 'shape=ellipse, URL="\\ref module_{0}"'.format(module.name)
-        if _is_legacy_module(module):
-            properties += 'style=filled, fillcolor=grey75'
-        node = Node(module.fullname, module.name, properties, is_file=False)
-        for childfile in module.files:
-            node.add_child(self._create_file_node(childfile, filenodes))
-        for childmodule in module.children.itervalues():
-            node.add_child(self.create_module_node(childmodule, filenodes))
-        return node
-
-    def create_file_graph(self):
-        filenodes = dict()
-        rootnode = self.create_module_node(self._deps.root, filenodes)
-        rootnode.set_root()
-        links = []
-        for scanfile in self._deps.files.itervalues():
-            links.extend(self._create_file_edges(scanfile, filenodes))
-        graph = Graph([rootnode], links)
-        return graph
-
-    def create_modules_graph(self):
-        filenodes = dict()
-        rootnode = self.create_module_node(self._deps.root, filenodes)
-        rootnode.set_root()
-        links = []
-        for scanfile in self._deps.files.itervalues():
-            links.extend(self._create_file_edges(scanfile, filenodes))
-        graph = Graph([rootnode], links)
-        for node in rootnode.get_children():
-            if node.label == 'gromacs':
-                module_nodes = []
-                header_nodes = []
-                for child in node.get_children():
-                    if child.is_file_node():
-                        header_nodes.append(child)
-                    else:
-                        graph.collapse_node(child)
-                        module_nodes.append(child)
-                for header in header_nodes:
-                    for module in module_nodes:
-                        if header.nodename.startswith(module.nodename[7:]):
-                            # graph.merge_nodes([header], module)
-                            node.remove_child(header)
-                            break
-            else:
-                graph.collapse_node(node)
-        graph.set_options(concentrate=False)
-        graph.prune_links()
-        return graph
-
-    def create_module_file_graph(self, module):
-        filenodes = dict()
-        rootnode = self.create_module_node(module, filenodes)
-        rootnode.set_root()
-        links = []
-        for scanfile in self._deps.files.itervalues():
-            links.extend(self._create_file_edges(scanfile, filenodes))
-        graph = Graph([rootnode], links)
-        graph.prune_links()
-        return graph
-
-
-def print_module_graph(outfile, graphbuilder, options):
-    graph = graphbuilder.create_modules_graph()
-    graph.write(outfile)
-
-def print_file_graph(outfile, graphbuilder, options):
-    graph = graphbuilder.create_file_graph()
-    graph.set_options(left_to_right=options.left_to_right)
-    graph.write(outfile)
-    #if options.source_at_top:
-    #    sourcenodes = []
-    #    for file in deps.files.itervalues():
-    #        if file.sourcefile:
-    #            sourcenodes.append(file.nodename)
-    #    if sourcenodes:
-    #        outfile.write('    { rank = min; ' + '; '.join(sourcenodes) + '}\n')
-    #if options.with_external and options.external_at_bottom:
-    #    extnodes = []
-    #    for file in deps.files.itervalues():
-    #        if not file.module:
-    #            extnodes.append(file.nodename)
-    #    if extnodes:
-    #        outfile.write('    { rank = max; ' + '; '.join(extnodes) + '}\n')
-
-def print_module_file_graph(outfile, graphbuilder, module, options):
-    graph = graphbuilder.create_module_file_graph(module)
-    graph.set_options(left_to_right=options.left_to_right)
-    graph.write(outfile)
-
-def main():
-    parser = OptionParser()
-    parser.add_option('-f', '--files',
-                      help='Read list of input files from given file')
-    parser.add_option('--installed',
-                      help='Read list of installed files from given file')
-    parser.add_option('-R', '--rootdir', action='append',
-                      help='Remove this prefix from all files')
-    parser.add_option('-I', '--includedir', action='append',
-                      help='Specify additional directories to search for '
-                           'include files')
-    parser.add_option('-o', '--outdir', default='.',
-                      help='Specify output directory for graphs')
-    #parser.add_option('--source-at-top', action='store_true',
-    #                  help='Force source files at the top of the graph')
-    #parser.add_option('--with-external', action='store_true',
-    #                  help='Include external dependencies in the graph')
-    #parser.add_option('--external-at-bottom', action='store_true',
-    #                  help='Force external dependencies files at the bottom '
-    #                       'of the graph')
-    parser.add_option('--left-to-right', action='store_true',
-                      help='Lay out from left to right')
-    parser.add_option('--file-graph',
-                      help='Write graph for individual files')
-    parser.add_option('--module-graph',
-                      help='Write graph for modules')
-    parser.add_option('--module-file-graphs', action='store_true',
-                      help='Write file graphs for each module')
-    options, args = parser.parse_args()
-
-    # Constructs lists of files
-    filelist = []
-    ignorelist = []
-    installedlist = []
-    if options.files:
-        with open(options.files, 'r') as outfile:
-            for line in outfile:
-                if line.startswith('!'):
-                    ignorelist.append(os.path.abspath(line[1:].strip()))
-                else:
-                    filelist.append(line.strip())
-    filelist.extend(args)
-    if options.installed:
-        with open(options.installed, 'r') as outfile:
-            for line in outfile:
-                installedlist.append(line.strip())
-
-    # Creates objects for all files and modules
-    reporter = ErrorReporter()
-    deps = Dependencies(options.rootdir, options.includedir, installedlist)
-    for filename in filelist:
-        deps.add_file(filename, reporter)
-
-    deps.scan_files(ignorelist, reporter)
-
-    #if options.with_external:
-    #    for filename in extrafiles:
-    #        file = files[filename]
-    #        if os.path.exists(filename):
-    #            with open(filename, 'r') as outfile:
-    #                for line in outfile:
-    #                    if not file.api:
-    #                        if line.startswith(' * \inpublicapi'):
-    #                            file.api = "public"
-    #                        elif line.startswith(' * \inlibraryapi'):
-    #                            file.api = "library"
-
-    # Prints out the graph
-    graphbuilder = GraphBuilder(deps)
-    if options.module_graph:
-        graphpath = os.path.join(options.outdir, options.module_graph)
-        with open(graphpath, 'w') as outfile:
-            print_module_graph(outfile, graphbuilder, options)
-    if options.file_graph:
-        graphpath = os.path.join(options.outdir, options.file_graph)
-        with open(graphpath, 'w') as outfile:
-            print_file_graph(outfile, graphbuilder, options)
-    if options.module_file_graphs:
-        options.left_to_right = True
-        for module in deps.get_toplevel_modules():
-            if not _is_legacy_module(module):
-                filename = 'module_{0}-deps.dot'.format(module.name)
-                filename = os.path.join(options.outdir, filename)
-                with open(filename, 'w') as outfile:
-                    print_module_file_graph(outfile, graphbuilder, module, options)
-
-main()
index 085d739431f9adf99cdd7e9de90c08469af37baf..1d607e28f3100f595407d6434726f0fb37895c9f 100644 (file)
@@ -149,23 +149,31 @@ if (DOXYGEN_FOUND)
         add_custom_target(doc-check COMMAND ${doc_check_command}
             COMMENT "Checking Doxygen documentation" VERBATIM)
         add_dependencies(doc-check doc-xml find-installed-headers)
-        # This target is the same as doc-check, but it doesn't rerun the
+
+        set(graphdir ${CMAKE_CURRENT_BINARY_DIR}/depgraphs)
+        set(dep_graphs_command_python
+            ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/graphbuilder.py
+            -S ${CMAKE_SOURCE_DIR} -B ${CMAKE_BINARY_DIR}
+            --installed ${CMAKE_CURRENT_BINARY_DIR}/installed-headers.txt
+            -o ${CMAKE_CURRENT_BINARY_DIR}/depgraphs)
+        set(dep_graphs_command_dot
+            ${CMAKE_COMMAND} -DGRAPHDIR=${graphdir}
+            -DDOT_EXECUTABLE=${DOXYGEN_DOT_EXECUTABLE}
+            -P ${CMAKE_CURRENT_SOURCE_DIR}/generateGraphs.cmake)
+        add_custom_target(dep-graphs
+            COMMAND ${dep_graphs_command_python}
+            COMMAND ${dep_graphs_command_dot}
+            COMMENT "Generating include dependency graphs" VERBATIM)
+        add_dependencies(dep-graphs doc-xml find-installed-headers)
+
+        # These targets are the same as above, but they don't rerun the
         # dependencies each time, making it faster and more convenient for
         # testing.
         add_custom_target(doc-check-fast COMMAND ${doc_check_command}
             COMMENT "Checking Doxygen documentation" VERBATIM)
+        add_custom_target(dep-graphs-fast
+            COMMAND ${dep_graphs_command_python}
+            COMMAND ${dep_graphs_command_dot}
+            COMMENT "Generating include dependency graphs" VERBATIM)
     endif()
 endif()
-
-if (USE_PYTHON_SCRIPTS)
-    add_custom_target(depgraphs
-        ${CMAKE_COMMAND}
-        -DSRCDIR=${CMAKE_SOURCE_DIR}
-        -DBUILDDIR=${CMAKE_BINARY_DIR}
-        -DOUTDIR=${CMAKE_BINARY_DIR}/doxygen/depgraphs
-        -DPYTHON_EXECUTABLE=${PYTHON_EXECUTABLE}
-        -DDOT_EXECUTABLE=${DOXYGEN_DOT_EXECUTABLE}
-        -DMODE=GRAPHS
-        -P ${CMAKE_SOURCE_DIR}/admin/includedeps.cmake
-        COMMENT "Generating include dependency graphs" VERBATIM)
-endif()
index a34258f005d92bdc61f731a5693b3fd6a9794e6d..a173ab76d0aa7c86faf5754c4e560499ba049ecb 100644 (file)
@@ -346,20 +346,83 @@ issues in the comments, like invalid Doxygen syntax.  The messages from the XML
 parsing are stored in `doxygen/doxygen-xml.log` in the build tree, similar to
 other Doxygen runs.
 
-There is an additional target `depgraphs` that generates include dependency
-graphs.  This is in the process of getting rewritten to take advantage of the
-mechanisms used for `doc-check`.  One graph is
-produced that shows all the modules under `src/gromacs/`, and their include
-dependencies.  Additionally, a file-level graph is produced for each module,
-showing the include dependencies within that module.  Currently, these are
-mostly for eye candy, but they can also be used for analyzing problematic
-dependencies to clean up the architecture.  A legend for the graphs is
-currently included as comments in `admin/includedeps.py`.  The output is put in
-`doxygen/depgraphs/`.
-
-These targets require Python 2.7 (other versions may work, but have not been
-tested); the graph generation additionally requires `graphviz`.
+The `doc-check` target requires Python 2.7 (other versions may work, but have
+not been tested).
 
+Include dependency graphs
+-------------------------
+
+The build system also provides an `dep-graphs` target that generates include
+dependency graphs with some additional annotations.
+One graph is produced that shows all the modules under `src/gromacs/`, and
+their include dependencies.  Additionally, a file-level graph is produced for
+each module, showing the include dependencies within that module.  Currently,
+these are mostly for eye candy, but they can also be used for analyzing
+problematic dependencies to clean up the architecture.
+The output is put in `doxygen/depgraphs/` in the build tree.
+
+As with `doc-check`, Python 2.7 is required (other versions may work, but have
+not been tested).  To get `.png` versions of the graphs, `graphviz` is
+additionally required.
+
+### Module graph ###
+
+The graph is written into `module-deps.dot.png`.
+
+Node colors:
+<dl>
+<dt>gray background</dt>
+<dd>undocumented module</dd>
+<dt>orange background</dt>
+<dd>documented utility modules</dd>
+<dt>red background</dt>
+<dd>documented analysis modules</dd>
+<dt>violet background</dt>
+<dd>documented MD execution modules</dd>
+<dt>light blue border</dt>
+<dd>module contains public API (installed) headers</dd>
+</dl>
+
+Edge colors (an edge with a certain color indicates that types above it in the
+list are not present):
+<dl>
+<dt>red</dt>
+<dd>invalid dependency</dd>
+<dt>gray</dt>
+<dd>legacy dependency
+(dependency on undocumented file, or to undocumented directories)</dd>
+<dt>solid black</dt>
+<dd>public header depends on the other module</dd>
+<dt>solid blue</dt>
+<dd>library header depends on the other module</dd>
+<dt>dashed blue</dt>
+<dd>source file depends on library header in the other module</dd>
+<dt>dashed black</dt>
+<dd>source file depends on public header in the other module</dd>
+<dt>dashed green</dt>
+<dd>test file depends on the other module</dd>
+</dl>
+
+### File graph ###
+
+The graphs are written to <em>module_name</em>`-deps.dot.png`.
+
+Node colors:
+<dl>
+<dt>light blue</dt>
+<dd>public API (installed) headers</dd>
+<dt>dark blue</dt>
+<dd>library API headers</dd>
+<dt>gray</dt>
+<dd>source files</dd>
+<dt>light green</dt>
+<dd>test files</dd>
+<dt>white</dt>
+<dd>other files</dd>
+</dl>
+
+Each edge signifies an include dependency; there is no additional information
+currently included.
 
 Documenting specific code constructs
 ====================================
index 6a8c21f28aee29be3049ef3fcc2ca99d376617f4..f26ad9cdbdf6102913fd8bed6760aefc60a9f120 100755 (executable)
@@ -1016,6 +1016,11 @@ class DocumentationSet(object):
     This constructs relations between compound entities, and initializes other
     attributes for the entities.
 
+    load_file_details() does the same as load_details(), except that it leaves
+    those compound XML files unloaded that do not affect file objects or their
+    parent hierarchy.  This saves some time if details for actual code
+    constructs like namespaces, classes or members are not necessary.
+
     merge_duplicates() can then be called to remove members with different IDs,
     but that actually reference the same code entity.  For some reason, Doxygen
     seems to produce these in certain cases.
@@ -1069,6 +1074,14 @@ class DocumentationSet(object):
                 member.add_parent_compound(compound)
                 compound.add_member(member)
 
+    def load_file_details(self):
+        """Load detailed XML files for all files and possible parents of files."""
+        for compound in self._compounds.itervalues():
+            if isinstance(compound, (File, Directory, Group)):
+                compound.load_details()
+                if isinstance(compound, File):
+                    self._files[compound.get_path()] = compound
+
     def load_details(self):
         """Load detailed XML files for each compound."""
         for compound in self._compounds.itervalues():
diff --git a/doxygen/generateGraphs.cmake b/doxygen/generateGraphs.cmake
new file mode 100644 (file)
index 0000000..3006e4a
--- /dev/null
@@ -0,0 +1,39 @@
+#
+# This file is part of the GROMACS molecular simulation package.
+#
+# Copyright (c) 2014, by the GROMACS development team, led by
+# Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
+# and including many others, as listed in the AUTHORS file in the
+# top-level source directory and at http://www.gromacs.org.
+#
+# GROMACS is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# as published by the Free Software Foundation; either version 2.1
+# of the License, or (at your option) any later version.
+#
+# GROMACS is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with GROMACS; if not, see
+# http://www.gnu.org/licenses, or write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
+#
+# If you want to redistribute modifications to GROMACS, please
+# consider that scientific software is very special. Version
+# control is crucial - bugs must be traceable. We will be happy to
+# consider code for inclusion in the official distribution, but
+# derived work must not be called official GROMACS. Details are found
+# in the README & COPYING files - if they are missing, get the
+# official version at http://www.gromacs.org.
+#
+# To help us fund GROMACS development, we humbly ask that you cite
+# the research papers on the package. Check out http://www.gromacs.org.
+
+if (DOT_EXECUTABLE)
+    message("Running dot...")
+    file(GLOB DOT_INPUT_FILES ${GRAPHDIR}/*.dot)
+    execute_process(COMMAND ${DOT_EXECUTABLE} -Tpng -O ${DOT_INPUT_FILES})
+endif()
index 836d1db426e6bc0ce380b35d2b34f4e9ebb1096a..6a8d602a244c3c1c17e5eb721978fed15f0ab5f8 100644 (file)
@@ -75,8 +75,8 @@ class IncludedFile(object):
 
     """Information about an #include directive in a file."""
 
-    def __init__(self, path, lineno, included_file, included_path, is_relative, is_system):
-        self._path = path
+    def __init__(self, abspath, lineno, included_file, included_path, is_relative, is_system):
+        self._abspath = abspath
         self._line_number = lineno
         self._included_file = included_file
         self._included_path = included_path
@@ -100,23 +100,25 @@ class IncludedFile(object):
         return self._included_file
 
     def get_reporter_location(self):
-        return reporter.Location(self._path, self._line_number)
+        return reporter.Location(self._abspath, self._line_number)
 
 class File(object):
 
     """Source/header file in the GROMACS tree."""
 
-    def __init__(self, path, directory):
+    def __init__(self, abspath, relpath, directory):
         """Initialize a file representation with basic information."""
-        self._path = path
+        self._abspath = abspath
+        self._relpath = relpath
         self._dir = directory
         self._rawdoc = None
         self._installed = False
-        extension = os.path.splitext(path)[1]
+        extension = os.path.splitext(abspath)[1]
         self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
         self._apitype = DocType.none
         self._modules = set()
         self._includes = []
+        directory.add_file(self)
 
     def set_doc_xml(self, rawdoc, sourcetree):
         """Assiociate Doxygen documentation entity with the file."""
@@ -141,21 +143,21 @@ class File(object):
         if is_system:
             fileobj = sourcetree.find_include_file(includedpath)
         else:
-            fullpath = os.path.join(self._dir.get_path(), includedpath)
+            fullpath = os.path.join(self._dir.get_abspath(), includedpath)
             fullpath = os.path.abspath(fullpath)
             if os.path.exists(fullpath):
                 is_relative = True
                 fileobj = sourcetree.get_file(fullpath)
             else:
                 fileobj = sourcetree.find_include_file(includedpath)
-        self._includes.append(IncludedFile(self.get_path(), lineno, fileobj, includedpath,
+        self._includes.append(IncludedFile(self.get_abspath(), lineno, fileobj, includedpath,
                 is_relative, is_system))
 
     def scan_contents(self, sourcetree):
         """Scan the file contents and initialize information based on it."""
         # TODO: Consider a more robust regex.
         include_re = r'^#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
-        with open(self._path, 'r') as scanfile:
+        with open(self._abspath, 'r') as scanfile:
             for lineno, line in enumerate(scanfile, 1):
                 match = re.match(include_re, line)
                 if match:
@@ -165,7 +167,7 @@ class File(object):
                             sourcetree)
 
     def get_reporter_location(self):
-        return reporter.Location(self._path, None)
+        return reporter.Location(self._abspath, None)
 
     def is_installed(self):
         return self._installed
@@ -185,8 +187,14 @@ class File(object):
     def has_brief_description(self):
         return self._rawdoc and self._rawdoc.has_brief_description()
 
-    def get_path(self):
-        return self._path
+    def get_abspath(self):
+        return self._abspath
+
+    def get_relpath(self):
+        return self._relpath
+
+    def get_name(self):
+        return os.path.basename(self._abspath)
 
     def get_documentation_type(self):
         if not self._rawdoc:
@@ -218,43 +226,51 @@ class Directory(object):
 
     """(Sub)directory in the GROMACS tree."""
 
-    def __init__(self, path, parent):
+    def __init__(self, abspath, relpath, parent):
         """Initialize a file representation with basic information."""
-        self._path = path
-        self._name = os.path.basename(path)
+        self._abspath = abspath
+        self._relpath = relpath
+        self._name = os.path.basename(abspath)
         self._parent = parent
         self._rawdoc = None
         self._module = None
         self._is_test_dir = False
         if parent and parent.is_test_directory() or \
-                os.path.basename(path) in ('tests', 'legacytests'):
+                self._name in ('tests', 'legacytests'):
             self._is_test_dir = True
         self._is_external = False
-        if parent and parent.is_external() or \
-                os.path.basename(path) == 'external':
+        if parent and parent.is_external() or self._name == 'external':
             self._is_external = True
         self._subdirs = set()
         if parent:
             parent._subdirs.add(self)
+        self._files = set()
+        self._has_installed_files = None
 
     def set_doc_xml(self, rawdoc, sourcetree):
         """Assiociate Doxygen documentation entity with the directory."""
         assert self._rawdoc is None
-        assert self._path == rawdoc.get_path().rstrip('/')
+        assert self._abspath == rawdoc.get_path().rstrip('/')
         self._rawdoc = rawdoc
 
     def set_module(self, module):
         assert self._module is None
         self._module = module
 
+    def add_file(self, fileobj):
+        self._files.add(fileobj)
+
     def get_name(self):
         return self._name
 
     def get_reporter_location(self):
-        return reporter.Location(self._path, None)
+        return reporter.Location(self._abspath, None)
+
+    def get_abspath(self):
+        return self._abspath
 
-    def get_path(self):
-        return self._path
+    def get_relpath(self):
+        return self._relpath
 
     def is_test_directory(self):
         return self._is_test_dir
@@ -262,6 +278,19 @@ class Directory(object):
     def is_external(self):
         return self._is_external
 
+    def has_installed_files(self):
+        if self._has_installed_files is None:
+            self._has_installed_files = False
+            for subdir in self._subdirs:
+                if subdir.has_installed_files():
+                    self._has_installed_files = True
+                    return True
+            for fileobj in self._files:
+                if fileobj.is_installed():
+                    self._has_installed_files = True
+                    return True
+        return self._has_installed_files
+
     def get_module(self):
         if self._module:
             return self._module
@@ -272,6 +301,13 @@ class Directory(object):
     def get_subdirectories(self):
         return self._subdirs
 
+    def get_files(self):
+        for subdir in self._subdirs:
+            for fileobj in subdir.get_files():
+                yield fileobj
+        for fileobj in self._files:
+            yield fileobj
+
 class Module(object):
 
     """Code module in the GROMACS source tree.
@@ -287,11 +323,18 @@ class Module(object):
         self._name = name
         self._rawdoc = None
         self._rootdir = rootdir
+        self._group = None
 
     def set_doc_xml(self, rawdoc, sourcetree):
         """Assiociate Doxygen documentation entity with the module."""
         assert self._rawdoc is None
         self._rawdoc = rawdoc
+        if self._rawdoc.is_documented():
+            groups = list(self._rawdoc.get_groups())
+            if len(groups) == 1:
+                groupname = groups[0].get_name()
+                if groupname.startswith('group_'):
+                    self._group = groupname[6:]
 
     def is_documented(self):
         return self._rawdoc is not None
@@ -299,6 +342,17 @@ class Module(object):
     def get_name(self):
         return self._name
 
+    def get_root_dir(self):
+        return self._rootdir
+
+    def get_files(self):
+        # TODO: Include public API convenience headers?
+        return self._rootdir.get_files()
+
+    def get_group(self):
+        return self._group
+
+
 class Class(object):
 
     """Class/struct/union in the GROMACS source code."""
@@ -379,9 +433,9 @@ class GromacsTree(object):
     def _get_rel_path(self, path):
         assert os.path.isabs(path)
         if path.startswith(self._build_root):
-            return path[len(self._build_root)+1:]
+            return os.path.relpath(path, self._build_root)
         if path.startswith(self._source_root):
-            return path[len(self._source_root)+1:]
+            return os.path.relpath(path, self._source_root)
         raise ValueError("path not under build nor source tree: {0}".format(path))
 
     def _walk_dir(self, rootpath):
@@ -389,7 +443,7 @@ class GromacsTree(object):
         assert os.path.isabs(rootpath)
         assert rootpath not in self._dirs
         relpath = self._get_rel_path(rootpath)
-        self._dirs[relpath] = Directory(rootpath, None)
+        self._dirs[relpath] = Directory(rootpath, relpath, None)
         for dirpath, dirnames, filenames in os.walk(rootpath):
             if 'contrib' in dirnames:
                 dirnames.remove('contrib')
@@ -403,21 +457,21 @@ class GromacsTree(object):
                     dirnames.remove(dirname)
                     continue
                 relpath = self._get_rel_path(fullpath)
-                self._dirs[relpath] = Directory(fullpath, currentdir)
+                self._dirs[relpath] = Directory(fullpath, relpath, currentdir)
             extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
             for filename in filenames:
                 basename, extension = os.path.splitext(filename)
                 if extension in extensions:
                     fullpath = os.path.join(dirpath, filename)
                     relpath = self._get_rel_path(fullpath)
-                    self._files[relpath] = File(fullpath, currentdir)
+                    self._files[relpath] = File(fullpath, relpath, currentdir)
                 elif extension == '.cmakein':
                     extension = os.path.splitext(basename)[1]
                     if extension in extensions:
                         fullpath = os.path.join(dirpath, basename)
                         relpath = self._get_rel_path(fullpath)
                         fullpath = os.path.join(dirpath, filename)
-                        self._files[relpath] = GeneratedFile(fullpath, currentdir)
+                        self._files[relpath] = GeneratedFile(fullpath, relpath, currentdir)
 
     def _create_module(self, rootdir):
         """Create module for a subdirectory."""
@@ -432,16 +486,24 @@ class GromacsTree(object):
             if not fileobj.is_external():
                 fileobj.scan_contents(self)
 
-    def load_xml(self):
-        """Load Doxygen XML information."""
+    def load_xml(self, only_files=False):
+        """Load Doxygen XML information.
+
+        If only_files is True, XML data is not loaded for code constructs, but
+        only for files, directories, and their potential parents.
+        """
         xmldir = os.path.join(self._build_root, 'doxygen', 'xml')
         self._docset = xml.DocumentationSet(xmldir, self._reporter)
-        self._docset.load_details()
-        self._docset.merge_duplicates()
+        if only_files:
+            self._docset.load_file_details()
+        else:
+            self._docset.load_details()
+            self._docset.merge_duplicates()
         self._load_dirs()
         self._load_modules()
         self._load_files()
-        self._load_classes()
+        if not only_files:
+            self._load_classes()
 
     def _load_dirs(self):
         """Load Doxygen XML directory information."""
@@ -460,7 +522,7 @@ class GromacsTree(object):
         relpath = self._get_rel_path(path)
         dirobj = self._dirs.get(relpath)
         if not dirobj:
-            dirobj = Directory(path, parent)
+            dirobj = Directory(path, relpath, parent)
             self._dirs[relpath] = dirobj
         dirobj.set_doc_xml(dirdoc, self)
         self._docmap[dirdoc] = dirobj
@@ -501,7 +563,7 @@ class GromacsTree(object):
             relpath = self._get_rel_path(path)
             fileobj = self._files.get(relpath)
             if not fileobj:
-                fileobj = File(path, self._docmap[dirdoc])
+                fileobj = File(path, relpath, self._docmap[dirdoc])
                 self._files[relpath] = fileobj
             fileobj.set_doc_xml(filedoc, self)
             self._docmap[filedoc] = fileobj
@@ -553,6 +615,10 @@ class GromacsTree(object):
         """Get iterable for all files in the source tree."""
         return self._files.itervalues()
 
+    def get_modules(self):
+        """Get iterable for all modules in the source tree."""
+        return self._modules.itervalues()
+
     def get_classes(self):
         """Get iterable for all classes in the source tree."""
         return self._classes
diff --git a/doxygen/graphbuilder.py b/doxygen/graphbuilder.py
new file mode 100755 (executable)
index 0000000..47f1f70
--- /dev/null
@@ -0,0 +1,529 @@
+#!/usr/bin/python
+#
+# This file is part of the GROMACS molecular simulation package.
+#
+# Copyright (c) 2012,2013,2014, by the GROMACS development team, led by
+# Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
+# and including many others, as listed in the AUTHORS file in the
+# top-level source directory and at http://www.gromacs.org.
+#
+# GROMACS is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public License
+# as published by the Free Software Foundation; either version 2.1
+# of the License, or (at your option) any later version.
+#
+# GROMACS is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with GROMACS; if not, see
+# http://www.gnu.org/licenses, or write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
+#
+# If you want to redistribute modifications to GROMACS, please
+# consider that scientific software is very special. Version
+# control is crucial - bugs must be traceable. We will be happy to
+# consider code for inclusion in the official distribution, but
+# derived work must not be called official GROMACS. Details are found
+# in the README & COPYING files - if they are missing, get the
+# official version at http://www.gromacs.org.
+#
+# To help us fund GROMACS development, we humbly ask that you cite
+# the research papers on the package. Check out http://www.gromacs.org.
+
+"""Generate include dependency graphs.
+
+This script generates include dependency graphs from the GROMACS source tree.
+One graph is generated to show inter-module dependencies, and separate graphs
+for each module to show file-level dependencies within the module.
+
+Output format for the graphs is suitable for processing with 'dot' in graphviz.
+
+The graphs are built from the source tree representation constructed in
+gmxtree.py.
+
+Classes Graph, Node, Edge, and EdgeType provide a relatively general
+implementation for constructing 'dot' graphs.  GraphBuilder is used to
+create Graph instances from a gmxtree.GromacsTree object; the actual graph
+objects will not contain any references to the gmxtree objects.
+
+When run in script mode, the GromacsTree object is first constructed, and then
+GraphBuilder is used to construct the necessary graphs, which are then written
+out.
+
+The produced graphs are documented in doxygen.md.
+"""
+
+import os.path
+import re
+
+from gmxtree import DocType
+
+class EdgeType(object):
+
+    """Enumeration type for edge types in include dependency graphs."""
+
+    # Mapping to string representation for the internal integer values
+    _names = ['test', 'pubimpl', 'libimpl', 'library', 'public',
+            'intramodule', 'legacy', 'undocumented']
+
+    def __init__(self, value):
+        """Initialize a EdgeType instance.
+
+        EdgeType.{test,pubimpl,...,undocumented} should be used outside the
+        class instead of calling the constructor.
+        """
+        self._value = value
+
+    def __str__(self):
+        """Return string representation for the edge type (for debugging)."""
+        return self._names[self._value]
+
+    def __cmp__(self, other):
+        """Order edge types in the order of increasing coupling."""
+        return cmp(self._value, other._value)
+
+# Tests depend on test
+EdgeType.test = EdgeType(0)
+# Implementation depends on public/library headers
+EdgeType.pubimpl = EdgeType(1)
+EdgeType.libimpl = EdgeType(2)
+# Library header depends on other module
+EdgeType.library = EdgeType(3)
+# Public header depends on other module
+EdgeType.public = EdgeType(4)
+# Intramodule dependency
+EdgeType.intramodule = EdgeType(5)
+EdgeType.legacy = EdgeType(6)
+# Invalid dependency
+EdgeType.undocumented = EdgeType(7)
+
+class Edge(object):
+
+    """Graph edge between two Node objects in 'dot' graph.
+
+    Signifies an include dependency between the two nodes, and manages types
+    associated with the dependencies.
+    """
+
+    def __init__(self, fromnode, tonode, edgetype):
+        """Create edge between given Nodes with given type."""
+        self._fromnode = fromnode
+        self._tonode = tonode
+        self._edgetype = edgetype
+
+    def merge_edge(self, other):
+        """Merge another edge into this one and choose an appropriate type.
+
+        Updates the type of this edge based on the types of the merged edges.
+        """
+        self._edgetype = max(self._edgetype, other._edgetype)
+
+    def format(self):
+        """Format this edge for 'dot'."""
+        if self._fromnode.is_file_node() and self._tonode.is_file_node():
+            properties = ''
+        elif self._edgetype == EdgeType.intramodule:
+            properties = ''
+        elif self._edgetype == EdgeType.test:
+            properties = 'color=".33 .8 .8", style=dashed'
+        elif self._edgetype == EdgeType.libimpl:
+            properties = 'color=".66 .8 .8", style=dashed'
+        elif self._edgetype == EdgeType.pubimpl:
+            properties = 'color=black, style=dashed'
+        elif self._edgetype == EdgeType.library:
+            properties = 'color=".66 .8 .8"'
+        elif self._edgetype == EdgeType.public:
+            properties = 'color=black'
+        elif self._edgetype == EdgeType.legacy:
+            properties = 'color=grey75'
+        else: # undocumented
+            properties = 'color=red'
+        return '{0} -> {1} [{2}]'.format(self._fromnode.get_nodename(),
+                                         self._tonode.get_nodename(),
+                                         properties)
+
+class Node(object):
+
+    """Node in 'dot' graph."""
+
+    def __init__(self, nodename, label, style=None, properties=None, is_file=False):
+        """Create node with given attributes.
+
+        is_file does not affect the appearance of the node, but is used for
+        formatting edges between two files differently from other edges.
+        style and properties should be iterables with graphviz attributes for
+        the node.
+
+        Node can have child nodes.  Such nodes are rendered as cluster
+        subgraphs for 'dot'.
+        """
+        self._nodename = nodename
+        self._label = label
+        if style:
+            self._style = ','.join(style)
+        else:
+            self._style = None
+        if properties:
+            self._properties = ', '.join(properties)
+        else:
+            self._properties = None
+        self._is_file = is_file
+        self._children = []
+
+    def add_child(self, child):
+        """Add a child node."""
+        self._children.append(child)
+
+    def clear_children(self):
+        """Remove all children from the node."""
+        self._children = []
+
+    def is_file_node(self):
+        """Return True if the node was created with is_file=True."""
+        return self._is_file
+
+    def get_nodename(self):
+        """Get internal name of the node in 'dot'."""
+        return self._nodename
+
+    def get_children(self, recursive=False):
+        """Get list of child nodes."""
+        if recursive:
+            result = list(self._children)
+            for child in self._children:
+                result.extend(child.get_children(recursive=True))
+            return result
+        else:
+            return self._children
+
+    def format(self):
+        """Format this node for 'dot'."""
+        # TODO: Take indent as a parameter to make output marginally nicer.
+        result = ''
+        if self._children:
+            result += '    subgraph cluster_{0} {{\n' \
+                          .format(self._nodename)
+            result += '        label = "{0}"\n'.format(self._label)
+            for child in self._children:
+                result += child.format()
+            result += '    }\n'
+        else:
+            properties = 'label="{0}"'.format(self._label)
+            if self._properties:
+                properties += ', ' + self._properties
+            if self._style:
+                properties += ', style="{0}"'.format(self._style)
+            result += '    {0} [{1}]\n'.format(self._nodename, properties)
+        return result
+
+
+class Graph(object):
+
+    """Graph for 'dot'."""
+
+    def __init__(self, nodes, edges):
+        """Create graph with given nodes and edges."""
+        self._nodes = set(nodes)
+        self._edges = edges
+        self._left_to_right = False
+        self._concentrate = True
+
+    def set_options(self, left_to_right=None, concentrate=None):
+        """Set output options for the graph."""
+        if left_to_right != None:
+            self._left_to_right = left_to_right
+        if concentrate != None:
+            self._concentrate = concentrate
+
+    def merge_nodes(self, nodes, target):
+        """Merge a set of nodes into a single node.
+
+        All nodes from the list nodes are merged into the target node.
+        All edges to or from the merged nodes are rerouted to/from target
+        instead.  Duplicate edges are not created.  Instead, if an edge already
+        exists, the edge types are merged.  All nodes from the list nodes are
+        removed from the graph after the merge is done.
+        """
+        nodes = set(nodes)
+        nodes.add(target)
+        newedges = []
+        edgesto = dict()
+        edgesfrom = dict()
+        for edge in self._edges:
+            isfrom = (edge._fromnode in nodes)
+            isto = (edge._tonode in nodes)
+            if isfrom and isto:
+                pass
+            elif isfrom:
+                if not edge._tonode in edgesfrom:
+                    edgesfrom[edge._tonode] = \
+                            Edge(target, edge._tonode, edge._edgetype)
+                else:
+                    edgesfrom[edge._tonode].merge_edge(edge)
+            elif isto:
+                if not edge._fromnode in edgesto:
+                    edgesto[edge._fromnode] = \
+                            Edge(edge._fromnode, target, edge._edgetype)
+                else:
+                    edgesto[edge._fromnode].merge_edge(edge)
+            else:
+                newedges.append(edge)
+        newedges.extend(edgesfrom.values())
+        newedges.extend(edgesto.values())
+        self._edges = newedges
+
+    def collapse_node(self, node):
+        """Merge all children of a node into the node.
+
+        All child nodes are removed after the merge is done.
+        """
+        nodes = node.get_children(recursive=True)
+        self.merge_nodes(nodes, node)
+        node.clear_children()
+
+    def write(self, outfile):
+        """Write the graph in 'dot' format."""
+        outfile.write('digraph includedeps {\n')
+        if self._left_to_right:
+            outfile.write('    rankdir = LR\n')
+        if self._concentrate:
+            outfile.write('    concentrate = true\n')
+        outfile.write('    node [fontname="FreeSans",fontsize=10,height=.2,'
+                                 'shape=box]\n')
+        for node in self._nodes:
+            outfile.write(node.format())
+        for edge in self._edges:
+            outfile.write('    ' + edge.format() + '\n')
+        outfile.write('}\n')
+
+class GraphBuilder(object):
+
+    """Builder for Graph objects from gmxtree.GromacsTree representation."""
+
+    def __init__(self, tree):
+        """Initialize builder for a given tree representation."""
+        self._tree = tree
+
+    def _create_file_node(self, fileobj, filenodes):
+        """Create graph node for a file object.
+
+        filenodes is a dict() that maps file objects to their nodes, and is
+        updated by this call.
+        """
+        nodename = re.subn(r'[-./]', '_', fileobj.get_relpath())[0]
+        style = []
+        properties = []
+        properties.append('URL="\\ref {0}"'.format(fileobj.get_name()))
+        if not fileobj.get_module():
+            style.append('bold')
+            properties.append('color=red')
+        if fileobj.is_test_file():
+            style.append('filled')
+            properties.append('fillcolor=".33 .2 1"')
+        elif fileobj.is_source_file():
+            style.append('filled')
+            properties.append('fillcolor=grey75')
+        elif fileobj.get_api_type() == DocType.public:
+            style.append('filled')
+            properties.append('fillcolor=".66 .2 1"')
+        elif fileobj.get_api_type() == DocType.library:
+            style.append('filled')
+            properties.append('fillcolor=".66 .5 1"')
+        node = Node(nodename, fileobj.get_name(), style, properties, is_file=True)
+        filenodes[fileobj] = node
+        return node
+
+    def _create_file_edge(self, fromfile, tofile, filenodes):
+        """Create edge between two file objects.
+
+        Determines the type for the edge from the information provided by
+        gmxtree.
+        """
+        intramodule = (fromfile.get_module() == tofile.get_module())
+        is_legacy = not tofile.get_module().is_documented()
+        if fromfile.get_module() == tofile.get_module():
+            edgetype = EdgeType.intramodule
+        elif tofile.get_api_type() == DocType.internal:
+            if is_legacy:
+                edgetype = EdgeType.legacy
+            else:
+                edgetype = EdgeType.undocumented
+        elif fromfile.is_test_file():
+            edgetype = EdgeType.test
+        elif tofile.is_test_file():
+            edgetype = EdgeType.undocumented
+        elif fromfile.is_source_file() or \
+                (fromfile.get_api_type() <= DocType.internal and \
+                not fromfile.is_installed()):
+            if tofile.get_api_type() == DocType.public:
+                edgetype = EdgeType.pubimpl
+            elif tofile.get_api_type() == DocType.library:
+                edgetype = EdgeType.libimpl
+            elif is_legacy or not tofile.is_documented():
+                edgetype = EdgeType.legacy
+            else:
+                raise ValueError('Unknown edge type between {0} and {1}'
+                        .format(fromfile.path, tofile.path))
+        elif fromfile.get_api_type() == DocType.library:
+            edgetype = EdgeType.library
+        elif fromfile.get_api_type() == DocType.public or fromfile.is_installed():
+            if tofile.get_api_type() == DocType.public or \
+                    tofile.get_documentation_type() == DocType.public or \
+                    (tofile.is_installed() and not tofile.is_documented()):
+                edgetype = EdgeType.public
+            else:
+                edgetype = EdgeType.undocumented
+        else:
+            raise ValueError('Unknown edge type between {0} and {1}'
+                    .format(fromfile.path, tofile.path))
+        return Edge(filenodes[fromfile], filenodes[tofile], edgetype)
+
+    def _create_file_edges(self, filenodes):
+        """Create edges between all file nodes.
+
+        Create edges between file nodes specified in filenodes from all include
+        dependencies.  An edge is created only if both ends of the dependency
+        are in the list of nodes.
+        """
+        edges = []
+        for fileobj in filenodes.iterkeys():
+            for includedfile in fileobj.get_includes():
+                otherfile = includedfile.get_file()
+                if otherfile and otherfile in filenodes:
+                    edge = self._create_file_edge(fileobj, otherfile, filenodes)
+                    edges.append(edge)
+        return edges
+
+    def _create_module_node(self, module, filenodes):
+        """Create node for a module.
+
+        The created node will have all files in the module as its child nodes.
+        All created file nodes are added to the filenodes dict.
+        """
+        style = []
+        properties = []
+        properties.append('shape=ellipse')
+        properties.append('URL="\\ref module_{0}"'.format(module.get_name()))
+        if not module.is_documented():
+            style.append('filled')
+            properties.append('fillcolor=grey75')
+        elif module.get_group() == 'analysismodules':
+            style.append('filled')
+            properties.append('fillcolor="0 .2 1"')
+        elif module.get_group() == 'utilitymodules':
+            style.append('filled')
+            properties.append('fillcolor=".08 .2 1"')
+        elif module.get_group() == 'mdrun':
+            style.append('filled')
+            properties.append('fillcolor=".75 .2 1"')
+        rootdir = module.get_root_dir()
+        if rootdir.has_installed_files():
+            properties.append('color=".66 .5 1"')
+            properties.append('penwidth=3')
+        nodename = 'module_' + re.subn(r'[-./]', '_', rootdir.get_relpath())[0]
+        label = module.get_name()[7:]
+        node = Node(nodename, label, style, properties)
+        for childfile in module.get_files():
+            node.add_child(self._create_file_node(childfile, filenodes))
+        return node
+
+    def create_modules_graph(self):
+        """Create module dependency graph."""
+        filenodes = dict()
+        nodes = []
+        modulenodes = []
+        libgromacsnode = Node('libgromacs', 'libgromacs')
+        nodes.append(libgromacsnode)
+        for moduleobj in self._tree.get_modules():
+            node = self._create_module_node(moduleobj, filenodes)
+            if moduleobj.get_root_dir().get_relpath().startswith('src/gromacs'):
+                libgromacsnode.add_child(node)
+            else:
+                nodes.append(node)
+            modulenodes.append(node)
+        edges = self._create_file_edges(filenodes)
+        graph = Graph(nodes, edges)
+        for node in modulenodes:
+            graph.collapse_node(node)
+        graph.set_options(concentrate=False)
+        return graph
+
+    def create_module_file_graph(self, module):
+        """Create file dependency graph for files within a module."""
+        filenodes = dict()
+        nodes = []
+        for fileobj in module.get_files():
+            nodes.append(self._create_file_node(fileobj, filenodes))
+        edges = self._create_file_edges(filenodes)
+        graph = Graph(nodes, edges)
+        graph.set_options(left_to_right=True)
+        return graph
+
+def main():
+    """Run the graph generation script."""
+    import os
+    import sys
+
+    from optparse import OptionParser
+
+    from gmxtree import GromacsTree
+    from reporter import Reporter
+
+    parser = OptionParser()
+    parser.add_option('-S', '--source-root',
+                      help='Source tree root directory')
+    parser.add_option('-B', '--build-root',
+                      help='Build tree root directory')
+    parser.add_option('--installed',
+                      help='Read list of installed files from given file')
+    parser.add_option('-o', '--outdir', default='.',
+                      help='Specify output directory for graphs')
+    parser.add_option('-q', '--quiet', action='store_true',
+                      help='Do not write status messages')
+    options, args = parser.parse_args()
+
+    installedlist = []
+    if options.installed:
+        with open(options.installed, 'r') as outfile:
+            for line in outfile:
+                installedlist.append(line.strip())
+
+    reporter = Reporter(quiet=True)
+
+    if not options.quiet:
+        sys.stderr.write('Scanning source tree...\n')
+    tree = GromacsTree(options.source_root, options.build_root, reporter)
+    tree.set_installed_file_list(installedlist)
+    if not options.quiet:
+        sys.stderr.write('Reading source files...\n')
+    tree.scan_files()
+    if not options.quiet:
+        sys.stderr.write('Reading Doxygen XML files...\n')
+    tree.load_xml(only_files=True)
+
+    if not options.quiet:
+        sys.stderr.write('Writing graphs...\n')
+    graphbuilder = GraphBuilder(tree)
+    if not os.path.exists(options.outdir):
+        os.mkdir(options.outdir)
+
+    filename = os.path.join(options.outdir, 'module-deps.dot')
+    graph = graphbuilder.create_modules_graph()
+    with open(filename, 'w') as outfile:
+        graph.write(outfile)
+
+    # Skip some modules that are too big to make any sense
+    skippedmodules = ('legacyheaders', 'gmxlib', 'mdlib', 'gmxana', 'gmxpreprocess')
+    for module in tree.get_modules():
+        if not module.get_name()[7:] in skippedmodules:
+            filename = '{0}-deps.dot'.format(module.get_name())
+            filename = os.path.join(options.outdir, filename)
+            graph = graphbuilder.create_module_file_graph(module)
+            with open(filename, 'w') as outfile:
+                graph.write(outfile)
+
+if __name__ == '__main__':
+    main()
index c486feb636ce5e323b44d7947115932a8758ea21..e541c54235f84f2fbf5c419f8d2e3397f49e6328 100644 (file)
@@ -151,17 +151,20 @@ class Reporter(object):
 
     """Collect and write out issues found by checker scripts."""
 
-    def __init__(self, logfile=None):
+    def __init__(self, logfile=None, quiet=False):
         """Initialize the reporter.
 
         If logfile is set to a file name, all issues will be written to this
         file in addition to stderr.
+
+        If quiet is set to True, the reporter will suppress all output.
         """
         self._logfp = None
         if logfile:
             self._logfp = open(logfile, 'w')
         self._messages = []
         self._filters = []
+        self._quiet = quiet
 
     def _write(self, message):
         """Implement actual message writing."""
@@ -178,6 +181,8 @@ class Reporter(object):
 
     def _report(self, message):
         """Handle a single reporter message."""
+        if self._quiet:
+            return
         for filterobj in self._filters:
             if filterobj.matches(message):
                 return