3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2014,2015, 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 source code and Doxygen documentation for issues
38 This script checks for some issues in the Doxygen documentation, as well as
39 general issues in the source code, mainly using Doxygen XML output and #include
40 dependencies parsed from source files. Part of the checks are generic, like
41 checking that all documented entities have brief descriptions. Other are
42 specific to GROMACS, like checking that only installed headers contribute to
43 the public API documentation.
45 The checks should be self-evident from the source code of the script
46 (they are also described in docs/dev-manual/gmxtree.rst).
47 All the logic of parsing the Doxygen XML output and creating a GROMACS-specific
48 representation of the source tree is separated into separate Python modules
49 (doxygenxml.py and gmxtree.py, respectively). Similarly, logic for handling
50 the output messages is in reporter.py. This leaves only the actual checks and
51 the script command-line interface in this file.
53 The script can be run using the 'check-source' target generated by CMake.
54 This target takes care of generating all the necessary input files and passing
59 from optparse import OptionParser
62 from gmxtree import GromacsTree, DocType
63 from includesorter import IncludeSorter
64 from reporter import Reporter
66 def check_file(fileobj, reporter):
67 """Check file-level issues."""
68 if not fileobj.is_external() and fileobj.get_relpath().startswith('src/'):
69 includes = fileobj.get_includes()
70 if fileobj.is_source_file():
72 firstinclude = includes[0].get_file()
73 if not firstinclude or firstinclude.get_name() != "gmxpre.h":
74 reporter.code_issue(includes[0],
75 "does not include \"gmxpre.h\" first")
77 reporter.code_issue(fileobj, "does not include \"gmxpre.h\"")
78 includes_config_h = False
79 for include in includes:
80 includedfile = include.get_file()
82 if includedfile.get_name() == 'config.h':
83 includes_config_h = True
85 if not fileobj.get_used_config_h_defines():
86 reporter.code_issue(fileobj,
87 "includes \"config.h\" unnecessarily")
89 if fileobj.get_used_config_h_defines():
90 reporter.code_issue(fileobj, "should include \"config.h\"")
92 if not fileobj.is_documented():
93 # TODO: Add rules for required documentation
96 if fileobj.is_source_file():
97 # TODO: Add rule to exclude examples from this check
98 if fileobj.is_installed():
99 reporter.file_error(fileobj, "source file is installed")
100 if fileobj.get_doc_type() != DocType.internal:
101 reporter.file_error(fileobj,
102 "source file documentation appears outside full documentation")
103 elif fileobj.get_api_type() != DocType.internal:
104 reporter.file_error(fileobj, "source file marked as non-internal")
105 elif fileobj.is_test_file() and fileobj.is_installed():
106 reporter.file_error(fileobj, "test file is installed")
107 elif fileobj.is_installed():
108 if fileobj.get_doc_type() != DocType.public:
109 reporter.file_error(fileobj,
110 "public header has non-public documentation")
111 elif fileobj.get_doc_type() == DocType.public:
112 reporter.file_error(fileobj,
113 "non-installed header has public documentation")
114 elif fileobj.get_api_type() == DocType.public:
115 reporter.file_error(fileobj,
116 "non-installed header specified as part of public API")
117 elif fileobj.get_doc_type() < fileobj.get_api_type():
118 reporter.file_error(fileobj,
119 "API type ({0}) conflicts with documentation visibility ({1})"
120 .format(fileobj.get_api_type(), fileobj.get_doc_type()))
122 if not fileobj.has_brief_description():
123 reporter.file_error(fileobj,
124 "is documented, but does not have brief description")
126 expectedmod = fileobj.get_expected_module()
128 docmodules = fileobj.get_doc_modules()
130 for module in docmodules:
131 if module != expectedmod:
132 reporter.file_error(fileobj,
133 "is documented in incorrect module: {0}"
134 .format(module.get_name()))
135 elif expectedmod.is_documented():
136 reporter.file_error(fileobj,
137 "is not documented in any module, but {0} exists"
138 .format(expectedmod.get_name()))
140 def check_include(fileobj, includedfile, reporter):
141 """Check an #include directive."""
142 otherfile = includedfile.get_file()
143 if includedfile.is_system():
146 reporter.code_issue(includedfile,
147 "includes local file as {0}".format(includedfile))
149 reporter.code_issue(includedfile,
150 "includes non-local file as {0}".format(includedfile))
153 if fileobj.is_installed() and not otherfile.is_installed():
154 reporter.code_issue(includedfile,
155 "installed header includes non-installed {0}"
156 .format(includedfile))
157 filemodule = fileobj.get_module()
158 othermodule = otherfile.get_module()
159 if fileobj.is_documented() and otherfile.is_documented():
160 filetype = fileobj.get_doc_type()
161 othertype = otherfile.get_doc_type()
162 if filetype > othertype:
163 reporter.code_issue(includedfile,
164 "{0} file includes {1} file {2}"
165 .format(filetype, othertype, includedfile))
166 check_api = (otherfile.api_type_is_reliable() and filemodule != othermodule)
167 if check_api and otherfile.get_api_type() < DocType.library:
168 reporter.code_issue(includedfile,
169 "included file {0} is not documented as exposed outside its module"
170 .format(includedfile))
172 def check_entity(entity, reporter):
173 """Check documentation for a code construct."""
174 if entity.is_documented():
175 if not entity.has_brief_description():
176 reporter.doc_error(entity,
177 "is documented, but does not have brief description")
179 def check_class(classobj, reporter):
180 """Check documentation for a class/struct/union."""
181 check_entity(classobj, reporter)
182 if classobj.is_documented():
183 classtype = classobj.get_doc_type()
184 filetype = classobj.get_file_doc_type()
185 if classtype == DocType.public and not classobj.is_in_installed_file():
186 reporter.doc_error(classobj,
187 "has public documentation, but is not in installed header")
188 elif filetype is not DocType.none and classtype > filetype:
189 reporter.doc_error(classobj,
190 "is in {0} file(s), but appears in {1} documentation"
191 .format(filetype, classtype))
193 def check_member(member, reporter, check_ignored):
194 """Check documentation for a generic member."""
195 check_entity(member, reporter)
196 if member.is_documented():
197 if check_ignored and not member.is_visible():
198 reporter.doc_note(member,
199 "is documented, but is ignored by Doxygen, because its scope is not documented")
200 if member.has_inbody_description():
201 reporter.doc_note(member, "has in-body comments, which are ignored")
203 def check_cycles(graph, reporter):
204 """Check cyclic dependencies in a dependency graph.
206 The graph parameter provides the graph to check. It should be an object
207 that has three methods:
209 Return the list of nodes in the graph.
211 Return the list of edges from a given node.
212 The list should contain (node, edge) pairs, where node is an object
213 returned by iternodes() and edge is any object.
214 report_cycle(cycle, reporter):
215 Process a found cycle. cycle contains a list of (node, edge) pairs
216 that describe the cycle. edge is the edge object that leads _to_
217 the node in the cycle.
219 This is implemented using an extended DFS-based strongly connected
220 component (SCC) search, written using a stack instead of recursion.
221 The base algorithm is Tarjan's SCC search:
222 http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
224 Each back edge that is encountered during the search is reported as a
225 cycle. Additionally, if a cross edge is encountered that is within the
226 current SCC, the target node and all its children in the current SCC will
227 be visited again to find all cycles. All steps except cycle detection are
228 omitted for such re-traversal.
230 To avoid duplicates from cycles that do not include all nodes in an SCC,
231 a cycle is only reported if the target of the back edge is still active
232 in the search, i.e., all edges from it have not yet been traversed.
234 # The DFS stack; next node is always popped from the end.
235 # Stores (node, edge) pairs.
236 # edge is None for start nodes and for post-order processing.
238 for node in graph.iternodes():
239 dfsstack.append((node, None))
240 # Stack of visited nodes that have not yet been assigned to a strongly
241 # connected component.
243 # List of nodes in the DFS recursion stack.
245 # Set of nodes in currlist for more efficient searching.
247 # Counter for initializing preorder.
249 # DFS pre-order for nodes: initialized when a node is first encountered
252 # Lowest pre-order index reachable from this node.
253 # Initialized to pre-order, and updated during post-order processing.
255 # Set to True for a node when first encountered, and set to False when
256 # a strongly connected component has been processed.
260 currnode, curredge = dfsstack.pop()
261 # curredge is None if this is a start node or post-order traversal.
262 # currlist is empty if this is a start node.
263 if curredge is None and currlist:
264 # All children visited: post-order processing.
265 done = currlist.pop()[0]
266 assert done == currnode
267 currset.remove(currnode)
268 # If this is the first time this node is encountered, fill
269 # linkorder and check for strongly connected components.
270 if linkorder[currnode] == preorder[currnode]:
271 children = [x for x, dummy in graph.iteredges(currnode) if in_progress[x]]
273 linkorder[currnode] = min([linkorder[x] for x in children])
274 if preorder[currnode] <= linkorder[currnode]:
275 # This is a root of a strongly connected component.
277 node = visitstack.pop()
278 in_progress[node] = False
284 if currnode not in preorder:
285 # First encounter of this node: pre-order processing.
286 preorder[currnode] = visit_count
287 linkorder[currnode] = visit_count
288 visitstack.append(currnode)
290 in_progress[currnode] = True
291 elif not in_progress[currnode]:
292 # Do not enter processed components again.
294 currlist.append((currnode, curredge))
295 currset.add(currnode)
296 # add entry for post-order traversal
297 dfsstack.append((currnode, None))
298 for nextnode, edge in graph.iteredges(currnode):
299 if nextnode not in preorder:
300 # Not seen previously: push
301 dfsstack.append((nextnode, edge))
303 # If an already visited node is in the same component, it is
304 # either part of a cycle, or we need to traverse it again to
306 if in_progress[nextnode]:
307 if nextnode not in currset:
308 dfsstack.append((nextnode, edge))
309 # Only report cycles to nodes that haven't been processed
310 # yet to avoid duplicates.
311 elif linkorder[nextnode] == preorder[nextnode]:
312 for index in xrange(len(currlist)):
313 if currlist[index][0] == nextnode:
314 cycle = [(nextnode, edge)]
315 cycle.extend(currlist[index+1:])
316 graph.report_cycle(cycle, reporter)
321 class ModuleDependencyGraph(object):
323 """Module dependency graph representation for check_cycles().
325 In the reported graph, the nodes are gmxtree.Module objects and the edges
326 are gmxtree.ModuleDependency objects.
329 def __init__(self, tree):
333 return self._tree.get_modules()
335 def iteredges(self, module):
336 for dependency in module.get_dependencies():
337 if not dependency.is_test_only_dependency():
338 yield (dependency.get_other_module(), dependency)
340 def report_cycle(self, cycle, reporter):
341 if any([x[1].is_cycle_suppressed() for x in cycle]):
342 # TODO: Report unused suppressions.
344 modulelist = ' -> '.join([x[0].get_name()[7:] for x in cycle])
345 summary = 'module-level cyclic dependency: ' + modulelist
346 reporter.cyclic_issue(summary)
348 def check_all(tree, reporter, check_ignored):
349 """Do all checks for the GROMACS tree."""
350 includesorter = IncludeSorter()
351 for fileobj in tree.get_files():
352 if isinstance(fileobj, gmxtree.GeneratorSourceFile):
354 check_file(fileobj, reporter)
355 for includedfile in fileobj.get_includes():
356 check_include(fileobj, includedfile, reporter)
357 if fileobj.should_includes_be_sorted():
358 is_sorted, details = includesorter.check_sorted(fileobj)
360 details.append("You can use includesorter.py to do the sorting automatically; see docs/dev-manual/gmxtree.rst")
361 reporter.code_issue(fileobj,
362 "include style/order is not consistent; see docs/dev-manual/includestyle.rst", details)
364 for classobj in tree.get_classes():
365 check_class(classobj, reporter)
367 for memberobj in tree.get_members():
368 check_member(memberobj, reporter, check_ignored)
370 check_cycles(ModuleDependencyGraph(tree), reporter)
373 """Run the checking script."""
374 parser = OptionParser()
375 parser.add_option('-S', '--source-root',
376 help='Source tree root directory')
377 parser.add_option('-B', '--build-root',
378 help='Build tree root directory')
379 parser.add_option('-l', '--log',
380 help='Write issues into a given log file in addition to stderr')
381 parser.add_option('--ignore',
382 help='Set file with patterns for messages to ignore')
383 parser.add_option('--ignore-cycles',
384 help='Set file with module dependencies to ignore in cycles')
385 parser.add_option('--check-ignored', action='store_true',
386 help='Issue notes for comments ignored by Doxygen')
387 parser.add_option('-q', '--quiet', action='store_true',
388 help='Do not write status messages')
389 parser.add_option('--exitcode', action='store_true',
390 help='Return non-zero exit code if there are warnings')
391 options, args = parser.parse_args()
393 reporter = Reporter(options.log)
395 reporter.load_filters(options.ignore)
397 if not options.quiet:
398 sys.stderr.write('Scanning source tree...\n')
399 tree = GromacsTree(options.source_root, options.build_root, reporter)
400 tree.load_git_attributes()
401 tree.load_installed_file_list()
402 if not options.quiet:
403 sys.stderr.write('Reading source files...\n')
404 # TODO: The checking should be possible without storing everything in memory
405 tree.scan_files(keep_contents=True)
406 if not options.quiet:
407 sys.stderr.write('Finding config.h uses...\n')
408 tree.find_config_h_uses()
409 if options.ignore_cycles:
410 tree.load_cycle_suppression_list(options.ignore_cycles)
411 if not options.quiet:
412 sys.stderr.write('Reading Doxygen XML files...\n')
415 reporter.write_pending()
417 if not options.quiet:
418 sys.stderr.write('Checking...\n')
420 check_all(tree, reporter, options.check_ignored)
422 reporter.write_pending()
423 reporter.report_unused_filters()
426 if options.exitcode and reporter.had_warnings():