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