Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2008 the V8 project authors. All rights reserved.
      4 # Redistribution and use in source and binary forms, with or without
      5 # modification, are permitted provided that the following conditions are
      6 # met:
      7 #
      8 #     * Redistributions of source code must retain the above copyright
      9 #       notice, this list of conditions and the following disclaimer.
     10 #     * Redistributions in binary form must reproduce the above
     11 #       copyright notice, this list of conditions and the following
     12 #       disclaimer in the documentation and/or other materials provided
     13 #       with the distribution.
     14 #     * Neither the name of Google Inc. nor the names of its
     15 #       contributors may be used to endorse or promote products derived
     16 #       from this software without specific prior written permission.
     17 #
     18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29 
     30 
     31 import md5
     32 import optparse
     33 import os
     34 from os.path import abspath, join, dirname, basename, exists
     35 import pickle
     36 import re
     37 import sys
     38 import subprocess
     39 
     40 # Disabled LINT rules and reason.
     41 # build/include_what_you_use: Started giving false positives for variables
     42 #  named "string" and "map" assuming that you needed to include STL headers.
     43 
     44 ENABLED_LINT_RULES = """
     45 build/class
     46 build/deprecated
     47 build/endif_comment
     48 build/forward_decl
     49 build/include_order
     50 build/printf_format
     51 build/storage_class
     52 legal/copyright
     53 readability/boost
     54 readability/braces
     55 readability/casting
     56 readability/check
     57 readability/constructors
     58 readability/fn_size
     59 readability/function
     60 readability/multiline_comment
     61 readability/multiline_string
     62 readability/streams
     63 readability/todo
     64 readability/utf8
     65 runtime/arrays
     66 runtime/casting
     67 runtime/deprecated_fn
     68 runtime/explicit
     69 runtime/int
     70 runtime/memset
     71 runtime/mutex
     72 runtime/nonconf
     73 runtime/printf
     74 runtime/printf_format
     75 runtime/references
     76 runtime/rtti
     77 runtime/sizeof
     78 runtime/string
     79 runtime/virtual
     80 runtime/vlog
     81 whitespace/blank_line
     82 whitespace/braces
     83 whitespace/comma
     84 whitespace/comments
     85 whitespace/end_of_line
     86 whitespace/ending_newline
     87 whitespace/indent
     88 whitespace/labels
     89 whitespace/line_length
     90 whitespace/newline
     91 whitespace/operators
     92 whitespace/parens
     93 whitespace/tab
     94 whitespace/todo
     95 """.split()
     96 
     97 
     98 class FileContentsCache(object):
     99 
    100   def __init__(self, sums_file_name):
    101     self.sums = {}
    102     self.sums_file_name = sums_file_name
    103 
    104   def Load(self):
    105     try:
    106       sums_file = None
    107       try:
    108         sums_file = open(self.sums_file_name, 'r')
    109         self.sums = pickle.load(sums_file)
    110       except IOError:
    111         # File might not exist, this is OK.
    112         pass
    113     finally:
    114       if sums_file:
    115         sums_file.close()
    116 
    117   def Save(self):
    118     try:
    119       sums_file = open(self.sums_file_name, 'w')
    120       pickle.dump(self.sums, sums_file)
    121     finally:
    122       sums_file.close()
    123 
    124   def FilterUnchangedFiles(self, files):
    125     changed_or_new = []
    126     for file in files:
    127       try:
    128         handle = open(file, "r")
    129         file_sum = md5.new(handle.read()).digest()
    130         if not file in self.sums or self.sums[file] != file_sum:
    131           changed_or_new.append(file)
    132           self.sums[file] = file_sum
    133       finally:
    134         handle.close()
    135     return changed_or_new
    136 
    137   def RemoveFile(self, file):
    138     if file in self.sums:
    139       self.sums.pop(file)
    140 
    141 
    142 class SourceFileProcessor(object):
    143   """
    144   Utility class that can run through a directory structure, find all relevant
    145   files and invoke a custom check on the files.
    146   """
    147 
    148   def Run(self, path):
    149     all_files = []
    150     for file in self.GetPathsToSearch():
    151       all_files += self.FindFilesIn(join(path, file))
    152     if not self.ProcessFiles(all_files, path):
    153       return False
    154     return True
    155 
    156   def IgnoreDir(self, name):
    157     return name.startswith('.') or name == 'data' or name == 'sputniktests'
    158 
    159   def IgnoreFile(self, name):
    160     return name.startswith('.')
    161 
    162   def FindFilesIn(self, path):
    163     result = []
    164     for (root, dirs, files) in os.walk(path):
    165       for ignored in [x for x in dirs if self.IgnoreDir(x)]:
    166         dirs.remove(ignored)
    167       for file in files:
    168         if not self.IgnoreFile(file) and self.IsRelevant(file):
    169           result.append(join(root, file))
    170     return result
    171 
    172 
    173 class CppLintProcessor(SourceFileProcessor):
    174   """
    175   Lint files to check that they follow the google code style.
    176   """
    177 
    178   def IsRelevant(self, name):
    179     return name.endswith('.cc') or name.endswith('.h')
    180 
    181   def IgnoreDir(self, name):
    182     return (super(CppLintProcessor, self).IgnoreDir(name)
    183               or (name == 'third_party'))
    184 
    185   IGNORE_LINT = ['flag-definitions.h']
    186 
    187   def IgnoreFile(self, name):
    188     return (super(CppLintProcessor, self).IgnoreFile(name)
    189               or (name in CppLintProcessor.IGNORE_LINT))
    190 
    191   def GetPathsToSearch(self):
    192     return ['src', 'public', 'samples', join('test', 'cctest')]
    193 
    194   def ProcessFiles(self, files, path):
    195     good_files_cache = FileContentsCache('.cpplint-cache')
    196     good_files_cache.Load()
    197     files = good_files_cache.FilterUnchangedFiles(files)
    198     if len(files) == 0:
    199       print 'No changes in files detected. Skipping cpplint check.'
    200       return True
    201 
    202     filt = '-,' + ",".join(['+' + n for n in ENABLED_LINT_RULES])
    203     command = ['cpplint.py', '--filter', filt] + join(files)
    204     local_cpplint = join(path, "tools", "cpplint.py")
    205     if exists(local_cpplint):
    206       command = ['python', local_cpplint, '--filter', filt] + join(files)
    207 
    208     process = subprocess.Popen(command, stderr=subprocess.PIPE)
    209     LINT_ERROR_PATTERN = re.compile(r'^(.+)[:(]\d+[:)]')
    210     while True:
    211       out_line = process.stderr.readline()
    212       if out_line == '' and process.poll() != None:
    213         break
    214       sys.stderr.write(out_line)
    215       m = LINT_ERROR_PATTERN.match(out_line)
    216       if m:
    217         good_files_cache.RemoveFile(m.group(1))
    218 
    219     good_files_cache.Save()
    220     return process.returncode == 0
    221 
    222 
    223 COPYRIGHT_HEADER_PATTERN = re.compile(
    224     r'Copyright [\d-]*20[0-1][0-9] the V8 project authors. All rights reserved.')
    225 
    226 class SourceProcessor(SourceFileProcessor):
    227   """
    228   Check that all files include a copyright notice.
    229   """
    230 
    231   RELEVANT_EXTENSIONS = ['.js', '.cc', '.h', '.py', '.c', 'SConscript',
    232       'SConstruct', '.status']
    233   def IsRelevant(self, name):
    234     for ext in SourceProcessor.RELEVANT_EXTENSIONS:
    235       if name.endswith(ext):
    236         return True
    237     return False
    238 
    239   def GetPathsToSearch(self):
    240     return ['.']
    241 
    242   def IgnoreDir(self, name):
    243     return (super(SourceProcessor, self).IgnoreDir(name)
    244               or (name == 'third_party')
    245               or (name == 'obj'))
    246 
    247   IGNORE_COPYRIGHTS = ['earley-boyer.js', 'raytrace.js', 'crypto.js',
    248       'libraries.cc', 'libraries-empty.cc', 'jsmin.py', 'regexp-pcre.js']
    249   IGNORE_TABS = IGNORE_COPYRIGHTS + ['unicode-test.js',
    250       'html-comments.js']
    251 
    252   def ProcessContents(self, name, contents):
    253     result = True
    254     base = basename(name)
    255     if not base in SourceProcessor.IGNORE_TABS:
    256       if '\t' in contents:
    257         print "%s contains tabs" % name
    258         result = False
    259     if not base in SourceProcessor.IGNORE_COPYRIGHTS:
    260       if not COPYRIGHT_HEADER_PATTERN.search(contents):
    261         print "%s is missing a correct copyright header." % name
    262         result = False
    263     return result
    264 
    265   def ProcessFiles(self, files, path):
    266     success = True
    267     for file in files:
    268       try:
    269         handle = open(file)
    270         contents = handle.read()
    271         success = self.ProcessContents(file, contents) and success
    272       finally:
    273         handle.close()
    274     return success
    275 
    276 
    277 def GetOptions():
    278   result = optparse.OptionParser()
    279   result.add_option('--no-lint', help="Do not run cpplint", default=False,
    280                     action="store_true")
    281   return result
    282 
    283 
    284 def Main():
    285   workspace = abspath(join(dirname(sys.argv[0]), '..'))
    286   parser = GetOptions()
    287   (options, args) = parser.parse_args()
    288   success = True
    289   if not options.no_lint:
    290     success = CppLintProcessor().Run(workspace) and success
    291   success = SourceProcessor().Run(workspace) and success
    292   if success:
    293     return 0
    294   else:
    295     return 1
    296 
    297 
    298 if __name__ == '__main__':
    299   sys.exit(Main())
    300