3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2014,2015,2016,2017,2018 by the GROMACS development team.
6 # Copyright (c) 2019,2020, by the GROMACS development team, led by
7 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
8 # and including many others, as listed in the AUTHORS file in the
9 # top-level source directory and at http://www.gromacs.org.
11 # GROMACS is free software; you can redistribute it and/or
12 # modify it under the terms of the GNU Lesser General Public License
13 # as published by the Free Software Foundation; either version 2.1
14 # of the License, or (at your option) any later version.
16 # GROMACS is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 # Lesser General Public License for more details.
21 # You should have received a copy of the GNU Lesser General Public
22 # License along with GROMACS; if not, see
23 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
24 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 # If you want to redistribute modifications to GROMACS, please
27 # consider that scientific software is very special. Version
28 # control is crucial - bugs must be traceable. We will be happy to
29 # consider code for inclusion in the official distribution, but
30 # derived work must not be called official GROMACS. Details are found
31 # in the README & COPYING files - if they are missing, get the
32 # official version at http://www.gromacs.org.
34 # To help us fund GROMACS development, we humbly ask that you cite
35 # the research papers on the package. Check out http://www.gromacs.org.
37 """GROMACS-specific representation for source tree and documentation.
39 This module provides classes that construct a GROMACS-specific representation
40 of the source tree and associate the Doxygen XML output with it. It constructs
41 an initial representation by walking the source tree in the file system, and
42 then associates information from the Doxygen XML output into this.
43 It also adds some additional knowledge from how the GROMACS source tree is
44 organized to construct a representation that is easy to process and check as
45 the top-level scripts expect.
47 The object model is rooted at a GromacsTree object. Currently, it constructs a
48 representation of the source tree from the file system, but is otherwise mostly
49 a thin wrapper around the Doxygen XML tree. It already adds some relations and
50 rules that come from GROMACS-specific knowledge. In the future, more such
51 customizations will be added.
60 import doxygenxml
as xml
62 # We import DocType directly so that it is exposed from this module as well.
63 from doxygenxml
import DocType
65 def _get_api_type_for_compound(grouplist
):
66 """Helper function to deduce API type from Doxygen group membership."""
67 result
= DocType
.internal
68 for group
in grouplist
:
69 if isinstance(group
, xml
.Group
):
70 if group
.get_name() == 'group_publicapi':
71 result
= DocType
.public
72 elif group
.get_name() == 'group_libraryapi':
73 result
= DocType
.library
74 # TODO: Check for multiple group membership
77 class IncludedFile(object):
79 """Information about an #include directive in a file."""
81 def __init__(self
, including_file
, lineno
, included_file
, included_path
, is_relative
, is_system
, line
):
82 self
._including
_file
= including_file
83 self
._line
_number
= lineno
84 self
._included
_file
= included_file
85 self
._included
_path
= included_path
86 #self._used_include_path = used_include_path
87 self
._is
_relative
= is_relative
88 self
._is
_system
= is_system
93 return '<{0}>'.format(self
._included
_path
)
95 return '"{0}"'.format(self
._included
_path
)
97 def __lt__(self
, other
):
98 return str(self
) < str(other
)
101 return self
._is
_system
103 def is_relative(self
):
104 return self
._is
_relative
106 def get_included_path(self
):
107 return self
._included
_path
109 def get_including_file(self
):
110 return self
._including
_file
113 return self
._included
_file
115 def get_line_number(self
):
116 return self
._line
_number
118 def get_full_line(self
):
119 """Return the full source line on which this include appears.
121 Trailing newline is included."""
124 def get_reporter_location(self
):
125 return reporter
.Location(self
._including
_file
.get_abspath(), self
._line
_number
)
127 class IncludeBlock(object):
129 """Block of consequent #include directives in a file."""
131 def __init__(self
, first_included_file
):
132 self
._first
_line
= first_included_file
.get_line_number()
133 self
._last
_line
= self
._first
_line
135 self
.add_file(first_included_file
)
137 def add_file(self
, included_file
):
138 self
._files
.append(included_file
)
139 self
._last
_line
= included_file
.get_line_number()
141 def get_includes(self
):
144 def get_first_line(self
):
145 return self
._first
_line
147 def get_last_line(self
):
148 return self
._last
_line
152 """Source/header file in the GROMACS tree."""
154 def __init__(self
, abspath
, relpath
, directory
):
155 """Initialize a file representation with basic information."""
156 self
._abspath
= abspath
157 self
._relpath
= relpath
158 self
._dir
= directory
160 extension
= os
.path
.splitext(abspath
)[1]
161 self
._sourcefile
= (extension
in ('.c', '.cc', '.cpp', '.cu'))
162 self
._apitype
= DocType
.none
163 self
._modules
= set()
165 self
._include
_blocks
= []
166 self
._main
_header
= None
169 self
._declared
_defines
= None
170 self
._used
_defines
= dict()
171 directory
.add_file(self
)
173 def set_doc_xml(self
, rawdoc
, sourcetree
):
174 """Assiociate Doxygen documentation entity with the file."""
175 assert self
._rawdoc
is None
176 assert rawdoc
.is_source_file() == self
._sourcefile
177 self
._rawdoc
= rawdoc
178 if self
._rawdoc
.is_documented():
179 grouplist
= self
._rawdoc
.get_groups()
180 self
._apitype
= _get_api_type_for_compound(grouplist
)
181 for group
in grouplist
:
182 module
= sourcetree
.get_object(group
)
184 self
._modules
.add(module
)
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
, detect_defines
):
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>[^">]*)[">]'
217 define_re
= r
'^\s*#.*define(?:01)?\s+(\w*)'
219 with
open(self
._abspath
, 'r', encoding
='utf8') as scanfile
:
220 contents
= scanfile
.read()
221 lines
= contents
.splitlines(True)
222 for lineno
, line
in enumerate(lines
, 1):
223 match
= re
.match(include_re
, line
)
225 is_system
= (match
.group('quote') == '<')
226 includedpath
= match
.group('path')
227 included_file
= self
._process
_include
(lineno
, is_system
,
228 includedpath
, line
, sourcetree
)
229 if current_block
is None:
230 current_block
= IncludeBlock(included_file
)
231 self
._include
_blocks
.append(current_block
)
233 current_block
.add_file(included_file
)
234 elif line
and not line
.isspace():
237 self
._declared
_defines
= []
239 match
= re
.match(define_re
, line
)
241 self
._declared
_defines
.append(match
.group(1))
245 def add_used_defines(self
, define_file
, defines
):
246 """Add defines used in this file.
248 Used internally by find_define_file_uses()."""
249 if define_file
not in self
._used
_defines
:
250 self
._used
_defines
[define_file
] = set()
251 self
._used
_defines
[define_file
].update(defines
)
253 def get_reporter_location(self
):
254 return reporter
.Location(self
._abspath
, None)
256 def is_external(self
):
257 return self
._dir
.is_external()
259 def is_source_file(self
):
260 return self
._sourcefile
262 def is_test_file(self
):
263 return self
._dir
.is_test_directory()
265 def should_includes_be_sorted(self
):
266 """Return whether the include directives in the file should be sorted."""
267 return self
._filter
in ('includesort', 'complete_formatting')
269 def is_documented(self
):
270 return self
._rawdoc
and self
._rawdoc
.is_documented()
272 def has_brief_description(self
):
273 return self
._rawdoc
and self
._rawdoc
.has_brief_description()
275 def get_abspath(self
):
278 def get_relpath(self
):
282 return os
.path
.basename(self
._abspath
)
284 def get_directory(self
):
287 def get_doc_type(self
):
290 return self
._rawdoc
.get_visibility()
292 def get_api_type(self
):
295 def api_type_is_reliable(self
):
296 if self
._apitype
in (DocType
.internal
, DocType
.library
):
298 module
= self
.get_module()
299 return module
and module
.is_documented()
302 return self
.get_api_type() == DocType
.public
304 def is_module_internal(self
):
305 if self
.is_source_file():
307 return self
.get_api_type() <= DocType
.internal
309 def get_expected_module(self
):
310 return self
._dir
.get_module()
312 def get_doc_modules(self
):
315 def get_module(self
):
316 module
= self
.get_expected_module()
317 if not module
and len(self
._modules
) == 1:
318 module
= list(self
._modules
)[0]
321 def get_includes(self
):
322 return self
._includes
324 def get_include_blocks(self
):
325 return self
._include
_blocks
327 def _get_included_files_recurse(self
, result
):
328 for include
in self
._includes
:
329 included_file
= include
.get_file()
330 if included_file
is not None and not included_file
in result
:
331 result
.add(included_file
)
332 included_file
._get
_included
_files
_recurse
(result
)
334 def get_included_files(self
, recursive
=False):
337 self
._get
_included
_files
_recurse
(result
)
339 return set([x
.get_file() for x
in self
._includes
])
341 def get_main_header(self
):
342 return self
._main
_header
344 def get_contents(self
):
347 def get_declared_defines(self
):
348 """Return set of defines declared in this file.
350 The information is only populated for selected files."""
351 return self
._declared
_defines
353 def get_used_define_files(self
):
354 """Return files like config.h whose defines are used in this file.
356 The return value is empty if find_define_file_uses() has not been called,
357 as well as for headers that declare these defines."""
358 return set(self
._used
_defines
.keys())
360 def get_used_defines(self
, define_file
):
361 """Return set of defines used in this file for a given file like config.h.
363 return self
._used
_defines
.get(define_file
, set())
365 class GeneratedFile(File
):
366 def __init__(self
, abspath
, relpath
, directory
):
367 File
.__init
__(self
, abspath
, relpath
, directory
)
368 self
._generator
_source
_file
= None
370 def scan_contents(self
, sourcetree
, keep_contents
, detect_defines
):
371 if os
.path
.exists(self
.get_abspath()):
372 File
.scan_contents(self
, sourcetree
, keep_contents
, False)
374 def set_generator_source(self
, sourcefile
):
375 self
._generator
_source
_file
= sourcefile
377 def get_generator_source(self
):
378 return self
._generator
_source
_file
380 def get_reporter_location(self
):
381 if self
._generator
_source
_file
:
382 return self
._generator
_source
_file
.get_reporter_location()
383 return File
.get_reporter_location(self
)
385 def get_declared_defines(self
):
386 if self
._generator
_source
_file
:
387 return self
._generator
_source
_file
.get_declared_defines()
388 return File
.get_declared_defines(self
)
390 class GeneratorSourceFile(File
):
393 class Directory(object):
395 """(Sub)directory in the GROMACS tree."""
397 def __init__(self
, abspath
, relpath
, parent
):
398 """Initialize a file representation with basic information."""
399 self
._abspath
= abspath
400 self
._relpath
= relpath
401 self
._name
= os
.path
.basename(abspath
)
402 self
._parent
= parent
405 self
._is
_test
_dir
= False
406 if parent
and parent
.is_test_directory() or \
407 self
._name
== 'tests':
408 self
._is
_test
_dir
= True
409 self
._is
_external
= False
410 if parent
and parent
.is_external() or self
._name
== 'external':
411 self
._is
_external
= True
412 self
._subdirs
= set()
414 parent
._subdirs
.add(self
)
417 def set_doc_xml(self
, rawdoc
, sourcetree
):
418 """Assiociate Doxygen documentation entity with the directory."""
419 assert self
._rawdoc
is None
420 assert rawdoc
.get_path().rstrip('/') in (self
._abspath
, self
._relpath
)
421 self
._rawdoc
= rawdoc
423 def set_module(self
, module
):
424 assert self
._module
is None
425 self
._module
= module
427 def add_file(self
, fileobj
):
428 self
._files
.add(fileobj
)
433 def get_reporter_location(self
):
434 return reporter
.Location(self
._abspath
, None)
436 def get_abspath(self
):
439 def get_relpath(self
):
442 def is_test_directory(self
):
443 return self
._is
_test
_dir
445 def is_external(self
):
446 return self
._is
_external
448 def get_module(self
):
452 return self
._parent
.get_module()
455 def get_subdirectories(self
):
459 for subdir
in self
._subdirs
:
460 for fileobj
in subdir
.get_files():
462 for fileobj
in self
._files
:
465 def contains(self
, fileobj
):
466 """Check whether file is within the directory or its subdirectories."""
467 dirobj
= fileobj
.get_directory()
471 dirobj
= dirobj
._parent
474 class ModuleDependency(object):
476 """Dependency between modules."""
478 def __init__(self
, othermodule
):
479 """Initialize empty dependency object with given module as dependency."""
480 self
._othermodule
= othermodule
481 self
._includedfiles
= []
482 self
._cyclesuppression
= None
483 self
._is
_test
_only
_dependency
= True
484 self
.suppression_used
= True
486 def add_included_file(self
, includedfile
):
487 """Add IncludedFile that is part of this dependency."""
488 assert includedfile
.get_file().get_module() == self
._othermodule
489 if not includedfile
.get_including_file().is_test_file():
490 self
._is
_test
_only
_dependency
= False
491 self
._includedfiles
.append(includedfile
)
493 def set_cycle_suppression(self
):
494 """Set suppression on cycles containing this dependency."""
495 self
._cyclesuppression
= True
496 self
.suppression_used
= False
498 def is_cycle_suppressed(self
):
499 """Return whether cycles containing this dependency are suppressed."""
500 self
.suppression_used
= True
501 return self
._cyclesuppression
is not None
503 def is_test_only_dependency(self
):
504 """Return whether this dependency is only from test code."""
505 return self
._is
_test
_only
_dependency
507 def get_other_module(self
):
508 """Get module that this dependency is to."""
509 return self
._othermodule
511 def get_included_files(self
):
512 """Get IncludedFile objects for the individual include dependencies."""
513 return self
._includedfiles
515 class Module(object):
517 """Code module in the GROMACS source tree.
519 Modules are specific subdirectories that host a more or less coherent
520 set of routines. Simplified, every subdirectory under src/gromacs/ is
521 a different module. This object provides that abstraction and also links
522 the subdirectory to the module documentation (documented as a group in
523 Doxygen) if that exists.
526 def __init__(self
, name
, rootdir
):
529 self
._rootdir
= rootdir
531 self
._dependencies
= dict()
533 def set_doc_xml(self
, rawdoc
, sourcetree
):
534 """Assiociate Doxygen documentation entity with the module."""
535 assert self
._rawdoc
is None
536 self
._rawdoc
= rawdoc
537 if self
._rawdoc
.is_documented():
538 groups
= list(self
._rawdoc
.get_groups())
540 groupname
= groups
[0].get_name()
541 if groupname
.startswith('group_'):
542 self
._group
= groupname
[6:]
544 def add_dependency(self
, othermodule
, includedfile
):
545 """Add #include dependency from a file in this module."""
546 assert includedfile
.get_file().get_module() == othermodule
547 if othermodule
not in self
._dependencies
:
548 self
._dependencies
[othermodule
] = ModuleDependency(othermodule
)
549 self
._dependencies
[othermodule
].add_included_file(includedfile
)
551 def is_documented(self
):
552 return self
._rawdoc
is not None
557 def get_root_dir(self
):
561 # TODO: Include public API convenience headers?
562 return self
._rootdir
.get_files()
567 def get_dependencies(self
):
568 return self
._dependencies
.values()
570 class Namespace(object):
572 """Namespace in the GROMACS source code."""
574 def __init__(self
, rawdoc
):
575 self
._rawdoc
= rawdoc
577 def is_anonymous(self
):
578 return self
._rawdoc
.is_anonymous()
582 """Class/struct/union in the GROMACS source code."""
584 def __init__(self
, rawdoc
, files
):
585 self
._rawdoc
= rawdoc
586 self
._files
= set(files
)
589 return self
._rawdoc
.get_name()
591 def get_reporter_location(self
):
592 return self
._rawdoc
.get_reporter_location()
597 def is_documented(self
):
598 return self
._rawdoc
.is_documented()
600 def has_brief_description(self
):
601 return self
._rawdoc
.has_brief_description()
603 def get_doc_type(self
):
604 """Return documentation type (visibility) for the class.
606 In addition to the actual code, this encodes GROMACS-specific logic
607 of setting EXTRACT_LOCAL_CLASSES=YES only for the full documentation.
608 Local classes never appear outside the full documentation, no matter
609 what is their visibility.
611 if not self
.is_documented():
613 if self
._rawdoc
.is_local():
614 return DocType
.internal
615 return self
._rawdoc
.get_visibility()
617 def get_file_doc_type(self
):
618 return max([fileobj
.get_doc_type() for fileobj
in self
._files
])
620 class Member(object):
622 """Member (in Doxygen terminology) in the GROMACS source tree.
624 Currently, modeling is limited to the minimal set of properties that the
628 def __init__(self
, rawdoc
, namespace
):
629 self
._rawdoc
= rawdoc
630 self
._namespace
= namespace
633 return self
._rawdoc
.get_name()
635 def get_reporter_location(self
):
636 return self
._rawdoc
.get_reporter_location()
638 def is_documented(self
):
639 return self
._rawdoc
.is_documented()
641 def has_brief_description(self
):
642 return self
._rawdoc
.has_brief_description()
644 def has_inbody_description(self
):
645 return self
._rawdoc
.has_inbody_description()
647 def is_visible(self
):
648 """Return whether the member is visible in Doxygen documentation.
650 Doxygen ignores members whose parent compounds are not documented.
651 However, when EXTRACT_ANON_NPACES=ON (which is set for our full
652 documentation), members of anonymous namespaces are extracted even if
653 the namespace is the only parent and is not documented.
655 if self
._namespace
and self
._namespace
.is_anonymous():
657 return self
._rawdoc
.get_inherited_visibility() != DocType
.none
660 class GromacsTree(object):
662 """Root object for navigating the GROMACS source tree.
664 On initialization, the list of files and directories is initialized by
665 walking the source tree, and modules are created for top-level
666 subdirectories. At this point, only information that is accessible from
667 file names and paths only is available.
669 load_git_attributes() can be called to load attribute information from
670 .gitattributes for all the files.
672 scan_files() can be called to read all the files and initialize #include
673 dependencies between the files based on the information. This is done like
674 this instead of relying on Doxygen-extracted include files to make the
675 dependency graph independent from preprocessor macro definitions
676 (Doxygen only sees those #includes that the preprocessor sees, which
677 depends on what #defines it has seen).
679 find_define_file_uses() can be called to find all uses of defines
680 declared in config.h and some other macro headers. In the current
681 implementation, scan_files() must have been called earlier.
683 load_xml() can be called to load information from Doxygen XML data in
684 the build tree (the Doxygen XML data must have been built separately).
687 def __init__(self
, source_root
, build_root
, reporter
):
688 """Initialize the tree object by walking the source tree."""
689 self
._source
_root
= os
.path
.abspath(source_root
)
690 self
._build
_root
= os
.path
.abspath(build_root
)
691 self
._reporter
= reporter
693 self
._docmap
= dict()
696 self
._modules
= dict()
697 self
._classes
= set()
698 self
._namespaces
= set()
699 self
._members
= set()
700 self
._walk
_dir
(os
.path
.join(self
._source
_root
, 'src'))
701 for fileobj
in self
.get_files():
702 if fileobj
and fileobj
.is_source_file() and not fileobj
.is_external():
703 (basedir
, name
) = os
.path
.split(fileobj
.get_abspath())
704 (basename
, ext
) = os
.path
.splitext(name
)
705 header
= self
.get_file(os
.path
.join(basedir
, basename
+ '.h'))
706 if not header
and ext
== '.cu':
707 header
= self
.get_file(os
.path
.join(basedir
, basename
+ '.cuh'))
708 if not header
and fileobj
.is_test_file():
709 basedir
= os
.path
.dirname(basedir
)
710 header
= self
.get_file(os
.path
.join(basedir
, basename
+ '.h'))
712 # Somewhat of a hack; currently, the tests for
713 # analysisdata/modules/ and trajectoryanalysis/modules/
714 # is at the top-level tests directory.
715 # TODO: It could be clearer to split the tests so that
716 # there would be a separate modules/tests/.
717 header
= self
.get_file(os
.path
.join(basedir
, 'modules', basename
+ '.h'))
718 if not header
and basename
.endswith('_tests'):
719 header
= self
.get_file(os
.path
.join(basedir
, basename
[:-6] + '.h'))
721 fileobj
.set_main_header(header
)
722 rootdir
= self
._get
_dir
(os
.path
.join('src', 'gromacs'))
723 for subdir
in rootdir
.get_subdirectories():
724 self
._create
_module
(subdir
)
725 rootdir
= self
._get
_dir
(os
.path
.join('src', 'testutils'))
726 self
._create
_module
(rootdir
)
728 def _get_rel_path(self
, path
):
729 assert os
.path
.isabs(path
)
730 if path
.startswith(self
._build
_root
):
731 return os
.path
.relpath(path
, self
._build
_root
)
732 if path
.startswith(self
._source
_root
):
733 return os
.path
.relpath(path
, self
._source
_root
)
734 raise ValueError("path not under build nor source tree: {0}".format(path
))
736 def _walk_dir(self
, rootpath
):
737 """Construct representation of the source tree by walking the file system."""
738 assert os
.path
.isabs(rootpath
)
739 assert rootpath
not in self
._dirs
740 relpath
= self
._get
_rel
_path
(rootpath
)
741 self
._dirs
[relpath
] = Directory(rootpath
, relpath
, None)
742 for dirpath
, dirnames
, filenames
in os
.walk(rootpath
):
743 if 'refdata' in dirnames
:
744 dirnames
.remove('refdata')
745 currentdir
= self
._dirs
[self
._get
_rel
_path
(dirpath
)]
746 # Loop through a copy so that we can modify dirnames.
747 for dirname
in list(dirnames
):
748 fullpath
= os
.path
.join(dirpath
, dirname
)
749 if fullpath
== self
._build
_root
:
750 dirnames
.remove(dirname
)
752 relpath
= self
._get
_rel
_path
(fullpath
)
753 self
._dirs
[relpath
] = Directory(fullpath
, relpath
, currentdir
)
754 extensions
= ('.h', '.cuh', '.hpp', '.c', '.cc', '.cpp', '.cu', '.bm')
755 for filename
in filenames
:
756 basename
, extension
= os
.path
.splitext(filename
)
757 if extension
in extensions
:
758 fullpath
= os
.path
.join(dirpath
, filename
)
759 relpath
= self
._get
_rel
_path
(fullpath
)
760 self
._files
[relpath
] = File(fullpath
, relpath
, currentdir
)
761 elif extension
== '.cmakein':
762 extension
= os
.path
.splitext(basename
)[1]
763 if extension
in extensions
:
764 fullpath
= os
.path
.join(dirpath
, filename
)
765 relpath
= self
._get
_rel
_path
(fullpath
)
766 sourcefile
= GeneratorSourceFile(fullpath
, relpath
, currentdir
)
767 self
._files
[relpath
] = sourcefile
768 fullpath
= os
.path
.join(dirpath
, basename
)
769 relpath
= self
._get
_rel
_path
(fullpath
)
770 fullpath
= os
.path
.join(self
._build
_root
, relpath
)
771 generatedfile
= GeneratedFile(fullpath
, relpath
, currentdir
)
772 self
._files
[relpath
] = generatedfile
773 generatedfile
.set_generator_source(sourcefile
)
774 elif extension
in ('.l', '.y', '.pre'):
775 fullpath
= os
.path
.join(dirpath
, filename
)
776 relpath
= self
._get
_rel
_path
(fullpath
)
777 self
._files
[relpath
] = GeneratorSourceFile(fullpath
, relpath
, currentdir
)
779 def _create_module(self
, rootdir
):
780 """Create module for a subdirectory."""
781 name
= 'module_' + rootdir
.get_name()
782 moduleobj
= Module(name
, rootdir
)
783 rootdir
.set_module(moduleobj
)
784 self
._modules
[name
] = moduleobj
786 def scan_files(self
, only_files
=None, keep_contents
=False):
787 """Read source files to initialize #include dependencies."""
789 filelist
= only_files
791 filelist
= self
._files
.values()
792 define_files
= list(self
.get_checked_define_files())
793 for define_file
in list(define_files
):
794 if isinstance(define_file
, GeneratedFile
) and \
795 define_file
.get_generator_source() is not None:
796 define_files
.append(define_file
.get_generator_source())
797 for fileobj
in filelist
:
798 if not fileobj
.is_external():
799 detect_defines
= fileobj
in define_files
800 fileobj
.scan_contents(self
, keep_contents
, detect_defines
)
801 module
= fileobj
.get_module()
803 for includedfile
in fileobj
.get_includes():
804 otherfile
= includedfile
.get_file()
806 othermodule
= otherfile
.get_module()
807 if othermodule
and othermodule
!= module
:
808 module
.add_dependency(othermodule
, includedfile
)
810 def load_xml(self
, only_files
=None):
811 """Load Doxygen XML information.
813 If only_files is True, XML data is not loaded for code constructs, but
814 only for files, directories, and their potential parents.
816 xmldir
= os
.path
.join(self
._build
_root
, 'docs', 'html', 'doxygen', 'xml')
817 self
._docset
= xml
.DocumentationSet(xmldir
, self
._reporter
)
819 if isinstance(only_files
, collections
.Iterable
):
820 filelist
= [x
.get_relpath() for x
in only_files
]
821 self
._docset
.load_file_details(filelist
)
823 self
._docset
.load_file_details()
825 self
._docset
.load_details()
826 self
._docset
.merge_duplicates()
831 self
._load
_namespaces
()
835 def _load_dirs(self
):
836 """Load Doxygen XML directory information."""
837 rootdirs
= self
._docset
.get_compounds(xml
.Directory
,
838 lambda x
: x
.get_parent() is None)
839 for dirdoc
in rootdirs
:
840 self
._load
_dir
(dirdoc
, None)
842 def _load_dir(self
, dirdoc
, parent
):
843 """Load Doxygen XML directory information for a single directory."""
844 path
= dirdoc
.get_path().rstrip('/')
845 if not os
.path
.isabs(path
):
846 path
= os
.path
.join(self
._source
_root
, path
)
847 relpath
= self
._get
_rel
_path
(path
)
848 dirobj
= self
._dirs
.get(relpath
)
850 dirobj
= Directory(path
, relpath
, parent
)
851 self
._dirs
[relpath
] = dirobj
852 dirobj
.set_doc_xml(dirdoc
, self
)
853 self
._docmap
[dirdoc
] = dirobj
854 for subdirdoc
in dirdoc
.get_subdirectories():
855 self
._load
_dir
(subdirdoc
, dirobj
)
857 def _load_modules(self
):
858 """Load Doxygen XML module (group) information."""
859 moduledocs
= self
._docset
.get_compounds(xml
.Group
,
860 lambda x
: x
.get_name().startswith('module_'))
861 for moduledoc
in moduledocs
:
862 moduleobj
= self
._modules
.get(moduledoc
.get_name())
864 self
._reporter
.input_error(
865 "no matching directory for module: {0}".format(moduledoc
))
867 moduleobj
.set_doc_xml(moduledoc
, self
)
868 self
._docmap
[moduledoc
] = moduleobj
870 def _load_files(self
):
871 """Load Doxygen XML file information."""
872 for filedoc
in self
._docset
.get_files():
873 path
= filedoc
.get_path()
875 # In case of only partially loaded file information,
876 # the path information is not set for unloaded files.
878 if not os
.path
.isabs(path
):
879 path
= os
.path
.join(self
._source
_root
, path
)
880 extension
= os
.path
.splitext(path
)[1]
881 # We don't care about Markdown files that only produce pages
882 # (and fail the directory check below).
883 if extension
== '.md':
885 dirdoc
= filedoc
.get_directory()
887 self
._reporter
.xml_assert(filedoc
.get_xml_path(),
888 "file is not in any directory in Doxygen")
890 relpath
= self
._get
_rel
_path
(path
)
891 fileobj
= self
._files
.get(relpath
)
893 fileobj
= File(path
, relpath
, self
._docmap
[dirdoc
])
894 self
._files
[relpath
] = fileobj
895 fileobj
.set_doc_xml(filedoc
, self
)
896 self
._docmap
[filedoc
] = fileobj
898 def _load_namespaces(self
):
899 """Load Doxygen XML namespace information."""
900 nsdocs
= self
._docset
.get_namespaces()
902 nsobj
= Namespace(nsdoc
)
903 self
._docmap
[nsdoc
] = nsobj
904 self
._namespaces
.add(nsobj
)
906 def _load_classes(self
):
907 """Load Doxygen XML class information."""
908 classdocs
= self
._docset
.get_classes()
909 for classdoc
in classdocs
:
910 files
= [self
._docmap
[filedoc
] for filedoc
in classdoc
.get_files()]
911 classobj
= Class(classdoc
, files
)
912 self
._docmap
[classdoc
] = classobj
913 self
._classes
.add(classobj
)
915 def _load_members(self
):
916 """Load Doxygen XML member information."""
917 memberdocs
= self
._docset
.get_members()
918 for memberdoc
in memberdocs
:
919 nsdoc
= memberdoc
.get_namespace()
920 nsobj
= self
.get_object(nsdoc
)
921 memberobj
= Member(memberdoc
, nsobj
)
922 self
._docmap
[memberdoc
] = memberobj
923 self
._members
.add(memberobj
)
925 def _get_dir(self
, relpath
):
926 """Get directory object for a path relative to source tree root."""
927 return self
._dirs
.get(relpath
)
929 def get_file(self
, path
):
930 """Get file object for a path relative to source tree root."""
931 return self
._files
.get(self
._get
_rel
_path
(path
))
933 def find_include_file(self
, includedpath
):
934 """Find a file object corresponding to an include path."""
935 for testdir
in ('src', 'src/external/thread_mpi/include',
936 'src/external/tng_io/include'):
937 testpath
= os
.path
.join(testdir
, includedpath
)
938 if testpath
in self
._files
:
939 return self
._files
[testpath
]
941 def load_git_attributes(self
):
942 """Load git attribute information for files."""
943 args
= ['git', 'check-attr', '--stdin', 'filter']
944 git_check_attr
= subprocess
.Popen(args
, stdin
=subprocess
.PIPE
,
945 stdout
=subprocess
.PIPE
, cwd
=self
._source
_root
)
946 filelist
= '\n'.join(map(File
.get_relpath
, self
._files
.values()))
947 filters
= git_check_attr
.communicate(filelist
.encode())[0].decode()
948 for fileinfo
in filters
.splitlines():
949 path
, dummy
, value
= fileinfo
.split(': ')
950 fileobj
= self
._files
.get(path
)
951 assert fileobj
is not None
952 fileobj
.set_git_filter_attribute(value
)
954 def find_define_file_uses(self
):
955 """Find files that use defines from config.h."""
956 # Executing git grep is substantially faster than using the define_re
957 # directly on the contents of the file in Python.
958 for define_file
in self
.get_checked_define_files():
959 excluded_files
= set([define_file
])
960 excluded_files
.update(define_file
.get_included_files(recursive
=True))
961 all_defines
= define_file
.get_declared_defines()
962 args
= ['git', 'grep', '-zwIF']
963 for define
in all_defines
:
964 args
.extend(['-e', define
])
965 args
.extend(['--', '*.cpp', '*.c', '*.cu', '*.h', '*.cuh'])
966 define_re
= r
'\b(?:' + '|'.join(all_defines
)+ r
')\b'
967 output
= subprocess
.check_output(args
, cwd
=self
._source
_root
).decode()
968 for line
in output
.splitlines():
969 (filename
, text
) = line
.split('\0')
970 fileobj
= self
._files
.get(filename
)
971 if fileobj
is not None and fileobj
not in excluded_files
:
972 defines
= re
.findall(define_re
, text
)
973 fileobj
.add_used_defines(define_file
, defines
)
975 def load_cycle_suppression_list(self
, filename
):
976 """Load a list of edges to suppress in cycles.
978 These edges between modules, if present, will be marked in the
979 corresponding ModuleDependency objects.
981 with
open(filename
, 'r') as fp
:
984 if not line
or line
.startswith('#'):
986 modulenames
= ['module_' + x
.strip() for x
in line
.split('->')]
987 if len(modulenames
) != 2:
988 self
._reporter
.input_error(
989 "invalid cycle suppression line: {0}".format(line
))
991 firstmodule
= self
._modules
.get(modulenames
[0])
992 secondmodule
= self
._modules
.get(modulenames
[1])
993 if not firstmodule
or not secondmodule
:
994 self
._reporter
.input_error(
995 "unknown modules mentioned on cycle suppression line: {0}".format(line
))
997 for dep
in firstmodule
.get_dependencies():
998 if dep
.get_other_module() == secondmodule
:
999 dep
.set_cycle_suppression()
1002 self
._reporter
.cyclic_issue("unused cycle suppression: {0}".format(line
))
1004 def report_unused_cycle_suppressions(self
, reporter
):
1005 """Reports unused cycle suppressions."""
1006 for module
in self
.get_modules():
1007 for dep
in module
.get_dependencies():
1008 if not dep
.suppression_used
:
1009 reporter
.cyclic_issue("unused cycle suppression: {0} -> {1}".format(module
.get_name()[7:], dep
.get_other_module().get_name()[7:]))
1011 def get_object(self
, docobj
):
1012 """Get tree object for a Doxygen XML object."""
1015 return self
._docmap
.get(docobj
)
1017 def get_files(self
):
1018 """Get iterable for all files in the source tree."""
1019 return self
._files
.values()
1021 def get_modules(self
):
1022 """Get iterable for all modules in the source tree."""
1023 return self
._modules
.values()
1025 def get_classes(self
):
1026 """Get iterable for all classes in the source tree."""
1027 return self
._classes
1029 def get_members(self
):
1030 """Get iterable for all members (in Doxygen terms) in the source tree."""
1031 return self
._members
1033 def get_checked_define_files(self
):
1034 """Get list of files that contain #define macros whose usage needs to
1036 return (self
._files
['src/config.h'],
1037 self
._files
['src/gromacs/simd/simd.h'],
1038 self
._files
['src/gromacs/ewald/pme_simd.h'],
1039 self
._files
['src/gromacs/nbnxm/nbnxm_simd.h'])