Merge release-5-0 into master
[alexxy/gromacs.git] / docs / doxygen / gmxtree.py
index c974d13a1454d3462cad74e9d46899b86a811270..aaf21e01c7450b47988cf711ef82d17db2acc5c1 100644 (file)
@@ -75,8 +75,8 @@ class IncludedFile(object):
 
     """Information about an #include directive in a file."""
 
-    def __init__(self, abspath, lineno, included_file, included_path, is_relative, is_system):
-        self._abspath = abspath
+    def __init__(self, including_file, lineno, included_file, included_path, is_relative, is_system):
+        self._including_file = including_file
         self._line_number = lineno
         self._included_file = included_file
         self._included_path = included_path
@@ -96,11 +96,14 @@ class IncludedFile(object):
     def is_relative(self):
         return self._is_relative
 
+    def get_including_file(self):
+        return self._including_file
+
     def get_file(self):
         return self._included_file
 
     def get_reporter_location(self):
-        return reporter.Location(self._abspath, self._line_number)
+        return reporter.Location(self._including_file.get_abspath(), self._line_number)
 
 class File(object):
 
@@ -150,7 +153,7 @@ class File(object):
                 fileobj = sourcetree.get_file(fullpath)
             else:
                 fileobj = sourcetree.find_include_file(includedpath)
-        self._includes.append(IncludedFile(self.get_abspath(), lineno, fileobj, includedpath,
+        self._includes.append(IncludedFile(self, lineno, fileobj, includedpath,
                 is_relative, is_system))
 
     def scan_contents(self, sourcetree):
@@ -196,7 +199,7 @@ class File(object):
     def get_name(self):
         return os.path.basename(self._abspath)
 
-    def get_documentation_type(self):
+    def get_doc_type(self):
         if not self._rawdoc:
             return DocType.none
         return self._rawdoc.get_visibility()
@@ -204,6 +207,22 @@ class File(object):
     def get_api_type(self):
         return self._apitype
 
+    def api_type_is_reliable(self):
+        if self._apitype > DocType.internal:
+            return True
+        module = self.get_module()
+        return module and module.is_documented()
+
+    def is_public(self):
+        if self.api_type_is_reliable():
+            return self.get_api_type() == DocType.public
+        return self.get_api_type() == DocType.public or self.is_installed()
+
+    def is_module_internal(self):
+        if self.is_source_file():
+            return True
+        return not self.is_installed() and self.get_api_type() <= DocType.internal
+
     def get_expected_module(self):
         return self._dir.get_module()
 
@@ -308,6 +327,37 @@ class Directory(object):
         for fileobj in self._files:
             yield fileobj
 
+class ModuleDependency(object):
+
+    """Dependency between modules."""
+
+    def __init__(self, othermodule):
+        """Initialize empty dependency object with given module as dependency."""
+        self._othermodule = othermodule
+        self._includedfiles = []
+        self._cyclesuppression = None
+
+    def add_included_file(self, includedfile):
+        """Add IncludedFile that is part of this dependency."""
+        assert includedfile.get_file().get_module() == self._othermodule
+        self._includedfiles.append(includedfile)
+
+    def set_cycle_suppression(self):
+        """Set suppression on cycles containing this dependency."""
+        self._cyclesuppression = True
+
+    def is_cycle_suppressed(self):
+        """Return whether cycles containing this dependency are suppressed."""
+        return self._cyclesuppression is not None
+
+    def get_other_module(self):
+        """Get module that this dependency is to."""
+        return self._othermodule
+
+    def get_included_files(self):
+        """Get IncludedFile objects for the individual include dependencies."""
+        return self._includedfiles
+
 class Module(object):
 
     """Code module in the GROMACS source tree.
@@ -324,6 +374,7 @@ class Module(object):
         self._rawdoc = None
         self._rootdir = rootdir
         self._group = None
+        self._dependencies = dict()
 
     def set_doc_xml(self, rawdoc, sourcetree):
         """Assiociate Doxygen documentation entity with the module."""
@@ -336,6 +387,13 @@ class Module(object):
                 if groupname.startswith('group_'):
                     self._group = groupname[6:]
 
+    def add_dependency(self, othermodule, includedfile):
+        """Add #include dependency from a file in this module."""
+        assert includedfile.get_file().get_module() == othermodule
+        if othermodule not in self._dependencies:
+            self._dependencies[othermodule] = ModuleDependency(othermodule)
+        self._dependencies[othermodule].add_included_file(includedfile)
+
     def is_documented(self):
         return self._rawdoc is not None
 
@@ -352,6 +410,18 @@ class Module(object):
     def get_group(self):
         return self._group
 
+    def get_dependencies(self):
+        return self._dependencies.itervalues()
+
+class Namespace(object):
+
+    """Namespace in the GROMACS source code."""
+
+    def __init__(self, rawdoc):
+        self._rawdoc = rawdoc
+
+    def is_anonymous(self):
+        return self._rawdoc.is_anonymous()
 
 class Class(object):
 
@@ -376,19 +446,66 @@ class Class(object):
     def has_brief_description(self):
         return self._rawdoc.has_brief_description()
 
-    def get_documentation_type(self):
+    def get_doc_type(self):
+        """Return documentation type (visibility) for the class.
+
+        In addition to the actual code, this encodes GROMACS-specific logic
+        of setting EXTRACT_LOCAL_CLASSES=YES only for the full documentation.
+        Local classes never appear outside the full documentation, no matter
+        what is their visibility.
+        """
         if not self.is_documented():
             return DocType.none
         if self._rawdoc.is_local():
             return DocType.internal
         return self._rawdoc.get_visibility()
 
-    def get_file_documentation_type(self):
-        return max([fileobj.get_documentation_type() for fileobj in self._files])
+    def get_file_doc_type(self):
+        return max([fileobj.get_doc_type() for fileobj in self._files])
 
     def is_in_installed_file(self):
         return any([fileobj.is_installed() for fileobj in self._files])
 
+class Member(object):
+
+    """Member (in Doxygen terminology) in the GROMACS source tree.
+
+    Currently, modeling is limited to the minimal set of properties that the
+    checker uses.
+    """
+
+    def __init__(self, rawdoc, namespace):
+        self._rawdoc = rawdoc
+        self._namespace = namespace
+
+    def get_name(self):
+        return self._rawdoc.get_name()
+
+    def get_reporter_location(self):
+        return self._rawdoc.get_reporter_location()
+
+    def is_documented(self):
+        return self._rawdoc.is_documented()
+
+    def has_brief_description(self):
+        return self._rawdoc.has_brief_description()
+
+    def has_inbody_description(self):
+        return self._rawdoc.has_inbody_description()
+
+    def is_visible(self):
+        """Return whether the member is visible in Doxygen documentation.
+
+        Doxygen ignores members whose parent compounds are not documented.
+        However, when EXTRACT_ANON_NPACES=ON (which is set for our full
+        documentation), members of anonymous namespaces are extracted even if
+        the namespace is the only parent and is not documented.
+        """
+        if self._namespace and self._namespace.is_anonymous():
+            return True
+        return self._rawdoc.get_inherited_visibility() != DocType.none
+
+
 class GromacsTree(object):
 
     """Root object for navigating the GROMACS source tree.
@@ -423,6 +540,8 @@ class GromacsTree(object):
         self._files = dict()
         self._modules = dict()
         self._classes = set()
+        self._namespaces = set()
+        self._members = set()
         self._walk_dir(os.path.join(self._source_root, 'src'))
         rootdir = self._get_dir(os.path.join('src', 'gromacs'))
         for subdir in rootdir.get_subdirectories():
@@ -485,6 +604,14 @@ class GromacsTree(object):
         for fileobj in self._files.itervalues():
             if not fileobj.is_external():
                 fileobj.scan_contents(self)
+                module = fileobj.get_module()
+                if module:
+                    for includedfile in fileobj.get_includes():
+                        otherfile = includedfile.get_file()
+                        if otherfile:
+                            othermodule = otherfile.get_module()
+                            if othermodule and othermodule != module:
+                                module.add_dependency(othermodule, includedfile)
 
     def load_xml(self, only_files=False):
         """Load Doxygen XML information.
@@ -503,7 +630,9 @@ class GromacsTree(object):
         self._load_modules()
         self._load_files()
         if not only_files:
+            self._load_namespaces()
             self._load_classes()
+            self._load_members()
 
     def _load_dirs(self):
         """Load Doxygen XML directory information."""
@@ -568,6 +697,14 @@ class GromacsTree(object):
             fileobj.set_doc_xml(filedoc, self)
             self._docmap[filedoc] = fileobj
 
+    def _load_namespaces(self):
+        """Load Doxygen XML namespace information."""
+        nsdocs = self._docset.get_namespaces()
+        for nsdoc in nsdocs:
+            nsobj = Namespace(nsdoc)
+            self._docmap[nsdoc] = nsobj
+            self._namespaces.add(nsobj)
+
     def _load_classes(self):
         """Load Doxygen XML class information."""
         classdocs = self._docset.get_classes()
@@ -577,6 +714,16 @@ class GromacsTree(object):
             self._docmap[classdoc] = classobj
             self._classes.add(classobj)
 
+    def _load_members(self):
+        """Load Doxygen XML member information."""
+        memberdocs = self._docset.get_members()
+        for memberdoc in memberdocs:
+            nsdoc = memberdoc.get_namespace()
+            nsobj = self.get_object(nsdoc)
+            memberobj = Member(memberdoc, nsobj)
+            self._docmap[memberdoc] = memberobj
+            self._members.add(memberobj)
+
     def _get_dir(self, relpath):
         """Get directory object for a path relative to source tree root."""
         return self._dirs.get(relpath)
@@ -587,7 +734,8 @@ class GromacsTree(object):
 
     def find_include_file(self, includedpath):
         """Find a file object corresponding to an include path."""
-        for testdir in ('src', 'src/gromacs/legacyheaders', 'src/external/thread_mpi/include'):
+        for testdir in ('src', 'src/external/thread_mpi/include',
+                'src/external/tng_io/include'):
             testpath = os.path.join(testdir, includedpath)
             if testpath in self._files:
                 return self._files[testpath]
@@ -607,8 +755,38 @@ class GromacsTree(object):
                 continue
             self._files[relpath].set_installed()
 
+    def load_cycle_suppression_list(self, filename):
+        """Load a list of edges to suppress in cycles.
+
+        These edges between modules, if present, will be marked in the
+        corresponding ModuleDependency objects.
+        """
+        with open(filename, 'r') as fp:
+            for line in fp:
+                line = line.strip()
+                if not line or line.startswith('#'):
+                    continue
+                modulenames = ['module_' + x.strip() for x in line.split('->')]
+                if len(modulenames) != 2:
+                    self._reporter.input_error(
+                            "invalid cycle suppression line: {0}".format(line))
+                    continue
+                firstmodule = self._modules.get(modulenames[0])
+                secondmodule = self._modules.get(modulenames[1])
+                if not firstmodule or not secondmodule:
+                    self._reporter.input_error(
+                            "unknown modules mentioned on cycle suppression line: {0}".format(line))
+                    continue
+                for dep in firstmodule.get_dependencies():
+                    if dep.get_other_module() == secondmodule:
+                        # TODO: Check that each suppression is actually part of
+                        # a cycle.
+                        dep.set_cycle_suppression()
+
     def get_object(self, docobj):
         """Get tree object for a Doxygen XML object."""
+        if docobj is None:
+            return None
         return self._docmap.get(docobj)
 
     def get_files(self):
@@ -625,5 +803,4 @@ class GromacsTree(object):
 
     def get_members(self):
         """Get iterable for all members (in Doxygen terms) in the source tree."""
-        # TODO: Add wrappers to solve some issues.
-        return self._docset.get_members()
+        return self._members