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.
59 import doxygenxml as xml
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
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
92 return '<{0}>'.format(self._included_path)
94 return '"{0}"'.format(self._included_path)
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
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."""
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
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):
140 def get_first_line(self):
141 return self._first_line
143 def get_last_line(self):
144 return self._last_line
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
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()
162 self._include_blocks = []
163 self._main_header = None
166 self._used_config_h_defines = set()
167 directory.add_file(self)
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)
180 self._modules.add(module)
182 def set_installed(self):
183 """Mark the file installed."""
184 self._installed = True
186 def set_git_filter_attribute(self, filtername):
187 """Set the git filter attribute associated with the file."""
188 self._filter = filtername
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
195 def _process_include(self, lineno, is_system, includedpath, line, sourcetree):
196 """Process #include directive during scan()."""
199 fileobj = sourcetree.find_include_file(includedpath)
201 fullpath = os.path.join(self._dir.get_abspath(), includedpath)
202 fullpath = os.path.abspath(fullpath)
203 if os.path.exists(fullpath):
205 fileobj = sourcetree.get_file(fullpath)
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)
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>[^">]*)[">]'
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)
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)
232 current_block.add_file(included_file)
233 elif line and not line.isspace():
238 def add_used_config_h_defines(self, defines):
239 """Set config.h defines used in this file.
241 Used internally by find_config_h_uses()."""
242 self._used_config_h_defines.update(defines)
244 def get_reporter_location(self):
245 return reporter.Location(self._abspath, None)
247 def is_installed(self):
248 return self._installed
250 def is_external(self):
251 return self._dir.is_external()
253 def is_source_file(self):
254 return self._sourcefile
256 def is_test_file(self):
257 return self._dir.is_test_directory()
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')
263 def is_documented(self):
264 return self._rawdoc and self._rawdoc.is_documented()
266 def has_brief_description(self):
267 return self._rawdoc and self._rawdoc.has_brief_description()
269 def get_abspath(self):
272 def get_relpath(self):
276 return os.path.basename(self._abspath)
278 def get_directory(self):
281 def get_doc_type(self):
284 return self._rawdoc.get_visibility()
286 def get_api_type(self):
289 def api_type_is_reliable(self):
290 if self._apitype in (DocType.internal, DocType.library):
292 module = self.get_module()
293 return module and module.is_documented()
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()
300 def is_module_internal(self):
301 if self.is_source_file():
303 return not self.is_installed() and self.get_api_type() <= DocType.internal
305 def get_expected_module(self):
306 return self._dir.get_module()
308 def get_doc_modules(self):
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]
317 def get_includes(self):
318 return self._includes
320 def get_include_blocks(self):
321 return self._include_blocks
323 def get_main_header(self):
324 return self._main_header
326 def get_contents(self):
329 def get_used_config_h_defines(self):
330 """Return set of defines from config.h that are used in this file.
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
336 class GeneratedFile(File):
337 def __init__(self, abspath, relpath, directory):
338 File.__init__(self, abspath, relpath, directory)
339 self._generator_source_file = None
341 def scan_contents(self, sourcetree, keep_contents):
342 if os.path.exists(self.get_abspath()):
343 File.scan_contents(self, sourcetree, keep_contents)
345 def set_generator_source(self, sourcefile):
346 self._generator_source_file = sourcefile
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)
353 class GeneratorSourceFile(File):
354 def __init__(self, abspath, relpath, directory):
355 File.__init__(self, abspath, relpath, directory)
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)
363 define_re = r'^#.*define\s+(\w*)'
364 for line in self.get_contents():
365 match = re.match(define_re, line)
367 self._defines.append(match.group(1))
369 def get_defines(self):
370 """Return set of possible defines from config.h.cmakein.
372 The information is only populated for config.h.cmakein."""
375 class Directory(object):
377 """(Sub)directory in the GROMACS tree."""
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
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()
396 parent._subdirs.add(self)
398 self._has_installed_files = None
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
406 def set_module(self, module):
407 assert self._module is None
408 self._module = module
410 def add_file(self, fileobj):
411 self._files.add(fileobj)
416 def get_reporter_location(self):
417 return reporter.Location(self._abspath, None)
419 def get_abspath(self):
422 def get_relpath(self):
425 def is_test_directory(self):
426 return self._is_test_dir
428 def is_external(self):
429 return self._is_external
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
438 for fileobj in self._files:
439 if fileobj.is_installed():
440 self._has_installed_files = True
442 return self._has_installed_files
444 def get_module(self):
448 return self._parent.get_module()
451 def get_subdirectories(self):
455 for subdir in self._subdirs:
456 for fileobj in subdir.get_files():
458 for fileobj in self._files:
461 def contains(self, fileobj):
462 """Check whether file is within the directory or its subdirectories."""
463 dirobj = fileobj.get_directory()
467 dirobj = dirobj._parent
470 class ModuleDependency(object):
472 """Dependency between modules."""
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
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)
485 def set_cycle_suppression(self):
486 """Set suppression on cycles containing this dependency."""
487 self._cyclesuppression = True
489 def is_cycle_suppressed(self):
490 """Return whether cycles containing this dependency are suppressed."""
491 return self._cyclesuppression is not None
493 def get_other_module(self):
494 """Get module that this dependency is to."""
495 return self._othermodule
497 def get_included_files(self):
498 """Get IncludedFile objects for the individual include dependencies."""
499 return self._includedfiles
501 class Module(object):
503 """Code module in the GROMACS source tree.
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.
512 def __init__(self, name, rootdir):
515 self._rootdir = rootdir
517 self._dependencies = dict()
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())
526 groupname = groups[0].get_name()
527 if groupname.startswith('group_'):
528 self._group = groupname[6:]
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)
537 def is_documented(self):
538 return self._rawdoc is not None
543 def get_root_dir(self):
547 # TODO: Include public API convenience headers?
548 return self._rootdir.get_files()
553 def get_dependencies(self):
554 return self._dependencies.itervalues()
556 class Namespace(object):
558 """Namespace in the GROMACS source code."""
560 def __init__(self, rawdoc):
561 self._rawdoc = rawdoc
563 def is_anonymous(self):
564 return self._rawdoc.is_anonymous()
568 """Class/struct/union in the GROMACS source code."""
570 def __init__(self, rawdoc, files):
571 self._rawdoc = rawdoc
572 self._files = set(files)
575 return self._rawdoc.get_name()
577 def get_reporter_location(self):
578 return self._rawdoc.get_reporter_location()
583 def is_documented(self):
584 return self._rawdoc.is_documented()
586 def has_brief_description(self):
587 return self._rawdoc.has_brief_description()
589 def get_doc_type(self):
590 """Return documentation type (visibility) for the class.
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.
597 if not self.is_documented():
599 if self._rawdoc.is_local():
600 return DocType.internal
601 return self._rawdoc.get_visibility()
603 def get_file_doc_type(self):
604 return max([fileobj.get_doc_type() for fileobj in self._files])
606 def is_in_installed_file(self):
607 return any([fileobj.is_installed() for fileobj in self._files])
609 class Member(object):
611 """Member (in Doxygen terminology) in the GROMACS source tree.
613 Currently, modeling is limited to the minimal set of properties that the
617 def __init__(self, rawdoc, namespace):
618 self._rawdoc = rawdoc
619 self._namespace = namespace
622 return self._rawdoc.get_name()
624 def get_reporter_location(self):
625 return self._rawdoc.get_reporter_location()
627 def is_documented(self):
628 return self._rawdoc.is_documented()
630 def has_brief_description(self):
631 return self._rawdoc.has_brief_description()
633 def has_inbody_description(self):
634 return self._rawdoc.has_inbody_description()
636 def is_visible(self):
637 """Return whether the member is visible in Doxygen documentation.
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.
644 if self._namespace and self._namespace.is_anonymous():
646 return self._rawdoc.get_inherited_visibility() != DocType.none
649 class GromacsTree(object):
651 """Root object for navigating the GROMACS source tree.
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.
658 load_git_attributes() can be called to load attribute information from
659 .gitattributes for all the files.
661 load_installed_file_list() can be called to load the list of installed
662 files from the build tree (generated by CMake).
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).
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
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).
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
685 self._docmap = 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'))
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'))
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)
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))
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)
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)
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
782 def scan_files(self, only_files=None, keep_contents=False):
783 """Read source files to initialize #include dependencies."""
785 filelist = only_files
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()
793 for includedfile in fileobj.get_includes():
794 otherfile = includedfile.get_file()
796 othermodule = otherfile.get_module()
797 if othermodule and othermodule != module:
798 module.add_dependency(othermodule, includedfile)
800 def load_xml(self, only_files=None):
801 """Load Doxygen XML information.
803 If only_files is True, XML data is not loaded for code constructs, but
804 only for files, directories, and their potential parents.
806 xmldir = os.path.join(self._build_root, 'docs', 'html', 'doxygen', 'xml')
807 self._docset = xml.DocumentationSet(xmldir, self._reporter)
809 if isinstance(only_files, collections.Iterable):
810 filelist = [x.get_abspath() for x in only_files]
811 self._docset.load_file_details(filelist)
813 self._docset.load_file_details()
815 self._docset.load_details()
816 self._docset.merge_duplicates()
821 self._load_namespaces()
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)
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")
839 relpath = self._get_rel_path(path)
840 dirobj = self._dirs.get(relpath)
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)
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())
856 self._reporter.input_error(
857 "no matching directory for module: {0}".format(moduledoc))
859 moduleobj.set_doc_xml(moduledoc, self)
860 self._docmap[moduledoc] = moduleobj
862 def _load_files(self):
863 """Load Doxygen XML file information."""
864 for filedoc in self._docset.get_files():
865 path = filedoc.get_path()
867 # In case of only partially loaded file information,
868 # the path information is not set for unloaded files.
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")
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':
879 dirdoc = filedoc.get_directory()
881 self._reporter.xml_assert(filedoc.get_xml_path(),
882 "file is not in any directory in Doxygen")
884 relpath = self._get_rel_path(path)
885 fileobj = self._files.get(relpath)
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
892 def _load_namespaces(self):
893 """Load Doxygen XML namespace information."""
894 nsdocs = self._docset.get_namespaces()
896 nsobj = Namespace(nsdoc)
897 self._docmap[nsdoc] = nsobj
898 self._namespaces.add(nsobj)
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)
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)
919 def _get_dir(self, relpath):
920 """Get directory object for a path relative to source tree root."""
921 return self._dirs.get(relpath)
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))
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]
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)
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)
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:
974 if not os.path.isabs(path):
975 self._reporter.input_error(
976 "installed file not specified with absolute path: {0}"
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))
984 self._files[relpath].set_installed()
986 def load_cycle_suppression_list(self, filename):
987 """Load a list of edges to suppress in cycles.
989 These edges between modules, if present, will be marked in the
990 corresponding ModuleDependency objects.
992 with open(filename, 'r') as fp:
995 if not line or line.startswith('#'):
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))
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))
1008 for dep in firstmodule.get_dependencies():
1009 if dep.get_other_module() == secondmodule:
1010 # TODO: Check that each suppression is actually part of
1012 dep.set_cycle_suppression()
1014 def get_object(self, docobj):
1015 """Get tree object for a Doxygen XML object."""
1018 return self._docmap.get(docobj)
1020 def get_files(self):
1021 """Get iterable for all files in the source tree."""
1022 return self._files.itervalues()
1024 def get_modules(self):
1025 """Get iterable for all modules in the source tree."""
1026 return self._modules.itervalues()
1028 def get_classes(self):
1029 """Get iterable for all classes in the source tree."""
1030 return self._classes
1032 def get_members(self):
1033 """Get iterable for all members (in Doxygen terms) in the source tree."""
1034 return self._members