verifiers.py revision 11828:36b064696175
1#!/usr/bin/env python2
2#
3# Copyright (c) 2014, 2016 ARM Limited
4# All rights reserved
5#
6# The license below extends only to copyright in the software and shall
7# not be construed as granting a license to any other intellectual
8# property including but not limited to intellectual property relating
9# to a hardware implementation of the functionality of the software
10# licensed hereunder.  You may use the software subject to the license
11# terms below provided that you ensure that this notice is replicated
12# unmodified and in its entirety in all distributions of the software,
13# modified or unmodified, in source code or in binary form.
14#
15# Copyright (c) 2006 The Regents of The University of Michigan
16# Copyright (c) 2007,2011 The Hewlett-Packard Development Company
17# Copyright (c) 2016 Advanced Micro Devices, Inc.
18# All rights reserved.
19#
20# Redistribution and use in source and binary forms, with or without
21# modification, are permitted provided that the following conditions are
22# met: redistributions of source code must retain the above copyright
23# notice, this list of conditions and the following disclaimer;
24# redistributions in binary form must reproduce the above copyright
25# notice, this list of conditions and the following disclaimer in the
26# documentation and/or other materials provided with the distribution;
27# neither the name of the copyright holders nor the names of its
28# contributors may be used to endorse or promote products derived from
29# this software without specific prior written permission.
30#
31# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
32# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
33# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
34# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
35# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
37# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
38# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
39# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
40# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
41# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42#
43# Authors: Nathan Binkert
44#          Steve Reinhardt
45#          Andreas Sandberg
46
47from abc import ABCMeta, abstractmethod
48from difflib import SequenceMatcher
49import inspect
50import os
51import re
52import sys
53
54import style
55import sort_includes
56from region import *
57from file_types import lang_type
58
59
60def safefix(fix_func):
61    """ Decorator for the fix functions of the Verifier class.
62        This function wraps the fix function and creates a backup file
63        just in case there is an error.
64    """
65    def safefix_wrapper(*args, **kwargs):
66        # Check to be sure that this is decorating a function we expect:
67        # a class method with filename as the first argument (after self)
68        assert(os.path.exists(args[1]))
69        self = args[0]
70        assert(is_verifier(self.__class__))
71        filename = args[1]
72
73        # Now, Let's make a backup file.
74        from shutil import copyfile
75        backup_name = filename+'.bak'
76        copyfile(filename, backup_name)
77
78        # Try to apply the fix. If it fails, then we revert the file
79        # Either way, we need to clean up our backup file
80        try:
81            fix_func(*args, **kwargs)
82        except Exception as e:
83            # Restore the original file to the backup file
84            self.ui.write("Error! Restoring the original file.\n")
85            copyfile(backup_name, filename)
86            raise
87        finally:
88            # Clean up the backup file
89            os.remove(backup_name)
90
91    return safefix_wrapper
92
93def _modified_regions(old, new):
94    try:
95        m = SequenceMatcher(a=old, b=new, autojunk=False)
96    except TypeError:
97        # autojunk was introduced in Python 2.7. We need a fallback
98        # mechanism to support old Python versions.
99        m = SequenceMatcher(a=old, b=new)
100    regions = Regions()
101    for tag, i1, i2, j1, j2 in m.get_opcodes():
102        if tag != "equal":
103            regions.extend(Region(i1, i2))
104    return regions
105
106
107class Verifier(object):
108    """Base class for style verifiers
109
110    Verifiers check for style violations and optionally fix such
111    violations. Implementations should either inherit from this class
112    (Verifier) if they need to work on entire files or LineVerifier if
113    they operate on a line-by-line basis.
114
115    Subclasses must define these class attributes:
116      languages = set of strings identifying applicable languages
117      test_name = long descriptive name of test, will be used in
118                  messages such as "error in <foo>" or "invalid <foo>"
119      opt_name = short name used to generate command-line options to
120                 control the test (--fix-<foo>, --ignore-<foo>, etc.)
121
122    """
123
124    __metaclass__ = ABCMeta
125
126    def __init__(self, ui, opts, base=None):
127        self.ui = ui
128        self.base = base
129
130        # opt_name must be defined as a class attribute of derived classes.
131        # Check test-specific opts first as these have precedence.
132        self.opt_fix = opts.get('fix_' + self.opt_name, False)
133        self.opt_ignore = opts.get('ignore_' + self.opt_name, False)
134        self.opt_skip = opts.get('skip_' + self.opt_name, False)
135        # If no test-specific opts were set, then set based on "-all" opts.
136        if not (self.opt_fix or self.opt_ignore or self.opt_skip):
137            self.opt_fix = opts.get('fix_all', False)
138            self.opt_ignore = opts.get('ignore_all', False)
139            self.opt_skip = opts.get('skip_all', False)
140
141    def normalize_filename(self, name):
142        abs_name = os.path.abspath(name)
143        if self.base is None:
144            return abs_name
145
146        abs_base = os.path.abspath(self.base)
147        return os.path.relpath(abs_name, start=abs_base)
148
149    def open(self, filename, mode):
150        try:
151            f = file(filename, mode)
152        except OSError, msg:
153            print 'could not open file %s: %s' % (filename, msg)
154            return None
155
156        return f
157
158    def skip(self, filename):
159        # We never want to handle symlinks, so always skip them: If the
160        # location pointed to is a directory, skip it. If the location is a
161        # file inside the gem5 directory, it will be checked as a file, so
162        # symlink can be skipped. If the location is a file outside gem5, we
163        # don't want to check it anyway.
164        if os.path.islink(filename):
165            return True
166        return lang_type(filename) not in self.languages
167
168    def apply(self, filename, regions=all_regions):
169        """Possibly apply to specified regions of file 'filename'.
170
171        Verifier is skipped if --skip-<test> option was provided or if
172        file is not of an applicable type.  Otherwise file is checked
173        and error messages printed.  Errors are fixed or ignored if
174        the corresponding --fix-<test> or --ignore-<test> options were
175        provided.  If neither, the user is prompted for an action.
176
177        Returns True to abort, False otherwise.
178        """
179        if not (self.opt_skip or self.skip(filename)):
180            errors = self.check(filename, regions)
181            if errors and not self.opt_ignore:
182                if self.opt_fix:
183                    self.fix(filename, regions)
184                else:
185                    result = self.ui.prompt("(a)bort, (i)gnore, or (f)ix?",
186                                            'aif', 'a')
187                    if result == 'f':
188                        self.fix(filename, regions)
189                    elif result == 'a':
190                        return True # abort
191
192        return False
193
194    @abstractmethod
195    def check(self, filename, regions=all_regions, fobj=None, silent=False):
196        """Check specified regions of file 'filename'.
197
198        Given that it is possible that the current contents of the file
199        differ from the file as 'staged to commit', for those cases, and
200        maybe others, the argument fobj should be a file object open and reset
201        with the contents matching what the file would look like after the
202        commit. This is needed keep the messages using 'filename' meaningful.
203
204        The argument silent is useful to prevent output when we run check in
205        the staged file vs the actual file to detect if the user forgot
206        staging fixes to the commit. This way, we prevent reporting errors
207        twice in stderr.
208
209        Line-by-line checks can simply provide a check_line() method
210        that returns True if the line is OK and False if it has an
211        error.  Verifiers that need a multi-line view (like
212        SortedIncludes) must override this entire function.
213
214        Returns a count of errors (0 if none), though actual non-zero
215        count value is not currently used anywhere.
216        """
217        pass
218
219    @abstractmethod
220    def fix(self, filename, regions=all_regions):
221        """Fix specified regions of file 'filename'.
222
223        Line-by-line fixes can simply provide a fix_line() method that
224        returns the fixed line. Verifiers that need a multi-line view
225        (like SortedIncludes) must override this entire function.
226        """
227        pass
228
229class LineVerifier(Verifier):
230    def check(self, filename, regions=all_regions, fobj=None, silent=False):
231        close = False
232        if fobj is None:
233            fobj = self.open(filename, 'r')
234            close = True
235
236        lang = lang_type(filename)
237        assert lang in self.languages
238
239        errors = 0
240        for num,line in enumerate(fobj):
241            if num not in regions:
242                continue
243            line = line.rstrip('\n')
244            if not self.check_line(line, language=lang):
245                if not silent:
246                    self.ui.write("invalid %s in %s:%d\n" % \
247                                  (self.test_name, filename, num + 1))
248                    if self.ui.verbose:
249                        self.ui.write(">>%s<<\n" % line[:-1])
250                errors += 1
251        if close:
252            fobj.close()
253        return errors
254
255    @safefix
256    def fix(self, filename, regions=all_regions):
257        f = self.open(filename, 'r+')
258
259        lang = lang_type(filename)
260        assert lang in self.languages
261
262        lines = list(f)
263
264        f.seek(0)
265        f.truncate()
266
267        for i,line in enumerate(lines):
268            line = line.rstrip('\n')
269            if i in regions:
270                line = self.fix_line(line, language=lang)
271
272            f.write(line)
273            f.write("\n")
274        f.close()
275        self.current_language = None
276
277    @abstractmethod
278    def check_line(self, line, **kwargs):
279        pass
280
281    @abstractmethod
282    def fix_line(self, line, **kwargs):
283        pass
284
285class Whitespace(LineVerifier):
286    """Check whitespace.
287
288    Specifically:
289    - No tabs used for indent
290    - No trailing whitespace
291    """
292
293    languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons',
294                     'make', 'dts'))
295    trail_only = set(('make', 'dts'))
296
297    test_name = 'whitespace'
298    opt_name = 'white'
299
300    _lead = re.compile(r'^([ \t]+)')
301    _trail = re.compile(r'([ \t]+)$')
302
303
304    def skip_lead(self, language):
305        return language in Whitespace.trail_only
306
307    def check_line(self, line, language):
308        if not self.skip_lead(language):
309            match = Whitespace._lead.search(line)
310            if match and match.group(1).find('\t') != -1:
311                return False
312
313        match = Whitespace._trail.search(line)
314        if match:
315            return False
316
317        return True
318
319    def fix_line(self, line, language):
320        if not self.skip_lead(language) and Whitespace._lead.search(line):
321            newline = ''
322            for i,c in enumerate(line):
323                if c == ' ':
324                    newline += ' '
325                elif c == '\t':
326                    newline += ' ' * (style.tabsize - \
327                                      len(newline) % style.tabsize)
328                else:
329                    newline += line[i:]
330                    break
331
332            line = newline
333
334        return line.rstrip()
335
336
337class SortedIncludes(Verifier):
338    """Check for proper sorting of include statements"""
339
340    languages = sort_includes.default_languages
341    test_name = 'include file order'
342    opt_name = 'include'
343
344    def __init__(self, *args, **kwargs):
345        super(SortedIncludes, self).__init__(*args, **kwargs)
346        self.sort_includes = sort_includes.SortIncludes()
347
348    def check(self, filename, regions=all_regions, fobj=None, silent=False):
349        close = False
350        if fobj is None:
351            fobj = self.open(filename, 'r')
352            close = True
353        norm_fname = self.normalize_filename(filename)
354
355        old = [ l.rstrip('\n') for l in fobj.xreadlines() ]
356        if close:
357            fobj.close()
358
359        if len(old) == 0:
360            return 0
361
362        language = lang_type(filename, old[0])
363        new = list(self.sort_includes(old, norm_fname, language))
364
365        modified = _modified_regions(old, new) & regions
366
367        if modified:
368            if not silent:
369                self.ui.write("invalid sorting of includes in %s\n"
370                                % (filename))
371                if self.ui.verbose:
372                    for start, end in modified.regions:
373                        self.ui.write("bad region [%d, %d)\n" % (start, end))
374            return 1
375
376        return 0
377
378    @safefix
379    def fix(self, filename, regions=all_regions):
380        f = self.open(filename, 'r+')
381
382        old = f.readlines()
383        lines = [ l.rstrip('\n') for l in old ]
384        language = lang_type(filename, lines[0])
385        sort_lines = list(self.sort_includes(lines, filename, language))
386        new = ''.join(line + '\n' for line in sort_lines)
387
388        f.seek(0)
389        f.truncate()
390
391        for i,line in enumerate(sort_lines):
392            f.write(line)
393            f.write('\n')
394        f.close()
395
396
397class ControlSpace(LineVerifier):
398    """Check for exactly one space after if/while/for"""
399
400    languages = set(('C', 'C++'))
401    test_name = 'spacing after if/while/for'
402    opt_name = 'control'
403
404    _any_control = re.compile(r'\b(if|while|for)([ \t]*)\(')
405
406    def check_line(self, line, **kwargs):
407        match = ControlSpace._any_control.search(line)
408        return not (match and match.group(2) != " ")
409
410    def fix_line(self, line, **kwargs):
411        new_line = ControlSpace._any_control.sub(r'\1 (', line)
412        return new_line
413
414
415class LineLength(LineVerifier):
416    languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons'))
417    test_name = 'line length'
418    opt_name = 'length'
419
420    def check_line(self, line, **kwargs):
421        return style.normalized_len(line) <= 79
422
423    def fix(self, filename, regions=all_regions, **kwargs):
424        self.ui.write("Warning: cannot automatically fix overly long lines.\n")
425
426    def fix_line(self, line):
427        pass
428
429class ControlCharacters(LineVerifier):
430    languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons'))
431    test_name = 'control character'
432    opt_name = 'ascii'
433
434    valid = ('\n', '\t')
435    invalid = "".join([chr(i) for i in range(0, 0x20) if chr(i) not in valid])
436
437    def check_line(self, line, **kwargs):
438        return self.fix_line(line) == line
439
440    def fix_line(self, line, **kwargs):
441        return line.translate(None, ControlCharacters.invalid)
442
443class BoolCompare(LineVerifier):
444    languages = set(('C', 'C++', 'python'))
445    test_name = 'boolean comparison'
446    opt_name = 'boolcomp'
447
448    regex = re.compile(r'\s*==\s*([Tt]rue|[Ff]alse)\b')
449
450    def check_line(self, line, **kwargs):
451        return self.regex.search(line) == None
452
453    def fix_line(self, line, **kwargs):
454        match = self.regex.search(line)
455        if match:
456            if match.group(1) in ('true', 'True'):
457                line = self.regex.sub('', line)
458            else:
459                self.ui.write("Warning: cannot automatically fix "
460                              "comparisons with false/False.\n")
461        return line
462
463def is_verifier(cls):
464    """Determine if a class is a Verifier that can be instantiated"""
465
466    return inspect.isclass(cls) and issubclass(cls, Verifier) and \
467        not inspect.isabstract(cls)
468
469# list of all verifier classes
470all_verifiers = [ v for n, v in \
471                  inspect.getmembers(sys.modules[__name__], is_verifier) ]
472