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 reporter import Reporter
62 def check_file(fileobj, reporter):
63 """Check file-level issues."""
64 if fileobj.is_source_file() and not fileobj.is_external() and \
65 fileobj.get_relpath().startswith('src/'):
66 includes = fileobj.get_includes()
68 firstinclude = includes[0].get_file()
69 if not firstinclude or firstinclude.get_name() != "gmxpre.h":
70 reporter.code_issue(includes[0],
71 "does not include \"gmxpre.h\" first")
73 reporter.code_issue(fileobj, "does not include \"gmxpre.h\"")
75 if not fileobj.is_documented():
76 # TODO: Add rules for required documentation
79 if fileobj.is_source_file():
80 # TODO: Add rule to exclude examples from this check
81 if fileobj.is_installed():
82 reporter.file_error(fileobj, "source file is installed")
83 if fileobj.get_doc_type() != DocType.internal:
84 reporter.file_error(fileobj,
85 "source file documentation appears outside full documentation")
86 elif fileobj.get_api_type() != DocType.internal:
87 reporter.file_error(fileobj, "source file marked as non-internal")
88 elif fileobj.is_test_file() and fileobj.is_installed():
89 reporter.file_error(fileobj, "test file is installed")
90 elif fileobj.is_installed():
91 if fileobj.get_doc_type() != DocType.public:
92 reporter.file_error(fileobj,
93 "public header has non-public documentation")
94 elif fileobj.get_doc_type() == DocType.public:
95 reporter.file_error(fileobj,
96 "non-installed header has public documentation")
97 elif fileobj.get_api_type() == DocType.public:
98 reporter.file_error(fileobj,
99 "non-installed header specified as part of public API")
100 elif fileobj.get_doc_type() < fileobj.get_api_type():
101 reporter.file_error(fileobj,
102 "API type ({0}) conflicts with documentation visibility ({1})"
103 .format(fileobj.get_api_type(), fileobj.get_doc_type()))
105 if not fileobj.has_brief_description():
106 reporter.file_error(fileobj,
107 "is documented, but does not have brief description")
109 expectedmod = fileobj.get_expected_module()
111 docmodules = fileobj.get_doc_modules()
113 for module in docmodules:
114 if module != expectedmod:
115 reporter.file_error(fileobj,
116 "is documented in incorrect module: {0}"
117 .format(module.get_name()))
118 elif expectedmod.is_documented():
119 reporter.file_error(fileobj,
120 "is not documented in any module, but {0} exists"
121 .format(expectedmod.get_name()))
123 def check_include(fileobj, includedfile, reporter):
124 """Check an #include directive."""
125 otherfile = includedfile.get_file()
126 if includedfile.is_system():
129 reporter.code_issue(includedfile,
130 "includes local file as {0}".format(includedfile))
132 reporter.code_issue(includedfile,
133 "includes non-local file as {0}".format(includedfile))
134 # TODO: Reinstantiate a check once there is clarity on what we want
136 #elif fileobj.is_installed() and not includedfile.is_relative():
137 # reporter.code_issue(includedfile,
138 # "installed header includes {0} using non-relative path"
139 # .format(includedfile))
142 if fileobj.is_installed() and not otherfile.is_installed():
143 reporter.code_issue(includedfile,
144 "installed header includes non-installed {0}"
145 .format(includedfile))
146 filemodule = fileobj.get_module()
147 othermodule = otherfile.get_module()
148 if fileobj.is_documented() and otherfile.is_documented():
149 filetype = fileobj.get_doc_type()
150 othertype = otherfile.get_doc_type()
151 if filetype > othertype:
152 reporter.code_issue(includedfile,
153 "{0} file includes {1} file {2}"
154 .format(filetype, othertype, includedfile))
155 check_api = (otherfile.api_type_is_reliable() and filemodule != othermodule)
156 if check_api and otherfile.get_api_type() < DocType.library:
157 reporter.code_issue(includedfile,
158 "included file {0} is not documented as exposed outside its module"
159 .format(includedfile))
161 def check_entity(entity, reporter):
162 """Check documentation for a code construct."""
163 if entity.is_documented():
164 if not entity.has_brief_description():
165 reporter.doc_error(entity,
166 "is documented, but does not have brief description")
168 def check_class(classobj, reporter):
169 """Check documentation for a class/struct/union."""
170 check_entity(classobj, reporter)
171 if classobj.is_documented():
172 classtype = classobj.get_doc_type()
173 filetype = classobj.get_file_doc_type()
174 if classtype == DocType.public and not classobj.is_in_installed_file():
175 reporter.doc_error(classobj,
176 "has public documentation, but is not in installed header")
177 elif filetype is not DocType.none and classtype > filetype:
178 reporter.doc_error(classobj,
179 "is in {0} file(s), but appears in {1} documentation"
180 .format(filetype, classtype))
182 def check_member(member, reporter, check_ignored):
183 """Check documentation for a generic member."""
184 check_entity(member, reporter)
185 if member.is_documented():
186 if check_ignored and not member.is_visible():
187 reporter.doc_note(member,
188 "is documented, but is ignored by Doxygen, because its scope is not documented")
189 if member.has_inbody_description():
190 reporter.doc_note(member, "has in-body comments, which are ignored")
192 def check_cycles(graph, reporter):
193 """Check cyclic dependencies in a dependency graph.
195 The graph parameter provides the graph to check. It should be an object
196 that has three methods:
198 Return the list of nodes in the graph.
200 Return the list of edges from a given node.
201 The list should contain (node, edge) pairs, where node is an object
202 returned by iternodes() and edge is any object.
203 report_cycle(cycle, reporter):
204 Process a found cycle. cycle contains a list of (node, edge) pairs
205 that describe the cycle. edge is the edge object that leads _to_
206 the node in the cycle.
208 This is implemented using an extended DFS-based strongly connected
209 component (SCC) search, written using a stack instead of recursion.
210 The base algorithm is Tarjan's SCC search:
211 http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
213 Each back edge that is encountered during the search is reported as a
214 cycle. Additionally, if a cross edge is encountered that is within the
215 current SCC, the target node and all its children in the current SCC will
216 be visited again to find all cycles. All steps except cycle detection are
217 omitted for such re-traversal.
219 To avoid duplicates from cycles that do not include all nodes in an SCC,
220 a cycle is only reported if the target of the back edge is still active
221 in the search, i.e., all edges from it have not yet been traversed.
223 # The DFS stack; next node is always popped from the end.
224 # Stores (node, edge) pairs.
225 # edge is None for start nodes and for post-order processing.
227 for node in graph.iternodes():
228 dfsstack.append((node, None))
229 # Stack of visited nodes that have not yet been assigned to a strongly
230 # connected component.
232 # List of nodes in the DFS recursion stack.
234 # Set of nodes in currlist for more efficient searching.
236 # Counter for initializing preorder.
238 # DFS pre-order for nodes: initialized when a node is first encountered
241 # Lowest pre-order index reachable from this node.
242 # Initialized to pre-order, and updated during post-order processing.
244 # Set to True for a node when first encountered, and set to False when
245 # a strongly connected component has been processed.
249 currnode, curredge = dfsstack.pop()
250 # curredge is None if this is a start node or post-order traversal.
251 # currlist is empty if this is a start node.
252 if curredge is None and currlist:
253 # All children visited: post-order processing.
254 done = currlist.pop()[0]
255 assert done == currnode
256 currset.remove(currnode)
257 # If this is the first time this node is encountered, fill
258 # linkorder and check for strongly connected components.
259 if linkorder[currnode] == preorder[currnode]:
260 children = [x for x, dummy in graph.iteredges(currnode) if in_progress[x]]
262 linkorder[currnode] = min([linkorder[x] for x in children])
263 if preorder[currnode] <= linkorder[currnode]:
264 # This is a root of a strongly connected component.
266 node = visitstack.pop()
267 in_progress[node] = False
273 if currnode not in preorder:
274 # First encounter of this node: pre-order processing.
275 preorder[currnode] = visit_count
276 linkorder[currnode] = visit_count
277 visitstack.append(currnode)
279 in_progress[currnode] = True
280 elif not in_progress[currnode]:
281 # Do not enter processed components again.
283 currlist.append((currnode, curredge))
284 currset.add(currnode)
285 # add entry for post-order traversal
286 dfsstack.append((currnode, None))
287 for nextnode, edge in graph.iteredges(currnode):
288 if nextnode not in preorder:
289 # Not seen previously: push
290 dfsstack.append((nextnode, edge))
292 # If an already visited node is in the same component, it is
293 # either part of a cycle, or we need to traverse it again to
295 if in_progress[nextnode]:
296 if nextnode not in currset:
297 dfsstack.append((nextnode, edge))
298 # Only report cycles to nodes that haven't been processed
299 # yet to avoid duplicates.
300 elif linkorder[nextnode] == preorder[nextnode]:
301 for index in xrange(len(currlist)):
302 if currlist[index][0] == nextnode:
303 cycle = [(nextnode, edge)]
304 cycle.extend(currlist[index+1:])
305 graph.report_cycle(cycle, reporter)
310 class ModuleDependencyGraph(object):
312 """Module dependency graph representation for check_cycles().
314 In the reported graph, the nodes are gmxtree.Module objects and the edges
315 are gmxtree.ModuleDependency objects.
318 def __init__(self, tree):
322 for module in self._tree.get_modules():
323 if module.get_name() != 'module_testutils':
326 def iteredges(self, module):
327 for dependency in module.get_dependencies():
328 if dependency.get_other_module().get_name() != 'module_testutils':
329 yield (dependency.get_other_module(), dependency)
331 def report_cycle(self, cycle, reporter):
332 if any([x[1].is_cycle_suppressed() for x in cycle]):
333 # TODO: Report unused suppressions.
335 modulelist = ' -> '.join([x[0].get_name()[7:] for x in cycle])
336 summary = 'module-level cyclic dependency: ' + modulelist
337 reporter.cyclic_issue(summary)
340 """Run the checking script."""
341 parser = OptionParser()
342 parser.add_option('-S', '--source-root',
343 help='Source tree root directory')
344 parser.add_option('-B', '--build-root',
345 help='Build tree root directory')
346 parser.add_option('-l', '--log',
347 help='Write issues into a given log file in addition to stderr')
348 parser.add_option('--ignore',
349 help='Set file with patterns for messages to ignore')
350 parser.add_option('--ignore-cycles',
351 help='Set file with module dependencies to ignore in cycles')
352 parser.add_option('--check-ignored', action='store_true',
353 help='Issue notes for comments ignored by Doxygen')
354 parser.add_option('-q', '--quiet', action='store_true',
355 help='Do not write status messages')
356 parser.add_option('--exitcode', action='store_true',
357 help='Return non-zero exit code if there are warnings')
358 options, args = parser.parse_args()
360 reporter = Reporter(options.log)
362 reporter.load_filters(options.ignore)
364 if not options.quiet:
365 sys.stderr.write('Scanning source tree...\n')
366 tree = GromacsTree(options.source_root, options.build_root, reporter)
367 tree.load_installed_file_list()
368 if not options.quiet:
369 sys.stderr.write('Reading source files...\n')
371 if options.ignore_cycles:
372 tree.load_cycle_suppression_list(options.ignore_cycles)
373 if not options.quiet:
374 sys.stderr.write('Reading Doxygen XML files...\n')
377 reporter.write_pending()
379 if not options.quiet:
380 sys.stderr.write('Checking...\n')
382 for fileobj in tree.get_files():
383 if isinstance(fileobj, gmxtree.GeneratorSourceFile):
385 check_file(fileobj, reporter)
386 for includedfile in fileobj.get_includes():
387 check_include(fileobj, includedfile, reporter)
389 for classobj in tree.get_classes():
390 check_class(classobj, reporter)
392 for memberobj in tree.get_members():
393 check_member(memberobj, reporter, options.check_ignored)
395 check_cycles(ModuleDependencyGraph(tree), reporter)
397 reporter.write_pending()
398 reporter.report_unused_filters()
401 if options.exitcode and reporter.had_warnings():