verifiers.py revision 11404:72b399971cbc
19983Sstever@gmail.com#!/usr/bin/env python
29983Sstever@gmail.com#
39983Sstever@gmail.com# Copyright (c) 2014, 2016 ARM Limited
49983Sstever@gmail.com# All rights reserved
59983Sstever@gmail.com#
69983Sstever@gmail.com# The license below extends only to copyright in the software and shall
79983Sstever@gmail.com# not be construed as granting a license to any other intellectual
89983Sstever@gmail.com# property including but not limited to intellectual property relating
99983Sstever@gmail.com# to a hardware implementation of the functionality of the software
109983Sstever@gmail.com# licensed hereunder.  You may use the software subject to the license
119983Sstever@gmail.com# terms below provided that you ensure that this notice is replicated
129983Sstever@gmail.com# unmodified and in its entirety in all distributions of the software,
139983Sstever@gmail.com# modified or unmodified, in source code or in binary form.
149983Sstever@gmail.com#
159983Sstever@gmail.com# Copyright (c) 2006 The Regents of The University of Michigan
169983Sstever@gmail.com# Copyright (c) 2007,2011 The Hewlett-Packard Development Company
179983Sstever@gmail.com# Copyright (c) 2016 Advanced Micro Devices, Inc.
189983Sstever@gmail.com# All rights reserved.
199983Sstever@gmail.com#
209983Sstever@gmail.com# Redistribution and use in source and binary forms, with or without
219983Sstever@gmail.com# modification, are permitted provided that the following conditions are
229983Sstever@gmail.com# met: redistributions of source code must retain the above copyright
239983Sstever@gmail.com# notice, this list of conditions and the following disclaimer;
249983Sstever@gmail.com# redistributions in binary form must reproduce the above copyright
259983Sstever@gmail.com# notice, this list of conditions and the following disclaimer in the
269983Sstever@gmail.com# documentation and/or other materials provided with the distribution;
279983Sstever@gmail.com# neither the name of the copyright holders nor the names of its
289983Sstever@gmail.com# contributors may be used to endorse or promote products derived from
299983Sstever@gmail.com# this software without specific prior written permission.
309983Sstever@gmail.com#
319983Sstever@gmail.com# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
329983Sstever@gmail.com# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
339983Sstever@gmail.com# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
349983Sstever@gmail.com# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
359983Sstever@gmail.com# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
369983Sstever@gmail.com# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
379983Sstever@gmail.com# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
389983Sstever@gmail.com# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
399983Sstever@gmail.com# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
409983Sstever@gmail.com# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
419983Sstever@gmail.com# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
429983Sstever@gmail.com#
439983Sstever@gmail.com# Authors: Nathan Binkert
449983Sstever@gmail.com#          Steve Reinhardt
459983Sstever@gmail.com#          Andreas Sandberg
469983Sstever@gmail.com
479983Sstever@gmail.comfrom abc import ABCMeta, abstractmethod
489983Sstever@gmail.comfrom difflib import SequenceMatcher
499983Sstever@gmail.comimport inspect
509983Sstever@gmail.comimport os
519983Sstever@gmail.comimport re
529983Sstever@gmail.comimport sys
539983Sstever@gmail.com
549983Sstever@gmail.comimport style
559983Sstever@gmail.comimport sort_includes
569983Sstever@gmail.comfrom region import *
579983Sstever@gmail.comfrom file_types import lang_type
589983Sstever@gmail.com
599983Sstever@gmail.comdef _modified_regions(old, new):
609983Sstever@gmail.com    m = SequenceMatcher(a=old, b=new, autojunk=False)
619983Sstever@gmail.com
629983Sstever@gmail.com    regions = Regions()
639983Sstever@gmail.com    for tag, i1, i2, j1, j2 in m.get_opcodes():
649983Sstever@gmail.com        if tag != "equal":
659983Sstever@gmail.com            regions.extend(Region(i1, i2))
669983Sstever@gmail.com    return regions
679983Sstever@gmail.com
689983Sstever@gmail.com
699983Sstever@gmail.comclass Verifier(object):
709983Sstever@gmail.com    """Base class for style verifiers
719983Sstever@gmail.com
729983Sstever@gmail.com    Verifiers check for style violations and optionally fix such
739983Sstever@gmail.com    violations. Implementations should either inherit from this class
749983Sstever@gmail.com    (Verifier) if they need to work on entire files or LineVerifier if
759983Sstever@gmail.com    they operate on a line-by-line basis.
769983Sstever@gmail.com
779983Sstever@gmail.com    Subclasses must define these class attributes:
789983Sstever@gmail.com      languages = set of strings identifying applicable languages
799983Sstever@gmail.com      test_name = long descriptive name of test, will be used in
809983Sstever@gmail.com                  messages such as "error in <foo>" or "invalid <foo>"
819983Sstever@gmail.com      opt_name = short name used to generate command-line options to
829983Sstever@gmail.com                 control the test (--fix-<foo>, --ignore-<foo>, etc.)
839983Sstever@gmail.com
849983Sstever@gmail.com    """
859983Sstever@gmail.com
869983Sstever@gmail.com    __metaclass__ = ABCMeta
879983Sstever@gmail.com
889983Sstever@gmail.com    def __init__(self, ui, opts, base=None):
899983Sstever@gmail.com        self.ui = ui
909983Sstever@gmail.com        self.base = base
919983Sstever@gmail.com
929983Sstever@gmail.com        # opt_name must be defined as a class attribute of derived classes.
939983Sstever@gmail.com        # Check test-specific opts first as these have precedence.
949983Sstever@gmail.com        self.opt_fix = opts.get('fix_' + self.opt_name, False)
959983Sstever@gmail.com        self.opt_ignore = opts.get('ignore_' + self.opt_name, False)
969983Sstever@gmail.com        self.opt_skip = opts.get('skip_' + self.opt_name, False)
979983Sstever@gmail.com        # If no test-specific opts were set, then set based on "-all" opts.
989983Sstever@gmail.com        if not (self.opt_fix or self.opt_ignore or self.opt_skip):
999983Sstever@gmail.com            self.opt_fix = opts.get('fix_all', False)
1009983Sstever@gmail.com            self.opt_ignore = opts.get('ignore_all', False)
1019983Sstever@gmail.com            self.opt_skip = opts.get('skip_all', False)
1029983Sstever@gmail.com
1039983Sstever@gmail.com    def normalize_filename(self, name):
1049983Sstever@gmail.com        abs_name = os.path.abspath(name)
1059983Sstever@gmail.com        if self.base is None:
1069983Sstever@gmail.com            return abs_name
1079983Sstever@gmail.com
1089983Sstever@gmail.com        abs_base = os.path.abspath(self.base)
1099983Sstever@gmail.com        return os.path.relpath(abs_name, start=abs_base)
1109983Sstever@gmail.com
1119983Sstever@gmail.com    def open(self, filename, mode):
1129983Sstever@gmail.com        try:
1139983Sstever@gmail.com            f = file(filename, mode)
1149983Sstever@gmail.com        except OSError, msg:
1159983Sstever@gmail.com            print 'could not open file %s: %s' % (filename, msg)
1169983Sstever@gmail.com            return None
1179983Sstever@gmail.com
1189983Sstever@gmail.com        return f
1199983Sstever@gmail.com
1209983Sstever@gmail.com    def skip(self, filename):
1219983Sstever@gmail.com        # We never want to handle symlinks, so always skip them: If the location
1229983Sstever@gmail.com        # pointed to is a directory, skip it. If the location is a file inside
1239983Sstever@gmail.com        # the gem5 directory, it will be checked as a file, so symlink can be
1249983Sstever@gmail.com        # skipped. If the location is a file outside gem5, we don't want to
1259983Sstever@gmail.com        # check it anyway.
1269983Sstever@gmail.com        if os.path.islink(filename):
1279983Sstever@gmail.com            return True
1289983Sstever@gmail.com        return lang_type(filename) not in self.languages
1299983Sstever@gmail.com
1309983Sstever@gmail.com    def apply(self, filename, regions=all_regions):
1319983Sstever@gmail.com        """Possibly apply to specified regions of file 'filename'.
1329983Sstever@gmail.com
1339983Sstever@gmail.com        Verifier is skipped if --skip-<test> option was provided or if
1349983Sstever@gmail.com        file is not of an applicable type.  Otherwise file is checked
1359983Sstever@gmail.com        and error messages printed.  Errors are fixed or ignored if
1369983Sstever@gmail.com        the corresponding --fix-<test> or --ignore-<test> options were
1379983Sstever@gmail.com        provided.  If neither, the user is prompted for an action.
1389983Sstever@gmail.com
1399983Sstever@gmail.com        Returns True to abort, False otherwise.
1409983Sstever@gmail.com        """
1419983Sstever@gmail.com        if not (self.opt_skip or self.skip(filename)):
1429983Sstever@gmail.com            errors = self.check(filename, regions)
1439983Sstever@gmail.com            if errors and not self.opt_ignore:
1449983Sstever@gmail.com                if self.opt_fix:
1459983Sstever@gmail.com                    self.fix(filename, regions)
1469983Sstever@gmail.com                else:
1479983Sstever@gmail.com                    result = self.ui.prompt("(a)bort, (i)gnore, or (f)ix?",
1489983Sstever@gmail.com                                            'aif', 'a')
1499983Sstever@gmail.com                    if result == 'f':
1509983Sstever@gmail.com                        self.fix(filename, regions)
1519983Sstever@gmail.com                    elif result == 'a':
1529983Sstever@gmail.com                        return True # abort
1539983Sstever@gmail.com
1549983Sstever@gmail.com        return False
1559983Sstever@gmail.com
1569983Sstever@gmail.com    @abstractmethod
1579983Sstever@gmail.com    def check(self, filename, regions=all_regions):
1589983Sstever@gmail.com        """Check specified regions of file 'filename'.
1599983Sstever@gmail.com
1609983Sstever@gmail.com        Line-by-line checks can simply provide a check_line() method
1619983Sstever@gmail.com        that returns True if the line is OK and False if it has an
1629983Sstever@gmail.com        error.  Verifiers that need a multi-line view (like
1639983Sstever@gmail.com        SortedIncludes) must override this entire function.
1649983Sstever@gmail.com
1659983Sstever@gmail.com        Returns a count of errors (0 if none), though actual non-zero
1669983Sstever@gmail.com        count value is not currently used anywhere.
1679983Sstever@gmail.com        """
1689983Sstever@gmail.com        pass
1699983Sstever@gmail.com
1709983Sstever@gmail.com    @abstractmethod
1719983Sstever@gmail.com    def fix(self, filename, regions=all_regions):
1729983Sstever@gmail.com        """Fix specified regions of file 'filename'.
1739983Sstever@gmail.com
1749983Sstever@gmail.com        Line-by-line fixes can simply provide a fix_line() method that
1759983Sstever@gmail.com        returns the fixed line. Verifiers that need a multi-line view
1769983Sstever@gmail.com        (like SortedIncludes) must override this entire function.
1779983Sstever@gmail.com        """
1789983Sstever@gmail.com        pass
1799983Sstever@gmail.com
1809983Sstever@gmail.comclass LineVerifier(Verifier):
1819983Sstever@gmail.com    def check(self, filename, regions=all_regions):
1829983Sstever@gmail.com        f = self.open(filename, 'r')
1839983Sstever@gmail.com
1849983Sstever@gmail.com        errors = 0
1859983Sstever@gmail.com        for num,line in enumerate(f):
1869983Sstever@gmail.com            if num not in regions:
1879983Sstever@gmail.com                continue
1889983Sstever@gmail.com            line = line.rstrip('\n')
1899983Sstever@gmail.com            if not self.check_line(line):
1909983Sstever@gmail.com                self.ui.write("invalid %s in %s:%d\n" % \
1919983Sstever@gmail.com                              (self.test_name, filename, num + 1))
1929983Sstever@gmail.com                if self.ui.verbose:
1939983Sstever@gmail.com                    self.ui.write(">>%s<<\n" % line[:-1])
1949983Sstever@gmail.com                errors += 1
1959983Sstever@gmail.com        return errors
1969983Sstever@gmail.com
1979983Sstever@gmail.com    def fix(self, filename, regions=all_regions):
1989983Sstever@gmail.com        f = self.open(filename, 'r+')
1999983Sstever@gmail.com
2009983Sstever@gmail.com        lines = list(f)
2019983Sstever@gmail.com
2029983Sstever@gmail.com        f.seek(0)
2039983Sstever@gmail.com        f.truncate()
2049983Sstever@gmail.com
2059983Sstever@gmail.com        for i,line in enumerate(lines):
2069983Sstever@gmail.com            line = line.rstrip('\n')
2079983Sstever@gmail.com            if i in regions:
2089983Sstever@gmail.com                line = self.fix_line(line)
2099983Sstever@gmail.com
2109983Sstever@gmail.com            f.write(line)
2119983Sstever@gmail.com            f.write("\n")
2129983Sstever@gmail.com        f.close()
2139983Sstever@gmail.com
2149983Sstever@gmail.com
2159983Sstever@gmail.com    @abstractmethod
2169983Sstever@gmail.com    def check_line(self, line):
2179983Sstever@gmail.com        pass
2189983Sstever@gmail.com
2199983Sstever@gmail.com    @abstractmethod
2209983Sstever@gmail.com    def fix_line(self, line):
2219983Sstever@gmail.com        pass
2229983Sstever@gmail.com
2239983Sstever@gmail.comclass Whitespace(LineVerifier):
2249983Sstever@gmail.com    """Check whitespace.
2259983Sstever@gmail.com
2269983Sstever@gmail.com    Specifically:
2279983Sstever@gmail.com    - No tabs used for indent
2289983Sstever@gmail.com    - No trailing whitespace
2299983Sstever@gmail.com    """
2309983Sstever@gmail.com
231    languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons'))
232    test_name = 'whitespace'
233    opt_name = 'white'
234
235    _lead = re.compile(r'^([ \t]+)')
236    _trail = re.compile(r'([ \t]+)$')
237
238    def check_line(self, line):
239        match = Whitespace._lead.search(line)
240        if match and match.group(1).find('\t') != -1:
241            return False
242
243        match = Whitespace._trail.search(line)
244        if match:
245            return False
246
247        return True
248
249    def fix_line(self, line):
250        if Whitespace._lead.search(line):
251            newline = ''
252            for i,c in enumerate(line):
253                if c == ' ':
254                    newline += ' '
255                elif c == '\t':
256                    newline += ' ' * (tabsize - len(newline) % tabsize)
257                else:
258                    newline += line[i:]
259                    break
260
261            line = newline
262
263        return line.rstrip() + '\n'
264
265
266class SortedIncludes(Verifier):
267    """Check for proper sorting of include statements"""
268
269    languages = sort_includes.default_languages
270    test_name = 'include file order'
271    opt_name = 'include'
272
273    def __init__(self, *args, **kwargs):
274        super(SortedIncludes, self).__init__(*args, **kwargs)
275        self.sort_includes = sort_includes.SortIncludes()
276
277    def check(self, filename, regions=all_regions):
278        f = self.open(filename, 'r')
279        norm_fname = self.normalize_filename(filename)
280
281        old = [ l.rstrip('\n') for l in f.xreadlines() ]
282        f.close()
283
284        if len(old) == 0:
285            return 0
286
287        language = lang_type(filename, old[0])
288        new = list(self.sort_includes(old, norm_fname, language))
289
290        modified = _modified_regions(old, new) & regions
291
292        if modified:
293            self.ui.write("invalid sorting of includes in %s\n" % (filename))
294            if self.ui.verbose:
295                for start, end in modified.regions:
296                    self.ui.write("bad region [%d, %d)\n" % (start, end))
297            return 1
298
299        return 0
300
301    def fix(self, filename, regions=all_regions):
302        f = self.open(filename, 'r+')
303
304        old = f.readlines()
305        lines = [ l.rstrip('\n') for l in old ]
306        language = lang_type(filename, lines[0])
307        sort_lines = list(self.sort_includes(lines, filename, language))
308        new = ''.join(line + '\n' for line in sort_lines)
309
310        f.seek(0)
311        f.truncate()
312
313        for i,line in enumerate(sort_lines):
314            f.write(line)
315            f.write('\n')
316        f.close()
317
318
319class ControlSpace(LineVerifier):
320    """Check for exactly one space after if/while/for"""
321
322    languages = set(('C', 'C++'))
323    test_name = 'spacing after if/while/for'
324    opt_name = 'control'
325
326    _any_control = re.compile(r'\b(if|while|for)([ \t]*)\(')
327
328    def check_line(self, line):
329        match = ControlSpace._any_control.search(line)
330        return not (match and match.group(2) != " ")
331
332    def fix_line(self, line):
333        new_line = _any_control.sub(r'\1 (', line)
334        return new_line
335
336
337class LineLength(LineVerifier):
338    languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons'))
339    test_name = 'line length'
340    opt_name = 'length'
341
342    def check_line(self, line):
343        return style.normalized_len(line) <= 78
344
345    def fix(self, filename, regions=all_regions):
346        self.ui.write("Warning: cannot automatically fix overly long lines.\n")
347
348    def fix_line(self, line):
349        pass
350
351class ControlCharacters(LineVerifier):
352    languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons'))
353    test_name = 'control character'
354    opt_name = 'ascii'
355
356    valid = ('\n', '\t')
357    invalid = "".join([chr(i) for i in range(0, 0x20) if chr(i) not in valid])
358
359    def check_line(self, line):
360        return self.fix_line(line) == line
361
362    def fix_line(self, line):
363        return line.translate(None, ControlCharacters.invalid)
364
365class BoolCompare(LineVerifier):
366    languages = set(('C', 'C++', 'python'))
367    test_name = 'boolean comparison'
368    opt_name = 'boolcomp'
369
370    regex = re.compile(r'\s*==\s*([Tt]rue|[Ff]alse)\b')
371
372    def check_line(self, line):
373        return self.regex.search(line) == None
374
375    def fix_line(self, line):
376        match = self.regex.search(line)
377        if match:
378            if match.group(1) in ('true', 'True'):
379                line = self.regex.sub('', line)
380            else:
381                self.ui.write("Warning: cannot automatically fix "
382                              "comparisons with false/False.\n")
383        return line
384
385def is_verifier(cls):
386    """Determine if a class is a Verifier that can be instantiated"""
387
388    return inspect.isclass(cls) and issubclass(cls, Verifier) and \
389        not inspect.isabstract(cls)
390
391# list of all verifier classes
392all_verifiers = [ v for n, v in \
393                  inspect.getmembers(sys.modules[__name__], is_verifier) ]
394