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_order 58build/printf_format 59build/storage_class 60legal/copyright 61readability/boost 62readability/braces 63readability/casting 64readability/check 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/references 84runtime/rtti 85runtime/sizeof 86runtime/string 87runtime/virtual 88runtime/vlog 89whitespace/blank_line 90whitespace/braces 91whitespace/comma 92whitespace/comments 93whitespace/ending_newline 94whitespace/indent 95whitespace/labels 96whitespace/line_length 97whitespace/newline 98whitespace/operators 99whitespace/parens 100whitespace/tab 101whitespace/todo 102""".split() 103 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 IOError: 148 # File might not exist, this is OK. 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 finally: 159 sums_file.close() 160 161 def FilterUnchangedFiles(self, files): 162 changed_or_new = [] 163 for file in files: 164 try: 165 handle = open(file, "r") 166 file_sum = md5er(handle.read()).digest() 167 if not file in self.sums or self.sums[file] != file_sum: 168 changed_or_new.append(file) 169 self.sums[file] = file_sum 170 finally: 171 handle.close() 172 return changed_or_new 173 174 def RemoveFile(self, file): 175 if file in self.sums: 176 self.sums.pop(file) 177 178 179class SourceFileProcessor(object): 180 """ 181 Utility class that can run through a directory structure, find all relevant 182 files and invoke a custom check on the files. 183 """ 184 185 def Run(self, path): 186 all_files = [] 187 for file in self.GetPathsToSearch(): 188 all_files += self.FindFilesIn(join(path, file)) 189 if not self.ProcessFiles(all_files, path): 190 return False 191 return True 192 193 def IgnoreDir(self, name): 194 return name.startswith('.') or name == 'data' or name == 'sputniktests' 195 196 def IgnoreFile(self, name): 197 return name.startswith('.') 198 199 def FindFilesIn(self, path): 200 result = [] 201 for (root, dirs, files) in os.walk(path): 202 for ignored in [x for x in dirs if self.IgnoreDir(x)]: 203 dirs.remove(ignored) 204 for file in files: 205 if not self.IgnoreFile(file) and self.IsRelevant(file): 206 result.append(join(root, file)) 207 return result 208 209 210class CppLintProcessor(SourceFileProcessor): 211 """ 212 Lint files to check that they follow the google code style. 213 """ 214 215 def IsRelevant(self, name): 216 return name.endswith('.cc') or name.endswith('.h') 217 218 def IgnoreDir(self, name): 219 return (super(CppLintProcessor, self).IgnoreDir(name) 220 or (name == 'third_party')) 221 222 IGNORE_LINT = ['flag-definitions.h'] 223 224 def IgnoreFile(self, name): 225 return (super(CppLintProcessor, self).IgnoreFile(name) 226 or (name in CppLintProcessor.IGNORE_LINT)) 227 228 def GetPathsToSearch(self): 229 return ['src', 'preparser', 'include', 'samples', join('test', 'cctest')] 230 231 def GetCpplintScript(self, prio_path): 232 for path in [prio_path] + os.environ["PATH"].split(os.pathsep): 233 path = path.strip('"') 234 cpplint = os.path.join(path, "cpplint.py") 235 if os.path.isfile(cpplint): 236 return cpplint 237 238 return None 239 240 def ProcessFiles(self, files, path): 241 good_files_cache = FileContentsCache('.cpplint-cache') 242 good_files_cache.Load() 243 files = good_files_cache.FilterUnchangedFiles(files) 244 if len(files) == 0: 245 print 'No changes in files detected. Skipping cpplint check.' 246 return True 247 248 filt = '-,' + ",".join(['+' + n for n in ENABLED_LINT_RULES]) 249 command = [sys.executable, 'cpplint.py', '--filter', filt] 250 cpplint = self.GetCpplintScript(join(path, "tools")) 251 if cpplint is None: 252 print('Could not find cpplint.py. Make sure ' 253 'depot_tools is installed and in the path.') 254 sys.exit(1) 255 256 command = [sys.executable, cpplint, '--filter', filt] 257 258 commands = join([command + [file] for file in files]) 259 count = multiprocessing.cpu_count() 260 pool = multiprocessing.Pool(count) 261 try: 262 results = pool.map_async(CppLintWorker, commands).get(999999) 263 except KeyboardInterrupt: 264 print "\nCaught KeyboardInterrupt, terminating workers." 265 sys.exit(1) 266 267 for i in range(len(files)): 268 if results[i] > 0: 269 good_files_cache.RemoveFile(files[i]) 270 271 total_errors = sum(results) 272 print "Total errors found: %d" % total_errors 273 good_files_cache.Save() 274 return total_errors == 0 275 276 277COPYRIGHT_HEADER_PATTERN = re.compile( 278 r'Copyright [\d-]*20[0-1][0-9] the V8 project authors. All rights reserved.') 279 280class SourceProcessor(SourceFileProcessor): 281 """ 282 Check that all files include a copyright notice and no trailing whitespaces. 283 """ 284 285 RELEVANT_EXTENSIONS = ['.js', '.cc', '.h', '.py', '.c', 'SConscript', 286 'SConstruct', '.status', '.gyp', '.gypi'] 287 288 # Overwriting the one in the parent class. 289 def FindFilesIn(self, path): 290 if os.path.exists(path+'/.git'): 291 output = subprocess.Popen('git ls-files --full-name', 292 stdout=PIPE, cwd=path, shell=True) 293 result = [] 294 for file in output.stdout.read().split(): 295 for dir_part in os.path.dirname(file).split(os.sep): 296 if self.IgnoreDir(dir_part): 297 break 298 else: 299 if self.IsRelevant(file) and not self.IgnoreFile(file): 300 result.append(join(path, file)) 301 if output.wait() == 0: 302 return result 303 return super(SourceProcessor, self).FindFilesIn(path) 304 305 def IsRelevant(self, name): 306 for ext in SourceProcessor.RELEVANT_EXTENSIONS: 307 if name.endswith(ext): 308 return True 309 return False 310 311 def GetPathsToSearch(self): 312 return ['.'] 313 314 def IgnoreDir(self, name): 315 return (super(SourceProcessor, self).IgnoreDir(name) 316 or (name == 'third_party') 317 or (name == 'gyp') 318 or (name == 'out') 319 or (name == 'obj') 320 or (name == 'DerivedSources')) 321 322 IGNORE_COPYRIGHTS = ['cpplint.py', 323 'daemon.py', 324 'earley-boyer.js', 325 'raytrace.js', 326 'crypto.js', 327 'libraries.cc', 328 'libraries-empty.cc', 329 'jsmin.py', 330 'regexp-pcre.js', 331 'gnuplot-4.6.3-emscripten.js'] 332 IGNORE_TABS = IGNORE_COPYRIGHTS + ['unicode-test.js', 'html-comments.js'] 333 334 def EndOfDeclaration(self, line): 335 return line == "}" or line == "};" 336 337 def StartOfDeclaration(self, line): 338 return line.find("//") == 0 or \ 339 line.find("/*") == 0 or \ 340 line.find(") {") != -1 341 342 def ProcessContents(self, name, contents): 343 result = True 344 base = basename(name) 345 if not base in SourceProcessor.IGNORE_TABS: 346 if '\t' in contents: 347 print "%s contains tabs" % name 348 result = False 349 if not base in SourceProcessor.IGNORE_COPYRIGHTS: 350 if not COPYRIGHT_HEADER_PATTERN.search(contents): 351 print "%s is missing a correct copyright header." % name 352 result = False 353 if ' \n' in contents or contents.endswith(' '): 354 line = 0 355 lines = [] 356 parts = contents.split(' \n') 357 if not contents.endswith(' '): 358 parts.pop() 359 for part in parts: 360 line += part.count('\n') + 1 361 lines.append(str(line)) 362 linenumbers = ', '.join(lines) 363 if len(lines) > 1: 364 print "%s has trailing whitespaces in lines %s." % (name, linenumbers) 365 else: 366 print "%s has trailing whitespaces in line %s." % (name, linenumbers) 367 result = False 368 # Check two empty lines between declarations. 369 if name.endswith(".cc"): 370 line = 0 371 lines = [] 372 parts = contents.split('\n') 373 while line < len(parts) - 2: 374 if self.EndOfDeclaration(parts[line]): 375 if self.StartOfDeclaration(parts[line + 1]): 376 lines.append(str(line + 1)) 377 line += 1 378 elif parts[line + 1] == "" and \ 379 self.StartOfDeclaration(parts[line + 2]): 380 lines.append(str(line + 1)) 381 line += 2 382 line += 1 383 if len(lines) >= 1: 384 linenumbers = ', '.join(lines) 385 if len(lines) > 1: 386 print "%s does not have two empty lines between declarations " \ 387 "in lines %s." % (name, linenumbers) 388 else: 389 print "%s does not have two empty lines between declarations " \ 390 "in line %s." % (name, linenumbers) 391 result = False 392 return result 393 394 def ProcessFiles(self, files, path): 395 success = True 396 violations = 0 397 for file in files: 398 try: 399 handle = open(file) 400 contents = handle.read() 401 if not self.ProcessContents(file, contents): 402 success = False 403 violations += 1 404 finally: 405 handle.close() 406 print "Total violating files: %s" % violations 407 return success 408 409 410def GetOptions(): 411 result = optparse.OptionParser() 412 result.add_option('--no-lint', help="Do not run cpplint", default=False, 413 action="store_true") 414 return result 415 416 417def Main(): 418 workspace = abspath(join(dirname(sys.argv[0]), '..')) 419 parser = GetOptions() 420 (options, args) = parser.parse_args() 421 success = True 422 print "Running C++ lint check..." 423 if not options.no_lint: 424 success = CppLintProcessor().Run(workspace) and success 425 print "Running copyright header, trailing whitespaces and " \ 426 "two empty lines between declarations check..." 427 success = SourceProcessor().Run(workspace) and success 428 if success: 429 return 0 430 else: 431 return 1 432 433 434if __name__ == '__main__': 435 sys.exit(Main()) 436