Include directive sorter
[alexxy/gromacs.git] / docs / doxygen / doxygen-check.py
1 #!/usr/bin/python
2 #
3 # This file is part of the GROMACS molecular simulation package.
4 #
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.
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 Doxygen documentation for issues that Doxygen does not warn about.
37
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.
42
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.
49
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
52 them to the script.
53 """
54
55 import sys
56 from optparse import OptionParser
57
58 import gmxtree
59 from gmxtree import GromacsTree, DocType
60 from reporter import Reporter
61
62 def check_file(fileobj, reporter):
63     """Check file-level issues."""
64     if fileobj.is_source_file() and not fileobj.is_external() and \
65             fileobj.get_relpath().startswith('src/'):
66         includes = fileobj.get_includes()
67         if includes:
68             firstinclude = includes[0].get_file()
69             if not firstinclude or firstinclude.get_name() != "gmxpre.h":
70                 reporter.code_issue(includes[0],
71                                     "does not include \"gmxpre.h\" first")
72         else:
73             reporter.code_issue(fileobj, "does not include \"gmxpre.h\"")
74
75     if not fileobj.is_documented():
76         # TODO: Add rules for required documentation
77         return
78
79     if fileobj.is_source_file():
80         # TODO: Add rule to exclude examples from this check
81         if fileobj.is_installed():
82             reporter.file_error(fileobj, "source file is installed")
83         if fileobj.get_doc_type() != DocType.internal:
84             reporter.file_error(fileobj,
85                     "source file documentation appears outside full documentation")
86         elif fileobj.get_api_type() != DocType.internal:
87             reporter.file_error(fileobj, "source file marked as non-internal")
88     elif fileobj.is_test_file() and fileobj.is_installed():
89         reporter.file_error(fileobj, "test file is installed")
90     elif fileobj.is_installed():
91         if fileobj.get_doc_type() != DocType.public:
92             reporter.file_error(fileobj,
93                     "public header has non-public documentation")
94     elif fileobj.get_doc_type() == DocType.public:
95         reporter.file_error(fileobj,
96                 "non-installed header has public documentation")
97     elif fileobj.get_api_type() == DocType.public:
98         reporter.file_error(fileobj,
99                 "non-installed header specified as part of public API")
100     elif fileobj.get_doc_type() < fileobj.get_api_type():
101         reporter.file_error(fileobj,
102                 "API type ({0}) conflicts with documentation visibility ({1})"
103                 .format(fileobj.get_api_type(), fileobj.get_doc_type()))
104
105     if not fileobj.has_brief_description():
106         reporter.file_error(fileobj,
107                 "is documented, but does not have brief description")
108
109     expectedmod = fileobj.get_expected_module()
110     if expectedmod:
111         docmodules = fileobj.get_doc_modules()
112         if docmodules:
113             for module in docmodules:
114                 if module != expectedmod:
115                     reporter.file_error(fileobj,
116                             "is documented in incorrect module: {0}"
117                             .format(module.get_name()))
118         elif expectedmod.is_documented():
119             reporter.file_error(fileobj,
120                     "is not documented in any module, but {0} exists"
121                     .format(expectedmod.get_name()))
122
123 def check_include(fileobj, includedfile, reporter):
124     """Check an #include directive."""
125     otherfile = includedfile.get_file()
126     if includedfile.is_system():
127         if not otherfile:
128             return
129         reporter.code_issue(includedfile,
130                 "includes local file as {0}".format(includedfile))
131     if not otherfile:
132         reporter.code_issue(includedfile,
133                 "includes non-local file as {0}".format(includedfile))
134     # TODO: Reinstantiate a check once there is clarity on what we want
135     # to enforce.
136     #elif fileobj.is_installed() and not includedfile.is_relative():
137     #    reporter.code_issue(includedfile,
138     #            "installed header includes {0} using non-relative path"
139     #            .format(includedfile))
140     if not otherfile:
141         return
142     if fileobj.is_installed() and not otherfile.is_installed():
143         reporter.code_issue(includedfile,
144                 "installed header includes non-installed {0}"
145                 .format(includedfile))
146     filemodule = fileobj.get_module()
147     othermodule = otherfile.get_module()
148     if fileobj.is_documented() and otherfile.is_documented():
149         filetype = fileobj.get_doc_type()
150         othertype = otherfile.get_doc_type()
151         if filetype > othertype:
152             reporter.code_issue(includedfile,
153                     "{0} file includes {1} file {2}"
154                     .format(filetype, othertype, includedfile))
155     check_api = (otherfile.api_type_is_reliable() and filemodule != othermodule)
156     if check_api and otherfile.get_api_type() < DocType.library:
157         reporter.code_issue(includedfile,
158                 "included file {0} is not documented as exposed outside its module"
159                 .format(includedfile))
160
161 def check_entity(entity, reporter):
162     """Check documentation for a code construct."""
163     if entity.is_documented():
164         if not entity.has_brief_description():
165             reporter.doc_error(entity,
166                     "is documented, but does not have brief description")
167
168 def check_class(classobj, reporter):
169     """Check documentation for a class/struct/union."""
170     check_entity(classobj, reporter)
171     if classobj.is_documented():
172         classtype = classobj.get_doc_type()
173         filetype = classobj.get_file_doc_type()
174         if classtype == DocType.public and not classobj.is_in_installed_file():
175             reporter.doc_error(classobj,
176                     "has public documentation, but is not in installed header")
177         elif filetype is not DocType.none and classtype > filetype:
178             reporter.doc_error(classobj,
179                     "is in {0} file(s), but appears in {1} documentation"
180                     .format(filetype, classtype))
181
182 def check_member(member, reporter, check_ignored):
183     """Check documentation for a generic member."""
184     check_entity(member, reporter)
185     if member.is_documented():
186         if check_ignored and not member.is_visible():
187             reporter.doc_note(member,
188                     "is documented, but is ignored by Doxygen, because its scope is not documented")
189         if member.has_inbody_description():
190             reporter.doc_note(member, "has in-body comments, which are ignored")
191
192 def check_cycles(graph, reporter):
193     """Check cyclic dependencies in a dependency graph.
194
195     The graph parameter provides the graph to check.  It should be an object
196     that has three methods:
197       iternodes():
198         Return the list of nodes in the graph.
199       iteredges(node):
200         Return the list of edges from a given node.
201         The list should contain (node, edge) pairs, where node is an object
202         returned by iternodes() and edge is any object.
203       report_cycle(cycle, reporter):
204         Process a found cycle. cycle contains a list of (node, edge) pairs
205         that describe the cycle.  edge is the edge object that leads _to_
206         the node in the cycle.
207
208     This is implemented using an extended DFS-based strongly connected
209     component (SCC) search, written using a stack instead of recursion.
210     The base algorithm is Tarjan's SCC search:
211       http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
212
213     Each back edge that is encountered during the search is reported as a
214     cycle.  Additionally, if a cross edge is encountered that is within the
215     current SCC, the target node and all its children in the current SCC will
216     be visited again to find all cycles.  All steps except cycle detection are
217     omitted for such re-traversal.
218
219     To avoid duplicates from cycles that do not include all nodes in an SCC,
220     a cycle is only reported if the target of the back edge is still active
221     in the search, i.e., all edges from it have not yet been traversed.
222     """
223     # The DFS stack; next node is always popped from the end.
224     # Stores (node, edge) pairs.
225     # edge is None for start nodes and for post-order processing.
226     dfsstack = []
227     for node in graph.iternodes():
228         dfsstack.append((node, None))
229     # Stack of visited nodes that have not yet been assigned to a strongly
230     # connected component.
231     visitstack = []
232     # List of nodes in the DFS recursion stack.
233     currlist = []
234     # Set of nodes in currlist for more efficient searching.
235     currset = set()
236     # Counter for initializing preorder.
237     visit_count = 0
238     # DFS pre-order for nodes: initialized when a node is first encountered
239     # in the search.
240     preorder = dict()
241     # Lowest pre-order index reachable from this node.
242     # Initialized to pre-order, and updated during post-order processing.
243     linkorder = dict()
244     # Set to True for a node when first encountered, and set to False when
245     # a strongly connected component has been processed.
246     in_progress = dict()
247     # The DFS search
248     while dfsstack:
249         currnode, curredge = dfsstack.pop()
250         # curredge is None if this is a start node or post-order traversal.
251         # currlist is empty if this is a start node.
252         if curredge is None and currlist:
253             # All children visited: post-order processing.
254             done = currlist.pop()[0]
255             assert done == currnode
256             currset.remove(currnode)
257             # If this is the first time this node is encountered, fill
258             # linkorder and check for strongly connected components.
259             if linkorder[currnode] == preorder[currnode]:
260                 children = [x for x, dummy in graph.iteredges(currnode) if in_progress[x]]
261                 if children:
262                     linkorder[currnode] = min([linkorder[x] for x in children])
263                 if preorder[currnode] <= linkorder[currnode]:
264                     # This is a root of a strongly connected component.
265                     while visitstack:
266                         node = visitstack.pop()
267                         in_progress[node] = False
268                         if node == currnode:
269                             break
270                     else:
271                         assert False
272             continue
273         if currnode not in preorder:
274             # First encounter of this node: pre-order processing.
275             preorder[currnode] = visit_count
276             linkorder[currnode] = visit_count
277             visitstack.append(currnode)
278             visit_count += 1
279             in_progress[currnode] = True
280         elif not in_progress[currnode]:
281             # Do not enter processed components again.
282             continue
283         currlist.append((currnode, curredge))
284         currset.add(currnode)
285         # add entry for post-order traversal
286         dfsstack.append((currnode, None))
287         for nextnode, edge in graph.iteredges(currnode):
288             if nextnode not in preorder:
289                 # Not seen previously: push
290                 dfsstack.append((nextnode, edge))
291             else:
292                 # If an already visited node is in the same component, it is
293                 # either part of a cycle, or we need to traverse it again to
294                 # find all cycles.
295                 if in_progress[nextnode]:
296                     if nextnode not in currset:
297                         dfsstack.append((nextnode, edge))
298                     # Only report cycles to nodes that haven't been processed
299                     # yet to avoid duplicates.
300                     elif linkorder[nextnode] == preorder[nextnode]:
301                         for index in xrange(len(currlist)):
302                             if currlist[index][0] == nextnode:
303                                 cycle = [(nextnode, edge)]
304                                 cycle.extend(currlist[index+1:])
305                                 graph.report_cycle(cycle, reporter)
306                                 break
307                         else:
308                             assert False
309
310 class ModuleDependencyGraph(object):
311
312     """Module dependency graph representation for check_cycles().
313
314     In the reported graph, the nodes are gmxtree.Module objects and the edges
315     are gmxtree.ModuleDependency objects.
316     """
317
318     def __init__(self, tree):
319         self._tree = tree
320
321     def iternodes(self):
322         for module in self._tree.get_modules():
323             if module.get_name() != 'module_testutils':
324                 yield module
325
326     def iteredges(self, module):
327         for dependency in module.get_dependencies():
328             if dependency.get_other_module().get_name() != 'module_testutils':
329                 yield (dependency.get_other_module(), dependency)
330
331     def report_cycle(self, cycle, reporter):
332         if any([x[1].is_cycle_suppressed() for x in cycle]):
333             # TODO: Report unused suppressions.
334             return
335         modulelist = ' -> '.join([x[0].get_name()[7:] for x in cycle])
336         summary = 'module-level cyclic dependency: ' + modulelist
337         reporter.cyclic_issue(summary)
338
339 def main():
340     """Run the checking script."""
341     parser = OptionParser()
342     parser.add_option('-S', '--source-root',
343                       help='Source tree root directory')
344     parser.add_option('-B', '--build-root',
345                       help='Build tree root directory')
346     parser.add_option('-l', '--log',
347                       help='Write issues into a given log file in addition to stderr')
348     parser.add_option('--ignore',
349                       help='Set file with patterns for messages to ignore')
350     parser.add_option('--ignore-cycles',
351                       help='Set file with module dependencies to ignore in cycles')
352     parser.add_option('--check-ignored', action='store_true',
353                       help='Issue notes for comments ignored by Doxygen')
354     parser.add_option('-q', '--quiet', action='store_true',
355                       help='Do not write status messages')
356     parser.add_option('--exitcode', action='store_true',
357                       help='Return non-zero exit code if there are warnings')
358     options, args = parser.parse_args()
359
360     reporter = Reporter(options.log)
361     if options.ignore:
362         reporter.load_filters(options.ignore)
363
364     if not options.quiet:
365         sys.stderr.write('Scanning source tree...\n')
366     tree = GromacsTree(options.source_root, options.build_root, reporter)
367     tree.load_installed_file_list()
368     if not options.quiet:
369         sys.stderr.write('Reading source files...\n')
370     tree.scan_files()
371     if options.ignore_cycles:
372         tree.load_cycle_suppression_list(options.ignore_cycles)
373     if not options.quiet:
374         sys.stderr.write('Reading Doxygen XML files...\n')
375     tree.load_xml()
376
377     reporter.write_pending()
378
379     if not options.quiet:
380         sys.stderr.write('Checking...\n')
381
382     for fileobj in tree.get_files():
383         if isinstance(fileobj, gmxtree.GeneratorSourceFile):
384             continue
385         check_file(fileobj, reporter)
386         for includedfile in fileobj.get_includes():
387             check_include(fileobj, includedfile, reporter)
388
389     for classobj in tree.get_classes():
390         check_class(classobj, reporter)
391
392     for memberobj in tree.get_members():
393         check_member(memberobj, reporter, options.check_ignored)
394
395     check_cycles(ModuleDependencyGraph(tree), reporter)
396
397     reporter.write_pending()
398     reporter.report_unused_filters()
399     reporter.close_log()
400
401     if options.exitcode and reporter.had_warnings():
402         sys.exit(1)
403
404 main()