Unify documentation for webpage build
[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 from gmxtree import GromacsTree, DocType
59 from reporter import Reporter
60
61 def check_file(fileobj, reporter):
62     """Check file-level documentation."""
63     if not fileobj.is_documented():
64         # TODO: Add rules for required documentation
65         return
66
67     if fileobj.is_source_file():
68         # TODO: Add rule to exclude examples from this check
69         if fileobj.is_installed():
70             reporter.file_error(fileobj, "source file is installed")
71         if fileobj.get_documentation_type() != DocType.internal:
72             reporter.file_error(fileobj,
73                     "source file documentation appears outside full documentation")
74         elif fileobj.get_api_type() != DocType.internal:
75             reporter.file_error(fileobj, "source file marked as non-internal")
76     elif fileobj.is_test_file() and fileobj.is_installed():
77         reporter.file_error(fileobj, "test file is installed")
78     elif fileobj.is_installed():
79         if fileobj.get_documentation_type() != DocType.public:
80             reporter.file_error(fileobj,
81                     "public header has non-public documentation")
82     elif fileobj.get_documentation_type() == DocType.public:
83         reporter.file_error(fileobj,
84                 "non-installed header has public documentation")
85     elif fileobj.get_api_type() == DocType.public:
86         reporter.file_error(fileobj,
87                 "non-installed header specified as part of public API")
88     elif fileobj.get_documentation_type() < fileobj.get_api_type():
89         reporter.file_error(fileobj,
90                 "API type ({0}) conflicts with documentation visibility ({1})"
91                 .format(fileobj.get_api_type(), fileobj.get_documentation_type()))
92
93     if not fileobj.has_brief_description():
94         reporter.file_error(fileobj,
95                 "is documented, but does not have brief description")
96
97     expectedmod = fileobj.get_expected_module()
98     if expectedmod:
99         docmodules = fileobj.get_doc_modules()
100         if docmodules:
101             for module in docmodules:
102                 if module != expectedmod:
103                     reporter.file_error(fileobj,
104                             "is documented in incorrect module: {0}"
105                             .format(module.get_name()))
106         elif expectedmod.is_documented():
107             reporter.file_error(fileobj,
108                     "is not documented in any module, but {0} exists"
109                     .format(expectedmod.get_name()))
110
111 def check_include(fileobj, includedfile, reporter):
112     """Check an #include directive."""
113     if includedfile.is_system():
114         if includedfile.get_file():
115             reporter.code_issue(includedfile,
116                     "includes local file as {0}".format(includedfile))
117     else:
118         otherfile = includedfile.get_file()
119         if not otherfile:
120             reporter.code_issue(includedfile,
121                     "includes non-local file as {0}".format(includedfile))
122         elif fileobj.is_installed() and not includedfile.is_relative():
123             reporter.code_issue(includedfile,
124                     "installed header includes {0} using non-relative path"
125                     .format(includedfile))
126         if not otherfile:
127             return
128         if fileobj.is_installed() and not otherfile.is_installed():
129             reporter.code_issue(includedfile,
130                     "installed header includes non-installed {0}"
131                     .format(includedfile))
132         filemodule = fileobj.get_module()
133         othermodule = otherfile.get_module()
134         if fileobj.is_documented() and otherfile.is_documented():
135             filetype = fileobj.get_documentation_type()
136             othertype = otherfile.get_documentation_type()
137             if filetype > othertype:
138                 reporter.code_issue(includedfile,
139                         "{0} file includes {1} file {2}"
140                         .format(filetype, othertype, includedfile))
141         check_api = (othermodule and othermodule.is_documented() and
142                 filemodule != othermodule)
143         if check_api and otherfile.get_api_type() < DocType.library:
144             reporter.code_issue(includedfile,
145                     "included file {0} is not documented as exposed outside its module"
146                     .format(includedfile))
147
148 def check_entity(entity, reporter):
149     """Check documentation for a code construct."""
150     if entity.is_documented():
151         if not entity.has_brief_description():
152             reporter.doc_error(entity,
153                     "is documented, but does not have brief description")
154
155 def check_class(classobj, reporter):
156     """Check documentation for a class/struct/union."""
157     check_entity(classobj, reporter)
158     if classobj.is_documented():
159         classtype = classobj.get_documentation_type()
160         filetype = classobj.get_file_documentation_type()
161         if classtype == DocType.public and not classobj.is_in_installed_file():
162             reporter.doc_error(classobj,
163                     "has public documentation, but is not in installed header")
164         elif filetype is not DocType.none and classtype > filetype:
165             reporter.doc_error(classobj,
166                     "is in {0} file(s), but appears in {1} documentation"
167                     .format(filetype, classtype))
168
169 def check_member(member, reporter):
170     """Check documentation for a generic member."""
171     check_entity(member, reporter)
172     if member.is_documented():
173         if not member.is_visible():
174             # TODO: This is triggered by members in anonymous namespaces.
175             reporter.doc_note(member,
176                     "is documented, but is ignored by Doxygen, because its scope is not documented")
177         if member.has_inbody_description():
178             reporter.doc_note(member, "has in-body comments, which are ignored")
179
180 def main():
181     """Run the checking script."""
182     parser = OptionParser()
183     parser.add_option('-S', '--source-root',
184                       help='Source tree root directory')
185     parser.add_option('-B', '--build-root',
186                       help='Build tree root directory')
187     parser.add_option('--installed',
188                       help='Read list of installed files from given file')
189     parser.add_option('-l', '--log',
190                       help='Write issues into a given log file in addition to stderr')
191     parser.add_option('--ignore',
192                       help='Set file with patterns for messages to ignore')
193     parser.add_option('--check-ignored', action='store_true',
194                       help='Check documentation ignored by Doxygen')
195     parser.add_option('-q', '--quiet', action='store_true',
196                       help='Do not write status messages')
197     options, args = parser.parse_args()
198
199     installedlist = []
200     if options.installed:
201         with open(options.installed, 'r') as outfile:
202             for line in outfile:
203                 installedlist.append(line.strip())
204
205     reporter = Reporter(options.log)
206     if options.ignore:
207         reporter.load_filters(options.ignore)
208
209     if not options.quiet:
210         sys.stderr.write('Scanning source tree...\n')
211     tree = GromacsTree(options.source_root, options.build_root, reporter)
212     tree.set_installed_file_list(installedlist)
213     if not options.quiet:
214         sys.stderr.write('Reading source files...\n')
215     tree.scan_files()
216     if not options.quiet:
217         sys.stderr.write('Reading Doxygen XML files...\n')
218     tree.load_xml()
219
220     reporter.write_pending()
221
222     if not options.quiet:
223         sys.stderr.write('Checking...\n')
224
225     for fileobj in tree.get_files():
226         check_file(fileobj, reporter)
227         for includedfile in fileobj.get_includes():
228             check_include(fileobj, includedfile, reporter)
229
230     for classobj in tree.get_classes():
231         check_class(classobj, reporter)
232
233     for memberobj in tree.get_members():
234         if memberobj.is_visible() or options.check_ignored:
235             check_member(memberobj, reporter)
236
237     reporter.write_pending()
238     reporter.report_unused_filters()
239     reporter.close_log()
240
241 main()