verifiers.py revision 11403:e8949ea6961f
1#!/usr/bin/env python
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
59def _modified_regions(old, new):
60    m = SequenceMatcher(a=old, b=new, autojunk=False)
61
62    regions = Regions()
63    for tag, i1, i2, j1, j2 in m.get_opcodes():
64        if tag != "equal":
65            regions.extend(Region(i1, i2))
66    return regions
67
68
69class Verifier(object):
70    """Base class for style verifiers
71
72    Verifiers check for style violations and optionally fix such
73    violations. Implementations should either inherit from this class
74    (Verifier) if they need to work on entire files or LineVerifier if
75    they operate on a line-by-line basis.
76
77    Subclasses must define these class attributes:
78      languages = set of strings identifying applicable languages
79      test_name = long descriptive name of test, will be used in
80                  messages such as "error in <foo>" or "invalid <foo>"
81      opt_name = short name used to generate command-line options to
82                 control the test (--fix-<foo>, --ignore-<foo>, etc.)
83
84    """
85
86    __metaclass__ = ABCMeta
87
88    def __init__(self, ui, opts, base=None):
89        self.ui = ui
90        self.base = base
91
92        # opt_name must be defined as a class attribute of derived classes.
93        # Check test-specific opts first as these have precedence.
94        self.opt_fix = opts.get('fix_' + self.opt_name, False)
95        self.opt_ignore = opts.get('ignore_' + self.opt_name, False)
96        self.opt_skip = opts.get('skip_' + self.opt_name, False)
97        # If no test-specific opts were set, then set based on "-all" opts.
98        if not (self.opt_fix or self.opt_ignore or self.opt_skip):
99            self.opt_fix = opts.get('fix_all', False)
100            self.opt_ignore = opts.get('ignore_all', False)
101            self.opt_skip = opts.get('skip_all', False)
102
103    def normalize_filename(self, name):
104        abs_name = os.path.abspath(name)
105        if self.base is None:
106            return abs_name
107
108        abs_base = os.path.abspath(self.base)
109        return os.path.relpath(abs_name, start=abs_base)
110
111    def open(self, filename, mode):
112        try:
113            f = file(filename, mode)
114        except OSError, msg:
115            print 'could not open file %s: %s' % (filename, msg)
116            return None
117
118        return f
119
120    def skip(self, filename):
121        # We never want to handle symlinks, so always skip them: If the location
122        # pointed to is a directory, skip it. If the location is a file inside
123        # the gem5 directory, it will be checked as a file, so symlink can be
124        # skipped. If the location is a file outside gem5, we don't want to
125        # check it anyway.
126        if os.path.islink(filename):
127            return True
128        return lang_type(filename) not in self.languages
129
130    def apply(self, filename, regions=all_regions):
131        """Possibly apply to specified regions of file 'filename'.
132
133        Verifier is skipped if --skip-<test> option was provided or if
134        file is not of an applicable type.  Otherwise file is checked
135        and error messages printed.  Errors are fixed or ignored if
136        the corresponding --fix-<test> or --ignore-<test> options were
137        provided.  If neither, the user is prompted for an action.
138
139        Returns True to abort, False otherwise.
140        """
141        if not (self.opt_skip or self.skip(filename)):
142            errors = self.check(filename, regions)
143            if errors and not self.opt_ignore:
144                if self.opt_fix:
145                    self.fix(filename, regions)
146                else:
147                    result = self.ui.prompt("(a)bort, (i)gnore, or (f)ix?",
148                                            'aif', 'a')
149                    if result == 'f':
150                        self.fix(filename, regions)
151                    elif result == 'a':
152                        return True # abort
153
154        return False
155
156    @abstractmethod
157    def check(self, filename, regions=all_regions):
158        """Check specified regions of file 'filename'.
159
160        Line-by-line checks can simply provide a check_line() method
161        that returns True if the line is OK and False if it has an
162        error.  Verifiers that need a multi-line view (like
163        SortedIncludes) must override this entire function.
164
165        Returns a count of errors (0 if none), though actual non-zero
166        count value is not currently used anywhere.
167        """
168        pass
169
170    @abstractmethod
171    def fix(self, filename, regions=all_regions):
172        """Fix specified regions of file 'filename'.
173
174        Line-by-line fixes can simply provide a fix_line() method that
175        returns the fixed line. Verifiers that need a multi-line view
176        (like SortedIncludes) must override this entire function.
177        """
178        pass
179
180class LineVerifier(Verifier):
181    def check(self, filename, regions=all_regions):
182        f = self.open(filename, 'r')
183
184        errors = 0
185        for num,line in enumerate(f):
186            if num not in regions:
187                continue
188            line = line.rstrip('\n')
189            if not self.check_line(line):
190                self.ui.write("invalid %s in %s:%d\n" % \
191                              (self.test_name, filename, num + 1))
192                if self.ui.verbose:
193                    self.ui.write(">>%s<<\n" % line[:-1])
194                errors += 1
195        return errors
196
197    def fix(self, filename, regions=all_regions):
198        f = self.open(filename, 'r+')
199
200        lines = list(f)
201
202        f.seek(0)
203        f.truncate()
204
205        for i,line in enumerate(lines):
206            line = line.rstrip('\n')
207            if i in regions:
208                line = self.fix_line(line)
209
210            f.write(line)
211            f.write("\n")
212        f.close()
213
214
215    @abstractmethod
216    def check_line(self, line):
217        pass
218
219    @abstractmethod
220    def fix_line(self, line):
221        pass
222
223class Whitespace(LineVerifier):
224    """Check whitespace.
225
226    Specifically:
227    - No tabs used for indent
228    - No trailing whitespace
229    """
230
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 BoolCompare(LineVerifier):
352    languages = set(('C', 'C++', 'python'))
353    test_name = 'boolean comparison'
354    opt_name = 'boolcomp'
355
356    regex = re.compile(r'\s*==\s*([Tt]rue|[Ff]alse)\b')
357
358    def check_line(self, line):
359        return self.regex.search(line) == None
360
361    def fix_line(self, line):
362        match = self.regex.search(line)
363        if match:
364            if match.group(1) in ('true', 'True'):
365                line = self.regex.sub('', line)
366            else:
367                self.ui.write("Warning: cannot automatically fix "
368                              "comparisons with false/False.\n")
369        return line
370
371def is_verifier(cls):
372    """Determine if a class is a Verifier that can be instantiated"""
373
374    return inspect.isclass(cls) and issubclass(cls, Verifier) and \
375        not inspect.isabstract(cls)
376
377# list of all verifier classes
378all_verifiers = [ v for n, v in \
379                  inspect.getmembers(sys.modules[__name__], is_verifier) ]
380