9be3cdae66df5993a88f5d36ec25b3abcd7d1940
[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
169     def _write(self, message):
170         """Implement actual message writing."""
171         wholemsg = ''
172         if message.location:
173             wholemsg += str(message.location) + ': '
174         wholemsg += message.message
175         if message.details:
176             wholemsg += '\n    ' + '\n    '.join(message.details)
177         wholemsg += '\n'
178         sys.stderr.write(wholemsg)
179         if self._logfp:
180             self._logfp.write(wholemsg)
181
182     def _report(self, message):
183         """Handle a single reporter message."""
184         if self._quiet:
185             return
186         for filterobj in self._filters:
187             if filterobj.matches(message):
188                 return
189         if not message.location:
190             self._write(message)
191         else:
192             self._messages.append(message)
193
194     def load_filters(self, filterfile):
195         """Load filters for excluding messages from a file."""
196         with open(filterfile, 'r') as fp:
197             for filterline in fp:
198                 filterline = filterline.strip()
199                 if not filterline or filterline.startswith('#'):
200                     continue
201                 self._filters.append(Filter(filterline))
202
203     def write_pending(self):
204         """Write out pending messages in sorted order."""
205         self._messages.sort()
206         for message in self._messages:
207             self._write(message)
208         self._messages = []
209
210     def report_unused_filters(self):
211         """Report filters that did not match any messages."""
212         for filterobj in self._filters:
213             if filterobj.get_match_count() == 0:
214                 # TODO: Consider adding the input filter file as location
215                 text = 'warning: unused filter: ' + filterobj.get_text()
216                 self._write(Message(text))
217
218     def close_log(self):
219         """Close the log file if one exists."""
220         assert not self._messages
221         if self._logfp:
222             self._logfp.close()
223             self._logfp = None
224
225     def xml_assert(self, xmlpath, message):
226         """Report issues in Doxygen XML that violate assumptions in the script."""
227         self._report(Message('warning: ' + message, filename=xmlpath))
228
229     def input_error(self, message):
230         """Report issues in input files."""
231         self._report(Message('error: ' + message))
232
233     def file_error(self, fileobj, message):
234         """Report file-level issues."""
235         self._report(Message('error: ' + message,
236             location=fileobj.get_reporter_location()))
237
238     def code_issue(self, entity, message, details=None):
239         """Report an issue in a code construct (not documentation related)."""
240         self._report(Message('warning: ' + message, details,
241             location=entity.get_reporter_location()))
242
243     def cyclic_issue(self, message, details=None):
244         """Report a cyclic dependency issue."""
245         self._report(Message('warning: ' + message, details))
246
247     def doc_error(self, entity, message):
248         """Report an issue in documentation."""
249         self._report(Message('error: ' + entity.get_name() + ': ' + message,
250             location=entity.get_reporter_location()))
251
252     def doc_note(self, entity, message):
253         """Report a potential issue in documentation."""
254         self._report(Message('note: ' + entity.get_name() + ': ' + message,
255             location=entity.get_reporter_location()))