687280c41a2bd603fbe48d5aa9b28eced6f7df9e
[alexxy/gromacs.git] / docs / doxygen / gmxtree.py
1 #!/usr/bin/python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
5 # Copyright (c) 2014,2015, 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 collections
54 import os
55 import os.path
56 import re
57 import subprocess
58
59 import doxygenxml as xml
60 import reporter
61 # We import DocType directly so that it is exposed from this module as well.
62 from doxygenxml import DocType
63
64 def _get_api_type_for_compound(grouplist):
65     """Helper function to deduce API type from Doxygen group membership."""
66     result = DocType.internal
67     for group in grouplist:
68         if isinstance(group, xml.Group):
69             if group.get_name() == 'group_publicapi':
70                 result = DocType.public
71             elif group.get_name() == 'group_libraryapi':
72                 result = DocType.library
73             # TODO: Check for multiple group membership
74     return result
75
76 class IncludedFile(object):
77
78     """Information about an #include directive in a file."""
79
80     def __init__(self, including_file, lineno, included_file, included_path, is_relative, is_system, line):
81         self._including_file = including_file
82         self._line_number = lineno
83         self._included_file = included_file
84         self._included_path = included_path
85         #self._used_include_path = used_include_path
86         self._is_relative = is_relative
87         self._is_system = is_system
88         self._line = line
89
90     def __str__(self):
91         if self._is_system:
92             return '<{0}>'.format(self._included_path)
93         else:
94             return '"{0}"'.format(self._included_path)
95
96     def is_system(self):
97         return self._is_system
98
99     def is_relative(self):
100         return self._is_relative
101
102     def get_included_path(self):
103         return self._included_path
104
105     def get_including_file(self):
106         return self._including_file
107
108     def get_file(self):
109         return self._included_file
110
111     def get_line_number(self):
112         return self._line_number
113
114     def get_full_line(self):
115         """Return the full source line on which this include appears.
116
117         Trailing newline is included."""
118         return self._line
119
120     def get_reporter_location(self):
121         return reporter.Location(self._including_file.get_abspath(), self._line_number)
122
123 class IncludeBlock(object):
124
125     """Block of consequent #include directives in a file."""
126
127     def __init__(self, first_included_file):
128         self._first_line = first_included_file.get_line_number()
129         self._last_line = self._first_line
130         self._files = []
131         self.add_file(first_included_file)
132
133     def add_file(self, included_file):
134         self._files.append(included_file)
135         self._last_line = included_file.get_line_number()
136
137     def get_includes(self):
138         return self._files
139
140     def get_first_line(self):
141         return self._first_line
142
143     def get_last_line(self):
144         return self._last_line
145
146 class File(object):
147
148     """Source/header file in the GROMACS tree."""
149
150     def __init__(self, abspath, relpath, directory):
151         """Initialize a file representation with basic information."""
152         self._abspath = abspath
153         self._relpath = relpath
154         self._dir = directory
155         self._rawdoc = None
156         self._installed = False
157         extension = os.path.splitext(abspath)[1]
158         self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
159         self._apitype = DocType.none
160         self._modules = set()
161         self._includes = []
162         self._include_blocks = []
163         self._main_header = None
164         self._lines = None
165         self._filter = None
166         self._used_config_h_defines = set()
167         directory.add_file(self)
168
169     def set_doc_xml(self, rawdoc, sourcetree):
170         """Assiociate Doxygen documentation entity with the file."""
171         assert self._rawdoc is None
172         assert rawdoc.is_source_file() == self._sourcefile
173         self._rawdoc = rawdoc
174         if self._rawdoc.is_documented():
175             grouplist = self._rawdoc.get_groups()
176             self._apitype = _get_api_type_for_compound(grouplist)
177             for group in grouplist:
178                 module = sourcetree.get_object(group)
179                 if module:
180                     self._modules.add(module)
181
182     def set_installed(self):
183         """Mark the file installed."""
184         self._installed = True
185
186     def set_git_filter_attribute(self, filtername):
187         """Set the git filter attribute associated with the file."""
188         self._filter = filtername
189
190     def set_main_header(self, included_file):
191         """Set the main header file for a source file."""
192         assert self.is_source_file()
193         self._main_header = included_file
194
195     def _process_include(self, lineno, is_system, includedpath, line, sourcetree):
196         """Process #include directive during scan()."""
197         is_relative = False
198         if is_system:
199             fileobj = sourcetree.find_include_file(includedpath)
200         else:
201             fullpath = os.path.join(self._dir.get_abspath(), includedpath)
202             fullpath = os.path.abspath(fullpath)
203             if os.path.exists(fullpath):
204                 is_relative = True
205                 fileobj = sourcetree.get_file(fullpath)
206             else:
207                 fileobj = sourcetree.find_include_file(includedpath)
208         included_file = IncludedFile(self, lineno, fileobj, includedpath,
209             is_relative, is_system, line)
210         self._includes.append(included_file)
211         return included_file
212
213     def scan_contents(self, sourcetree, keep_contents):
214         """Scan the file contents and initialize information based on it."""
215         # TODO: Consider a more robust regex.
216         include_re = r'^\s*#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
217         current_block = None
218         with open(self._abspath, 'r') as scanfile:
219             contents = scanfile.read()
220         lines = contents.splitlines(True)
221         for lineno, line in enumerate(lines, 1):
222             match = re.match(include_re, line)
223             if match:
224                 is_system = (match.group('quote') == '<')
225                 includedpath = match.group('path')
226                 included_file = self._process_include(lineno, is_system,
227                         includedpath, line, sourcetree)
228                 if current_block is None:
229                     current_block = IncludeBlock(included_file)
230                     self._include_blocks.append(current_block)
231                 else:
232                     current_block.add_file(included_file)
233             elif line and not line.isspace():
234                 current_block = None
235         if keep_contents:
236             self._lines = lines
237
238     def add_used_config_h_defines(self, defines):
239         """Set config.h defines used in this file.
240
241         Used internally by find_config_h_uses()."""
242         self._used_config_h_defines.update(defines)
243
244     def get_reporter_location(self):
245         return reporter.Location(self._abspath, None)
246
247     def is_installed(self):
248         return self._installed
249
250     def is_external(self):
251         return self._dir.is_external()
252
253     def is_source_file(self):
254         return self._sourcefile
255
256     def is_test_file(self):
257         return self._dir.is_test_directory()
258
259     def should_includes_be_sorted(self):
260         """Return whether the include directives in the file should be sorted."""
261         return self._filter in ('includesort', 'uncrustify')
262
263     def is_documented(self):
264         return self._rawdoc and self._rawdoc.is_documented()
265
266     def has_brief_description(self):
267         return self._rawdoc and self._rawdoc.has_brief_description()
268
269     def get_abspath(self):
270         return self._abspath
271
272     def get_relpath(self):
273         return self._relpath
274
275     def get_name(self):
276         return os.path.basename(self._abspath)
277
278     def get_directory(self):
279         return self._dir
280
281     def get_doc_type(self):
282         if not self._rawdoc:
283             return DocType.none
284         return self._rawdoc.get_visibility()
285
286     def get_api_type(self):
287         return self._apitype
288
289     def api_type_is_reliable(self):
290         if self._apitype in (DocType.internal, DocType.library):
291             return True
292         module = self.get_module()
293         return module and module.is_documented()
294
295     def is_public(self):
296         if self.api_type_is_reliable():
297             return self.get_api_type() == DocType.public
298         return self.get_api_type() == DocType.public or self.is_installed()
299
300     def is_module_internal(self):
301         if self.is_source_file():
302             return True
303         return not self.is_installed() and self.get_api_type() <= DocType.internal
304
305     def get_expected_module(self):
306         return self._dir.get_module()
307
308     def get_doc_modules(self):
309         return self._modules
310
311     def get_module(self):
312         module = self.get_expected_module()
313         if not module and len(self._modules) == 1:
314             module = list(self._modules)[0]
315         return module
316
317     def get_includes(self):
318         return self._includes
319
320     def get_include_blocks(self):
321         return self._include_blocks
322
323     def get_main_header(self):
324         return self._main_header
325
326     def get_contents(self):
327         return self._lines
328
329     def get_used_config_h_defines(self):
330         """Return set of defines from config.h that are used in this file.
331
332         The return value is empty if find_config_h_uses() has not been called,
333         as well as for headers that declare these defines."""
334         return self._used_config_h_defines
335
336 class GeneratedFile(File):
337     def __init__(self, abspath, relpath, directory):
338         File.__init__(self, abspath, relpath, directory)
339         self._generator_source_file = None
340
341     def scan_contents(self, sourcetree, keep_contents):
342         if os.path.exists(self.get_abspath()):
343             File.scan_contents(self, sourcetree, keep_contents)
344
345     def set_generator_source(self, sourcefile):
346         self._generator_source_file = sourcefile
347
348     def get_reporter_location(self):
349         if self._generator_source_file:
350             return self._generator_source_file.get_reporter_location()
351         return File.get_reporter_location(self)
352
353 class GeneratorSourceFile(File):
354     def __init__(self, abspath, relpath, directory):
355         File.__init__(self, abspath, relpath, directory)
356         self._defines = None
357
358     def scan_contents(self, sourcetree, keep_contents):
359         detect_defines = (self.get_name() == 'config.h.cmakein')
360         File.scan_contents(self, sourcetree, keep_contents or detect_defines)
361         if detect_defines:
362             self._defines = []
363             define_re = r'^#.*define\s+(\w*)'
364             for line in self.get_contents():
365                 match = re.match(define_re, line)
366                 if match:
367                     self._defines.append(match.group(1))
368
369     def get_defines(self):
370         """Return set of possible defines from config.h.cmakein.
371
372         The information is only populated for config.h.cmakein."""
373         return self._defines
374
375 class Directory(object):
376
377     """(Sub)directory in the GROMACS tree."""
378
379     def __init__(self, abspath, relpath, parent):
380         """Initialize a file representation with basic information."""
381         self._abspath = abspath
382         self._relpath = relpath
383         self._name = os.path.basename(abspath)
384         self._parent = parent
385         self._rawdoc = None
386         self._module = None
387         self._is_test_dir = False
388         if parent and parent.is_test_directory() or \
389                 self._name in ('tests', 'legacytests'):
390             self._is_test_dir = True
391         self._is_external = False
392         if parent and parent.is_external() or self._name == 'external':
393             self._is_external = True
394         self._subdirs = set()
395         if parent:
396             parent._subdirs.add(self)
397         self._files = set()
398         self._has_installed_files = None
399
400     def set_doc_xml(self, rawdoc, sourcetree):
401         """Assiociate Doxygen documentation entity with the directory."""
402         assert self._rawdoc is None
403         assert self._abspath == rawdoc.get_path().rstrip('/')
404         self._rawdoc = rawdoc
405
406     def set_module(self, module):
407         assert self._module is None
408         self._module = module
409
410     def add_file(self, fileobj):
411         self._files.add(fileobj)
412
413     def get_name(self):
414         return self._name
415
416     def get_reporter_location(self):
417         return reporter.Location(self._abspath, None)
418
419     def get_abspath(self):
420         return self._abspath
421
422     def get_relpath(self):
423         return self._relpath
424
425     def is_test_directory(self):
426         return self._is_test_dir
427
428     def is_external(self):
429         return self._is_external
430
431     def has_installed_files(self):
432         if self._has_installed_files is None:
433             self._has_installed_files = False
434             for subdir in self._subdirs:
435                 if subdir.has_installed_files():
436                     self._has_installed_files = True
437                     return True
438             for fileobj in self._files:
439                 if fileobj.is_installed():
440                     self._has_installed_files = True
441                     return True
442         return self._has_installed_files
443
444     def get_module(self):
445         if self._module:
446             return self._module
447         if self._parent:
448             return self._parent.get_module()
449         return None
450
451     def get_subdirectories(self):
452         return self._subdirs
453
454     def get_files(self):
455         for subdir in self._subdirs:
456             for fileobj in subdir.get_files():
457                 yield fileobj
458         for fileobj in self._files:
459             yield fileobj
460
461     def contains(self, fileobj):
462         """Check whether file is within the directory or its subdirectories."""
463         dirobj = fileobj.get_directory()
464         while dirobj:
465             if dirobj == self:
466                 return True
467             dirobj = dirobj._parent
468         return False
469
470 class ModuleDependency(object):
471
472     """Dependency between modules."""
473
474     def __init__(self, othermodule):
475         """Initialize empty dependency object with given module as dependency."""
476         self._othermodule = othermodule
477         self._includedfiles = []
478         self._cyclesuppression = None
479
480     def add_included_file(self, includedfile):
481         """Add IncludedFile that is part of this dependency."""
482         assert includedfile.get_file().get_module() == self._othermodule
483         self._includedfiles.append(includedfile)
484
485     def set_cycle_suppression(self):
486         """Set suppression on cycles containing this dependency."""
487         self._cyclesuppression = True
488
489     def is_cycle_suppressed(self):
490         """Return whether cycles containing this dependency are suppressed."""
491         return self._cyclesuppression is not None
492
493     def get_other_module(self):
494         """Get module that this dependency is to."""
495         return self._othermodule
496
497     def get_included_files(self):
498         """Get IncludedFile objects for the individual include dependencies."""
499         return self._includedfiles
500
501 class Module(object):
502
503     """Code module in the GROMACS source tree.
504
505     Modules are specific subdirectories that host a more or less coherent
506     set of routines.  Simplified, every subdirectory under src/gromacs/ is
507     a different module.  This object provides that abstraction and also links
508     the subdirectory to the module documentation (documented as a group in
509     Doxygen) if that exists.
510     """
511
512     def __init__(self, name, rootdir):
513         self._name = name
514         self._rawdoc = None
515         self._rootdir = rootdir
516         self._group = None
517         self._dependencies = dict()
518
519     def set_doc_xml(self, rawdoc, sourcetree):
520         """Assiociate Doxygen documentation entity with the module."""
521         assert self._rawdoc is None
522         self._rawdoc = rawdoc
523         if self._rawdoc.is_documented():
524             groups = list(self._rawdoc.get_groups())
525             if len(groups) == 1:
526                 groupname = groups[0].get_name()
527                 if groupname.startswith('group_'):
528                     self._group = groupname[6:]
529
530     def add_dependency(self, othermodule, includedfile):
531         """Add #include dependency from a file in this module."""
532         assert includedfile.get_file().get_module() == othermodule
533         if othermodule not in self._dependencies:
534             self._dependencies[othermodule] = ModuleDependency(othermodule)
535         self._dependencies[othermodule].add_included_file(includedfile)
536
537     def is_documented(self):
538         return self._rawdoc is not None
539
540     def get_name(self):
541         return self._name
542
543     def get_root_dir(self):
544         return self._rootdir
545
546     def get_files(self):
547         # TODO: Include public API convenience headers?
548         return self._rootdir.get_files()
549
550     def get_group(self):
551         return self._group
552
553     def get_dependencies(self):
554         return self._dependencies.itervalues()
555
556 class Namespace(object):
557
558     """Namespace in the GROMACS source code."""
559
560     def __init__(self, rawdoc):
561         self._rawdoc = rawdoc
562
563     def is_anonymous(self):
564         return self._rawdoc.is_anonymous()
565
566 class Class(object):
567
568     """Class/struct/union in the GROMACS source code."""
569
570     def __init__(self, rawdoc, files):
571         self._rawdoc = rawdoc
572         self._files = set(files)
573
574     def get_name(self):
575         return self._rawdoc.get_name()
576
577     def get_reporter_location(self):
578         return self._rawdoc.get_reporter_location()
579
580     def get_files(self):
581         return self._files
582
583     def is_documented(self):
584         return self._rawdoc.is_documented()
585
586     def has_brief_description(self):
587         return self._rawdoc.has_brief_description()
588
589     def get_doc_type(self):
590         """Return documentation type (visibility) for the class.
591
592         In addition to the actual code, this encodes GROMACS-specific logic
593         of setting EXTRACT_LOCAL_CLASSES=YES only for the full documentation.
594         Local classes never appear outside the full documentation, no matter
595         what is their visibility.
596         """
597         if not self.is_documented():
598             return DocType.none
599         if self._rawdoc.is_local():
600             return DocType.internal
601         return self._rawdoc.get_visibility()
602
603     def get_file_doc_type(self):
604         return max([fileobj.get_doc_type() for fileobj in self._files])
605
606     def is_in_installed_file(self):
607         return any([fileobj.is_installed() for fileobj in self._files])
608
609 class Member(object):
610
611     """Member (in Doxygen terminology) in the GROMACS source tree.
612
613     Currently, modeling is limited to the minimal set of properties that the
614     checker uses.
615     """
616
617     def __init__(self, rawdoc, namespace):
618         self._rawdoc = rawdoc
619         self._namespace = namespace
620
621     def get_name(self):
622         return self._rawdoc.get_name()
623
624     def get_reporter_location(self):
625         return self._rawdoc.get_reporter_location()
626
627     def is_documented(self):
628         return self._rawdoc.is_documented()
629
630     def has_brief_description(self):
631         return self._rawdoc.has_brief_description()
632
633     def has_inbody_description(self):
634         return self._rawdoc.has_inbody_description()
635
636     def is_visible(self):
637         """Return whether the member is visible in Doxygen documentation.
638
639         Doxygen ignores members whose parent compounds are not documented.
640         However, when EXTRACT_ANON_NPACES=ON (which is set for our full
641         documentation), members of anonymous namespaces are extracted even if
642         the namespace is the only parent and is not documented.
643         """
644         if self._namespace and self._namespace.is_anonymous():
645             return True
646         return self._rawdoc.get_inherited_visibility() != DocType.none
647
648
649 class GromacsTree(object):
650
651     """Root object for navigating the GROMACS source tree.
652
653     On initialization, the list of files and directories is initialized by
654     walking the source tree, and modules are created for top-level
655     subdirectories.  At this point, only information that is accessible from
656     file names and paths only is available.
657
658     load_git_attributes() can be called to load attribute information from
659     .gitattributes for all the files.
660
661     load_installed_file_list() can be called to load the list of installed
662     files from the build tree (generated by CMake).
663
664     scan_files() can be called to read all the files and initialize #include
665     dependencies between the files based on the information.  This is done like
666     this instead of relying on Doxygen-extracted include files to make the
667     dependency graph independent from preprocessor macro definitions
668     (Doxygen only sees those #includes that the preprocessor sees, which
669     depends on what #defines it has seen).
670
671     find_config_h_uses() can be called to find all uses of defines declared in
672     config.h.  In the current implementation, scan_files() must have been
673     called earlier.
674
675     load_xml() can be called to load information from Doxygen XML data in
676     the build tree (the Doxygen XML data must have been built separately).
677     """
678
679     def __init__(self, source_root, build_root, reporter):
680         """Initialize the tree object by walking the source tree."""
681         self._source_root = os.path.abspath(source_root)
682         self._build_root = os.path.abspath(build_root)
683         self._reporter = reporter
684         self._docset = None
685         self._docmap = dict()
686         self._dirs = dict()
687         self._files = dict()
688         self._modules = dict()
689         self._classes = set()
690         self._namespaces = set()
691         self._members = set()
692         self._walk_dir(os.path.join(self._source_root, 'src'))
693         for fileobj in self.get_files():
694             if fileobj and fileobj.is_source_file() and not fileobj.is_external():
695                 (basedir, name) = os.path.split(fileobj.get_abspath())
696                 (basename, ext) = os.path.splitext(name)
697                 header = self.get_file(os.path.join(basedir, basename + '.h'))
698                 if not header and ext == '.cu':
699                     header = self.get_file(os.path.join(basedir, basename + '.cuh'))
700                 if not header and fileobj.is_test_file():
701                     basedir = os.path.dirname(basedir)
702                     header = self.get_file(os.path.join(basedir, basename + '.h'))
703                     if not header:
704                         # Somewhat of a hack; currently, the tests for
705                         # analysisdata/modules/ and trajectoryanalysis/modules/
706                         # is at the top-level tests directory.
707                         # TODO: It could be clearer to split the tests so that
708                         # there would be a separate modules/tests/.
709                         header = self.get_file(os.path.join(basedir, 'modules', basename + '.h'))
710                     if not header and basename.endswith('_tests'):
711                         header = self.get_file(os.path.join(basedir, basename[:-6] + '.h'))
712                 if not header and fileobj.get_relpath().startswith('src/gromacs'):
713                     header = self._files.get(os.path.join('src/gromacs/legacyheaders', basename + '.h'))
714                 if header:
715                     fileobj.set_main_header(header)
716         rootdir = self._get_dir(os.path.join('src', 'gromacs'))
717         for subdir in rootdir.get_subdirectories():
718             self._create_module(subdir)
719         rootdir = self._get_dir(os.path.join('src', 'testutils'))
720         self._create_module(rootdir)
721
722     def _get_rel_path(self, path):
723         assert os.path.isabs(path)
724         if path.startswith(self._build_root):
725             return os.path.relpath(path, self._build_root)
726         if path.startswith(self._source_root):
727             return os.path.relpath(path, self._source_root)
728         raise ValueError("path not under build nor source tree: {0}".format(path))
729
730     def _walk_dir(self, rootpath):
731         """Construct representation of the source tree by walking the file system."""
732         assert os.path.isabs(rootpath)
733         assert rootpath not in self._dirs
734         relpath = self._get_rel_path(rootpath)
735         self._dirs[relpath] = Directory(rootpath, relpath, None)
736         for dirpath, dirnames, filenames in os.walk(rootpath):
737             if 'contrib' in dirnames:
738                 dirnames.remove('contrib')
739             if 'refdata' in dirnames:
740                 dirnames.remove('refdata')
741             currentdir = self._dirs[self._get_rel_path(dirpath)]
742             # Loop through a copy so that we can modify dirnames.
743             for dirname in list(dirnames):
744                 fullpath = os.path.join(dirpath, dirname)
745                 if fullpath == self._build_root:
746                     dirnames.remove(dirname)
747                     continue
748                 relpath = self._get_rel_path(fullpath)
749                 self._dirs[relpath] = Directory(fullpath, relpath, currentdir)
750             extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
751             for filename in filenames:
752                 basename, extension = os.path.splitext(filename)
753                 if extension in extensions:
754                     fullpath = os.path.join(dirpath, filename)
755                     relpath = self._get_rel_path(fullpath)
756                     self._files[relpath] = File(fullpath, relpath, currentdir)
757                 elif extension == '.cmakein':
758                     extension = os.path.splitext(basename)[1]
759                     if extension in extensions:
760                         fullpath = os.path.join(dirpath, filename)
761                         relpath = self._get_rel_path(fullpath)
762                         sourcefile = GeneratorSourceFile(fullpath, relpath, currentdir)
763                         self._files[relpath] = sourcefile
764                         fullpath = os.path.join(dirpath, basename)
765                         relpath = self._get_rel_path(fullpath)
766                         fullpath = os.path.join(self._build_root, relpath)
767                         generatedfile = GeneratedFile(fullpath, relpath, currentdir)
768                         self._files[relpath] = generatedfile
769                         generatedfile.set_generator_source(sourcefile)
770                 elif extension in ('.l', '.y', '.pre'):
771                     fullpath = os.path.join(dirpath, filename)
772                     relpath = self._get_rel_path(fullpath)
773                     self._files[relpath] = GeneratorSourceFile(fullpath, relpath, currentdir)
774
775     def _create_module(self, rootdir):
776         """Create module for a subdirectory."""
777         name = 'module_' + rootdir.get_name()
778         moduleobj = Module(name, rootdir)
779         rootdir.set_module(moduleobj)
780         self._modules[name] = moduleobj
781
782     def scan_files(self, only_files=None, keep_contents=False):
783         """Read source files to initialize #include dependencies."""
784         if only_files:
785             filelist = only_files
786         else:
787             filelist = self._files.itervalues()
788         for fileobj in filelist:
789             if not fileobj.is_external():
790                 fileobj.scan_contents(self, keep_contents)
791                 module = fileobj.get_module()
792                 if module:
793                     for includedfile in fileobj.get_includes():
794                         otherfile = includedfile.get_file()
795                         if otherfile:
796                             othermodule = otherfile.get_module()
797                             if othermodule and othermodule != module:
798                                 module.add_dependency(othermodule, includedfile)
799
800     def load_xml(self, only_files=None):
801         """Load Doxygen XML information.
802
803         If only_files is True, XML data is not loaded for code constructs, but
804         only for files, directories, and their potential parents.
805         """
806         xmldir = os.path.join(self._build_root, 'docs', 'html', 'doxygen', 'xml')
807         self._docset = xml.DocumentationSet(xmldir, self._reporter)
808         if only_files:
809             if isinstance(only_files, collections.Iterable):
810                 filelist = [x.get_abspath() for x in only_files]
811                 self._docset.load_file_details(filelist)
812             else:
813                 self._docset.load_file_details()
814         else:
815             self._docset.load_details()
816             self._docset.merge_duplicates()
817         self._load_dirs()
818         self._load_modules()
819         self._load_files()
820         if not only_files:
821             self._load_namespaces()
822             self._load_classes()
823             self._load_members()
824
825     def _load_dirs(self):
826         """Load Doxygen XML directory information."""
827         rootdirs = self._docset.get_compounds(xml.Directory,
828                 lambda x: x.get_parent() is None)
829         for dirdoc in rootdirs:
830             self._load_dir(dirdoc, None)
831
832     def _load_dir(self, dirdoc, parent):
833         """Load Doxygen XML directory information for a single directory."""
834         path = dirdoc.get_path().rstrip('/')
835         if not os.path.isabs(path):
836             self._reporter.xml_assert(dirdoc.get_xml_path(),
837                     "expected absolute path in Doxygen-produced XML file")
838             return
839         relpath = self._get_rel_path(path)
840         dirobj = self._dirs.get(relpath)
841         if not dirobj:
842             dirobj = Directory(path, relpath, parent)
843             self._dirs[relpath] = dirobj
844         dirobj.set_doc_xml(dirdoc, self)
845         self._docmap[dirdoc] = dirobj
846         for subdirdoc in dirdoc.get_subdirectories():
847             self._load_dir(subdirdoc, dirobj)
848
849     def _load_modules(self):
850         """Load Doxygen XML module (group) information."""
851         moduledocs = self._docset.get_compounds(xml.Group,
852                 lambda x: x.get_name().startswith('module_'))
853         for moduledoc in moduledocs:
854             moduleobj = self._modules.get(moduledoc.get_name())
855             if not moduleobj:
856                 self._reporter.input_error(
857                         "no matching directory for module: {0}".format(moduledoc))
858                 continue
859             moduleobj.set_doc_xml(moduledoc, self)
860             self._docmap[moduledoc] = moduleobj
861
862     def _load_files(self):
863         """Load Doxygen XML file information."""
864         for filedoc in self._docset.get_files():
865             path = filedoc.get_path()
866             if not path:
867                 # In case of only partially loaded file information,
868                 # the path information is not set for unloaded files.
869                 continue
870             if not os.path.isabs(path):
871                 self._reporter.xml_assert(filedoc.get_xml_path(),
872                         "expected absolute path in Doxygen-produced XML file")
873                 continue
874             extension = os.path.splitext(path)[1]
875             # We don't care about Markdown files that only produce pages
876             # (and fail the directory check below).
877             if extension == '.md':
878                 continue
879             dirdoc = filedoc.get_directory()
880             if not dirdoc:
881                 self._reporter.xml_assert(filedoc.get_xml_path(),
882                         "file is not in any directory in Doxygen")
883                 continue
884             relpath = self._get_rel_path(path)
885             fileobj = self._files.get(relpath)
886             if not fileobj:
887                 fileobj = File(path, relpath, self._docmap[dirdoc])
888                 self._files[relpath] = fileobj
889             fileobj.set_doc_xml(filedoc, self)
890             self._docmap[filedoc] = fileobj
891
892     def _load_namespaces(self):
893         """Load Doxygen XML namespace information."""
894         nsdocs = self._docset.get_namespaces()
895         for nsdoc in nsdocs:
896             nsobj = Namespace(nsdoc)
897             self._docmap[nsdoc] = nsobj
898             self._namespaces.add(nsobj)
899
900     def _load_classes(self):
901         """Load Doxygen XML class information."""
902         classdocs = self._docset.get_classes()
903         for classdoc in classdocs:
904             files = [self._docmap[filedoc] for filedoc in classdoc.get_files()]
905             classobj = Class(classdoc, files)
906             self._docmap[classdoc] = classobj
907             self._classes.add(classobj)
908
909     def _load_members(self):
910         """Load Doxygen XML member information."""
911         memberdocs = self._docset.get_members()
912         for memberdoc in memberdocs:
913             nsdoc = memberdoc.get_namespace()
914             nsobj = self.get_object(nsdoc)
915             memberobj = Member(memberdoc, nsobj)
916             self._docmap[memberdoc] = memberobj
917             self._members.add(memberobj)
918
919     def _get_dir(self, relpath):
920         """Get directory object for a path relative to source tree root."""
921         return self._dirs.get(relpath)
922
923     def get_file(self, path):
924         """Get file object for a path relative to source tree root."""
925         return self._files.get(self._get_rel_path(path))
926
927     def find_include_file(self, includedpath):
928         """Find a file object corresponding to an include path."""
929         for testdir in ('src', 'src/external/thread_mpi/include',
930                 'src/external/tng_io/include'):
931             testpath = os.path.join(testdir, includedpath)
932             if testpath in self._files:
933                 return self._files[testpath]
934
935     def load_git_attributes(self):
936         """Load git attribute information for files."""
937         args = ['git', 'check-attr', '--stdin', 'filter']
938         git_check_attr = subprocess.Popen(args, stdin=subprocess.PIPE,
939                 stdout=subprocess.PIPE, cwd=self._source_root)
940         filelist = '\n'.join(map(File.get_relpath, self._files.itervalues()))
941         filters = git_check_attr.communicate(filelist)[0]
942         for fileinfo in filters.splitlines():
943             path, dummy, value = fileinfo.split(': ')
944             fileobj = self._files.get(path)
945             assert fileobj is not None
946             fileobj.set_git_filter_attribute(value)
947
948     def find_config_h_uses(self):
949         """Find files that use defines from config.h."""
950         # Executing git grep is substantially faster than using the define_re
951         # directly on the contents of the file in Python.
952         args = ['git', 'grep', '-zwIF']
953         configfile = self._files['src/config.h.cmakein']
954         for define in configfile.get_defines():
955             args.extend(['-e', define])
956         args.extend(['--', '*.cpp', '*.c', '*.cu', '*.h', '*.cuh'])
957         define_re = r'\b(?:' + '|'.join(configfile.get_defines())+ r')\b'
958         output = subprocess.check_output(args, cwd=self._source_root)
959         for line in output.splitlines():
960             (filename, text) = line.split('\0')
961             fileobj = self._files.get(filename)
962             if fileobj is not None:
963                 if fileobj.get_name() not in ('config.h', 'config.h.cmakein',
964                         'gmxpre-config.h', 'gmxpre-config.h.cmakein'):
965                     defines = re.findall(define_re, text)
966                     fileobj.add_used_config_h_defines(defines)
967
968     def load_installed_file_list(self):
969         """Load list of installed files from the build tree."""
970         listpath = os.path.join(self._build_root, 'src', 'gromacs', 'installed-headers.txt')
971         with open(listpath, 'r') as installedfp:
972             for line in installedfp:
973                 path = line.strip()
974                 if not os.path.isabs(path):
975                     self._reporter.input_error(
976                             "installed file not specified with absolute path: {0}"
977                             .format(path))
978                     continue
979                 relpath = self._get_rel_path(path)
980                 if relpath not in self._files:
981                     self._reporter.input_error(
982                             "installed file not in source tree: {0}".format(path))
983                     continue
984                 self._files[relpath].set_installed()
985
986     def load_cycle_suppression_list(self, filename):
987         """Load a list of edges to suppress in cycles.
988
989         These edges between modules, if present, will be marked in the
990         corresponding ModuleDependency objects.
991         """
992         with open(filename, 'r') as fp:
993             for line in fp:
994                 line = line.strip()
995                 if not line or line.startswith('#'):
996                     continue
997                 modulenames = ['module_' + x.strip() for x in line.split('->')]
998                 if len(modulenames) != 2:
999                     self._reporter.input_error(
1000                             "invalid cycle suppression line: {0}".format(line))
1001                     continue
1002                 firstmodule = self._modules.get(modulenames[0])
1003                 secondmodule = self._modules.get(modulenames[1])
1004                 if not firstmodule or not secondmodule:
1005                     self._reporter.input_error(
1006                             "unknown modules mentioned on cycle suppression line: {0}".format(line))
1007                     continue
1008                 for dep in firstmodule.get_dependencies():
1009                     if dep.get_other_module() == secondmodule:
1010                         # TODO: Check that each suppression is actually part of
1011                         # a cycle.
1012                         dep.set_cycle_suppression()
1013
1014     def get_object(self, docobj):
1015         """Get tree object for a Doxygen XML object."""
1016         if docobj is None:
1017             return None
1018         return self._docmap.get(docobj)
1019
1020     def get_files(self):
1021         """Get iterable for all files in the source tree."""
1022         return self._files.itervalues()
1023
1024     def get_modules(self):
1025         """Get iterable for all modules in the source tree."""
1026         return self._modules.itervalues()
1027
1028     def get_classes(self):
1029         """Get iterable for all classes in the source tree."""
1030         return self._classes
1031
1032     def get_members(self):
1033         """Get iterable for all members (in Doxygen terms) in the source tree."""
1034         return self._members