3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2014,2015,2018,2019, 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.
43 The checks should be self-evident from the source code of the script
44 (they are also described in docs/dev-manual/gmxtree.rst).
45 All the logic of parsing the Doxygen XML output and creating a GROMACS-specific
46 representation of the source tree is separated into separate Python modules
47 (doxygenxml.py and gmxtree.py, respectively). Similarly, logic for handling
48 the output messages is in reporter.py. This leaves only the actual checks and
49 the script command-line interface in this file.
51 The script can be run using the 'check-source' target generated by CMake.
52 This target takes care of generating all the necessary input files and passing
57 from optparse import OptionParser
60 from gmxtree import GromacsTree, DocType
61 from includesorter import IncludeSorter
62 from reporter import Reporter
64 def check_file(fileobj, tree, reporter):
65 """Check file-level issues."""
66 if not fileobj.is_external() and fileobj.get_relpath().startswith('src/'):
67 includes = fileobj.get_includes()
68 if fileobj.is_source_file():
70 firstinclude = includes[0].get_file()
71 if not firstinclude or firstinclude.get_name() != "gmxpre.h":
72 reporter.code_issue(includes[0],
73 "does not include \"gmxpre.h\" first")
75 reporter.code_issue(fileobj, "does not include \"gmxpre.h\"")
76 used_define_files = fileobj.get_used_define_files()
77 for define_file in tree.get_checked_define_files():
79 for include in includes:
80 if include.get_file() == define_file:
84 if not define_file in used_define_files:
85 reporter.code_issue(fileobj,
86 "includes \"{0}\" unnecessarily".format(define_file.get_name()))
88 if define_file in used_define_files:
89 used_defines = list(fileobj.get_used_defines(define_file))
90 if len(used_defines) > 3:
91 used_defines = used_defines[:3] + ['...']
92 used_defines = ', '.join(used_defines)
93 reporter.code_issue(fileobj,
94 "should include \"{0}\"".format(define_file.get_name()),
95 details=["uses " + used_defines])
97 if not fileobj.is_documented():
98 # TODO: Add rules for required documentation
101 if fileobj.is_source_file():
102 # TODO: Add rule to exclude examples from this check
103 if fileobj.get_doc_type() != DocType.internal:
104 reporter.file_error(fileobj,
105 "source file documentation appears outside full documentation")
106 elif fileobj.get_api_type() != DocType.internal:
107 reporter.file_error(fileobj, "source file marked as non-internal")
108 elif fileobj.get_doc_type() < fileobj.get_api_type():
109 reporter.file_error(fileobj,
110 "API type ({0}) conflicts with documentation visibility ({1})"
111 .format(fileobj.get_api_type(), fileobj.get_doc_type()))
113 if not fileobj.has_brief_description():
114 reporter.file_error(fileobj,
115 "is documented, but does not have brief description")
117 expectedmod = fileobj.get_expected_module()
119 docmodules = fileobj.get_doc_modules()
121 for module in docmodules:
122 if module != expectedmod:
123 reporter.file_error(fileobj,
124 "is documented in incorrect module: {0}"
125 .format(module.get_name()))
126 elif expectedmod.is_documented():
127 reporter.file_error(fileobj,
128 "is not documented in any module, but {0} exists"
129 .format(expectedmod.get_name()))
131 def check_include(fileobj, includedfile, reporter):
132 """Check an #include directive."""
133 otherfile = includedfile.get_file()
134 if includedfile.is_system():
137 reporter.code_issue(includedfile,
138 "includes local file as {0}".format(includedfile))
139 # To report compiler flags in the log file, include files must be
140 # generated by CMake, with a name that is only known at generation
141 # time (not even configuration time). This looks like the
142 # inclusion of a non-local file to the source checker. We usually
143 # suppress issues with generated files using file-name matching,
144 # but this is not suitable in this case because it is hard to map
145 # the repository file named src/compilerflaginfo.h.cmakein to two
146 # generated header files
147 # $builddir/src/compilerflaginfo-$buildtype-CXX.h and
148 # $builddir/src/compilerflaginfo-$buildtype-C.h. So we use a hack
149 # to just avoid the warning for this special source file.
150 if "compilerflaginfo" in str(includedfile):
153 reporter.code_issue(includedfile,
154 "includes non-local file as {0}".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)
168 def check_entity(entity, reporter):
169 """Check documentation for a code construct."""
170 if entity.is_documented():
171 if not entity.has_brief_description():
172 reporter.doc_error(entity,
173 "is documented, but does not have brief description")
175 def check_class(classobj, reporter):
176 """Check documentation for a class/struct/union."""
177 check_entity(classobj, reporter)
178 if classobj.is_documented():
179 classtype = classobj.get_doc_type()
180 filetype = classobj.get_file_doc_type()
181 if filetype is not DocType.none and classtype > filetype:
182 reporter.doc_error(classobj,
183 "is in {0} file(s), but appears in {1} documentation"
184 .format(filetype, classtype))
186 def check_member(member, reporter, check_ignored):
187 """Check documentation for a generic member."""
188 check_entity(member, reporter)
189 if member.is_documented():
190 if check_ignored and not member.is_visible():
191 reporter.doc_note(member,
192 "is documented, but is ignored by Doxygen, because its scope is not documented")
193 if member.has_inbody_description():
194 reporter.doc_note(member, "has in-body comments, which are ignored")
196 def check_cycles(graph, reporter):
197 """Check cyclic dependencies in a dependency graph.
199 The graph parameter provides the graph to check. It should be an object
200 that has three methods:
202 Return the list of nodes in the graph.
204 Return the list of edges from a given node.
205 The list should contain (node, edge) pairs, where node is an object
206 returned by iternodes() and edge is any object.
207 report_cycle(cycle, reporter):
208 Process a found cycle. cycle contains a list of (node, edge) pairs
209 that describe the cycle. edge is the edge object that leads _to_
210 the node in the cycle.
212 This is implemented using an extended DFS-based strongly connected
213 component (SCC) search, written using a stack instead of recursion.
214 The base algorithm is Tarjan's SCC search:
215 http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
217 Each back edge that is encountered during the search is reported as a
218 cycle. Additionally, if a cross edge is encountered that is within the
219 current SCC, the target node and all its children in the current SCC will
220 be visited again to find all cycles. All steps except cycle detection are
221 omitted for such re-traversal.
223 To avoid duplicates from cycles that do not include all nodes in an SCC,
224 a cycle is only reported if the target of the back edge is still active
225 in the search, i.e., all edges from it have not yet been traversed.
227 # The DFS stack; next node is always popped from the end.
228 # Stores (node, edge) pairs.
229 # edge is None for start nodes and for post-order processing.
231 for node in graph.iternodes():
232 dfsstack.append((node, None))
233 # Stack of visited nodes that have not yet been assigned to a strongly
234 # connected component.
236 # List of nodes in the DFS recursion stack.
238 # Set of nodes in currlist for more efficient searching.
240 # Counter for initializing preorder.
242 # DFS pre-order for nodes: initialized when a node is first encountered
245 # Lowest pre-order index reachable from this node.
246 # Initialized to pre-order, and updated during post-order processing.
248 # Set to True for a node when first encountered, and set to False when
249 # a strongly connected component has been processed.
253 currnode, curredge = dfsstack.pop()
254 # curredge is None if this is a start node or post-order traversal.
255 # currlist is empty if this is a start node.
256 if curredge is None and currlist:
257 # All children visited: post-order processing.
258 done = currlist.pop()[0]
259 assert done == currnode
260 currset.remove(currnode)
261 # If this is the first time this node is encountered, fill
262 # linkorder and check for strongly connected components.
263 if linkorder[currnode] == preorder[currnode]:
264 children = [x for x, dummy in graph.iteredges(currnode) if in_progress[x]]
266 linkorder[currnode] = min([linkorder[x] for x in children])
267 if preorder[currnode] <= linkorder[currnode]:
268 # This is a root of a strongly connected component.
270 node = visitstack.pop()
271 in_progress[node] = False
277 if currnode not in preorder:
278 # First encounter of this node: pre-order processing.
279 preorder[currnode] = visit_count
280 linkorder[currnode] = visit_count
281 visitstack.append(currnode)
283 in_progress[currnode] = True
284 elif not in_progress[currnode]:
285 # Do not enter processed components again.
287 currlist.append((currnode, curredge))
288 currset.add(currnode)
289 # add entry for post-order traversal
290 dfsstack.append((currnode, None))
291 for nextnode, edge in graph.iteredges(currnode):
292 if nextnode not in preorder:
293 # Not seen previously: push
294 dfsstack.append((nextnode, edge))
296 # If an already visited node is in the same component, it is
297 # either part of a cycle, or we need to traverse it again to
299 if in_progress[nextnode]:
300 if nextnode not in currset:
301 dfsstack.append((nextnode, edge))
302 # Only report cycles to nodes that haven't been processed
303 # yet to avoid duplicates.
304 elif linkorder[nextnode] == preorder[nextnode]:
305 for index, node in enumerate(currlist):
306 if node[0] == nextnode:
307 cycle = [(nextnode, edge)]
308 cycle.extend(currlist[index+1:])
309 graph.report_cycle(cycle, reporter)
314 class ModuleDependencyGraph(object):
316 """Module dependency graph representation for check_cycles().
318 In the reported graph, the nodes are gmxtree.Module objects and the edges
319 are gmxtree.ModuleDependency objects.
322 def __init__(self, tree):
326 return self._tree.get_modules()
328 def iteredges(self, module):
329 for dependency in module.get_dependencies():
330 if not dependency.is_test_only_dependency():
331 yield (dependency.get_other_module(), dependency)
333 def report_cycle(self, cycle, reporter):
334 if any([x[1].is_cycle_suppressed() for x in cycle]):
335 # TODO: Report unused suppressions.
337 modulelist = ' -> '.join([x[0].get_name()[7:] for x in cycle])
338 summary = 'module-level cyclic dependency: ' + modulelist
339 reporter.cyclic_issue(summary)
341 def check_all(tree, reporter, check_ignored):
342 """Do all checks for the GROMACS tree."""
343 includesorter = IncludeSorter()
344 for fileobj in tree.get_files():
345 if isinstance(fileobj, gmxtree.GeneratorSourceFile):
347 check_file(fileobj, tree, reporter)
348 for includedfile in fileobj.get_includes():
349 check_include(fileobj, includedfile, reporter)
350 if fileobj.should_includes_be_sorted():
351 is_sorted, details = includesorter.check_sorted(fileobj)
353 details.append("You can use includesorter.py to do the sorting automatically; see docs/dev-manual/gmxtree.rst")
354 reporter.code_issue(fileobj,
355 "include style/order is not consistent; see docs/dev-manual/includestyle.rst", details)
357 for classobj in tree.get_classes():
358 check_class(classobj, reporter)
360 for memberobj in tree.get_members():
361 check_member(memberobj, reporter, check_ignored)
363 check_cycles(ModuleDependencyGraph(tree), reporter)
364 tree.report_unused_cycle_suppressions(reporter)
367 """Run the checking script."""
368 parser = OptionParser()
369 parser.add_option('-S', '--source-root',
370 help='Source tree root directory')
371 parser.add_option('-B', '--build-root',
372 help='Build tree root directory')
373 parser.add_option('-l', '--log',
374 help='Write issues into a given log file in addition to stderr')
375 parser.add_option('--ignore',
376 help='Set file with patterns for messages to ignore')
377 parser.add_option('--ignore-cycles',
378 help='Set file with module dependencies to ignore in cycles')
379 parser.add_option('--check-ignored', action='store_true',
380 help='Issue notes for comments ignored by Doxygen')
381 parser.add_option('-q', '--quiet', action='store_true',
382 help='Do not write status messages')
383 parser.add_option('--exitcode', action='store_true',
384 help='Return non-zero exit code if there are warnings')
385 options, args = parser.parse_args()
387 reporter = Reporter(options.log)
389 reporter.load_filters(options.ignore)
391 if not options.quiet:
392 sys.stderr.write('Scanning source tree...\n')
393 tree = GromacsTree(options.source_root, options.build_root, reporter)
394 tree.load_git_attributes()
395 if not options.quiet:
396 sys.stderr.write('Reading source files...\n')
397 # TODO: The checking should be possible without storing everything in memory
398 tree.scan_files(keep_contents=True)
399 if not options.quiet:
400 sys.stderr.write('Finding config.h and other preprocessor macro uses...\n')
401 tree.find_define_file_uses()
402 if options.ignore_cycles:
403 tree.load_cycle_suppression_list(options.ignore_cycles)
404 if not options.quiet:
405 sys.stderr.write('Reading Doxygen XML files...\n')
408 reporter.write_pending()
410 if not options.quiet:
411 sys.stderr.write('Checking...\n')
413 check_all(tree, reporter, options.check_ignored)
415 reporter.write_pending()
416 reporter.report_unused_filters()
419 if options.exitcode and reporter.had_warnings():