3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2014, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
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 directory.add_file(self)
168 def set_doc_xml(self, rawdoc, sourcetree):
169 """Assiociate Doxygen documentation entity with the file."""
170 assert self._rawdoc is None
171 assert rawdoc.is_source_file() == self._sourcefile
172 self._rawdoc = rawdoc
173 if self._rawdoc.is_documented():
174 grouplist = self._rawdoc.get_groups()
175 self._apitype = _get_api_type_for_compound(grouplist)
176 for group in grouplist:
177 module = sourcetree.get_object(group)
179 self._modules.add(module)
181 def set_installed(self):
182 """Mark the file installed."""
183 self._installed = True
185 def set_git_filter_attribute(self, filtername):
186 """Set the git filter attribute associated with the file."""
187 self._filter = filtername
189 def set_main_header(self, included_file):
190 """Set the main header file for a source file."""
191 assert self.is_source_file()
192 self._main_header = included_file
194 def _process_include(self, lineno, is_system, includedpath, line, sourcetree):
195 """Process #include directive during scan()."""
198 fileobj = sourcetree.find_include_file(includedpath)
200 fullpath = os.path.join(self._dir.get_abspath(), includedpath)
201 fullpath = os.path.abspath(fullpath)
202 if os.path.exists(fullpath):
204 fileobj = sourcetree.get_file(fullpath)
206 fileobj = sourcetree.find_include_file(includedpath)
207 included_file = IncludedFile(self, lineno, fileobj, includedpath,
208 is_relative, is_system, line)
209 self._includes.append(included_file)
212 def scan_contents(self, sourcetree, keep_contents):
213 """Scan the file contents and initialize information based on it."""
214 # TODO: Consider a more robust regex.
215 include_re = r'^\s*#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
217 # TODO: Consider reading directly into this list, and iterate that.
219 with open(self._abspath, 'r') as scanfile:
220 for lineno, line in enumerate(scanfile, 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 get_reporter_location(self):
239 return reporter.Location(self._abspath, None)
241 def is_installed(self):
242 return self._installed
244 def is_external(self):
245 return self._dir.is_external()
247 def is_source_file(self):
248 return self._sourcefile
250 def is_test_file(self):
251 return self._dir.is_test_directory()
253 def should_includes_be_sorted(self):
254 """Return whether the include directives in the file should be sorted."""
255 return self._filter in ('includesort', 'uncrustify')
257 def is_documented(self):
258 return self._rawdoc and self._rawdoc.is_documented()
260 def has_brief_description(self):
261 return self._rawdoc and self._rawdoc.has_brief_description()
263 def get_abspath(self):
266 def get_relpath(self):
270 return os.path.basename(self._abspath)
272 def get_directory(self):
275 def get_doc_type(self):
278 return self._rawdoc.get_visibility()
280 def get_api_type(self):
283 def api_type_is_reliable(self):
284 if self._apitype in (DocType.internal, DocType.library):
286 module = self.get_module()
287 return module and module.is_documented()
290 if self.api_type_is_reliable():
291 return self.get_api_type() == DocType.public
292 return self.get_api_type() == DocType.public or self.is_installed()
294 def is_module_internal(self):
295 if self.is_source_file():
297 return not self.is_installed() and self.get_api_type() <= DocType.internal
299 def get_expected_module(self):
300 return self._dir.get_module()
302 def get_doc_modules(self):
305 def get_module(self):
306 module = self.get_expected_module()
307 if not module and len(self._modules) == 1:
308 module = list(self._modules)[0]
311 def get_includes(self):
312 return self._includes
314 def get_include_blocks(self):
315 return self._include_blocks
317 def get_main_header(self):
318 return self._main_header
320 def get_contents(self):
323 class GeneratedFile(File):
324 def __init__(self, abspath, relpath, directory):
325 File.__init__(self, abspath, relpath, directory)
326 self._generator_source_file = None
328 def scan_contents(self, sourcetree, keep_contents):
329 if os.path.exists(self.get_abspath()):
330 File.scan_contents(self, sourcetree, keep_contents)
332 def set_generator_source(self, sourcefile):
333 self._generator_source_file = sourcefile
335 def get_reporter_location(self):
336 if self._generator_source_file:
337 return self._generator_source_file.get_reporter_location()
338 return File.get_reporter_location(self)
340 class GeneratorSourceFile(File):
343 class Directory(object):
345 """(Sub)directory in the GROMACS tree."""
347 def __init__(self, abspath, relpath, parent):
348 """Initialize a file representation with basic information."""
349 self._abspath = abspath
350 self._relpath = relpath
351 self._name = os.path.basename(abspath)
352 self._parent = parent
355 self._is_test_dir = False
356 if parent and parent.is_test_directory() or \
357 self._name in ('tests', 'legacytests'):
358 self._is_test_dir = True
359 self._is_external = False
360 if parent and parent.is_external() or self._name == 'external':
361 self._is_external = True
362 self._subdirs = set()
364 parent._subdirs.add(self)
366 self._has_installed_files = None
368 def set_doc_xml(self, rawdoc, sourcetree):
369 """Assiociate Doxygen documentation entity with the directory."""
370 assert self._rawdoc is None
371 assert self._abspath == rawdoc.get_path().rstrip('/')
372 self._rawdoc = rawdoc
374 def set_module(self, module):
375 assert self._module is None
376 self._module = module
378 def add_file(self, fileobj):
379 self._files.add(fileobj)
384 def get_reporter_location(self):
385 return reporter.Location(self._abspath, None)
387 def get_abspath(self):
390 def get_relpath(self):
393 def is_test_directory(self):
394 return self._is_test_dir
396 def is_external(self):
397 return self._is_external
399 def has_installed_files(self):
400 if self._has_installed_files is None:
401 self._has_installed_files = False
402 for subdir in self._subdirs:
403 if subdir.has_installed_files():
404 self._has_installed_files = True
406 for fileobj in self._files:
407 if fileobj.is_installed():
408 self._has_installed_files = True
410 return self._has_installed_files
412 def get_module(self):
416 return self._parent.get_module()
419 def get_subdirectories(self):
423 for subdir in self._subdirs:
424 for fileobj in subdir.get_files():
426 for fileobj in self._files:
429 def contains(self, fileobj):
430 """Check whether file is within the directory or its subdirectories."""
431 dirobj = fileobj.get_directory()
435 dirobj = dirobj._parent
438 class ModuleDependency(object):
440 """Dependency between modules."""
442 def __init__(self, othermodule):
443 """Initialize empty dependency object with given module as dependency."""
444 self._othermodule = othermodule
445 self._includedfiles = []
446 self._cyclesuppression = None
448 def add_included_file(self, includedfile):
449 """Add IncludedFile that is part of this dependency."""
450 assert includedfile.get_file().get_module() == self._othermodule
451 self._includedfiles.append(includedfile)
453 def set_cycle_suppression(self):
454 """Set suppression on cycles containing this dependency."""
455 self._cyclesuppression = True
457 def is_cycle_suppressed(self):
458 """Return whether cycles containing this dependency are suppressed."""
459 return self._cyclesuppression is not None
461 def get_other_module(self):
462 """Get module that this dependency is to."""
463 return self._othermodule
465 def get_included_files(self):
466 """Get IncludedFile objects for the individual include dependencies."""
467 return self._includedfiles
469 class Module(object):
471 """Code module in the GROMACS source tree.
473 Modules are specific subdirectories that host a more or less coherent
474 set of routines. Simplified, every subdirectory under src/gromacs/ is
475 a different module. This object provides that abstraction and also links
476 the subdirectory to the module documentation (documented as a group in
477 Doxygen) if that exists.
480 def __init__(self, name, rootdir):
483 self._rootdir = rootdir
485 self._dependencies = dict()
487 def set_doc_xml(self, rawdoc, sourcetree):
488 """Assiociate Doxygen documentation entity with the module."""
489 assert self._rawdoc is None
490 self._rawdoc = rawdoc
491 if self._rawdoc.is_documented():
492 groups = list(self._rawdoc.get_groups())
494 groupname = groups[0].get_name()
495 if groupname.startswith('group_'):
496 self._group = groupname[6:]
498 def add_dependency(self, othermodule, includedfile):
499 """Add #include dependency from a file in this module."""
500 assert includedfile.get_file().get_module() == othermodule
501 if othermodule not in self._dependencies:
502 self._dependencies[othermodule] = ModuleDependency(othermodule)
503 self._dependencies[othermodule].add_included_file(includedfile)
505 def is_documented(self):
506 return self._rawdoc is not None
511 def get_root_dir(self):
515 # TODO: Include public API convenience headers?
516 return self._rootdir.get_files()
521 def get_dependencies(self):
522 return self._dependencies.itervalues()
524 class Namespace(object):
526 """Namespace in the GROMACS source code."""
528 def __init__(self, rawdoc):
529 self._rawdoc = rawdoc
531 def is_anonymous(self):
532 return self._rawdoc.is_anonymous()
536 """Class/struct/union in the GROMACS source code."""
538 def __init__(self, rawdoc, files):
539 self._rawdoc = rawdoc
540 self._files = set(files)
543 return self._rawdoc.get_name()
545 def get_reporter_location(self):
546 return self._rawdoc.get_reporter_location()
551 def is_documented(self):
552 return self._rawdoc.is_documented()
554 def has_brief_description(self):
555 return self._rawdoc.has_brief_description()
557 def get_doc_type(self):
558 """Return documentation type (visibility) for the class.
560 In addition to the actual code, this encodes GROMACS-specific logic
561 of setting EXTRACT_LOCAL_CLASSES=YES only for the full documentation.
562 Local classes never appear outside the full documentation, no matter
563 what is their visibility.
565 if not self.is_documented():
567 if self._rawdoc.is_local():
568 return DocType.internal
569 return self._rawdoc.get_visibility()
571 def get_file_doc_type(self):
572 return max([fileobj.get_doc_type() for fileobj in self._files])
574 def is_in_installed_file(self):
575 return any([fileobj.is_installed() for fileobj in self._files])
577 class Member(object):
579 """Member (in Doxygen terminology) in the GROMACS source tree.
581 Currently, modeling is limited to the minimal set of properties that the
585 def __init__(self, rawdoc, namespace):
586 self._rawdoc = rawdoc
587 self._namespace = namespace
590 return self._rawdoc.get_name()
592 def get_reporter_location(self):
593 return self._rawdoc.get_reporter_location()
595 def is_documented(self):
596 return self._rawdoc.is_documented()
598 def has_brief_description(self):
599 return self._rawdoc.has_brief_description()
601 def has_inbody_description(self):
602 return self._rawdoc.has_inbody_description()
604 def is_visible(self):
605 """Return whether the member is visible in Doxygen documentation.
607 Doxygen ignores members whose parent compounds are not documented.
608 However, when EXTRACT_ANON_NPACES=ON (which is set for our full
609 documentation), members of anonymous namespaces are extracted even if
610 the namespace is the only parent and is not documented.
612 if self._namespace and self._namespace.is_anonymous():
614 return self._rawdoc.get_inherited_visibility() != DocType.none
617 class GromacsTree(object):
619 """Root object for navigating the GROMACS source tree.
621 On initialization, the list of files and directories is initialized by
622 walking the source tree, and modules are created for top-level
623 subdirectories. At this point, only information that is accessible from
624 file names and paths only is available.
626 load_git_attributes() can be called to load attribute information from
627 .gitattributes for all the files.
629 load_installed_file_list() can be called to load the list of installed
630 files from the build tree (generated by the find-installed-headers target).
632 scan_files() can be called to read all the files and initialize #include
633 dependencies between the files based on the information. This is done like
634 this instead of relying on Doxygen-extracted include files to make the
635 dependency graph independent from preprocessor macro definitions
636 (Doxygen only sees those #includes that the preprocessor sees, which
637 depends on what #defines it has seen).
639 load_xml() can be called to load information from Doxygen XML data in
640 the build tree (the Doxygen XML data must have been built separately).
643 def __init__(self, source_root, build_root, reporter):
644 """Initialize the tree object by walking the source tree."""
645 self._source_root = os.path.abspath(source_root)
646 self._build_root = os.path.abspath(build_root)
647 self._reporter = reporter
649 self._docmap = dict()
652 self._modules = dict()
653 self._classes = set()
654 self._namespaces = set()
655 self._members = set()
656 self._walk_dir(os.path.join(self._source_root, 'src'))
657 for fileobj in self.get_files():
658 if fileobj and fileobj.is_source_file() and not fileobj.is_external():
659 (basedir, name) = os.path.split(fileobj.get_abspath())
660 (basename, ext) = os.path.splitext(name)
661 header = self.get_file(os.path.join(basedir, basename + '.h'))
662 if not header and ext == '.cu':
663 header = self.get_file(os.path.join(basedir, basename + '.cuh'))
664 if not header and fileobj.is_test_file():
665 basedir = os.path.dirname(basedir)
666 header = self.get_file(os.path.join(basedir, basename + '.h'))
668 # Somewhat of a hack; currently, the tests for
669 # analysisdata/modules/ and trajectoryanalysis/modules/
670 # is at the top-level tests directory.
671 # TODO: It could be clearer to split the tests so that
672 # there would be a separate modules/tests/.
673 header = self.get_file(os.path.join(basedir, 'modules', basename + '.h'))
674 if not header and basename.endswith('_tests'):
675 header = self.get_file(os.path.join(basedir, basename[:-6] + '.h'))
676 if not header and fileobj.get_relpath().startswith('src/gromacs'):
677 header = self._files.get(os.path.join('src/gromacs/legacyheaders', basename + '.h'))
679 fileobj.set_main_header(header)
680 rootdir = self._get_dir(os.path.join('src', 'gromacs'))
681 for subdir in rootdir.get_subdirectories():
682 self._create_module(subdir)
683 rootdir = self._get_dir(os.path.join('src', 'testutils'))
684 self._create_module(rootdir)
686 def _get_rel_path(self, path):
687 assert os.path.isabs(path)
688 if path.startswith(self._build_root):
689 return os.path.relpath(path, self._build_root)
690 if path.startswith(self._source_root):
691 return os.path.relpath(path, self._source_root)
692 raise ValueError("path not under build nor source tree: {0}".format(path))
694 def _walk_dir(self, rootpath):
695 """Construct representation of the source tree by walking the file system."""
696 assert os.path.isabs(rootpath)
697 assert rootpath not in self._dirs
698 relpath = self._get_rel_path(rootpath)
699 self._dirs[relpath] = Directory(rootpath, relpath, None)
700 for dirpath, dirnames, filenames in os.walk(rootpath):
701 if 'contrib' in dirnames:
702 dirnames.remove('contrib')
703 if 'refdata' in dirnames:
704 dirnames.remove('refdata')
705 currentdir = self._dirs[self._get_rel_path(dirpath)]
706 # Loop through a copy so that we can modify dirnames.
707 for dirname in list(dirnames):
708 fullpath = os.path.join(dirpath, dirname)
709 if fullpath == self._build_root:
710 dirnames.remove(dirname)
712 relpath = self._get_rel_path(fullpath)
713 self._dirs[relpath] = Directory(fullpath, relpath, currentdir)
714 extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
715 for filename in filenames:
716 basename, extension = os.path.splitext(filename)
717 if extension in extensions:
718 fullpath = os.path.join(dirpath, filename)
719 relpath = self._get_rel_path(fullpath)
720 self._files[relpath] = File(fullpath, relpath, currentdir)
721 elif extension == '.cmakein':
722 extension = os.path.splitext(basename)[1]
723 if extension in extensions:
724 fullpath = os.path.join(dirpath, filename)
725 relpath = self._get_rel_path(fullpath)
726 sourcefile = GeneratorSourceFile(fullpath, relpath, currentdir)
727 self._files[relpath] = sourcefile
728 fullpath = os.path.join(dirpath, basename)
729 relpath = self._get_rel_path(fullpath)
730 fullpath = os.path.join(self._build_root, relpath)
731 generatedfile = GeneratedFile(fullpath, relpath, currentdir)
732 self._files[relpath] = generatedfile
733 generatedfile.set_generator_source(sourcefile)
734 elif extension in ('.l', '.y', '.pre'):
735 fullpath = os.path.join(dirpath, filename)
736 relpath = self._get_rel_path(fullpath)
737 self._files[relpath] = GeneratorSourceFile(fullpath, relpath, currentdir)
739 def _create_module(self, rootdir):
740 """Create module for a subdirectory."""
741 name = 'module_' + rootdir.get_name()
742 moduleobj = Module(name, rootdir)
743 rootdir.set_module(moduleobj)
744 self._modules[name] = moduleobj
746 def scan_files(self, only_files=None, keep_contents=False):
747 """Read source files to initialize #include dependencies."""
749 filelist = only_files
751 filelist = self._files.itervalues()
752 for fileobj in filelist:
753 if not fileobj.is_external():
754 fileobj.scan_contents(self, keep_contents)
755 module = fileobj.get_module()
757 for includedfile in fileobj.get_includes():
758 otherfile = includedfile.get_file()
760 othermodule = otherfile.get_module()
761 if othermodule and othermodule != module:
762 module.add_dependency(othermodule, includedfile)
764 def load_xml(self, only_files=None):
765 """Load Doxygen XML information.
767 If only_files is True, XML data is not loaded for code constructs, but
768 only for files, directories, and their potential parents.
770 xmldir = os.path.join(self._build_root, 'docs', 'html', 'doxygen', 'xml')
771 self._docset = xml.DocumentationSet(xmldir, self._reporter)
773 if isinstance(only_files, collections.Iterable):
774 filelist = [x.get_abspath() for x in only_files]
775 self._docset.load_file_details(filelist)
777 self._docset.load_file_details()
779 self._docset.load_details()
780 self._docset.merge_duplicates()
785 self._load_namespaces()
789 def _load_dirs(self):
790 """Load Doxygen XML directory information."""
791 rootdirs = self._docset.get_compounds(xml.Directory,
792 lambda x: x.get_parent() is None)
793 for dirdoc in rootdirs:
794 self._load_dir(dirdoc, None)
796 def _load_dir(self, dirdoc, parent):
797 """Load Doxygen XML directory information for a single directory."""
798 path = dirdoc.get_path().rstrip('/')
799 if not os.path.isabs(path):
800 self._reporter.xml_assert(dirdoc.get_xml_path(),
801 "expected absolute path in Doxygen-produced XML file")
803 relpath = self._get_rel_path(path)
804 dirobj = self._dirs.get(relpath)
806 dirobj = Directory(path, relpath, parent)
807 self._dirs[relpath] = dirobj
808 dirobj.set_doc_xml(dirdoc, self)
809 self._docmap[dirdoc] = dirobj
810 for subdirdoc in dirdoc.get_subdirectories():
811 self._load_dir(subdirdoc, dirobj)
813 def _load_modules(self):
814 """Load Doxygen XML module (group) information."""
815 moduledocs = self._docset.get_compounds(xml.Group,
816 lambda x: x.get_name().startswith('module_'))
817 for moduledoc in moduledocs:
818 moduleobj = self._modules.get(moduledoc.get_name())
820 self._reporter.input_error(
821 "no matching directory for module: {0}".format(moduledoc))
823 moduleobj.set_doc_xml(moduledoc, self)
824 self._docmap[moduledoc] = moduleobj
826 def _load_files(self):
827 """Load Doxygen XML file information."""
828 for filedoc in self._docset.get_files():
829 path = filedoc.get_path()
831 # In case of only partially loaded file information,
832 # the path information is not set for unloaded files.
834 if not os.path.isabs(path):
835 self._reporter.xml_assert(filedoc.get_xml_path(),
836 "expected absolute path in Doxygen-produced XML file")
838 extension = os.path.splitext(path)[1]
839 # We don't care about Markdown files that only produce pages
840 # (and fail the directory check below).
841 if extension == '.md':
843 dirdoc = filedoc.get_directory()
845 self._reporter.xml_assert(filedoc.get_xml_path(),
846 "file is not in any directory in Doxygen")
848 relpath = self._get_rel_path(path)
849 fileobj = self._files.get(relpath)
851 fileobj = File(path, relpath, self._docmap[dirdoc])
852 self._files[relpath] = fileobj
853 fileobj.set_doc_xml(filedoc, self)
854 self._docmap[filedoc] = fileobj
856 def _load_namespaces(self):
857 """Load Doxygen XML namespace information."""
858 nsdocs = self._docset.get_namespaces()
860 nsobj = Namespace(nsdoc)
861 self._docmap[nsdoc] = nsobj
862 self._namespaces.add(nsobj)
864 def _load_classes(self):
865 """Load Doxygen XML class information."""
866 classdocs = self._docset.get_classes()
867 for classdoc in classdocs:
868 files = [self._docmap[filedoc] for filedoc in classdoc.get_files()]
869 classobj = Class(classdoc, files)
870 self._docmap[classdoc] = classobj
871 self._classes.add(classobj)
873 def _load_members(self):
874 """Load Doxygen XML member information."""
875 memberdocs = self._docset.get_members()
876 for memberdoc in memberdocs:
877 nsdoc = memberdoc.get_namespace()
878 nsobj = self.get_object(nsdoc)
879 memberobj = Member(memberdoc, nsobj)
880 self._docmap[memberdoc] = memberobj
881 self._members.add(memberobj)
883 def _get_dir(self, relpath):
884 """Get directory object for a path relative to source tree root."""
885 return self._dirs.get(relpath)
887 def get_file(self, path):
888 """Get file object for a path relative to source tree root."""
889 return self._files.get(self._get_rel_path(path))
891 def find_include_file(self, includedpath):
892 """Find a file object corresponding to an include path."""
893 for testdir in ('src', 'src/external/thread_mpi/include',
894 'src/external/tng_io/include'):
895 testpath = os.path.join(testdir, includedpath)
896 if testpath in self._files:
897 return self._files[testpath]
899 def load_git_attributes(self):
900 """Load git attribute information for files."""
901 args = ['git', 'check-attr', '--stdin', 'filter']
902 git_check_attr = subprocess.Popen(args, stdin=subprocess.PIPE,
903 stdout=subprocess.PIPE, cwd=self._source_root)
904 filelist = '\n'.join(map(File.get_relpath, self._files.itervalues()))
905 filters = git_check_attr.communicate(filelist)[0]
906 for fileinfo in filters.splitlines():
907 path, dummy, value = fileinfo.split(': ')
908 fileobj = self._files.get(path)
909 assert fileobj is not None
910 fileobj.set_git_filter_attribute(value)
912 def load_installed_file_list(self):
913 """Load list of installed files from the build tree."""
914 listpath = os.path.join(self._build_root, 'docs', 'doxygen', 'installed-headers.txt')
915 with open(listpath, 'r') as installedfp:
916 for line in installedfp:
918 if not os.path.isabs(path):
919 self._reporter.input_error(
920 "installed file not specified with absolute path: {0}"
923 relpath = self._get_rel_path(path)
924 if relpath not in self._files:
925 self._reporter.input_error(
926 "installed file not in source tree: {0}".format(path))
928 self._files[relpath].set_installed()
930 def load_cycle_suppression_list(self, filename):
931 """Load a list of edges to suppress in cycles.
933 These edges between modules, if present, will be marked in the
934 corresponding ModuleDependency objects.
936 with open(filename, 'r') as fp:
939 if not line or line.startswith('#'):
941 modulenames = ['module_' + x.strip() for x in line.split('->')]
942 if len(modulenames) != 2:
943 self._reporter.input_error(
944 "invalid cycle suppression line: {0}".format(line))
946 firstmodule = self._modules.get(modulenames[0])
947 secondmodule = self._modules.get(modulenames[1])
948 if not firstmodule or not secondmodule:
949 self._reporter.input_error(
950 "unknown modules mentioned on cycle suppression line: {0}".format(line))
952 for dep in firstmodule.get_dependencies():
953 if dep.get_other_module() == secondmodule:
954 # TODO: Check that each suppression is actually part of
956 dep.set_cycle_suppression()
958 def get_object(self, docobj):
959 """Get tree object for a Doxygen XML object."""
962 return self._docmap.get(docobj)
965 """Get iterable for all files in the source tree."""
966 return self._files.itervalues()
968 def get_modules(self):
969 """Get iterable for all modules in the source tree."""
970 return self._modules.itervalues()
972 def get_classes(self):
973 """Get iterable for all classes in the source tree."""
976 def get_members(self):
977 """Get iterable for all members (in Doxygen terms) in the source tree."""