2595c447e604c441c2b0bd3c296cf4b608b70cbe
[alexxy/gromacs.git] / doxygen / gmxtree.py
1 #!/usr/bin/python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
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.
9 #
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
14 #
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 # Lesser General Public License for more details.
19 #
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA.
24 #
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
32 #
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
35
36 """GROMACS-specific representation for source tree and documentation.
37
38 This module provides classes that construct a GROMACS-specific representation
39 of the source tree and associate the Doxygen XML output with it.  It constructs
40 an initial representation by walking the source tree in the file system, and
41 then associates information from the Doxygen XML output into this.
42 It also adds some additional knowledge from how the GROMACS source tree is
43 organized to construct a representation that is easy to process and check as
44 the top-level scripts expect.
45
46 The object model is rooted at a GromacsTree object.  Currently, it constructs a
47 representation of the source tree from the file system, but is otherwise mostly
48 a thin wrapper around the Doxygen XML tree.  It already adds some relations and
49 rules that come from GROMACS-specific knowledge.  In the future, more such
50 customizations will be added.
51 """
52
53 import os
54 import os.path
55
56 import doxygenxml as xml
57 import reporter
58 # We import DocType directly so that it is exposed from this module as well.
59 from doxygenxml import DocType
60
61 def _get_api_type_for_compound(grouplist):
62     """Helper function to deduce API type from Doxygen group membership."""
63     result = DocType.internal
64     for group in grouplist:
65         if isinstance(group, xml.Group):
66             if group.get_name() == 'group_publicapi':
67                 result = DocType.public
68             elif group.get_name() == 'group_libraryapi':
69                 result = DocType.library
70             # TODO: Check for multiple group membership
71     return result
72
73 class File(object):
74
75     """Source/header file in the GROMACS tree."""
76
77     def __init__(self, path, directory):
78         """Initialize a file representation with basic information."""
79         self._path = path
80         self._dir = directory
81         self._rawdoc = None
82         self._installed = False
83         extension = os.path.splitext(path)[1]
84         self._sourcefile = (extension in ('.c', '.cc', '.cpp', '.cu'))
85         self._apitype = DocType.none
86         self._modules = set()
87
88     def set_doc_xml(self, rawdoc, sourcetree):
89         """Assiociate Doxygen documentation entity with the file."""
90         assert self._rawdoc is None
91         assert rawdoc.is_source_file() == self._sourcefile
92         self._rawdoc = rawdoc
93         if self._rawdoc.is_documented():
94             grouplist = self._rawdoc.get_groups()
95             self._apitype = _get_api_type_for_compound(grouplist)
96             for group in grouplist:
97                 module = sourcetree.get_object(group)
98                 if module:
99                     self._modules.add(module)
100
101     def set_installed(self):
102         """Mark the file installed."""
103         self._installed = True
104
105     def get_reporter_location(self):
106         return reporter.Location(self._path, None)
107
108     def is_installed(self):
109         return self._installed
110
111     def is_source_file(self):
112         return self._sourcefile
113
114     def is_test_file(self):
115         return self._dir.is_test_directory()
116
117     def is_documented(self):
118         return self._rawdoc and self._rawdoc.is_documented()
119
120     def has_brief_description(self):
121         return self._rawdoc and self._rawdoc.has_brief_description()
122
123     def get_path(self):
124         return self._path
125
126     def get_documentation_type(self):
127         if not self._rawdoc:
128             return DocType.none
129         return self._rawdoc.get_visibility()
130
131     def get_api_type(self):
132         return self._apitype
133
134     def get_expected_module(self):
135         return self._dir.get_module()
136
137     def get_doc_modules(self):
138         return self._modules
139
140 class GeneratedFile(File):
141     pass
142
143 class Directory(object):
144
145     """(Sub)directory in the GROMACS tree."""
146
147     def __init__(self, path, parent):
148         """Initialize a file representation with basic information."""
149         self._path = path
150         self._name = os.path.basename(path)
151         self._parent = parent
152         self._rawdoc = None
153         self._module = None
154         self._is_test_dir = False
155         if parent and parent.is_test_directory() or \
156                 os.path.basename(path) in ('tests', 'legacytests'):
157             self._is_test_dir = True
158         self._subdirs = set()
159         if parent:
160             parent._subdirs.add(self)
161
162     def set_doc_xml(self, rawdoc, sourcetree):
163         """Assiociate Doxygen documentation entity with the directory."""
164         assert self._rawdoc is None
165         assert self._path == rawdoc.get_path().rstrip('/')
166         self._rawdoc = rawdoc
167
168     def set_module(self, module):
169         assert self._module is None
170         self._module = module
171
172     def get_name(self):
173         return self._name
174
175     def get_reporter_location(self):
176         return reporter.Location(self._path, None)
177
178     def is_test_directory(self):
179         return self._is_test_dir
180
181     def get_module(self):
182         if self._module:
183             return self._module
184         if self._parent:
185             return self._parent.get_module()
186         return None
187
188     def get_subdirectories(self):
189         return self._subdirs
190
191 class Module(object):
192
193     """Code module in the GROMACS source tree.
194
195     Modules are specific subdirectories that host a more or less coherent
196     set of routines.  Simplified, every subdirectory under src/gromacs/ is
197     a different module.  This object provides that abstraction and also links
198     the subdirectory to the module documentation (documented as a group in
199     Doxygen) if that exists.
200     """
201
202     def __init__(self, name, rootdir):
203         self._name = name
204         self._rawdoc = None
205         self._rootdir = rootdir
206
207     def set_doc_xml(self, rawdoc, sourcetree):
208         """Assiociate Doxygen documentation entity with the module."""
209         assert self._rawdoc is None
210         self._rawdoc = rawdoc
211
212     def is_documented(self):
213         return self._rawdoc is not None
214
215     def get_name(self):
216         return self._name
217
218 class Class(object):
219
220     """Class/struct/union in the GROMACS source code."""
221
222     def __init__(self, rawdoc, files):
223         self._rawdoc = rawdoc
224         self._files = set(files)
225
226     def get_name(self):
227         return self._rawdoc.get_name()
228
229     def get_reporter_location(self):
230         return self._rawdoc.get_reporter_location()
231
232     def get_files(self):
233         return self._files
234
235     def is_documented(self):
236         return self._rawdoc.is_documented()
237
238     def has_brief_description(self):
239         return self._rawdoc.has_brief_description()
240
241     def get_documentation_type(self):
242         if not self.is_documented():
243             return DocType.none
244         if self._rawdoc.is_local():
245             return DocType.internal
246         return self._rawdoc.get_visibility()
247
248     def get_file_documentation_type(self):
249         return max([fileobj.get_documentation_type() for fileobj in self._files])
250
251     def is_in_installed_file(self):
252         return any([fileobj.is_installed() for fileobj in self._files])
253
254 class GromacsTree(object):
255
256     """Root object for navigating the GROMACS source tree.
257
258     On initialization, the list of files and directories is initialized by
259     walking the source tree, and modules are created for top-level
260     subdirectories.  At this point, only information that is accessible from
261     file names and paths only is available.
262
263     set_installed_file_list() can be called to set the list of installed
264     files.
265
266     load_xml() can be called to load information from Doxygen XML data in
267     the build tree (the Doxygen XML data must have been built separately).
268     """
269
270     def __init__(self, source_root, build_root, reporter):
271         """Initialize the tree object by walking the source tree."""
272         self._source_root = os.path.abspath(source_root)
273         self._build_root = os.path.abspath(build_root)
274         self._reporter = reporter
275         self._docset = None
276         self._docmap = dict()
277         self._dirs = dict()
278         self._files = dict()
279         self._modules = dict()
280         self._classes = set()
281         self._walk_dir(os.path.join(self._source_root, 'src'))
282         rootdir = self._get_dir(os.path.join('src', 'gromacs'))
283         for subdir in rootdir.get_subdirectories():
284             self._create_module(subdir)
285         rootdir = self._get_dir(os.path.join('src', 'testutils'))
286         self._create_module(rootdir)
287
288     def _get_rel_path(self, path):
289         assert os.path.isabs(path)
290         if path.startswith(self._build_root):
291             return path[len(self._build_root)+1:]
292         if path.startswith(self._source_root):
293             return path[len(self._source_root)+1:]
294         raise ValueError("path not under build nor source tree: {0}".format(path))
295
296     def _walk_dir(self, rootpath):
297         """Construct representation of the source tree by walking the file system."""
298         assert os.path.isabs(rootpath)
299         assert rootpath not in self._dirs
300         relpath = self._get_rel_path(rootpath)
301         self._dirs[relpath] = Directory(rootpath, None)
302         for dirpath, dirnames, filenames in os.walk(rootpath):
303             if 'contrib' in dirnames:
304                 dirnames.remove('contrib')
305             if 'refdata' in dirnames:
306                 dirnames.remove('refdata')
307             currentdir = self._dirs[self._get_rel_path(dirpath)]
308             # Loop through a copy so that we can modify dirnames.
309             for dirname in list(dirnames):
310                 fullpath = os.path.join(dirpath, dirname)
311                 if fullpath == self._build_root:
312                     dirnames.remove(dirname)
313                     continue
314                 relpath = self._get_rel_path(fullpath)
315                 self._dirs[relpath] = Directory(fullpath, currentdir)
316             extensions = ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu')
317             for filename in filenames:
318                 basename, extension = os.path.splitext(filename)
319                 if extension in extensions:
320                     fullpath = os.path.join(dirpath, filename)
321                     relpath = self._get_rel_path(fullpath)
322                     self._files[relpath] = File(fullpath, currentdir)
323                 elif extension == '.cmakein':
324                     extension = os.path.splitext(basename)[1]
325                     if extension in extensions:
326                         fullpath = os.path.join(dirpath, basename)
327                         relpath = self._get_rel_path(fullpath)
328                         fullpath = os.path.join(dirpath, filename)
329                         self._files[relpath] = GeneratedFile(fullpath, currentdir)
330
331     def _create_module(self, rootdir):
332         """Create module for a subdirectory."""
333         name = 'module_' + rootdir.get_name()
334         moduleobj = Module(name, rootdir)
335         rootdir.set_module(moduleobj)
336         self._modules[name] = moduleobj
337
338     def load_xml(self):
339         """Load Doxygen XML information."""
340         xmldir = os.path.join(self._build_root, 'doxygen', 'xml')
341         self._docset = xml.DocumentationSet(xmldir, self._reporter)
342         self._docset.load_details()
343         self._docset.merge_duplicates()
344         self._load_dirs()
345         self._load_modules()
346         self._load_files()
347         self._load_classes()
348
349     def _load_dirs(self):
350         """Load Doxygen XML directory information."""
351         rootdirs = self._docset.get_compounds(xml.Directory,
352                 lambda x: x.get_parent() is None)
353         for dirdoc in rootdirs:
354             self._load_dir(dirdoc, None)
355
356     def _load_dir(self, dirdoc, parent):
357         """Load Doxygen XML directory information for a single directory."""
358         path = dirdoc.get_path().rstrip('/')
359         if not os.path.isabs(path):
360             self._reporter.xml_assert(dirdoc.get_xml_path(),
361                     "expected absolute path in Doxygen-produced XML file")
362             return
363         relpath = self._get_rel_path(path)
364         dirobj = self._dirs.get(relpath)
365         if not dirobj:
366             dirobj = Directory(path, parent)
367             self._dirs[relpath] = dirobj
368         dirobj.set_doc_xml(dirdoc, self)
369         self._docmap[dirdoc] = dirobj
370         for subdirdoc in dirdoc.get_subdirectories():
371             self._load_dir(subdirdoc, dirobj)
372
373     def _load_modules(self):
374         """Load Doxygen XML module (group) information."""
375         moduledocs = self._docset.get_compounds(xml.Group,
376                 lambda x: x.get_name().startswith('module_'))
377         for moduledoc in moduledocs:
378             moduleobj = self._modules.get(moduledoc.get_name())
379             if not moduleobj:
380                 self._reporter.input_error(
381                         "no matching directory for module: {0}".format(moduledoc))
382                 continue
383             moduleobj.set_doc_xml(moduledoc, self)
384             self._docmap[moduledoc] = moduleobj
385
386     def _load_files(self):
387         """Load Doxygen XML file information."""
388         for filedoc in self._docset.get_files():
389             path = filedoc.get_path()
390             if not os.path.isabs(path):
391                 self._reporter.xml_assert(filedoc.get_xml_path(),
392                         "expected absolute path in Doxygen-produced XML file")
393                 continue
394             extension = os.path.splitext(filedoc.get_path())[1]
395             # We don't care about Markdown files that only produce pages
396             # (and fail the directory check below).
397             if extension == '.md':
398                 continue
399             dirdoc = filedoc.get_directory()
400             if not dirdoc:
401                 self._reporter.xml_assert(filedoc.get_xml_path(),
402                         "file is not in any directory in Doxygen")
403                 continue
404             relpath = self._get_rel_path(path)
405             fileobj = self._files.get(relpath)
406             if not fileobj:
407                 fileobj = File(path, self._docmap[dirdoc])
408                 self._files[relpath] = fileobj
409             fileobj.set_doc_xml(filedoc, self)
410             self._docmap[filedoc] = fileobj
411
412     def _load_classes(self):
413         """Load Doxygen XML class information."""
414         classdocs = self._docset.get_classes()
415         for classdoc in classdocs:
416             files = [self._docmap[filedoc] for filedoc in classdoc.get_files()]
417             classobj = Class(classdoc, files)
418             self._docmap[classdoc] = classobj
419             self._classes.add(classobj)
420
421     def _get_dir(self, relpath):
422         """Get directory object for a path relative to source tree root."""
423         return self._dirs.get(relpath)
424
425     def set_installed_file_list(self, installedfiles):
426         """Set list of installed files."""
427         for path in installedfiles:
428             if not os.path.isabs(path):
429                 self._reporter.input_error(
430                         "installed file not specified with absolute path: {0}"
431                         .format(path))
432                 continue
433             relpath = self._get_rel_path(path)
434             if relpath not in self._files:
435                 self._reporter.input_error(
436                         "installed file not in source tree: {0}".format(path))
437                 continue
438             self._files[relpath].set_installed()
439
440     def get_object(self, docobj):
441         """Get tree object for a Doxygen XML object."""
442         return self._docmap.get(docobj)
443
444     def get_files(self):
445         """Get iterable for all files in the source tree."""
446         return self._files.itervalues()
447
448     def get_classes(self):
449         """Get iterable for all classes in the source tree."""
450         return self._classes
451
452     def get_members(self):
453         """Get iterable for all members (in Doxygen terms) in the source tree."""
454         # TODO: Add wrappers to solve some issues.
455         return self._docset.get_members()