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