Home | History | Annotate | Download | only in tools
      1 #!/usr/bin/env python
      2 #
      3 # Copyright 2012 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 import multiprocessing
     46 from subprocess import PIPE
     47 
     48 # Disabled LINT rules and reason.
     49 # build/include_what_you_use: Started giving false positives for variables
     50 #  named "string" and "map" assuming that you needed to include STL headers.
     51 
     52 ENABLED_LINT_RULES = """
     53 build/class
     54 build/deprecated
     55 build/endif_comment
     56 build/forward_decl
     57 build/include_alpha
     58 build/include_order
     59 build/printf_format
     60 build/storage_class
     61 legal/copyright
     62 readability/boost
     63 readability/braces
     64 readability/casting
     65 readability/constructors
     66 readability/fn_size
     67 readability/function
     68 readability/multiline_comment
     69 readability/multiline_string
     70 readability/streams
     71 readability/todo
     72 readability/utf8
     73 runtime/arrays
     74 runtime/casting
     75 runtime/deprecated_fn
     76 runtime/explicit
     77 runtime/int
     78 runtime/memset
     79 runtime/mutex
     80 runtime/nonconf
     81 runtime/printf
     82 runtime/printf_format
     83 runtime/rtti
     84 runtime/sizeof
     85 runtime/string
     86 runtime/virtual
     87 runtime/vlog
     88 whitespace/blank_line
     89 whitespace/braces
     90 whitespace/comma
     91 whitespace/comments
     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 # TODO(bmeurer): Fix and re-enable readability/check
    104 
    105 LINT_OUTPUT_PATTERN = re.compile(r'^.+[:(]\d+[:)]|^Done processing')
    106 
    107 
    108 def CppLintWorker(command):
    109   try:
    110     process = subprocess.Popen(command, stderr=subprocess.PIPE)
    111     process.wait()
    112     out_lines = ""
    113     error_count = -1
    114     while True:
    115       out_line = process.stderr.readline()
    116       if out_line == '' and process.poll() != None:
    117         if error_count == -1:
    118           print "Failed to process %s" % command.pop()
    119           return 1
    120         break
    121       m = LINT_OUTPUT_PATTERN.match(out_line)
    122       if m:
    123         out_lines += out_line
    124         error_count += 1
    125     sys.stdout.write(out_lines)
    126     return error_count
    127   except KeyboardInterrupt:
    128     process.kill()
    129   except:
    130     print('Error running cpplint.py. Please make sure you have depot_tools' +
    131           ' in your $PATH. Lint check skipped.')
    132     process.kill()
    133 
    134 
    135 class FileContentsCache(object):
    136 
    137   def __init__(self, sums_file_name):
    138     self.sums = {}
    139     self.sums_file_name = sums_file_name
    140 
    141   def Load(self):
    142     try:
    143       sums_file = None
    144       try:
    145         sums_file = open(self.sums_file_name, 'r')
    146         self.sums = pickle.load(sums_file)
    147       except:
    148         # Cannot parse pickle for any reason. Not much we can do about it.
    149         pass
    150     finally:
    151       if sums_file:
    152         sums_file.close()
    153 
    154   def Save(self):
    155     try:
    156       sums_file = open(self.sums_file_name, 'w')
    157       pickle.dump(self.sums, sums_file)
    158     except:
    159       # Failed to write pickle. Try to clean-up behind us.
    160       if sums_file:
    161         sums_file.close()
    162       try:
    163         os.unlink(self.sums_file_name)
    164       except:
    165         pass
    166     finally:
    167       sums_file.close()
    168 
    169   def FilterUnchangedFiles(self, files):
    170     changed_or_new = []
    171     for file in files:
    172       try:
    173         handle = open(file, "r")
    174         file_sum = md5er(handle.read()).digest()
    175         if not file in self.sums or self.sums[file] != file_sum:
    176           changed_or_new.append(file)
    177           self.sums[file] = file_sum
    178       finally:
    179         handle.close()
    180     return changed_or_new
    181 
    182   def RemoveFile(self, file):
    183     if file in self.sums:
    184       self.sums.pop(file)
    185 
    186 
    187 class SourceFileProcessor(object):
    188   """
    189   Utility class that can run through a directory structure, find all relevant
    190   files and invoke a custom check on the files.
    191   """
    192 
    193   def Run(self, path):
    194     all_files = []
    195     for file in self.GetPathsToSearch():
    196       all_files += self.FindFilesIn(join(path, file))
    197     if not self.ProcessFiles(all_files, path):
    198       return False
    199     return True
    200 
    201   def IgnoreDir(self, name):
    202     return (name.startswith('.') or
    203             name in ('buildtools', 'data', 'gmock', 'gtest', 'kraken',
    204                      'octane', 'sunspider'))
    205 
    206   def IgnoreFile(self, name):
    207     return name.startswith('.')
    208 
    209   def FindFilesIn(self, path):
    210     result = []
    211     for (root, dirs, files) in os.walk(path):
    212       for ignored in [x for x in dirs if self.IgnoreDir(x)]:
    213         dirs.remove(ignored)
    214       for file in files:
    215         if not self.IgnoreFile(file) and self.IsRelevant(file):
    216           result.append(join(root, file))
    217     return result
    218 
    219 
    220 class CppLintProcessor(SourceFileProcessor):
    221   """
    222   Lint files to check that they follow the google code style.
    223   """
    224 
    225   def IsRelevant(self, name):
    226     return name.endswith('.cc') or name.endswith('.h')
    227 
    228   def IgnoreDir(self, name):
    229     return (super(CppLintProcessor, self).IgnoreDir(name)
    230               or (name == 'third_party'))
    231 
    232   IGNORE_LINT = ['flag-definitions.h']
    233 
    234   def IgnoreFile(self, name):
    235     return (super(CppLintProcessor, self).IgnoreFile(name)
    236               or (name in CppLintProcessor.IGNORE_LINT))
    237 
    238   def GetPathsToSearch(self):
    239     return ['src', 'include', 'samples', join('test', 'cctest')]
    240 
    241   def GetCpplintScript(self, prio_path):
    242     for path in [prio_path] + os.environ["PATH"].split(os.pathsep):
    243       path = path.strip('"')
    244       cpplint = os.path.join(path, "cpplint.py")
    245       if os.path.isfile(cpplint):
    246         return cpplint
    247 
    248     return None
    249 
    250   def ProcessFiles(self, files, path):
    251     good_files_cache = FileContentsCache('.cpplint-cache')
    252     good_files_cache.Load()
    253     files = good_files_cache.FilterUnchangedFiles(files)
    254     if len(files) == 0:
    255       print 'No changes in files detected. Skipping cpplint check.'
    256       return True
    257 
    258     filt = '-,' + ",".join(['+' + n for n in ENABLED_LINT_RULES])
    259     command = [sys.executable, 'cpplint.py', '--filter', filt]
    260     cpplint = self.GetCpplintScript(join(path, "tools"))
    261     if cpplint is None:
    262       print('Could not find cpplint.py. Make sure '
    263             'depot_tools is installed and in the path.')
    264       sys.exit(1)
    265 
    266     command = [sys.executable, cpplint, '--filter', filt]
    267 
    268     commands = join([command + [file] for file in files])
    269     count = multiprocessing.cpu_count()
    270     pool = multiprocessing.Pool(count)
    271     try:
    272       results = pool.map_async(CppLintWorker, commands).get(999999)
    273     except KeyboardInterrupt:
    274       print "\nCaught KeyboardInterrupt, terminating workers."
    275       sys.exit(1)
    276 
    277     for i in range(len(files)):
    278       if results[i] > 0:
    279         good_files_cache.RemoveFile(files[i])
    280 
    281     total_errors = sum(results)
    282     print "Total errors found: %d" % total_errors
    283     good_files_cache.Save()
    284     return total_errors == 0
    285 
    286 
    287 COPYRIGHT_HEADER_PATTERN = re.compile(
    288     r'Copyright [\d-]*20[0-1][0-9] the V8 project authors. All rights reserved.')
    289 
    290 class SourceProcessor(SourceFileProcessor):
    291   """
    292   Check that all files include a copyright notice and no trailing whitespaces.
    293   """
    294 
    295   RELEVANT_EXTENSIONS = ['.js', '.cc', '.h', '.py', '.c',
    296                          '.status', '.gyp', '.gypi']
    297 
    298   # Overwriting the one in the parent class.
    299   def FindFilesIn(self, path):
    300     if os.path.exists(path+'/.git'):
    301       output = subprocess.Popen('git ls-files --full-name',
    302                                 stdout=PIPE, cwd=path, shell=True)
    303       result = []
    304       for file in output.stdout.read().split():
    305         for dir_part in os.path.dirname(file).replace(os.sep, '/').split('/'):
    306           if self.IgnoreDir(dir_part):
    307             break
    308         else:
    309           if (self.IsRelevant(file) and os.path.exists(file)
    310               and not self.IgnoreFile(file)):
    311             result.append(join(path, file))
    312       if output.wait() == 0:
    313         return result
    314     return super(SourceProcessor, self).FindFilesIn(path)
    315 
    316   def IsRelevant(self, name):
    317     for ext in SourceProcessor.RELEVANT_EXTENSIONS:
    318       if name.endswith(ext):
    319         return True
    320     return False
    321 
    322   def GetPathsToSearch(self):
    323     return ['.']
    324 
    325   def IgnoreDir(self, name):
    326     return (super(SourceProcessor, self).IgnoreDir(name) or
    327             name in ('third_party', 'gyp', 'out', 'obj', 'DerivedSources'))
    328 
    329   IGNORE_COPYRIGHTS = ['cpplint.py',
    330                        'daemon.py',
    331                        'earley-boyer.js',
    332                        'raytrace.js',
    333                        'crypto.js',
    334                        'libraries.cc',
    335                        'libraries-empty.cc',
    336                        'jsmin.py',
    337                        'regexp-pcre.js',
    338                        'gnuplot-4.6.3-emscripten.js']
    339   IGNORE_TABS = IGNORE_COPYRIGHTS + ['unicode-test.js', 'html-comments.js']
    340 
    341   def EndOfDeclaration(self, line):
    342     return line == "}" or line == "};"
    343 
    344   def StartOfDeclaration(self, line):
    345     return line.find("//") == 0 or \
    346            line.find("/*") == 0 or \
    347            line.find(") {") != -1
    348 
    349   def ProcessContents(self, name, contents):
    350     result = True
    351     base = basename(name)
    352     if not base in SourceProcessor.IGNORE_TABS:
    353       if '\t' in contents:
    354         print "%s contains tabs" % name
    355         result = False
    356     if not base in SourceProcessor.IGNORE_COPYRIGHTS:
    357       if not COPYRIGHT_HEADER_PATTERN.search(contents):
    358         print "%s is missing a correct copyright header." % name
    359         result = False
    360     if ' \n' in contents or contents.endswith(' '):
    361       line = 0
    362       lines = []
    363       parts = contents.split(' \n')
    364       if not contents.endswith(' '):
    365         parts.pop()
    366       for part in parts:
    367         line += part.count('\n') + 1
    368         lines.append(str(line))
    369       linenumbers = ', '.join(lines)
    370       if len(lines) > 1:
    371         print "%s has trailing whitespaces in lines %s." % (name, linenumbers)
    372       else:
    373         print "%s has trailing whitespaces in line %s." % (name, linenumbers)
    374       result = False
    375     if not contents.endswith('\n') or contents.endswith('\n\n'):
    376       print "%s does not end with a single new line." % name
    377       result = False
    378     # Check two empty lines between declarations.
    379     if name.endswith(".cc"):
    380       line = 0
    381       lines = []
    382       parts = contents.split('\n')
    383       while line < len(parts) - 2:
    384         if self.EndOfDeclaration(parts[line]):
    385           if self.StartOfDeclaration(parts[line + 1]):
    386             lines.append(str(line + 1))
    387             line += 1
    388           elif parts[line + 1] == "" and \
    389                self.StartOfDeclaration(parts[line + 2]):
    390             lines.append(str(line + 1))
    391             line += 2
    392         line += 1
    393       if len(lines) >= 1:
    394         linenumbers = ', '.join(lines)
    395         if len(lines) > 1:
    396           print "%s does not have two empty lines between declarations " \
    397                 "in lines %s." % (name, linenumbers)
    398         else:
    399           print "%s does not have two empty lines between declarations " \
    400                 "in line %s." % (name, linenumbers)
    401         result = False
    402     return result
    403 
    404   def ProcessFiles(self, files, path):
    405     success = True
    406     violations = 0
    407     for file in files:
    408       try:
    409         handle = open(file)
    410         contents = handle.read()
    411         if not self.ProcessContents(file, contents):
    412           success = False
    413           violations += 1
    414       finally:
    415         handle.close()
    416     print "Total violating files: %s" % violations
    417     return success
    418 
    419 
    420 def CheckRuntimeVsNativesNameClashes(workspace):
    421   code = subprocess.call(
    422       [sys.executable, join(workspace, "tools", "check-name-clashes.py")])
    423   return code == 0
    424 
    425 
    426 def CheckExternalReferenceRegistration(workspace):
    427   code = subprocess.call(
    428       [sys.executable, join(workspace, "tools", "external-reference-check.py")])
    429   return code == 0
    430 
    431 
    432 def GetOptions():
    433   result = optparse.OptionParser()
    434   result.add_option('--no-lint', help="Do not run cpplint", default=False,
    435                     action="store_true")
    436   return result
    437 
    438 
    439 def Main():
    440   workspace = abspath(join(dirname(sys.argv[0]), '..'))
    441   parser = GetOptions()
    442   (options, args) = parser.parse_args()
    443   success = True
    444   print "Running C++ lint check..."
    445   if not options.no_lint:
    446     success = CppLintProcessor().Run(workspace) and success
    447   print "Running copyright header, trailing whitespaces and " \
    448         "two empty lines between declarations check..."
    449   success = SourceProcessor().Run(workspace) and success
    450   success = CheckRuntimeVsNativesNameClashes(workspace) and success
    451   success = CheckExternalReferenceRegistration(workspace) and success
    452   if success:
    453     return 0
    454   else:
    455     return 1
    456 
    457 
    458 if __name__ == '__main__':
    459   sys.exit(Main())
    460