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