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.md).
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))
151 # TODO: Reinstantiate a check once there is clarity on what we want
153 #elif fileobj.is_installed() and not includedfile.is_relative():
154 # reporter.code_issue(includedfile,
155 # "installed header includes {0} using non-relative path"
156 # .format(includedfile))
159 if fileobj.is_installed() and not otherfile.is_installed():
160 reporter.code_issue(includedfile,
161 "installed header includes non-installed {0}"
162 .format(includedfile))
163 filemodule = fileobj.get_module()
164 othermodule = otherfile.get_module()
165 if fileobj.is_documented() and otherfile.is_documented():
166 filetype = fileobj.get_doc_type()
167 othertype = otherfile.get_doc_type()
168 if filetype > othertype:
169 reporter.code_issue(includedfile,
170 "{0} file includes {1} file {2}"
171 .format(filetype, othertype, includedfile))
172 check_api = (otherfile.api_type_is_reliable() and filemodule != othermodule)
173 if check_api and otherfile.get_api_type() < DocType.library:
174 reporter.code_issue(includedfile,
175 "included file {0} is not documented as exposed outside its module"
176 .format(includedfile))
178 def check_entity(entity, reporter):
179 """Check documentation for a code construct."""
180 if entity.is_documented():
181 if not entity.has_brief_description():
182 reporter.doc_error(entity,
183 "is documented, but does not have brief description")
185 def check_class(classobj, reporter):
186 """Check documentation for a class/struct/union."""
187 check_entity(classobj, reporter)
188 if classobj.is_documented():
189 classtype = classobj.get_doc_type()
190 filetype = classobj.get_file_doc_type()
191 if classtype == DocType.public and not classobj.is_in_installed_file():
192 reporter.doc_error(classobj,
193 "has public documentation, but is not in installed header")
194 elif filetype is not DocType.none and classtype > filetype:
195 reporter.doc_error(classobj,
196 "is in {0} file(s), but appears in {1} documentation"
197 .format(filetype, classtype))
199 def check_member(member, reporter, check_ignored):
200 """Check documentation for a generic member."""
201 check_entity(member, reporter)
202 if member.is_documented():
203 if check_ignored and not member.is_visible():
204 reporter.doc_note(member,
205 "is documented, but is ignored by Doxygen, because its scope is not documented")
206 if member.has_inbody_description():
207 reporter.doc_note(member, "has in-body comments, which are ignored")
209 def check_cycles(graph, reporter):
210 """Check cyclic dependencies in a dependency graph.
212 The graph parameter provides the graph to check. It should be an object
213 that has three methods:
215 Return the list of nodes in the graph.
217 Return the list of edges from a given node.
218 The list should contain (node, edge) pairs, where node is an object
219 returned by iternodes() and edge is any object.
220 report_cycle(cycle, reporter):
221 Process a found cycle. cycle contains a list of (node, edge) pairs
222 that describe the cycle. edge is the edge object that leads _to_
223 the node in the cycle.
225 This is implemented using an extended DFS-based strongly connected
226 component (SCC) search, written using a stack instead of recursion.
227 The base algorithm is Tarjan's SCC search:
228 http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
230 Each back edge that is encountered during the search is reported as a
231 cycle. Additionally, if a cross edge is encountered that is within the
232 current SCC, the target node and all its children in the current SCC will
233 be visited again to find all cycles. All steps except cycle detection are
234 omitted for such re-traversal.
236 To avoid duplicates from cycles that do not include all nodes in an SCC,
237 a cycle is only reported if the target of the back edge is still active
238 in the search, i.e., all edges from it have not yet been traversed.
240 # The DFS stack; next node is always popped from the end.
241 # Stores (node, edge) pairs.
242 # edge is None for start nodes and for post-order processing.
244 for node in graph.iternodes():
245 dfsstack.append((node, None))
246 # Stack of visited nodes that have not yet been assigned to a strongly
247 # connected component.
249 # List of nodes in the DFS recursion stack.
251 # Set of nodes in currlist for more efficient searching.
253 # Counter for initializing preorder.
255 # DFS pre-order for nodes: initialized when a node is first encountered
258 # Lowest pre-order index reachable from this node.
259 # Initialized to pre-order, and updated during post-order processing.
261 # Set to True for a node when first encountered, and set to False when
262 # a strongly connected component has been processed.
266 currnode, curredge = dfsstack.pop()
267 # curredge is None if this is a start node or post-order traversal.
268 # currlist is empty if this is a start node.
269 if curredge is None and currlist:
270 # All children visited: post-order processing.
271 done = currlist.pop()[0]
272 assert done == currnode
273 currset.remove(currnode)
274 # If this is the first time this node is encountered, fill
275 # linkorder and check for strongly connected components.
276 if linkorder[currnode] == preorder[currnode]:
277 children = [x for x, dummy in graph.iteredges(currnode) if in_progress[x]]
279 linkorder[currnode] = min([linkorder[x] for x in children])
280 if preorder[currnode] <= linkorder[currnode]:
281 # This is a root of a strongly connected component.
283 node = visitstack.pop()
284 in_progress[node] = False
290 if currnode not in preorder:
291 # First encounter of this node: pre-order processing.
292 preorder[currnode] = visit_count
293 linkorder[currnode] = visit_count
294 visitstack.append(currnode)
296 in_progress[currnode] = True
297 elif not in_progress[currnode]:
298 # Do not enter processed components again.
300 currlist.append((currnode, curredge))
301 currset.add(currnode)
302 # add entry for post-order traversal
303 dfsstack.append((currnode, None))
304 for nextnode, edge in graph.iteredges(currnode):
305 if nextnode not in preorder:
306 # Not seen previously: push
307 dfsstack.append((nextnode, edge))
309 # If an already visited node is in the same component, it is
310 # either part of a cycle, or we need to traverse it again to
312 if in_progress[nextnode]:
313 if nextnode not in currset:
314 dfsstack.append((nextnode, edge))
315 # Only report cycles to nodes that haven't been processed
316 # yet to avoid duplicates.
317 elif linkorder[nextnode] == preorder[nextnode]:
318 for index in xrange(len(currlist)):
319 if currlist[index][0] == nextnode:
320 cycle = [(nextnode, edge)]
321 cycle.extend(currlist[index+1:])
322 graph.report_cycle(cycle, reporter)
327 class ModuleDependencyGraph(object):
329 """Module dependency graph representation for check_cycles().
331 In the reported graph, the nodes are gmxtree.Module objects and the edges
332 are gmxtree.ModuleDependency objects.
335 def __init__(self, tree):
339 return self._tree.get_modules()
341 def iteredges(self, module):
342 for dependency in module.get_dependencies():
343 if not dependency.is_test_only_dependency():
344 yield (dependency.get_other_module(), dependency)
346 def report_cycle(self, cycle, reporter):
347 if any([x[1].is_cycle_suppressed() for x in cycle]):
348 # TODO: Report unused suppressions.
350 modulelist = ' -> '.join([x[0].get_name()[7:] for x in cycle])
351 summary = 'module-level cyclic dependency: ' + modulelist
352 reporter.cyclic_issue(summary)
354 def check_all(tree, reporter, check_ignored):
355 """Do all checks for the GROMACS tree."""
356 includesorter = IncludeSorter()
357 for fileobj in tree.get_files():
358 if isinstance(fileobj, gmxtree.GeneratorSourceFile):
360 check_file(fileobj, reporter)
361 for includedfile in fileobj.get_includes():
362 check_include(fileobj, includedfile, reporter)
363 if fileobj.should_includes_be_sorted() \
364 and not includesorter.check_sorted(fileobj):
365 reporter.code_issue(fileobj, "include style/order is not consistent")
367 for classobj in tree.get_classes():
368 check_class(classobj, reporter)
370 for memberobj in tree.get_members():
371 check_member(memberobj, reporter, check_ignored)
373 check_cycles(ModuleDependencyGraph(tree), reporter)
376 """Run the checking script."""
377 parser = OptionParser()
378 parser.add_option('-S', '--source-root',
379 help='Source tree root directory')
380 parser.add_option('-B', '--build-root',
381 help='Build tree root directory')
382 parser.add_option('-l', '--log',
383 help='Write issues into a given log file in addition to stderr')
384 parser.add_option('--ignore',
385 help='Set file with patterns for messages to ignore')
386 parser.add_option('--ignore-cycles',
387 help='Set file with module dependencies to ignore in cycles')
388 parser.add_option('--check-ignored', action='store_true',
389 help='Issue notes for comments ignored by Doxygen')
390 parser.add_option('-q', '--quiet', action='store_true',
391 help='Do not write status messages')
392 parser.add_option('--exitcode', action='store_true',
393 help='Return non-zero exit code if there are warnings')
394 options, args = parser.parse_args()
396 reporter = Reporter(options.log)
398 reporter.load_filters(options.ignore)
400 if not options.quiet:
401 sys.stderr.write('Scanning source tree...\n')
402 tree = GromacsTree(options.source_root, options.build_root, reporter)
403 tree.load_git_attributes()
404 tree.load_installed_file_list()
405 if not options.quiet:
406 sys.stderr.write('Reading source files...\n')
407 # TODO: The checking should be possible without storing everything in memory
408 tree.scan_files(keep_contents=True)
409 if not options.quiet:
410 sys.stderr.write('Finding config.h uses...\n')
411 tree.find_config_h_uses()
412 if options.ignore_cycles:
413 tree.load_cycle_suppression_list(options.ignore_cycles)
414 if not options.quiet:
415 sys.stderr.write('Reading Doxygen XML files...\n')
418 reporter.write_pending()
420 if not options.quiet:
421 sys.stderr.write('Checking...\n')
423 check_all(tree, reporter, options.check_ignored)
425 reporter.write_pending()
426 reporter.report_unused_filters()
429 if options.exitcode and reporter.had_warnings():