Module dependency cycle checker for 'doc-check'
[alexxy/gromacs.git] / doxygen / gmxtree.py
1 #!/usr/bin/python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
5 # Copyright (c) 2014, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
9 #
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
14 #
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 # Lesser General Public License for more details.
19 #
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
24 #
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
32 #
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
35
36 """GROMACS-specific representation for source tree and documentation.
37
38 This module provides classes that construct a GROMACS-specific representation
39 of the source tree and associate the Doxygen XML output with it.  It constructs
40 an initial representation by walking the source tree in the file system, and
41 then associates information from the Doxygen XML output into this.
42 It also adds some additional knowledge from how the GROMACS source tree is
43 organized to construct a representation that is easy to process and check as
44 the top-level scripts expect.
45
46 The object model is rooted at a GromacsTree object.  Currently, it constructs a
47 representation of the source tree from the file system, but is otherwise mostly
48 a thin wrapper around the Doxygen XML tree.  It already adds some relations and
49 rules that come from GROMACS-specific knowledge.  In the future, more such
50 customizations will be added.
51 """
52
53 import os
54 import os.path
55 import re
56
57 import doxygenxml as xml
58 import reporter
59 # We import DocType directly so that it is exposed from this module as well.
60 from doxygenxml import DocType
61
62 def _get_api_type_for_compound(grouplist):
63     """Helper function to deduce API type from Doxygen group membership."""
64     result = DocType.internal
65     for group in grouplist:
66         if isinstance(group, xml.Group):
67             if group.get_name() == 'group_publicapi':
68                 result = DocType.public
69             elif group.get_name() == 'group_libraryapi':
70                 result = DocType.library
71             # TODO: Check for multiple group membership
72     return result
73
74 class IncludedFile(object):
75
76     """Information about an #include directive in a file."""
77
78     def __init__(self, abspath, lineno, included_file, included_path, is_relative, is_system):
79         self._abspath = abspath
80         self._line_number = lineno
81         self._included_file = included_file
82         self._included_path = included_path
83         #self._used_include_path = used_include_path
84         self._is_relative = is_relative
85         self._is_system = is_system
86
87     def __str__(self):
88         if self._is_system:
89             return '<{0}>'.format(self._included_path)
90         else:
91             return '"{0}"'.format(self._included_path)
92
93     def is_system(self):
94         return self._is_system
95
96     def is_relative(self):
97         return self._is_relative
98
99     def get_file(self):
100         return self._included_file
101
102     def get_reporter_location(self):
103         return reporter.Location(self._abspath, self._line_number)
104
105 class File(object):
106
107     """Source/header file in the GROMACS tree."""
108
109     def __init__(self, abspath, relpath, directory):
110         """Initialize a file representation with basic information."""
111         self._abspath = abspath
112         self._relpath = relpath
113         self._dir = directory
114         self._rawdoc = None
115         self._installed = False
116         extension = os.path.splitext(abspath)[1]
117         self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
118         self._apitype = DocType.none
119         self._modules = set()
120         self._includes = []
121         directory.add_file(self)
122
123     def set_doc_xml(self, rawdoc, sourcetree):
124         """Assiociate Doxygen documentation entity with the file."""
125         assert self._rawdoc is None
126         assert rawdoc.is_source_file() == self._sourcefile
127         self._rawdoc = rawdoc
128         if self._rawdoc.is_documented():
129             grouplist = self._rawdoc.get_groups()
130             self._apitype = _get_api_type_for_compound(grouplist)
131             for group in grouplist:
132                 module = sourcetree.get_object(group)
133                 if module:
134                     self._modules.add(module)
135
136     def set_installed(self):
137         """Mark the file installed."""
138         self._installed = True
139
140     def _process_include(self, lineno, is_system, includedpath, sourcetree):
141         """Process #include directive during scan()."""
142         is_relative = False
143         if is_system:
144             fileobj = sourcetree.find_include_file(includedpath)
145         else:
146             fullpath = os.path.join(self._dir.get_abspath(), includedpath)
147             fullpath = os.path.abspath(fullpath)
148             if os.path.exists(fullpath):
149                 is_relative = True
150                 fileobj = sourcetree.get_file(fullpath)
151             else:
152                 fileobj = sourcetree.find_include_file(includedpath)
153         self._includes.append(IncludedFile(self.get_abspath(), lineno, fileobj, includedpath,
154                 is_relative, is_system))
155
156     def scan_contents(self, sourcetree):
157         """Scan the file contents and initialize information based on it."""
158         # TODO: Consider a more robust regex.
159         include_re = r'^#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
160         with open(self._abspath, 'r') as scanfile:
161             for lineno, line in enumerate(scanfile, 1):
162                 match = re.match(include_re, line)
163                 if match:
164                     is_system = (match.group('quote') == '<')
165                     includedpath = match.group('path')
166                     self._process_include(lineno, is_system, includedpath,
167                             sourcetree)
168
169     def get_reporter_location(self):
170         return reporter.Location(self._abspath, None)
171
172     def is_installed(self):
173         return self._installed
174
175     def is_external(self):
176         return self._dir.is_external()
177
178     def is_source_file(self):
179         return self._sourcefile
180
181     def is_test_file(self):
182         return self._dir.is_test_directory()
183
184     def is_documented(self):
185         return self._rawdoc and self._rawdoc.is_documented()
186
187     def has_brief_description(self):
188         return self._rawdoc and self._rawdoc.has_brief_description()
189
190     def get_abspath(self):
191         return self._abspath
192
193     def get_relpath(self):
194         return self._relpath
195
196     def get_name(self):
197         return os.path.basename(self._abspath)
198
199     def get_doc_type(self):
200         if not self._rawdoc:
201             return DocType.none
202         return self._rawdoc.get_visibility()
203
204     def get_api_type(self):
205         return self._apitype
206
207     def api_type_is_reliable(self):
208         if self._apitype > DocType.internal:
209             return True
210         module = self.get_module()
211         return module and module.is_documented()
212
213     def is_public(self):
214         if self.api_type_is_reliable():
215             return self.get_api_type() == DocType.public
216         return self.get_api_type() == DocType.public or self.is_installed()
217
218     def is_module_internal(self):
219         if self.is_source_file():
220             return True
221         return not self.is_installed() and self.get_api_type() <= DocType.internal
222
223     def get_expected_module(self):
224         return self._dir.get_module()
225
226     def get_doc_modules(self):
227         return self._modules
228
229     def get_module(self):
230         module = self.get_expected_module()
231         if not module and len(self._modules) == 1:
232             module = list(self._modules)[0]
233         return module
234
235     def get_includes(self):
236         return self._includes
237
238 class GeneratedFile(File):
239     pass
240
241 class Directory(object):
242
243     """(Sub)directory in the GROMACS tree."""
244
245     def __init__(self, abspath, relpath, parent):
246         """Initialize a file representation with basic information."""
247         self._abspath = abspath
248         self._relpath = relpath
249         self._name = os.path.basename(abspath)
250         self._parent = parent
251         self._rawdoc = None
252         self._module = None
253         self._is_test_dir = False
254         if parent and parent.is_test_directory() or \
255                 self._name in ('tests', 'legacytests'):
256             self._is_test_dir = True
257         self._is_external = False
258         if parent and parent.is_external() or self._name == 'external':
259             self._is_external = True
260         self._subdirs = set()
261         if parent:
262             parent._subdirs.add(self)
263         self._files = set()
264         self._has_installed_files = None
265
266     def set_doc_xml(self, rawdoc, sourcetree):
267         """Assiociate Doxygen documentation entity with the directory."""
268         assert self._rawdoc is None
269         assert self._abspath == rawdoc.get_path().rstrip('/')
270         self._rawdoc = rawdoc
271
272     def set_module(self, module):
273         assert self._module is None
274         self._module = module
275
276     def add_file(self, fileobj):
277         self._files.add(fileobj)
278
279     def get_name(self):
280         return self._name
281
282     def get_reporter_location(self):
283         return reporter.Location(self._abspath, None)
284
285     def get_abspath(self):
286         return self._abspath
287
288     def get_relpath(self):
289         return self._relpath
290
291     def is_test_directory(self):
292         return self._is_test_dir
293
294     def is_external(self):
295         return self._is_external
296
297     def has_installed_files(self):
298         if self._has_installed_files is None:
299             self._has_installed_files = False
300             for subdir in self._subdirs:
301                 if subdir.has_installed_files():
302                     self._has_installed_files = True
303                     return True
304             for fileobj in self._files:
305                 if fileobj.is_installed():
306                     self._has_installed_files = True
307                     return True
308         return self._has_installed_files
309
310     def get_module(self):
311         if self._module:
312             return self._module
313         if self._parent:
314             return self._parent.get_module()
315         return None
316
317     def get_subdirectories(self):
318         return self._subdirs
319
320     def get_files(self):
321         for subdir in self._subdirs:
322             for fileobj in subdir.get_files():
323                 yield fileobj
324         for fileobj in self._files:
325             yield fileobj
326
327 class ModuleDependency(object):
328
329     """Dependency between modules."""
330
331     def __init__(self, othermodule):
332         """Initialize empty dependency object with given module as dependency."""
333         self._othermodule = othermodule
334         self._includedfiles = []
335         self._cyclesuppression = None
336
337     def add_included_file(self, includedfile):
338         """Add IncludedFile that is part of this dependency."""
339         assert includedfile.get_file().get_module() == self._othermodule
340         self._includedfiles.append(includedfile)
341
342     def set_cycle_suppression(self):
343         """Set suppression on cycles containing this dependency."""
344         self._cyclesuppression = True
345
346     def is_cycle_suppressed(self):
347         """Return whether cycles containing this dependency are suppressed."""
348         return self._cyclesuppression is not None
349
350     def get_other_module(self):
351         """Get module that this dependency is to."""
352         return self._othermodule
353
354 class Module(object):
355
356     """Code module in the GROMACS source tree.
357
358     Modules are specific subdirectories that host a more or less coherent
359     set of routines.  Simplified, every subdirectory under src/gromacs/ is
360     a different module.  This object provides that abstraction and also links
361     the subdirectory to the module documentation (documented as a group in
362     Doxygen) if that exists.
363     """
364
365     def __init__(self, name, rootdir):
366         self._name = name
367         self._rawdoc = None
368         self._rootdir = rootdir
369         self._group = None
370         self._dependencies = dict()
371
372     def set_doc_xml(self, rawdoc, sourcetree):
373         """Assiociate Doxygen documentation entity with the module."""
374         assert self._rawdoc is None
375         self._rawdoc = rawdoc
376         if self._rawdoc.is_documented():
377             groups = list(self._rawdoc.get_groups())
378             if len(groups) == 1:
379                 groupname = groups[0].get_name()
380                 if groupname.startswith('group_'):
381                     self._group = groupname[6:]
382
383     def add_dependency(self, othermodule, includedfile):
384         """Add #include dependency from a file in this module."""
385         assert includedfile.get_file().get_module() == othermodule
386         if othermodule not in self._dependencies:
387             self._dependencies[othermodule] = ModuleDependency(othermodule)
388         self._dependencies[othermodule].add_included_file(includedfile)
389
390     def is_documented(self):
391         return self._rawdoc is not None
392
393     def get_name(self):
394         return self._name
395
396     def get_root_dir(self):
397         return self._rootdir
398
399     def get_files(self):
400         # TODO: Include public API convenience headers?
401         return self._rootdir.get_files()
402
403     def get_group(self):
404         return self._group
405
406     def get_dependencies(self):
407         return self._dependencies.itervalues()
408
409 class Namespace(object):
410
411     """Namespace in the GROMACS source code."""
412
413     def __init__(self, rawdoc):
414         self._rawdoc = rawdoc
415
416     def is_anonymous(self):
417         return self._rawdoc.is_anonymous()
418
419 class Class(object):
420
421     """Class/struct/union in the GROMACS source code."""
422
423     def __init__(self, rawdoc, files):
424         self._rawdoc = rawdoc
425         self._files = set(files)
426
427     def get_name(self):
428         return self._rawdoc.get_name()
429
430     def get_reporter_location(self):
431         return self._rawdoc.get_reporter_location()
432
433     def get_files(self):
434         return self._files
435
436     def is_documented(self):
437         return self._rawdoc.is_documented()
438
439     def has_brief_description(self):
440         return self._rawdoc.has_brief_description()
441
442     def get_doc_type(self):
443         """Return documentation type (visibility) for the class.
444
445         In addition to the actual code, this encodes GROMACS-specific logic
446         of setting EXTRACT_LOCAL_CLASSES=YES only for the full documentation.
447         Local classes never appear outside the full documentation, no matter
448         what is their visibility.
449         """
450         if not self.is_documented():
451             return DocType.none
452         if self._rawdoc.is_local():
453             return DocType.internal
454         return self._rawdoc.get_visibility()
455
456     def get_file_doc_type(self):
457         return max([fileobj.get_doc_type() for fileobj in self._files])
458
459     def is_in_installed_file(self):
460         return any([fileobj.is_installed() for fileobj in self._files])
461
462 class Member(object):
463
464     """Member (in Doxygen terminology) in the GROMACS source tree.
465
466     Currently, modeling is limited to the minimal set of properties that the
467     checker uses.
468     """
469
470     def __init__(self, rawdoc, namespace):
471         self._rawdoc = rawdoc
472         self._namespace = namespace
473
474     def get_name(self):
475         return self._rawdoc.get_name()
476
477     def get_reporter_location(self):
478         return self._rawdoc.get_reporter_location()
479
480     def is_documented(self):
481         return self._rawdoc.is_documented()
482
483     def has_brief_description(self):
484         return self._rawdoc.has_brief_description()
485
486     def has_inbody_description(self):
487         return self._rawdoc.has_inbody_description()
488
489     def is_visible(self):
490         """Return whether the member is visible in Doxygen documentation.
491
492         Doxygen ignores members whose parent compounds are not documented.
493         However, when EXTRACT_ANON_NPACES=ON (which is set for our full
494         documentation), members of anonymous namespaces are extracted even if
495         the namespace is the only parent and is not documented.
496         """
497         if self._namespace and self._namespace.is_anonymous():
498             return True
499         return self._rawdoc.get_inherited_visibility() != DocType.none
500
501
502 class GromacsTree(object):
503
504     """Root object for navigating the GROMACS source tree.
505
506     On initialization, the list of files and directories is initialized by
507     walking the source tree, and modules are created for top-level
508     subdirectories.  At this point, only information that is accessible from
509     file names and paths only is available.
510
511     set_installed_file_list() can be called to set the list of installed
512     files.
513
514     scan_files() can be called to read all the files and initialize #include
515     dependencies between the files based on the information.  This is done like
516     this instead of relying on Doxygen-extracted include files to make the
517     dependency graph independent from preprocessor macro definitions
518     (Doxygen only sees those #includes that the preprocessor sees, which
519     depends on what #defines it has seen).
520
521     load_xml() can be called to load information from Doxygen XML data in
522     the build tree (the Doxygen XML data must have been built separately).
523     """
524
525     def __init__(self, source_root, build_root, reporter):
526         """Initialize the tree object by walking the source tree."""
527         self._source_root = os.path.abspath(source_root)
528         self._build_root = os.path.abspath(build_root)
529         self._reporter = reporter
530         self._docset = None
531         self._docmap = dict()
532         self._dirs = dict()
533         self._files = dict()
534         self._modules = dict()
535         self._classes = set()
536         self._namespaces = set()
537         self._members = set()
538         self._walk_dir(os.path.join(self._source_root, 'src'))
539         rootdir = self._get_dir(os.path.join('src', 'gromacs'))
540         for subdir in rootdir.get_subdirectories():
541             self._create_module(subdir)
542         rootdir = self._get_dir(os.path.join('src', 'testutils'))
543         self._create_module(rootdir)
544
545     def _get_rel_path(self, path):
546         assert os.path.isabs(path)
547         if path.startswith(self._build_root):
548             return os.path.relpath(path, self._build_root)
549         if path.startswith(self._source_root):
550             return os.path.relpath(path, self._source_root)
551         raise ValueError("path not under build nor source tree: {0}".format(path))
552
553     def _walk_dir(self, rootpath):
554         """Construct representation of the source tree by walking the file system."""
555         assert os.path.isabs(rootpath)
556         assert rootpath not in self._dirs
557         relpath = self._get_rel_path(rootpath)
558         self._dirs[relpath] = Directory(rootpath, relpath, None)
559         for dirpath, dirnames, filenames in os.walk(rootpath):
560             if 'contrib' in dirnames:
561                 dirnames.remove('contrib')
562             if 'refdata' in dirnames:
563                 dirnames.remove('refdata')
564             currentdir = self._dirs[self._get_rel_path(dirpath)]
565             # Loop through a copy so that we can modify dirnames.
566             for dirname in list(dirnames):
567                 fullpath = os.path.join(dirpath, dirname)
568                 if fullpath == self._build_root:
569                     dirnames.remove(dirname)
570                     continue
571                 relpath = self._get_rel_path(fullpath)
572                 self._dirs[relpath] = Directory(fullpath, relpath, currentdir)
573             extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
574             for filename in filenames:
575                 basename, extension = os.path.splitext(filename)
576                 if extension in extensions:
577                     fullpath = os.path.join(dirpath, filename)
578                     relpath = self._get_rel_path(fullpath)
579                     self._files[relpath] = File(fullpath, relpath, currentdir)
580                 elif extension == '.cmakein':
581                     extension = os.path.splitext(basename)[1]
582                     if extension in extensions:
583                         fullpath = os.path.join(dirpath, basename)
584                         relpath = self._get_rel_path(fullpath)
585                         fullpath = os.path.join(dirpath, filename)
586                         self._files[relpath] = GeneratedFile(fullpath, relpath, currentdir)
587
588     def _create_module(self, rootdir):
589         """Create module for a subdirectory."""
590         name = 'module_' + rootdir.get_name()
591         moduleobj = Module(name, rootdir)
592         rootdir.set_module(moduleobj)
593         self._modules[name] = moduleobj
594
595     def scan_files(self):
596         """Read source files to initialize #include dependencies."""
597         for fileobj in self._files.itervalues():
598             if not fileobj.is_external():
599                 fileobj.scan_contents(self)
600                 module = fileobj.get_module()
601                 if module:
602                     for includedfile in fileobj.get_includes():
603                         otherfile = includedfile.get_file()
604                         if otherfile:
605                             othermodule = otherfile.get_module()
606                             if othermodule and othermodule != module:
607                                 module.add_dependency(othermodule, includedfile)
608
609     def load_xml(self, only_files=False):
610         """Load Doxygen XML information.
611
612         If only_files is True, XML data is not loaded for code constructs, but
613         only for files, directories, and their potential parents.
614         """
615         xmldir = os.path.join(self._build_root, 'doxygen', 'xml')
616         self._docset = xml.DocumentationSet(xmldir, self._reporter)
617         if only_files:
618             self._docset.load_file_details()
619         else:
620             self._docset.load_details()
621             self._docset.merge_duplicates()
622         self._load_dirs()
623         self._load_modules()
624         self._load_files()
625         if not only_files:
626             self._load_namespaces()
627             self._load_classes()
628             self._load_members()
629
630     def _load_dirs(self):
631         """Load Doxygen XML directory information."""
632         rootdirs = self._docset.get_compounds(xml.Directory,
633                 lambda x: x.get_parent() is None)
634         for dirdoc in rootdirs:
635             self._load_dir(dirdoc, None)
636
637     def _load_dir(self, dirdoc, parent):
638         """Load Doxygen XML directory information for a single directory."""
639         path = dirdoc.get_path().rstrip('/')
640         if not os.path.isabs(path):
641             self._reporter.xml_assert(dirdoc.get_xml_path(),
642                     "expected absolute path in Doxygen-produced XML file")
643             return
644         relpath = self._get_rel_path(path)
645         dirobj = self._dirs.get(relpath)
646         if not dirobj:
647             dirobj = Directory(path, relpath, parent)
648             self._dirs[relpath] = dirobj
649         dirobj.set_doc_xml(dirdoc, self)
650         self._docmap[dirdoc] = dirobj
651         for subdirdoc in dirdoc.get_subdirectories():
652             self._load_dir(subdirdoc, dirobj)
653
654     def _load_modules(self):
655         """Load Doxygen XML module (group) information."""
656         moduledocs = self._docset.get_compounds(xml.Group,
657                 lambda x: x.get_name().startswith('module_'))
658         for moduledoc in moduledocs:
659             moduleobj = self._modules.get(moduledoc.get_name())
660             if not moduleobj:
661                 self._reporter.input_error(
662                         "no matching directory for module: {0}".format(moduledoc))
663                 continue
664             moduleobj.set_doc_xml(moduledoc, self)
665             self._docmap[moduledoc] = moduleobj
666
667     def _load_files(self):
668         """Load Doxygen XML file information."""
669         for filedoc in self._docset.get_files():
670             path = filedoc.get_path()
671             if not os.path.isabs(path):
672                 self._reporter.xml_assert(filedoc.get_xml_path(),
673                         "expected absolute path in Doxygen-produced XML file")
674                 continue
675             extension = os.path.splitext(filedoc.get_path())[1]
676             # We don't care about Markdown files that only produce pages
677             # (and fail the directory check below).
678             if extension == '.md':
679                 continue
680             dirdoc = filedoc.get_directory()
681             if not dirdoc:
682                 self._reporter.xml_assert(filedoc.get_xml_path(),
683                         "file is not in any directory in Doxygen")
684                 continue
685             relpath = self._get_rel_path(path)
686             fileobj = self._files.get(relpath)
687             if not fileobj:
688                 fileobj = File(path, relpath, self._docmap[dirdoc])
689                 self._files[relpath] = fileobj
690             fileobj.set_doc_xml(filedoc, self)
691             self._docmap[filedoc] = fileobj
692
693     def _load_namespaces(self):
694         """Load Doxygen XML namespace information."""
695         nsdocs = self._docset.get_namespaces()
696         for nsdoc in nsdocs:
697             nsobj = Namespace(nsdoc)
698             self._docmap[nsdoc] = nsobj
699             self._namespaces.add(nsobj)
700
701     def _load_classes(self):
702         """Load Doxygen XML class information."""
703         classdocs = self._docset.get_classes()
704         for classdoc in classdocs:
705             files = [self._docmap[filedoc] for filedoc in classdoc.get_files()]
706             classobj = Class(classdoc, files)
707             self._docmap[classdoc] = classobj
708             self._classes.add(classobj)
709
710     def _load_members(self):
711         """Load Doxygen XML member information."""
712         memberdocs = self._docset.get_members()
713         for memberdoc in memberdocs:
714             nsdoc = memberdoc.get_namespace()
715             nsobj = self.get_object(nsdoc)
716             memberobj = Member(memberdoc, nsobj)
717             self._docmap[memberdoc] = memberobj
718             self._members.add(memberobj)
719
720     def _get_dir(self, relpath):
721         """Get directory object for a path relative to source tree root."""
722         return self._dirs.get(relpath)
723
724     def get_file(self, path):
725         """Get file object for a path relative to source tree root."""
726         return self._files.get(self._get_rel_path(path))
727
728     def find_include_file(self, includedpath):
729         """Find a file object corresponding to an include path."""
730         for testdir in ('src', 'src/gromacs/legacyheaders', 'src/external/thread_mpi/include',
731                 'src/external/tng_io/include'):
732             testpath = os.path.join(testdir, includedpath)
733             if testpath in self._files:
734                 return self._files[testpath]
735
736     def set_installed_file_list(self, installedfiles):
737         """Set list of installed files."""
738         for path in installedfiles:
739             if not os.path.isabs(path):
740                 self._reporter.input_error(
741                         "installed file not specified with absolute path: {0}"
742                         .format(path))
743                 continue
744             relpath = self._get_rel_path(path)
745             if relpath not in self._files:
746                 self._reporter.input_error(
747                         "installed file not in source tree: {0}".format(path))
748                 continue
749             self._files[relpath].set_installed()
750
751     def load_cycle_suppression_list(self, filename):
752         """Load a list of edges to suppress in cycles.
753
754         These edges between modules, if present, will be marked in the
755         corresponding ModuleDependency objects.
756         """
757         with open(filename, 'r') as fp:
758             for line in fp:
759                 line = line.strip()
760                 if not line or line.startswith('#'):
761                     continue
762                 modulenames = ['module_' + x.strip() for x in line.split('->')]
763                 if len(modulenames) != 2:
764                     self._reporter.input_error(
765                             "invalid cycle suppression line: {0}".format(line))
766                     continue
767                 firstmodule = self._modules.get(modulenames[0])
768                 secondmodule = self._modules.get(modulenames[1])
769                 if not firstmodule or not secondmodule:
770                     self._reporter.input_error(
771                             "unknown modules mentioned on cycle suppression line: {0}".format(line))
772                     continue
773                 for dep in firstmodule.get_dependencies():
774                     if dep.get_other_module() == secondmodule:
775                         # TODO: Check that each suppression is actually part of
776                         # a cycle.
777                         dep.set_cycle_suppression()
778
779     def get_object(self, docobj):
780         """Get tree object for a Doxygen XML object."""
781         if docobj is None:
782             return None
783         return self._docmap.get(docobj)
784
785     def get_files(self):
786         """Get iterable for all files in the source tree."""
787         return self._files.itervalues()
788
789     def get_modules(self):
790         """Get iterable for all modules in the source tree."""
791         return self._modules.itervalues()
792
793     def get_classes(self):
794         """Get iterable for all classes in the source tree."""
795         return self._classes
796
797     def get_members(self):
798         """Get iterable for all members (in Doxygen terms) in the source tree."""
799         return self._members