• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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        break
118      m = LINT_OUTPUT_PATTERN.match(out_line)
119      if m:
120        out_lines += out_line
121        error_count += 1
122    sys.stderr.write(out_lines)
123    return error_count
124  except KeyboardInterrupt:
125    process.kill()
126  except:
127    print('Error running cpplint.py. Please make sure you have depot_tools' +
128          ' in your $PATH. Lint check skipped.')
129    process.kill()
130
131
132class FileContentsCache(object):
133
134  def __init__(self, sums_file_name):
135    self.sums = {}
136    self.sums_file_name = sums_file_name
137
138  def Load(self):
139    try:
140      sums_file = None
141      try:
142        sums_file = open(self.sums_file_name, 'r')
143        self.sums = pickle.load(sums_file)
144      except IOError:
145        # File might not exist, this is OK.
146        pass
147    finally:
148      if sums_file:
149        sums_file.close()
150
151  def Save(self):
152    try:
153      sums_file = open(self.sums_file_name, 'w')
154      pickle.dump(self.sums, sums_file)
155    finally:
156      sums_file.close()
157
158  def FilterUnchangedFiles(self, files):
159    changed_or_new = []
160    for file in files:
161      try:
162        handle = open(file, "r")
163        file_sum = md5er(handle.read()).digest()
164        if not file in self.sums or self.sums[file] != file_sum:
165          changed_or_new.append(file)
166          self.sums[file] = file_sum
167      finally:
168        handle.close()
169    return changed_or_new
170
171  def RemoveFile(self, file):
172    if file in self.sums:
173      self.sums.pop(file)
174
175
176class SourceFileProcessor(object):
177  """
178  Utility class that can run through a directory structure, find all relevant
179  files and invoke a custom check on the files.
180  """
181
182  def Run(self, path):
183    all_files = []
184    for file in self.GetPathsToSearch():
185      all_files += self.FindFilesIn(join(path, file))
186    if not self.ProcessFiles(all_files, path):
187      return False
188    return True
189
190  def IgnoreDir(self, name):
191    return name.startswith('.') or name == 'data' or name == 'sputniktests'
192
193  def IgnoreFile(self, name):
194    return name.startswith('.')
195
196  def FindFilesIn(self, path):
197    result = []
198    for (root, dirs, files) in os.walk(path):
199      for ignored in [x for x in dirs if self.IgnoreDir(x)]:
200        dirs.remove(ignored)
201      for file in files:
202        if not self.IgnoreFile(file) and self.IsRelevant(file):
203          result.append(join(root, file))
204    return result
205
206
207class CppLintProcessor(SourceFileProcessor):
208  """
209  Lint files to check that they follow the google code style.
210  """
211
212  def IsRelevant(self, name):
213    return name.endswith('.cc') or name.endswith('.h')
214
215  def IgnoreDir(self, name):
216    return (super(CppLintProcessor, self).IgnoreDir(name)
217              or (name == 'third_party'))
218
219  IGNORE_LINT = ['flag-definitions.h']
220
221  def IgnoreFile(self, name):
222    return (super(CppLintProcessor, self).IgnoreFile(name)
223              or (name in CppLintProcessor.IGNORE_LINT))
224
225  def GetPathsToSearch(self):
226    return ['src', 'preparser', 'include', 'samples', join('test', 'cctest')]
227
228  def ProcessFiles(self, files, path):
229    good_files_cache = FileContentsCache('.cpplint-cache')
230    good_files_cache.Load()
231    files = good_files_cache.FilterUnchangedFiles(files)
232    if len(files) == 0:
233      print 'No changes in files detected. Skipping cpplint check.'
234      return True
235
236    filt = '-,' + ",".join(['+' + n for n in ENABLED_LINT_RULES])
237    command = ['cpplint.py', '--filter', filt]
238    local_cpplint = join(path, "tools", "cpplint.py")
239    if exists(local_cpplint):
240      command = ['python', local_cpplint, '--filter', filt]
241
242    commands = join([command + [file] for file in files])
243    count = multiprocessing.cpu_count()
244    pool = multiprocessing.Pool(count)
245    try:
246      results = pool.map_async(CppLintWorker, commands).get(999999)
247    except KeyboardInterrupt:
248      print "\nCaught KeyboardInterrupt, terminating workers."
249      sys.exit(1)
250
251    for i in range(len(files)):
252      if results[i] > 0:
253        good_files_cache.RemoveFile(files[i])
254
255    total_errors = sum(results)
256    print "Total errors found: %d" % total_errors
257    good_files_cache.Save()
258    return total_errors == 0
259
260
261COPYRIGHT_HEADER_PATTERN = re.compile(
262    r'Copyright [\d-]*20[0-1][0-9] the V8 project authors. All rights reserved.')
263
264class SourceProcessor(SourceFileProcessor):
265  """
266  Check that all files include a copyright notice and no trailing whitespaces.
267  """
268
269  RELEVANT_EXTENSIONS = ['.js', '.cc', '.h', '.py', '.c', 'SConscript',
270      'SConstruct', '.status', '.gyp', '.gypi']
271
272  # Overwriting the one in the parent class.
273  def FindFilesIn(self, path):
274    if os.path.exists(path+'/.git'):
275      output = subprocess.Popen('git ls-files --full-name',
276                                stdout=PIPE, cwd=path, shell=True)
277      result = []
278      for file in output.stdout.read().split():
279        for dir_part in os.path.dirname(file).split(os.sep):
280          if self.IgnoreDir(dir_part):
281            break
282        else:
283          if self.IsRelevant(file) and not self.IgnoreFile(file):
284            result.append(join(path, file))
285      if output.wait() == 0:
286        return result
287    return super(SourceProcessor, self).FindFilesIn(path)
288
289  def IsRelevant(self, name):
290    for ext in SourceProcessor.RELEVANT_EXTENSIONS:
291      if name.endswith(ext):
292        return True
293    return False
294
295  def GetPathsToSearch(self):
296    return ['.']
297
298  def IgnoreDir(self, name):
299    return (super(SourceProcessor, self).IgnoreDir(name)
300              or (name == 'third_party')
301              or (name == 'gyp')
302              or (name == 'out')
303              or (name == 'obj'))
304
305  IGNORE_COPYRIGHTS = ['cpplint.py',
306                       'earley-boyer.js',
307                       'raytrace.js',
308                       'crypto.js',
309                       'libraries.cc',
310                       'libraries-empty.cc',
311                       'jsmin.py',
312                       'regexp-pcre.js']
313  IGNORE_TABS = IGNORE_COPYRIGHTS + ['unicode-test.js', 'html-comments.js']
314
315  def ProcessContents(self, name, contents):
316    result = True
317    base = basename(name)
318    if not base in SourceProcessor.IGNORE_TABS:
319      if '\t' in contents:
320        print "%s contains tabs" % name
321        result = False
322    if not base in SourceProcessor.IGNORE_COPYRIGHTS:
323      if not COPYRIGHT_HEADER_PATTERN.search(contents):
324        print "%s is missing a correct copyright header." % name
325        result = False
326    ext = base.split('.').pop()
327    if ' \n' in contents or contents.endswith(' '):
328      line = 0
329      lines = []
330      parts = contents.split(' \n')
331      if not contents.endswith(' '):
332        parts.pop()
333      for part in parts:
334        line += part.count('\n') + 1
335        lines.append(str(line))
336      linenumbers = ', '.join(lines)
337      if len(lines) > 1:
338        print "%s has trailing whitespaces in lines %s." % (name, linenumbers)
339      else:
340        print "%s has trailing whitespaces in line %s." % (name, linenumbers)
341      result = False
342    return result
343
344  def ProcessFiles(self, files, path):
345    success = True
346    violations = 0
347    for file in files:
348      try:
349        handle = open(file)
350        contents = handle.read()
351        if not self.ProcessContents(file, contents):
352          success = False
353          violations += 1
354      finally:
355        handle.close()
356    print "Total violating files: %s" % violations
357    return success
358
359
360def GetOptions():
361  result = optparse.OptionParser()
362  result.add_option('--no-lint', help="Do not run cpplint", default=False,
363                    action="store_true")
364  return result
365
366
367def Main():
368  workspace = abspath(join(dirname(sys.argv[0]), '..'))
369  parser = GetOptions()
370  (options, args) = parser.parse_args()
371  success = True
372  print "Running C++ lint check..."
373  if not options.no_lint:
374    success = CppLintProcessor().Run(workspace) and success
375  print "Running copyright header and trailing whitespaces check..."
376  success = SourceProcessor().Run(workspace) and success
377  if success:
378    return 0
379  else:
380    return 1
381
382
383if __name__ == '__main__':
384  sys.exit(Main())
385