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.
57 import doxygenxml as xml
59 # We import DocType directly so that it is exposed from this module as well.
60 from doxygenxml import DocType
62 def _get_api_type_for_compound(grouplist):
63 """Helper function to deduce API type from Doxygen group membership."""
64 result = DocType.internal
65 for group in grouplist:
66 if isinstance(group, xml.Group):
67 if group.get_name() == 'group_publicapi':
68 result = DocType.public
69 elif group.get_name() == 'group_libraryapi':
70 result = DocType.library
71 # TODO: Check for multiple group membership
74 class IncludedFile(object):
76 """Information about an #include directive in a file."""
78 def __init__(self, abspath, lineno, included_file, included_path, is_relative, is_system):
79 self._abspath = abspath
80 self._line_number = lineno
81 self._included_file = included_file
82 self._included_path = included_path
83 #self._used_include_path = used_include_path
84 self._is_relative = is_relative
85 self._is_system = is_system
89 return '<{0}>'.format(self._included_path)
91 return '"{0}"'.format(self._included_path)
94 return self._is_system
96 def is_relative(self):
97 return self._is_relative
100 return self._included_file
102 def get_reporter_location(self):
103 return reporter.Location(self._abspath, self._line_number)
107 """Source/header file in the GROMACS tree."""
109 def __init__(self, abspath, relpath, directory):
110 """Initialize a file representation with basic information."""
111 self._abspath = abspath
112 self._relpath = relpath
113 self._dir = directory
115 self._installed = False
116 extension = os.path.splitext(abspath)[1]
117 self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
118 self._apitype = DocType.none
119 self._modules = set()
121 directory.add_file(self)
123 def set_doc_xml(self, rawdoc, sourcetree):
124 """Assiociate Doxygen documentation entity with the file."""
125 assert self._rawdoc is None
126 assert rawdoc.is_source_file() == self._sourcefile
127 self._rawdoc = rawdoc
128 if self._rawdoc.is_documented():
129 grouplist = self._rawdoc.get_groups()
130 self._apitype = _get_api_type_for_compound(grouplist)
131 for group in grouplist:
132 module = sourcetree.get_object(group)
134 self._modules.add(module)
136 def set_installed(self):
137 """Mark the file installed."""
138 self._installed = True
140 def _process_include(self, lineno, is_system, includedpath, sourcetree):
141 """Process #include directive during scan()."""
144 fileobj = sourcetree.find_include_file(includedpath)
146 fullpath = os.path.join(self._dir.get_abspath(), includedpath)
147 fullpath = os.path.abspath(fullpath)
148 if os.path.exists(fullpath):
150 fileobj = sourcetree.get_file(fullpath)
152 fileobj = sourcetree.find_include_file(includedpath)
153 self._includes.append(IncludedFile(self.get_abspath(), lineno, fileobj, includedpath,
154 is_relative, is_system))
156 def scan_contents(self, sourcetree):
157 """Scan the file contents and initialize information based on it."""
158 # TODO: Consider a more robust regex.
159 include_re = r'^#\s*include\s+(?P<quote>["<])(?P<path>[^">]*)[">]'
160 with open(self._abspath, 'r') as scanfile:
161 for lineno, line in enumerate(scanfile, 1):
162 match = re.match(include_re, line)
164 is_system = (match.group('quote') == '<')
165 includedpath = match.group('path')
166 self._process_include(lineno, is_system, includedpath,
169 def get_reporter_location(self):
170 return reporter.Location(self._abspath, None)
172 def is_installed(self):
173 return self._installed
175 def is_external(self):
176 return self._dir.is_external()
178 def is_source_file(self):
179 return self._sourcefile
181 def is_test_file(self):
182 return self._dir.is_test_directory()
184 def is_documented(self):
185 return self._rawdoc and self._rawdoc.is_documented()
187 def has_brief_description(self):
188 return self._rawdoc and self._rawdoc.has_brief_description()
190 def get_abspath(self):
193 def get_relpath(self):
197 return os.path.basename(self._abspath)
199 def get_doc_type(self):
202 return self._rawdoc.get_visibility()
204 def get_api_type(self):
207 def api_type_is_reliable(self):
208 if self._apitype > DocType.internal:
210 module = self.get_module()
211 return module and module.is_documented()
214 if self.api_type_is_reliable():
215 return self.get_api_type() == DocType.public
216 return self.get_api_type() == DocType.public or self.is_installed()
218 def is_module_internal(self):
219 if self.is_source_file():
221 return not self.is_installed() and self.get_api_type() <= DocType.internal
223 def get_expected_module(self):
224 return self._dir.get_module()
226 def get_doc_modules(self):
229 def get_module(self):
230 module = self.get_expected_module()
231 if not module and len(self._modules) == 1:
232 module = list(self._modules)[0]
235 def get_includes(self):
236 return self._includes
238 class GeneratedFile(File):
241 class Directory(object):
243 """(Sub)directory in the GROMACS tree."""
245 def __init__(self, abspath, relpath, parent):
246 """Initialize a file representation with basic information."""
247 self._abspath = abspath
248 self._relpath = relpath
249 self._name = os.path.basename(abspath)
250 self._parent = parent
253 self._is_test_dir = False
254 if parent and parent.is_test_directory() or \
255 self._name in ('tests', 'legacytests'):
256 self._is_test_dir = True
257 self._is_external = False
258 if parent and parent.is_external() or self._name == 'external':
259 self._is_external = True
260 self._subdirs = set()
262 parent._subdirs.add(self)
264 self._has_installed_files = None
266 def set_doc_xml(self, rawdoc, sourcetree):
267 """Assiociate Doxygen documentation entity with the directory."""
268 assert self._rawdoc is None
269 assert self._abspath == rawdoc.get_path().rstrip('/')
270 self._rawdoc = rawdoc
272 def set_module(self, module):
273 assert self._module is None
274 self._module = module
276 def add_file(self, fileobj):
277 self._files.add(fileobj)
282 def get_reporter_location(self):
283 return reporter.Location(self._abspath, None)
285 def get_abspath(self):
288 def get_relpath(self):
291 def is_test_directory(self):
292 return self._is_test_dir
294 def is_external(self):
295 return self._is_external
297 def has_installed_files(self):
298 if self._has_installed_files is None:
299 self._has_installed_files = False
300 for subdir in self._subdirs:
301 if subdir.has_installed_files():
302 self._has_installed_files = True
304 for fileobj in self._files:
305 if fileobj.is_installed():
306 self._has_installed_files = True
308 return self._has_installed_files
310 def get_module(self):
314 return self._parent.get_module()
317 def get_subdirectories(self):
321 for subdir in self._subdirs:
322 for fileobj in subdir.get_files():
324 for fileobj in self._files:
327 class ModuleDependency(object):
329 """Dependency between modules."""
331 def __init__(self, othermodule):
332 """Initialize empty dependency object with given module as dependency."""
333 self._othermodule = othermodule
334 self._includedfiles = []
335 self._cyclesuppression = None
337 def add_included_file(self, includedfile):
338 """Add IncludedFile that is part of this dependency."""
339 assert includedfile.get_file().get_module() == self._othermodule
340 self._includedfiles.append(includedfile)
342 def set_cycle_suppression(self):
343 """Set suppression on cycles containing this dependency."""
344 self._cyclesuppression = True
346 def is_cycle_suppressed(self):
347 """Return whether cycles containing this dependency are suppressed."""
348 return self._cyclesuppression is not None
350 def get_other_module(self):
351 """Get module that this dependency is to."""
352 return self._othermodule
354 class Module(object):
356 """Code module in the GROMACS source tree.
358 Modules are specific subdirectories that host a more or less coherent
359 set of routines. Simplified, every subdirectory under src/gromacs/ is
360 a different module. This object provides that abstraction and also links
361 the subdirectory to the module documentation (documented as a group in
362 Doxygen) if that exists.
365 def __init__(self, name, rootdir):
368 self._rootdir = rootdir
370 self._dependencies = dict()
372 def set_doc_xml(self, rawdoc, sourcetree):
373 """Assiociate Doxygen documentation entity with the module."""
374 assert self._rawdoc is None
375 self._rawdoc = rawdoc
376 if self._rawdoc.is_documented():
377 groups = list(self._rawdoc.get_groups())
379 groupname = groups[0].get_name()
380 if groupname.startswith('group_'):
381 self._group = groupname[6:]
383 def add_dependency(self, othermodule, includedfile):
384 """Add #include dependency from a file in this module."""
385 assert includedfile.get_file().get_module() == othermodule
386 if othermodule not in self._dependencies:
387 self._dependencies[othermodule] = ModuleDependency(othermodule)
388 self._dependencies[othermodule].add_included_file(includedfile)
390 def is_documented(self):
391 return self._rawdoc is not None
396 def get_root_dir(self):
400 # TODO: Include public API convenience headers?
401 return self._rootdir.get_files()
406 def get_dependencies(self):
407 return self._dependencies.itervalues()
409 class Namespace(object):
411 """Namespace in the GROMACS source code."""
413 def __init__(self, rawdoc):
414 self._rawdoc = rawdoc
416 def is_anonymous(self):
417 return self._rawdoc.is_anonymous()
421 """Class/struct/union in the GROMACS source code."""
423 def __init__(self, rawdoc, files):
424 self._rawdoc = rawdoc
425 self._files = set(files)
428 return self._rawdoc.get_name()
430 def get_reporter_location(self):
431 return self._rawdoc.get_reporter_location()
436 def is_documented(self):
437 return self._rawdoc.is_documented()
439 def has_brief_description(self):
440 return self._rawdoc.has_brief_description()
442 def get_doc_type(self):
443 """Return documentation type (visibility) for the class.
445 In addition to the actual code, this encodes GROMACS-specific logic
446 of setting EXTRACT_LOCAL_CLASSES=YES only for the full documentation.
447 Local classes never appear outside the full documentation, no matter
448 what is their visibility.
450 if not self.is_documented():
452 if self._rawdoc.is_local():
453 return DocType.internal
454 return self._rawdoc.get_visibility()
456 def get_file_doc_type(self):
457 return max([fileobj.get_doc_type() for fileobj in self._files])
459 def is_in_installed_file(self):
460 return any([fileobj.is_installed() for fileobj in self._files])
462 class Member(object):
464 """Member (in Doxygen terminology) in the GROMACS source tree.
466 Currently, modeling is limited to the minimal set of properties that the
470 def __init__(self, rawdoc, namespace):
471 self._rawdoc = rawdoc
472 self._namespace = namespace
475 return self._rawdoc.get_name()
477 def get_reporter_location(self):
478 return self._rawdoc.get_reporter_location()
480 def is_documented(self):
481 return self._rawdoc.is_documented()
483 def has_brief_description(self):
484 return self._rawdoc.has_brief_description()
486 def has_inbody_description(self):
487 return self._rawdoc.has_inbody_description()
489 def is_visible(self):
490 """Return whether the member is visible in Doxygen documentation.
492 Doxygen ignores members whose parent compounds are not documented.
493 However, when EXTRACT_ANON_NPACES=ON (which is set for our full
494 documentation), members of anonymous namespaces are extracted even if
495 the namespace is the only parent and is not documented.
497 if self._namespace and self._namespace.is_anonymous():
499 return self._rawdoc.get_inherited_visibility() != DocType.none
502 class GromacsTree(object):
504 """Root object for navigating the GROMACS source tree.
506 On initialization, the list of files and directories is initialized by
507 walking the source tree, and modules are created for top-level
508 subdirectories. At this point, only information that is accessible from
509 file names and paths only is available.
511 set_installed_file_list() can be called to set the list of installed
514 scan_files() can be called to read all the files and initialize #include
515 dependencies between the files based on the information. This is done like
516 this instead of relying on Doxygen-extracted include files to make the
517 dependency graph independent from preprocessor macro definitions
518 (Doxygen only sees those #includes that the preprocessor sees, which
519 depends on what #defines it has seen).
521 load_xml() can be called to load information from Doxygen XML data in
522 the build tree (the Doxygen XML data must have been built separately).
525 def __init__(self, source_root, build_root, reporter):
526 """Initialize the tree object by walking the source tree."""
527 self._source_root = os.path.abspath(source_root)
528 self._build_root = os.path.abspath(build_root)
529 self._reporter = reporter
531 self._docmap = dict()
534 self._modules = dict()
535 self._classes = set()
536 self._namespaces = set()
537 self._members = set()
538 self._walk_dir(os.path.join(self._source_root, 'src'))
539 rootdir = self._get_dir(os.path.join('src', 'gromacs'))
540 for subdir in rootdir.get_subdirectories():
541 self._create_module(subdir)
542 rootdir = self._get_dir(os.path.join('src', 'testutils'))
543 self._create_module(rootdir)
545 def _get_rel_path(self, path):
546 assert os.path.isabs(path)
547 if path.startswith(self._build_root):
548 return os.path.relpath(path, self._build_root)
549 if path.startswith(self._source_root):
550 return os.path.relpath(path, self._source_root)
551 raise ValueError("path not under build nor source tree: {0}".format(path))
553 def _walk_dir(self, rootpath):
554 """Construct representation of the source tree by walking the file system."""
555 assert os.path.isabs(rootpath)
556 assert rootpath not in self._dirs
557 relpath = self._get_rel_path(rootpath)
558 self._dirs[relpath] = Directory(rootpath, relpath, None)
559 for dirpath, dirnames, filenames in os.walk(rootpath):
560 if 'contrib' in dirnames:
561 dirnames.remove('contrib')
562 if 'refdata' in dirnames:
563 dirnames.remove('refdata')
564 currentdir = self._dirs[self._get_rel_path(dirpath)]
565 # Loop through a copy so that we can modify dirnames.
566 for dirname in list(dirnames):
567 fullpath = os.path.join(dirpath, dirname)
568 if fullpath == self._build_root:
569 dirnames.remove(dirname)
571 relpath = self._get_rel_path(fullpath)
572 self._dirs[relpath] = Directory(fullpath, relpath, currentdir)
573 extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
574 for filename in filenames:
575 basename, extension = os.path.splitext(filename)
576 if extension in extensions:
577 fullpath = os.path.join(dirpath, filename)
578 relpath = self._get_rel_path(fullpath)
579 self._files[relpath] = File(fullpath, relpath, currentdir)
580 elif extension == '.cmakein':
581 extension = os.path.splitext(basename)[1]
582 if extension in extensions:
583 fullpath = os.path.join(dirpath, basename)
584 relpath = self._get_rel_path(fullpath)
585 fullpath = os.path.join(dirpath, filename)
586 self._files[relpath] = GeneratedFile(fullpath, relpath, currentdir)
588 def _create_module(self, rootdir):
589 """Create module for a subdirectory."""
590 name = 'module_' + rootdir.get_name()
591 moduleobj = Module(name, rootdir)
592 rootdir.set_module(moduleobj)
593 self._modules[name] = moduleobj
595 def scan_files(self):
596 """Read source files to initialize #include dependencies."""
597 for fileobj in self._files.itervalues():
598 if not fileobj.is_external():
599 fileobj.scan_contents(self)
600 module = fileobj.get_module()
602 for includedfile in fileobj.get_includes():
603 otherfile = includedfile.get_file()
605 othermodule = otherfile.get_module()
606 if othermodule and othermodule != module:
607 module.add_dependency(othermodule, includedfile)
609 def load_xml(self, only_files=False):
610 """Load Doxygen XML information.
612 If only_files is True, XML data is not loaded for code constructs, but
613 only for files, directories, and their potential parents.
615 xmldir = os.path.join(self._build_root, 'doxygen', 'xml')
616 self._docset = xml.DocumentationSet(xmldir, self._reporter)
618 self._docset.load_file_details()
620 self._docset.load_details()
621 self._docset.merge_duplicates()
626 self._load_namespaces()
630 def _load_dirs(self):
631 """Load Doxygen XML directory information."""
632 rootdirs = self._docset.get_compounds(xml.Directory,
633 lambda x: x.get_parent() is None)
634 for dirdoc in rootdirs:
635 self._load_dir(dirdoc, None)
637 def _load_dir(self, dirdoc, parent):
638 """Load Doxygen XML directory information for a single directory."""
639 path = dirdoc.get_path().rstrip('/')
640 if not os.path.isabs(path):
641 self._reporter.xml_assert(dirdoc.get_xml_path(),
642 "expected absolute path in Doxygen-produced XML file")
644 relpath = self._get_rel_path(path)
645 dirobj = self._dirs.get(relpath)
647 dirobj = Directory(path, relpath, parent)
648 self._dirs[relpath] = dirobj
649 dirobj.set_doc_xml(dirdoc, self)
650 self._docmap[dirdoc] = dirobj
651 for subdirdoc in dirdoc.get_subdirectories():
652 self._load_dir(subdirdoc, dirobj)
654 def _load_modules(self):
655 """Load Doxygen XML module (group) information."""
656 moduledocs = self._docset.get_compounds(xml.Group,
657 lambda x: x.get_name().startswith('module_'))
658 for moduledoc in moduledocs:
659 moduleobj = self._modules.get(moduledoc.get_name())
661 self._reporter.input_error(
662 "no matching directory for module: {0}".format(moduledoc))
664 moduleobj.set_doc_xml(moduledoc, self)
665 self._docmap[moduledoc] = moduleobj
667 def _load_files(self):
668 """Load Doxygen XML file information."""
669 for filedoc in self._docset.get_files():
670 path = filedoc.get_path()
671 if not os.path.isabs(path):
672 self._reporter.xml_assert(filedoc.get_xml_path(),
673 "expected absolute path in Doxygen-produced XML file")
675 extension = os.path.splitext(filedoc.get_path())[1]
676 # We don't care about Markdown files that only produce pages
677 # (and fail the directory check below).
678 if extension == '.md':
680 dirdoc = filedoc.get_directory()
682 self._reporter.xml_assert(filedoc.get_xml_path(),
683 "file is not in any directory in Doxygen")
685 relpath = self._get_rel_path(path)
686 fileobj = self._files.get(relpath)
688 fileobj = File(path, relpath, self._docmap[dirdoc])
689 self._files[relpath] = fileobj
690 fileobj.set_doc_xml(filedoc, self)
691 self._docmap[filedoc] = fileobj
693 def _load_namespaces(self):
694 """Load Doxygen XML namespace information."""
695 nsdocs = self._docset.get_namespaces()
697 nsobj = Namespace(nsdoc)
698 self._docmap[nsdoc] = nsobj
699 self._namespaces.add(nsobj)
701 def _load_classes(self):
702 """Load Doxygen XML class information."""
703 classdocs = self._docset.get_classes()
704 for classdoc in classdocs:
705 files = [self._docmap[filedoc] for filedoc in classdoc.get_files()]
706 classobj = Class(classdoc, files)
707 self._docmap[classdoc] = classobj
708 self._classes.add(classobj)
710 def _load_members(self):
711 """Load Doxygen XML member information."""
712 memberdocs = self._docset.get_members()
713 for memberdoc in memberdocs:
714 nsdoc = memberdoc.get_namespace()
715 nsobj = self.get_object(nsdoc)
716 memberobj = Member(memberdoc, nsobj)
717 self._docmap[memberdoc] = memberobj
718 self._members.add(memberobj)
720 def _get_dir(self, relpath):
721 """Get directory object for a path relative to source tree root."""
722 return self._dirs.get(relpath)
724 def get_file(self, path):
725 """Get file object for a path relative to source tree root."""
726 return self._files.get(self._get_rel_path(path))
728 def find_include_file(self, includedpath):
729 """Find a file object corresponding to an include path."""
730 for testdir in ('src', 'src/gromacs/legacyheaders', 'src/external/thread_mpi/include',
731 'src/external/tng_io/include'):
732 testpath = os.path.join(testdir, includedpath)
733 if testpath in self._files:
734 return self._files[testpath]
736 def set_installed_file_list(self, installedfiles):
737 """Set list of installed files."""
738 for path in installedfiles:
739 if not os.path.isabs(path):
740 self._reporter.input_error(
741 "installed file not specified with absolute path: {0}"
744 relpath = self._get_rel_path(path)
745 if relpath not in self._files:
746 self._reporter.input_error(
747 "installed file not in source tree: {0}".format(path))
749 self._files[relpath].set_installed()
751 def load_cycle_suppression_list(self, filename):
752 """Load a list of edges to suppress in cycles.
754 These edges between modules, if present, will be marked in the
755 corresponding ModuleDependency objects.
757 with open(filename, 'r') as fp:
760 if not line or line.startswith('#'):
762 modulenames = ['module_' + x.strip() for x in line.split('->')]
763 if len(modulenames) != 2:
764 self._reporter.input_error(
765 "invalid cycle suppression line: {0}".format(line))
767 firstmodule = self._modules.get(modulenames[0])
768 secondmodule = self._modules.get(modulenames[1])
769 if not firstmodule or not secondmodule:
770 self._reporter.input_error(
771 "unknown modules mentioned on cycle suppression line: {0}".format(line))
773 for dep in firstmodule.get_dependencies():
774 if dep.get_other_module() == secondmodule:
775 # TODO: Check that each suppression is actually part of
777 dep.set_cycle_suppression()
779 def get_object(self, docobj):
780 """Get tree object for a Doxygen XML object."""
783 return self._docmap.get(docobj)
786 """Get iterable for all files in the source tree."""
787 return self._files.itervalues()
789 def get_modules(self):
790 """Get iterable for all modules in the source tree."""
791 return self._modules.itervalues()
793 def get_classes(self):
794 """Get iterable for all classes in the source tree."""
797 def get_members(self):
798 """Get iterable for all members (in Doxygen terms) in the source tree."""