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 try:
     31   import hashlib
     32   md5er = hashlib.md5
     33 except ImportError, e:
     34   import md5
     35   md5er = md5.new
     36 
     37 
     38 import optparse
     39 import os
     40 from os.path import abspath, join, dirname, basename, exists
     41 import pickle
     42 import re
     43 import sys
     44 import subprocess
     45 
     46 # Disabled LINT rules and reason.
     47 # build/include_what_you_use: Started giving false positives for variables
     48 #  named "string" and "map" assuming that you needed to include STL headers.
     49 
     50 ENABLED_LINT_RULES = """
     51 build/class
     52 build/deprecated
     53 build/endif_comment
     54 build/forward_decl
     55 build/include_order
     56 build/printf_format
     57 build/storage_class
     58 legal/copyright
     59 readability/boost
     60 readability/braces
     61 readability/casting
     62 readability/check
     63 readability/constructors
     64 readability/fn_size
     65 readability/function
     66 readability/multiline_comment
     67 readability/multiline_string
     68 readability/streams
     69 readability/todo
     70 readability/utf8
     71 runtime/arrays
     72 runtime/casting
     73 runtime/deprecated_fn
     74 runtime/explicit
     75 runtime/int
     76 runtime/memset
     77 runtime/mutex
     78 runtime/nonconf
     79 runtime/printf
     80 runtime/printf_format
     81 runtime/references
     82 runtime/rtti
     83 runtime/sizeof
     84 runtime/string
     85 runtime/virtual
     86 runtime/vlog
     87 whitespace/blank_line
     88 whitespace/braces
     89 whitespace/comma
     90 whitespace/comments
     91 whitespace/end_of_line
     92 whitespace/ending_newline
     93 whitespace/indent
     94 whitespace/labels
     95 whitespace/line_length
     96 whitespace/newline
     97 whitespace/operators
     98 whitespace/parens
     99 whitespace/tab
    100 whitespace/todo
    101 """.split()
    102 
    103 
    104 class FileContentsCache(object):
    105 
    106   def __init__(self, sums_file_name):
    107     self.sums = {}
    108     self.sums_file_name = sums_file_name
    109 
    110   def Load(self):
    111     try:
    112       sums_file = None
    113       try:
    114         sums_file = open(self.sums_file_name, 'r')
    115         self.sums = pickle.load(sums_file)
    116       except IOError:
    117         # File might not exist, this is OK.
    118         pass
    119     finally:
    120       if sums_file:
    121         sums_file.close()
    122 
    123   def Save(self):
    124     try:
    125       sums_file = open(self.sums_file_name, 'w')
    126       pickle.dump(self.sums, sums_file)
    127     finally:
    128       sums_file.close()
    129 
    130   def FilterUnchangedFiles(self, files):
    131     changed_or_new = []
    132     for file in files:
    133       try:
    134         handle = open(file, "r")
    135         file_sum = md5er(handle.read()).digest()
    136         if not file in self.sums or self.sums[file] != file_sum:
    137           changed_or_new.append(file)
    138           self.sums[file] = file_sum
    139       finally:
    140         handle.close()
    141     return changed_or_new
    142 
    143   def RemoveFile(self, file):
    144     if file in self.sums:
    145       self.sums.pop(file)
    146 
    147 
    148 class SourceFileProcessor(object):
    149   """
    150   Utility class that can run through a directory structure, find all relevant
    151   files and invoke a custom check on the files.
    152   """
    153 
    154   def Run(self, path):
    155     all_files = []
    156     for file in self.GetPathsToSearch():
    157       all_files += self.FindFilesIn(join(path, file))
    158     if not self.ProcessFiles(all_files, path):
    159       return False
    160     return True
    161 
    162   def IgnoreDir(self, name):
    163     return name.startswith('.') or name == 'data' or name == 'sputniktests'
    164 
    165   def IgnoreFile(self, name):
    166     return name.startswith('.')
    167 
    168   def FindFilesIn(self, path):
    169     result = []
    170     for (root, dirs, files) in os.walk(path):
    171       for ignored in [x for x in dirs if self.IgnoreDir(x)]:
    172         dirs.remove(ignored)
    173       for file in files:
    174         if not self.IgnoreFile(file) and self.IsRelevant(file):
    175           result.append(join(root, file))
    176     return result
    177 
    178 
    179 class CppLintProcessor(SourceFileProcessor):
    180   """
    181   Lint files to check that they follow the google code style.
    182   """
    183 
    184   def IsRelevant(self, name):
    185     return name.endswith('.cc') or name.endswith('.h')
    186 
    187   def IgnoreDir(self, name):
    188     return (super(CppLintProcessor, self).IgnoreDir(name)
    189               or (name == 'third_party'))
    190 
    191   IGNORE_LINT = ['flag-definitions.h']
    192 
    193   def IgnoreFile(self, name):
    194     return (super(CppLintProcessor, self).IgnoreFile(name)
    195               or (name in CppLintProcessor.IGNORE_LINT))
    196 
    197   def GetPathsToSearch(self):
    198     return ['src', 'preparser', 'include', 'samples', join('test', 'cctest')]
    199 
    200   def ProcessFiles(self, files, path):
    201     good_files_cache = FileContentsCache('.cpplint-cache')
    202     good_files_cache.Load()
    203     files = good_files_cache.FilterUnchangedFiles(files)
    204     if len(files) == 0:
    205       print 'No changes in files detected. Skipping cpplint check.'
    206       return True
    207 
    208     filt = '-,' + ",".join(['+' + n for n in ENABLED_LINT_RULES])
    209     command = ['cpplint.py', '--filter', filt] + join(files)
    210     local_cpplint = join(path, "tools", "cpplint.py")
    211     if exists(local_cpplint):
    212       command = ['python', local_cpplint, '--filter', filt] + join(files)
    213 
    214     process = subprocess.Popen(command, stderr=subprocess.PIPE)
    215     LINT_ERROR_PATTERN = re.compile(r'^(.+)[:(]\d+[:)]')
    216     while True:
    217       out_line = process.stderr.readline()
    218       if out_line == '' and process.poll() != None:
    219         break
    220       sys.stderr.write(out_line)
    221       m = LINT_ERROR_PATTERN.match(out_line)
    222       if m:
    223         good_files_cache.RemoveFile(m.group(1))
    224 
    225     good_files_cache.Save()
    226     return process.returncode == 0
    227 
    228 
    229 COPYRIGHT_HEADER_PATTERN = re.compile(
    230     r'Copyright [\d-]*20[0-1][0-9] the V8 project authors. All rights reserved.')
    231 
    232 class SourceProcessor(SourceFileProcessor):
    233   """
    234   Check that all files include a copyright notice.
    235   """
    236 
    237   RELEVANT_EXTENSIONS = ['.js', '.cc', '.h', '.py', '.c', 'SConscript',
    238       'SConstruct', '.status']
    239   def IsRelevant(self, name):
    240     for ext in SourceProcessor.RELEVANT_EXTENSIONS:
    241       if name.endswith(ext):
    242         return True
    243     return False
    244 
    245   def GetPathsToSearch(self):
    246     return ['.']
    247 
    248   def IgnoreDir(self, name):
    249     return (super(SourceProcessor, self).IgnoreDir(name)
    250               or (name == 'third_party')
    251               or (name == 'obj'))
    252 
    253   IGNORE_COPYRIGHTS = ['earley-boyer.js', 'raytrace.js', 'crypto.js',
    254       'libraries.cc', 'libraries-empty.cc', 'jsmin.py', 'regexp-pcre.js']
    255   IGNORE_TABS = IGNORE_COPYRIGHTS + ['unicode-test.js',
    256       'html-comments.js']
    257 
    258   def ProcessContents(self, name, contents):
    259     result = True
    260     base = basename(name)
    261     if not base in SourceProcessor.IGNORE_TABS:
    262       if '\t' in contents:
    263         print "%s contains tabs" % name
    264         result = False
    265     if not base in SourceProcessor.IGNORE_COPYRIGHTS:
    266       if not COPYRIGHT_HEADER_PATTERN.search(contents):
    267         print "%s is missing a correct copyright header." % name
    268         result = False
    269     return result
    270 
    271   def ProcessFiles(self, files, path):
    272     success = True
    273     for file in files:
    274       try:
    275         handle = open(file)
    276         contents = handle.read()
    277         success = self.ProcessContents(file, contents) and success
    278       finally:
    279         handle.close()
    280     return success
    281 
    282 
    283 def GetOptions():
    284   result = optparse.OptionParser()
    285   result.add_option('--no-lint', help="Do not run cpplint", default=False,
    286                     action="store_true")
    287   return result
    288 
    289 
    290 def Main():
    291   workspace = abspath(join(dirname(sys.argv[0]), '..'))
    292   parser = GetOptions()
    293   (options, args) = parser.parse_args()
    294   success = True
    295   if not options.no_lint:
    296     success = CppLintProcessor().Run(workspace) and success
    297   success = SourceProcessor().Run(workspace) and success
    298   if success:
    299     return 0
    300   else:
    301     return 1
    302 
    303 
    304 if __name__ == '__main__':
    305   sys.exit(Main())
    306