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 30try: 31 import hashlib 32 md5er = hashlib.md5 33except ImportError, e: 34 import md5 35 md5er = md5.new 36 37 38import optparse 39import os 40from os.path import abspath, join, dirname, basename, exists 41import pickle 42import re 43import sys 44import subprocess 45import multiprocessing 46from 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 52ENABLED_LINT_RULES = """ 53build/class 54build/deprecated 55build/endif_comment 56build/forward_decl 57build/include_alpha 58build/include_order 59build/printf_format 60build/storage_class 61legal/copyright 62readability/boost 63readability/braces 64readability/casting 65readability/constructors 66readability/fn_size 67readability/function 68readability/multiline_comment 69readability/multiline_string 70readability/streams 71readability/todo 72readability/utf8 73runtime/arrays 74runtime/casting 75runtime/deprecated_fn 76runtime/explicit 77runtime/int 78runtime/memset 79runtime/mutex 80runtime/nonconf 81runtime/printf 82runtime/printf_format 83runtime/rtti 84runtime/sizeof 85runtime/string 86runtime/virtual 87runtime/vlog 88whitespace/blank_line 89whitespace/braces 90whitespace/comma 91whitespace/comments 92whitespace/ending_newline 93whitespace/indent 94whitespace/labels 95whitespace/line_length 96whitespace/newline 97whitespace/operators 98whitespace/parens 99whitespace/tab 100whitespace/todo 101""".split() 102 103# TODO(bmeurer): Fix and re-enable readability/check 104 105LINT_OUTPUT_PATTERN = re.compile(r'^.+[:(]\d+[:)]|^Done processing') 106 107 108def 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 135class 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 187class 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 220class 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 287COPYRIGHT_HEADER_PATTERN = re.compile( 288 r'Copyright [\d-]*20[0-1][0-9] the V8 project authors. All rights reserved.') 289 290class 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 420def CheckRuntimeVsNativesNameClashes(workspace): 421 code = subprocess.call( 422 [sys.executable, join(workspace, "tools", "check-name-clashes.py")]) 423 return code == 0 424 425 426def CheckExternalReferenceRegistration(workspace): 427 code = subprocess.call( 428 [sys.executable, join(workspace, "tools", "external-reference-check.py")]) 429 return code == 0 430 431 432def 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 439def 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 458if __name__ == '__main__': 459 sys.exit(Main()) 460