verifiers.py revision 11449:4511f239d1ba
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    try:
61        m = SequenceMatcher(a=old, b=new, autojunk=False)
62    except TypeError:
63        # autojunk was introduced in Python 2.7. We need a fallback
64        # mechanism to support old Python versions.
65        m = SequenceMatcher(a=old, b=new)
66    regions = Regions()
67    for tag, i1, i2, j1, j2 in m.get_opcodes():
68        if tag != "equal":
69            regions.extend(Region(i1, i2))
70    return regions
71
72
73class Verifier(object):
74    """Base class for style verifiers
75
76    Verifiers check for style violations and optionally fix such
77    violations. Implementations should either inherit from this class
78    (Verifier) if they need to work on entire files or LineVerifier if
79    they operate on a line-by-line basis.
80
81    Subclasses must define these class attributes:
82      languages = set of strings identifying applicable languages
83      test_name = long descriptive name of test, will be used in
84                  messages such as "error in <foo>" or "invalid <foo>"
85      opt_name = short name used to generate command-line options to
86                 control the test (--fix-<foo>, --ignore-<foo>, etc.)
87
88    """
89
90    __metaclass__ = ABCMeta
91
92    def __init__(self, ui, opts, base=None):
93        self.ui = ui
94        self.base = base
95
96        # opt_name must be defined as a class attribute of derived classes.
97        # Check test-specific opts first as these have precedence.
98        self.opt_fix = opts.get('fix_' + self.opt_name, False)
99        self.opt_ignore = opts.get('ignore_' + self.opt_name, False)
100        self.opt_skip = opts.get('skip_' + self.opt_name, False)
101        # If no test-specific opts were set, then set based on "-all" opts.
102        if not (self.opt_fix or self.opt_ignore or self.opt_skip):
103            self.opt_fix = opts.get('fix_all', False)
104            self.opt_ignore = opts.get('ignore_all', False)
105            self.opt_skip = opts.get('skip_all', False)
106
107    def normalize_filename(self, name):
108        abs_name = os.path.abspath(name)
109        if self.base is None:
110            return abs_name
111
112        abs_base = os.path.abspath(self.base)
113        return os.path.relpath(abs_name, start=abs_base)
114
115    def open(self, filename, mode):
116        try:
117            f = file(filename, mode)
118        except OSError, msg:
119            print 'could not open file %s: %s' % (filename, msg)
120            return None
121
122        return f
123
124    def skip(self, filename):
125        # We never want to handle symlinks, so always skip them: If the location
126        # pointed to is a directory, skip it. If the location is a file inside
127        # the gem5 directory, it will be checked as a file, so symlink can be
128        # skipped. If the location is a file outside gem5, we don't want to
129        # check it anyway.
130        if os.path.islink(filename):
131            return True
132        return lang_type(filename) not in self.languages
133
134    def apply(self, filename, regions=all_regions):
135        """Possibly apply to specified regions of file 'filename'.
136
137        Verifier is skipped if --skip-<test> option was provided or if
138        file is not of an applicable type.  Otherwise file is checked
139        and error messages printed.  Errors are fixed or ignored if
140        the corresponding --fix-<test> or --ignore-<test> options were
141        provided.  If neither, the user is prompted for an action.
142
143        Returns True to abort, False otherwise.
144        """
145        if not (self.opt_skip or self.skip(filename)):
146            errors = self.check(filename, regions)
147            if errors and not self.opt_ignore:
148                if self.opt_fix:
149                    self.fix(filename, regions)
150                else:
151                    result = self.ui.prompt("(a)bort, (i)gnore, or (f)ix?",
152                                            'aif', 'a')
153                    if result == 'f':
154                        self.fix(filename, regions)
155                    elif result == 'a':
156                        return True # abort
157
158        return False
159
160    @abstractmethod
161    def check(self, filename, regions=all_regions):
162        """Check specified regions of file 'filename'.
163
164        Line-by-line checks can simply provide a check_line() method
165        that returns True if the line is OK and False if it has an
166        error.  Verifiers that need a multi-line view (like
167        SortedIncludes) must override this entire function.
168
169        Returns a count of errors (0 if none), though actual non-zero
170        count value is not currently used anywhere.
171        """
172        pass
173
174    @abstractmethod
175    def fix(self, filename, regions=all_regions):
176        """Fix specified regions of file 'filename'.
177
178        Line-by-line fixes can simply provide a fix_line() method that
179        returns the fixed line. Verifiers that need a multi-line view
180        (like SortedIncludes) must override this entire function.
181        """
182        pass
183
184class LineVerifier(Verifier):
185    def check(self, filename, regions=all_regions):
186        f = self.open(filename, 'r')
187
188        errors = 0
189        for num,line in enumerate(f):
190            if num not in regions:
191                continue
192            line = line.rstrip('\n')
193            if not self.check_line(line):
194                self.ui.write("invalid %s in %s:%d\n" % \
195                              (self.test_name, filename, num + 1))
196                if self.ui.verbose:
197                    self.ui.write(">>%s<<\n" % line[:-1])
198                errors += 1
199        return errors
200
201    def fix(self, filename, regions=all_regions):
202        f = self.open(filename, 'r+')
203
204        lines = list(f)
205
206        f.seek(0)
207        f.truncate()
208
209        for i,line in enumerate(lines):
210            line = line.rstrip('\n')
211            if i in regions:
212                line = self.fix_line(line)
213
214            f.write(line)
215            f.write("\n")
216        f.close()
217
218
219    @abstractmethod
220    def check_line(self, line):
221        pass
222
223    @abstractmethod
224    def fix_line(self, line):
225        pass
226
227class Whitespace(LineVerifier):
228    """Check whitespace.
229
230    Specifically:
231    - No tabs used for indent
232    - No trailing whitespace
233    """
234
235    languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons'))
236    test_name = 'whitespace'
237    opt_name = 'white'
238
239    _lead = re.compile(r'^([ \t]+)')
240    _trail = re.compile(r'([ \t]+)$')
241
242    def check_line(self, line):
243        match = Whitespace._lead.search(line)
244        if match and match.group(1).find('\t') != -1:
245            return False
246
247        match = Whitespace._trail.search(line)
248        if match:
249            return False
250
251        return True
252
253    def fix_line(self, line):
254        if Whitespace._lead.search(line):
255            newline = ''
256            for i,c in enumerate(line):
257                if c == ' ':
258                    newline += ' '
259                elif c == '\t':
260                    newline += ' ' * (tabsize - len(newline) % tabsize)
261                else:
262                    newline += line[i:]
263                    break
264
265            line = newline
266
267        return line.rstrip() + '\n'
268
269
270class SortedIncludes(Verifier):
271    """Check for proper sorting of include statements"""
272
273    languages = sort_includes.default_languages
274    test_name = 'include file order'
275    opt_name = 'include'
276
277    def __init__(self, *args, **kwargs):
278        super(SortedIncludes, self).__init__(*args, **kwargs)
279        self.sort_includes = sort_includes.SortIncludes()
280
281    def check(self, filename, regions=all_regions):
282        f = self.open(filename, 'r')
283        norm_fname = self.normalize_filename(filename)
284
285        old = [ l.rstrip('\n') for l in f.xreadlines() ]
286        f.close()
287
288        if len(old) == 0:
289            return 0
290
291        language = lang_type(filename, old[0])
292        new = list(self.sort_includes(old, norm_fname, language))
293
294        modified = _modified_regions(old, new) & regions
295
296        if modified:
297            self.ui.write("invalid sorting of includes in %s\n" % (filename))
298            if self.ui.verbose:
299                for start, end in modified.regions:
300                    self.ui.write("bad region [%d, %d)\n" % (start, end))
301            return 1
302
303        return 0
304
305    def fix(self, filename, regions=all_regions):
306        f = self.open(filename, 'r+')
307
308        old = f.readlines()
309        lines = [ l.rstrip('\n') for l in old ]
310        language = lang_type(filename, lines[0])
311        sort_lines = list(self.sort_includes(lines, filename, language))
312        new = ''.join(line + '\n' for line in sort_lines)
313
314        f.seek(0)
315        f.truncate()
316
317        for i,line in enumerate(sort_lines):
318            f.write(line)
319            f.write('\n')
320        f.close()
321
322
323class ControlSpace(LineVerifier):
324    """Check for exactly one space after if/while/for"""
325
326    languages = set(('C', 'C++'))
327    test_name = 'spacing after if/while/for'
328    opt_name = 'control'
329
330    _any_control = re.compile(r'\b(if|while|for)([ \t]*)\(')
331
332    def check_line(self, line):
333        match = ControlSpace._any_control.search(line)
334        return not (match and match.group(2) != " ")
335
336    def fix_line(self, line):
337        new_line = _any_control.sub(r'\1 (', line)
338        return new_line
339
340
341class LineLength(LineVerifier):
342    languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons'))
343    test_name = 'line length'
344    opt_name = 'length'
345
346    def check_line(self, line):
347        return style.normalized_len(line) <= 79
348
349    def fix(self, filename, regions=all_regions):
350        self.ui.write("Warning: cannot automatically fix overly long lines.\n")
351
352    def fix_line(self, line):
353        pass
354
355class ControlCharacters(LineVerifier):
356    languages = set(('C', 'C++', 'swig', 'python', 'asm', 'isa', 'scons'))
357    test_name = 'control character'
358    opt_name = 'ascii'
359
360    valid = ('\n', '\t')
361    invalid = "".join([chr(i) for i in range(0, 0x20) if chr(i) not in valid])
362
363    def check_line(self, line):
364        return self.fix_line(line) == line
365
366    def fix_line(self, line):
367        return line.translate(None, ControlCharacters.invalid)
368
369class BoolCompare(LineVerifier):
370    languages = set(('C', 'C++', 'python'))
371    test_name = 'boolean comparison'
372    opt_name = 'boolcomp'
373
374    regex = re.compile(r'\s*==\s*([Tt]rue|[Ff]alse)\b')
375
376    def check_line(self, line):
377        return self.regex.search(line) == None
378
379    def fix_line(self, line):
380        match = self.regex.search(line)
381        if match:
382            if match.group(1) in ('true', 'True'):
383                line = self.regex.sub('', line)
384            else:
385                self.ui.write("Warning: cannot automatically fix "
386                              "comparisons with false/False.\n")
387        return line
388
389def is_verifier(cls):
390    """Determine if a class is a Verifier that can be instantiated"""
391
392    return inspect.isclass(cls) and issubclass(cls, Verifier) and \
393        not inspect.isabstract(cls)
394
395# list of all verifier classes
396all_verifiers = [ v for n, v in \
397                  inspect.getmembers(sys.modules[__name__], is_verifier) ]
398