Print correct order for include order/style issues
[alexxy/gromacs.git] / docs / doxygen / check-source.py
1 #!/usr/bin/python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
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.
9 #
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.
14 #
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.
19 #
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.
24 #
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.
32 #
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.
35
36 """Check source code and Doxygen documentation for issues
37
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.
44
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.
52
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
55 them to the script.
56 """
57
58 import sys
59 from optparse import OptionParser
60
61 import gmxtree
62 from gmxtree import GromacsTree, DocType
63 from includesorter import IncludeSorter
64 from reporter import Reporter
65
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():
71             if includes:
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")
76             else:
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()
81             if includedfile:
82                 if includedfile.get_name() == 'config.h':
83                     includes_config_h = True
84         if includes_config_h:
85             if not fileobj.get_used_config_h_defines():
86                 reporter.code_issue(fileobj,
87                         "includes \"config.h\" unnecessarily")
88         else:
89             if fileobj.get_used_config_h_defines():
90                 reporter.code_issue(fileobj, "should include \"config.h\"")
91
92     if not fileobj.is_documented():
93         # TODO: Add rules for required documentation
94         return
95
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()))
121
122     if not fileobj.has_brief_description():
123         reporter.file_error(fileobj,
124                 "is documented, but does not have brief description")
125
126     expectedmod = fileobj.get_expected_module()
127     if expectedmod:
128         docmodules = fileobj.get_doc_modules()
129         if docmodules:
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()))
139
140 def check_include(fileobj, includedfile, reporter):
141     """Check an #include directive."""
142     otherfile = includedfile.get_file()
143     if includedfile.is_system():
144         if not otherfile:
145             return
146         reporter.code_issue(includedfile,
147                 "includes local file as {0}".format(includedfile))
148     if not otherfile:
149         reporter.code_issue(includedfile,
150                 "includes non-local file as {0}".format(includedfile))
151     if not otherfile:
152         return
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))
171
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")
178
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))
192
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")
202
203 def check_cycles(graph, reporter):
204     """Check cyclic dependencies in a dependency graph.
205
206     The graph parameter provides the graph to check.  It should be an object
207     that has three methods:
208       iternodes():
209         Return the list of nodes in the graph.
210       iteredges(node):
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.
218
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
223
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.
229
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.
233     """
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.
237     dfsstack = []
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.
242     visitstack = []
243     # List of nodes in the DFS recursion stack.
244     currlist = []
245     # Set of nodes in currlist for more efficient searching.
246     currset = set()
247     # Counter for initializing preorder.
248     visit_count = 0
249     # DFS pre-order for nodes: initialized when a node is first encountered
250     # in the search.
251     preorder = dict()
252     # Lowest pre-order index reachable from this node.
253     # Initialized to pre-order, and updated during post-order processing.
254     linkorder = dict()
255     # Set to True for a node when first encountered, and set to False when
256     # a strongly connected component has been processed.
257     in_progress = dict()
258     # The DFS search
259     while dfsstack:
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]]
272                 if children:
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.
276                     while visitstack:
277                         node = visitstack.pop()
278                         in_progress[node] = False
279                         if node == currnode:
280                             break
281                     else:
282                         assert False
283             continue
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)
289             visit_count += 1
290             in_progress[currnode] = True
291         elif not in_progress[currnode]:
292             # Do not enter processed components again.
293             continue
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))
302             else:
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
305                 # find all cycles.
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)
317                                 break
318                         else:
319                             assert False
320
321 class ModuleDependencyGraph(object):
322
323     """Module dependency graph representation for check_cycles().
324
325     In the reported graph, the nodes are gmxtree.Module objects and the edges
326     are gmxtree.ModuleDependency objects.
327     """
328
329     def __init__(self, tree):
330         self._tree = tree
331
332     def iternodes(self):
333         return self._tree.get_modules()
334
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)
339
340     def report_cycle(self, cycle, reporter):
341         if any([x[1].is_cycle_suppressed() for x in cycle]):
342             # TODO: Report unused suppressions.
343             return
344         modulelist = ' -> '.join([x[0].get_name()[7:] for x in cycle])
345         summary = 'module-level cyclic dependency: ' + modulelist
346         reporter.cyclic_issue(summary)
347
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):
353             continue
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)
359             if not is_sorted:
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)
363
364     for classobj in tree.get_classes():
365         check_class(classobj, reporter)
366
367     for memberobj in tree.get_members():
368         check_member(memberobj, reporter, check_ignored)
369
370     check_cycles(ModuleDependencyGraph(tree), reporter)
371
372 def main():
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()
392
393     reporter = Reporter(options.log)
394     if options.ignore:
395         reporter.load_filters(options.ignore)
396
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')
413     tree.load_xml()
414
415     reporter.write_pending()
416
417     if not options.quiet:
418         sys.stderr.write('Checking...\n')
419
420     check_all(tree, reporter, options.check_ignored)
421
422     reporter.write_pending()
423     reporter.report_unused_filters()
424     reporter.close_log()
425
426     if options.exitcode and reporter.had_warnings():
427         sys.exit(1)
428
429 main()