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