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 """Check Doxygen documentation for issues that Doxygen does not warn about.
38 This script for some issues in the Doxygen documentation, using Doxygen XML
39 output. Part of the checks are generic, like checking that all documented
40 entities have brief descriptions. Other are specific to GROMACS, like checking
41 that only installed headers contribute to the public API documentation.
43 The checks should be self-evident from the source code of the script.
44 All the logic of parsing the Doxygen XML output and creating a GROMACS-specific
45 representation of the source tree is separated into separate Python modules
46 (doxygenxml.py and gmxtree.py, respectively). Similarly, logic for handling
47 the output messages is in reporter.py. This leaves only the actual checks and
48 the script command-line interface in this file.
50 The script can be run using the 'doc-check' target generated by CMake.
51 This target takes care of generating all the necessary input files and passing
56 from optparse import OptionParser
59 from gmxtree import GromacsTree, DocType
60 from includesorter import IncludeSorter
61 from reporter import Reporter
63 def check_file(fileobj, reporter):
64 """Check file-level issues."""
65 if fileobj.is_source_file() and not fileobj.is_external() and \
66 fileobj.get_relpath().startswith('src/'):
67 includes = fileobj.get_includes()
69 firstinclude = includes[0].get_file()
70 if not firstinclude or firstinclude.get_name() != "gmxpre.h":
71 reporter.code_issue(includes[0],
72 "does not include \"gmxpre.h\" first")
74 reporter.code_issue(fileobj, "does not include \"gmxpre.h\"")
76 if not fileobj.is_documented():
77 # TODO: Add rules for required documentation
80 if fileobj.is_source_file():
81 # TODO: Add rule to exclude examples from this check
82 if fileobj.is_installed():
83 reporter.file_error(fileobj, "source file is installed")
84 if fileobj.get_doc_type() != DocType.internal:
85 reporter.file_error(fileobj,
86 "source file documentation appears outside full documentation")
87 elif fileobj.get_api_type() != DocType.internal:
88 reporter.file_error(fileobj, "source file marked as non-internal")
89 elif fileobj.is_test_file() and fileobj.is_installed():
90 reporter.file_error(fileobj, "test file is installed")
91 elif fileobj.is_installed():
92 if fileobj.get_doc_type() != DocType.public:
93 reporter.file_error(fileobj,
94 "public header has non-public documentation")
95 elif fileobj.get_doc_type() == DocType.public:
96 reporter.file_error(fileobj,
97 "non-installed header has public documentation")
98 elif fileobj.get_api_type() == DocType.public:
99 reporter.file_error(fileobj,
100 "non-installed header specified as part of public API")
101 elif fileobj.get_doc_type() < fileobj.get_api_type():
102 reporter.file_error(fileobj,
103 "API type ({0}) conflicts with documentation visibility ({1})"
104 .format(fileobj.get_api_type(), fileobj.get_doc_type()))
106 if not fileobj.has_brief_description():
107 reporter.file_error(fileobj,
108 "is documented, but does not have brief description")
110 expectedmod = fileobj.get_expected_module()
112 docmodules = fileobj.get_doc_modules()
114 for module in docmodules:
115 if module != expectedmod:
116 reporter.file_error(fileobj,
117 "is documented in incorrect module: {0}"
118 .format(module.get_name()))
119 elif expectedmod.is_documented():
120 reporter.file_error(fileobj,
121 "is not documented in any module, but {0} exists"
122 .format(expectedmod.get_name()))
124 def check_include(fileobj, includedfile, reporter):
125 """Check an #include directive."""
126 otherfile = includedfile.get_file()
127 if includedfile.is_system():
130 reporter.code_issue(includedfile,
131 "includes local file as {0}".format(includedfile))
133 reporter.code_issue(includedfile,
134 "includes non-local file as {0}".format(includedfile))
135 # TODO: Reinstantiate a check once there is clarity on what we want
137 #elif fileobj.is_installed() and not includedfile.is_relative():
138 # reporter.code_issue(includedfile,
139 # "installed header includes {0} using non-relative path"
140 # .format(includedfile))
143 if fileobj.is_installed() and not otherfile.is_installed():
144 reporter.code_issue(includedfile,
145 "installed header includes non-installed {0}"
146 .format(includedfile))
147 filemodule = fileobj.get_module()
148 othermodule = otherfile.get_module()
149 if fileobj.is_documented() and otherfile.is_documented():
150 filetype = fileobj.get_doc_type()
151 othertype = otherfile.get_doc_type()
152 if filetype > othertype:
153 reporter.code_issue(includedfile,
154 "{0} file includes {1} file {2}"
155 .format(filetype, othertype, includedfile))
156 check_api = (otherfile.api_type_is_reliable() and filemodule != othermodule)
157 if check_api and otherfile.get_api_type() < DocType.library:
158 reporter.code_issue(includedfile,
159 "included file {0} is not documented as exposed outside its module"
160 .format(includedfile))
162 def check_entity(entity, reporter):
163 """Check documentation for a code construct."""
164 if entity.is_documented():
165 if not entity.has_brief_description():
166 reporter.doc_error(entity,
167 "is documented, but does not have brief description")
169 def check_class(classobj, reporter):
170 """Check documentation for a class/struct/union."""
171 check_entity(classobj, reporter)
172 if classobj.is_documented():
173 classtype = classobj.get_doc_type()
174 filetype = classobj.get_file_doc_type()
175 if classtype == DocType.public and not classobj.is_in_installed_file():
176 reporter.doc_error(classobj,
177 "has public documentation, but is not in installed header")
178 elif filetype is not DocType.none and classtype > filetype:
179 reporter.doc_error(classobj,
180 "is in {0} file(s), but appears in {1} documentation"
181 .format(filetype, classtype))
183 def check_member(member, reporter, check_ignored):
184 """Check documentation for a generic member."""
185 check_entity(member, reporter)
186 if member.is_documented():
187 if check_ignored and not member.is_visible():
188 reporter.doc_note(member,
189 "is documented, but is ignored by Doxygen, because its scope is not documented")
190 if member.has_inbody_description():
191 reporter.doc_note(member, "has in-body comments, which are ignored")
193 def check_cycles(graph, reporter):
194 """Check cyclic dependencies in a dependency graph.
196 The graph parameter provides the graph to check. It should be an object
197 that has three methods:
199 Return the list of nodes in the graph.
201 Return the list of edges from a given node.
202 The list should contain (node, edge) pairs, where node is an object
203 returned by iternodes() and edge is any object.
204 report_cycle(cycle, reporter):
205 Process a found cycle. cycle contains a list of (node, edge) pairs
206 that describe the cycle. edge is the edge object that leads _to_
207 the node in the cycle.
209 This is implemented using an extended DFS-based strongly connected
210 component (SCC) search, written using a stack instead of recursion.
211 The base algorithm is Tarjan's SCC search:
212 http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
214 Each back edge that is encountered during the search is reported as a
215 cycle. Additionally, if a cross edge is encountered that is within the
216 current SCC, the target node and all its children in the current SCC will
217 be visited again to find all cycles. All steps except cycle detection are
218 omitted for such re-traversal.
220 To avoid duplicates from cycles that do not include all nodes in an SCC,
221 a cycle is only reported if the target of the back edge is still active
222 in the search, i.e., all edges from it have not yet been traversed.
224 # The DFS stack; next node is always popped from the end.
225 # Stores (node, edge) pairs.
226 # edge is None for start nodes and for post-order processing.
228 for node in graph.iternodes():
229 dfsstack.append((node, None))
230 # Stack of visited nodes that have not yet been assigned to a strongly
231 # connected component.
233 # List of nodes in the DFS recursion stack.
235 # Set of nodes in currlist for more efficient searching.
237 # Counter for initializing preorder.
239 # DFS pre-order for nodes: initialized when a node is first encountered
242 # Lowest pre-order index reachable from this node.
243 # Initialized to pre-order, and updated during post-order processing.
245 # Set to True for a node when first encountered, and set to False when
246 # a strongly connected component has been processed.
250 currnode, curredge = dfsstack.pop()
251 # curredge is None if this is a start node or post-order traversal.
252 # currlist is empty if this is a start node.
253 if curredge is None and currlist:
254 # All children visited: post-order processing.
255 done = currlist.pop()[0]
256 assert done == currnode
257 currset.remove(currnode)
258 # If this is the first time this node is encountered, fill
259 # linkorder and check for strongly connected components.
260 if linkorder[currnode] == preorder[currnode]:
261 children = [x for x, dummy in graph.iteredges(currnode) if in_progress[x]]
263 linkorder[currnode] = min([linkorder[x] for x in children])
264 if preorder[currnode] <= linkorder[currnode]:
265 # This is a root of a strongly connected component.
267 node = visitstack.pop()
268 in_progress[node] = False
274 if currnode not in preorder:
275 # First encounter of this node: pre-order processing.
276 preorder[currnode] = visit_count
277 linkorder[currnode] = visit_count
278 visitstack.append(currnode)
280 in_progress[currnode] = True
281 elif not in_progress[currnode]:
282 # Do not enter processed components again.
284 currlist.append((currnode, curredge))
285 currset.add(currnode)
286 # add entry for post-order traversal
287 dfsstack.append((currnode, None))
288 for nextnode, edge in graph.iteredges(currnode):
289 if nextnode not in preorder:
290 # Not seen previously: push
291 dfsstack.append((nextnode, edge))
293 # If an already visited node is in the same component, it is
294 # either part of a cycle, or we need to traverse it again to
296 if in_progress[nextnode]:
297 if nextnode not in currset:
298 dfsstack.append((nextnode, edge))
299 # Only report cycles to nodes that haven't been processed
300 # yet to avoid duplicates.
301 elif linkorder[nextnode] == preorder[nextnode]:
302 for index in xrange(len(currlist)):
303 if currlist[index][0] == nextnode:
304 cycle = [(nextnode, edge)]
305 cycle.extend(currlist[index+1:])
306 graph.report_cycle(cycle, reporter)
311 class ModuleDependencyGraph(object):
313 """Module dependency graph representation for check_cycles().
315 In the reported graph, the nodes are gmxtree.Module objects and the edges
316 are gmxtree.ModuleDependency objects.
319 def __init__(self, tree):
323 for module in self._tree.get_modules():
324 if module.get_name() != 'module_testutils':
327 def iteredges(self, module):
328 for dependency in module.get_dependencies():
329 if dependency.get_other_module().get_name() != 'module_testutils':
330 yield (dependency.get_other_module(), dependency)
332 def report_cycle(self, cycle, reporter):
333 if any([x[1].is_cycle_suppressed() for x in cycle]):
334 # TODO: Report unused suppressions.
336 modulelist = ' -> '.join([x[0].get_name()[7:] for x in cycle])
337 summary = 'module-level cyclic dependency: ' + modulelist
338 reporter.cyclic_issue(summary)
340 def check_all(tree, reporter, check_ignored):
341 """Do all checks for the GROMACS tree."""
342 includesorter = IncludeSorter()
343 for fileobj in tree.get_files():
344 if isinstance(fileobj, gmxtree.GeneratorSourceFile):
346 check_file(fileobj, reporter)
347 for includedfile in fileobj.get_includes():
348 check_include(fileobj, includedfile, reporter)
349 if fileobj.should_includes_be_sorted() \
350 and not includesorter.check_sorted(fileobj):
351 reporter.code_issue(fileobj, "include order is not consistent")
353 for classobj in tree.get_classes():
354 check_class(classobj, reporter)
356 for memberobj in tree.get_members():
357 check_member(memberobj, reporter, check_ignored)
359 check_cycles(ModuleDependencyGraph(tree), reporter)
362 """Run the checking script."""
363 parser = OptionParser()
364 parser.add_option('-S', '--source-root',
365 help='Source tree root directory')
366 parser.add_option('-B', '--build-root',
367 help='Build tree root directory')
368 parser.add_option('-l', '--log',
369 help='Write issues into a given log file in addition to stderr')
370 parser.add_option('--ignore',
371 help='Set file with patterns for messages to ignore')
372 parser.add_option('--ignore-cycles',
373 help='Set file with module dependencies to ignore in cycles')
374 parser.add_option('--check-ignored', action='store_true',
375 help='Issue notes for comments ignored by Doxygen')
376 parser.add_option('-q', '--quiet', action='store_true',
377 help='Do not write status messages')
378 parser.add_option('--exitcode', action='store_true',
379 help='Return non-zero exit code if there are warnings')
380 options, args = parser.parse_args()
382 reporter = Reporter(options.log)
384 reporter.load_filters(options.ignore)
386 if not options.quiet:
387 sys.stderr.write('Scanning source tree...\n')
388 tree = GromacsTree(options.source_root, options.build_root, reporter)
389 tree.load_git_attributes()
390 tree.load_installed_file_list()
391 if not options.quiet:
392 sys.stderr.write('Reading source files...\n')
393 # TODO: The checking should be possible without storing everything in memory
394 tree.scan_files(keep_contents=True)
395 if options.ignore_cycles:
396 tree.load_cycle_suppression_list(options.ignore_cycles)
397 if not options.quiet:
398 sys.stderr.write('Reading Doxygen XML files...\n')
401 reporter.write_pending()
403 if not options.quiet:
404 sys.stderr.write('Checking...\n')
406 check_all(tree, reporter, options.check_ignored)
408 reporter.write_pending()
409 reporter.report_unused_filters()
412 if options.exitcode and reporter.had_warnings():