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