Make doc-check fail the build on warnings
[alexxy/gromacs.git] / docs / doxygen / reporter.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 import sys
37
38 from fnmatch import fnmatch
39
40 """Central issue reporting implementation.
41
42 This module implements a Reporter class that is used by other Python modules in
43 this directory to report issues.  This allows central customization of the
44 output format, and also a central implementation for redirecting/copying
45 the output into a log file.  This class also implements sorting for the
46 messages such that all issues from a single file are reported next to each
47 other in the output, as well as filtering to make it possible to suppress
48 certain messages.
49 """
50
51 class Location(object):
52
53     """Location for a reported message."""
54
55     def __init__(self, filename, line):
56         """Create a location with the given file and line number.
57
58         One or both of the parameters can be None, but filename should be
59         specified if line is.
60         """
61         self.filename = filename
62         self.line = line
63
64     def __nonzero__(self):
65         """Make empty locations False in boolean context."""
66         return self.filename is not None
67
68     def __str__(self):
69         """Format the location as a string."""
70         if self.line:
71             return '{0}:{1}'.format(self.filename, self.line)
72         elif self.filename:
73             return self.filename
74         else:
75             return '<unknown>'
76
77     def __cmp__(self, other):
78         """Sort locations based on file name and line number."""
79         result = cmp(self.filename, other.filename)
80         if not self.filename or result != 0:
81             return result
82         return cmp(self.line, other.line)
83
84 class Message(object):
85
86     """Single reported message.
87
88     This class stores the contents of a reporter message for later output to
89     allow sorting the output messages reasonably by the reported location.
90     """
91
92     def __init__(self, message, details=None, filename=None, location=None):
93         """Create a message object.
94
95         The message parameter provides the actual text, while optional details
96         provides a list of extra lines that provide context information for the
97         error.  filename and location provide two alternative ways of
98         specifying the location of the issue:
99          - if filename is provided, the issue is reported in that file, without
100            a line number
101          - if location is provided, it should be a Location instance
102         """
103         if filename:
104             self.location = Location(filename, None)
105         elif location:
106             self.location = location
107         else:
108             self.location = Location(None, None)
109         self.message = message
110         self.details = details
111
112     def __cmp__(self, other):
113         """Sort messages based on file name and line number."""
114         return cmp(self.location, other.location)
115
116 class Filter(object):
117
118     """Filter expression to exclude messages."""
119
120     def __init__(self, filterline):
121         """Initialize a filter from a line in a filter file."""
122         self._orgline = filterline
123         filepattern, text = filterline.split(':', 1)
124         if filepattern == '*':
125             self._filematcher = lambda x: x is not None
126         elif filepattern:
127             self._filematcher = lambda x: x and fnmatch(x, '*/' + filepattern)
128         else:
129             self._filematcher = lambda x: x is None
130         self._textpattern = text.strip()
131         self._count = 0
132
133     def matches(self, message):
134         """Check whether the filter matches a message."""
135         if not self._filematcher(message.location.filename):
136             return False
137         if not fnmatch(message.message, self._textpattern):
138             return False
139         self._count += 1
140         return True
141
142     def get_match_count(self):
143         """Return the number of times this filter has matched."""
144         return self._count
145
146     def get_text(self):
147         """Return original line used to specify the filter."""
148         return self._orgline
149
150 class Reporter(object):
151
152     """Collect and write out issues found by checker scripts."""
153
154     def __init__(self, logfile=None, quiet=False):
155         """Initialize the reporter.
156
157         If logfile is set to a file name, all issues will be written to this
158         file in addition to stderr.
159
160         If quiet is set to True, the reporter will suppress all output.
161         """
162         self._logfp = None
163         if logfile:
164             self._logfp = open(logfile, 'w')
165         self._messages = []
166         self._filters = []
167         self._quiet = quiet
168         self._had_warnings = False
169
170     def _write(self, message):
171         """Implement actual message writing."""
172         wholemsg = ''
173         if message.location:
174             wholemsg += str(message.location) + ': '
175         wholemsg += message.message
176         if message.details:
177             wholemsg += '\n    ' + '\n    '.join(message.details)
178         wholemsg += '\n'
179         sys.stderr.write(wholemsg)
180         if self._logfp:
181             self._logfp.write(wholemsg)
182         self._had_warnings = True
183
184     def _report(self, message):
185         """Handle a single reporter message."""
186         if self._quiet:
187             return
188         for filterobj in self._filters:
189             if filterobj.matches(message):
190                 return
191         if not message.location:
192             self._write(message)
193         else:
194             self._messages.append(message)
195
196     def load_filters(self, filterfile):
197         """Load filters for excluding messages from a file."""
198         with open(filterfile, 'r') as fp:
199             for filterline in fp:
200                 filterline = filterline.strip()
201                 if not filterline or filterline.startswith('#'):
202                     continue
203                 self._filters.append(Filter(filterline))
204
205     def write_pending(self):
206         """Write out pending messages in sorted order."""
207         self._messages.sort()
208         for message in self._messages:
209             self._write(message)
210         self._messages = []
211
212     def report_unused_filters(self):
213         """Report filters that did not match any messages."""
214         for filterobj in self._filters:
215             if filterobj.get_match_count() == 0:
216                 # TODO: Consider adding the input filter file as location
217                 text = 'warning: unused filter: ' + filterobj.get_text()
218                 self._write(Message(text))
219
220     def had_warnings(self):
221         """Return true if any warnings have been reported."""
222         return self._had_warnings
223
224     def close_log(self):
225         """Close the log file if one exists."""
226         assert not self._messages
227         if self._logfp:
228             self._logfp.close()
229             self._logfp = None
230
231     def xml_assert(self, xmlpath, message):
232         """Report issues in Doxygen XML that violate assumptions in the script."""
233         self._report(Message('warning: ' + message, filename=xmlpath))
234
235     def input_error(self, message):
236         """Report issues in input files."""
237         self._report(Message('error: ' + message))
238
239     def file_error(self, fileobj, message):
240         """Report file-level issues."""
241         self._report(Message('error: ' + message,
242             location=fileobj.get_reporter_location()))
243
244     def code_issue(self, entity, message, details=None):
245         """Report an issue in a code construct (not documentation related)."""
246         self._report(Message('warning: ' + message, details,
247             location=entity.get_reporter_location()))
248
249     def cyclic_issue(self, message, details=None):
250         """Report a cyclic dependency issue."""
251         self._report(Message('warning: ' + message, details))
252
253     def doc_error(self, entity, message):
254         """Report an issue in documentation."""
255         self._report(Message('error: ' + entity.get_name() + ': ' + message,
256             location=entity.get_reporter_location()))
257
258     def doc_note(self, entity, message):
259         """Report a potential issue in documentation."""
260         self._report(Message('note: ' + entity.get_name() + ': ' + message,
261             location=entity.get_reporter_location()))