• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python2.7
2
3# Copyright 2015, VIXL authors
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8#
9#   * Redistributions of source code must retain the above copyright notice,
10#     this list of conditions and the following disclaimer.
11#   * Redistributions in binary form must reproduce the above copyright notice,
12#     this list of conditions and the following disclaimer in the documentation
13#     and/or other materials provided with the distribution.
14#   * Neither the name of ARM Limited nor the names of its contributors may be
15#     used to endorse or promote products derived from this software without
16#     specific prior written permission.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS CONTRIBUTORS "AS IS" AND
19# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
22# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29import argparse
30import fnmatch
31import hashlib
32import multiprocessing
33import os
34import pickle
35import re
36import signal
37import subprocess
38import sys
39
40import config
41import git
42import printer
43import util
44
45
46# Catch SIGINT to gracefully exit when ctrl+C is pressed.
47def sigint_handler(signal, frame):
48  sys.exit(1)
49signal.signal(signal.SIGINT, sigint_handler)
50
51def BuildOptions():
52  parser = argparse.ArgumentParser(
53      description =
54      '''This tool lints C++ files and produces a summary of the errors found.
55      If no files are provided on the command-line, all C++ source files are
56      processed, except for the test traces.
57      Results are cached to speed up the process.
58      ''',
59      # Print default values.
60      formatter_class=argparse.ArgumentDefaultsHelpFormatter)
61  parser.add_argument('files', nargs = '*')
62  parser.add_argument('--jobs', '-j', metavar='N', type=int, nargs='?',
63                      default=multiprocessing.cpu_count(),
64                      const=multiprocessing.cpu_count(),
65                      help='''Runs the tests using N jobs. If the option is set
66                      but no value is provided, the script will use as many jobs
67                      as it thinks useful.''')
68  parser.add_argument('--no-cache',
69                      action='store_true', default=False,
70                      help='Do not use cached lint results.')
71  return parser.parse_args()
72
73
74
75# Returns a tuple (filename, number of lint errors).
76def Lint(filename, progress_prefix = ''):
77  command = ['cpplint.py', filename]
78  process = subprocess.Popen(command,
79                             stdout=subprocess.PIPE,
80                             stderr=subprocess.STDOUT)
81
82  outerr, _ = process.communicate()
83
84  if process.returncode == 0:
85    printer.PrintOverwritableLine(
86      progress_prefix + "Done processing %s" % filename,
87      type = printer.LINE_TYPE_LINTER)
88    return (filename, 0)
89
90  if progress_prefix:
91    outerr = re.sub('^', progress_prefix, outerr, flags=re.MULTILINE)
92  printer.Print(outerr)
93
94  # Find the number of errors in this file.
95  res = re.search('Total errors found: (\d+)', outerr)
96  if res:
97    n_errors_str = res.string[res.start(1):res.end(1)]
98    n_errors = int(n_errors_str)
99  else:
100    print("Couldn't parse cpplint.py output.")
101    n_errors = -1
102
103  return (filename, n_errors)
104
105
106# The multiprocessing map_async function does not allow passing multiple
107# arguments directly, so use a wrapper.
108def LintWrapper(args):
109  # Run under a try-catch  to avoid flooding the output when the script is
110  # interrupted from the keyboard with ctrl+C.
111  try:
112    return Lint(*args)
113  except:
114    sys.exit(1)
115
116
117def ShouldLint(filename, cached_results):
118  filename = os.path.realpath(filename)
119  if filename not in cached_results:
120    return True
121  with open(filename, 'rb') as f:
122    file_hash = hashlib.md5(f.read()).hexdigest()
123  return file_hash != cached_results[filename]
124
125
126# Returns the total number of errors found in the files linted.
127# `cached_results` must be a dictionary, with the format:
128#     { 'filename': file_hash, 'other_filename': other_hash, ... }
129# If not `None`, `cached_results` is used to avoid re-linting files, and new
130# results are stored in it.
131def LintFiles(files,
132              jobs = 1,
133              progress_prefix = '',
134              cached_results = None):
135  if not IsCppLintAvailable():
136    print(
137      printer.COLOUR_RED + \
138      ("cpplint.py not found. Please ensure the depot"
139       " tools are installed and in your PATH. See"
140       " http://dev.chromium.org/developers/how-tos/install-depot-tools for"
141       " details.") + \
142      printer.NO_COLOUR)
143    return -1
144
145  # Filter out directories.
146  files = filter(os.path.isfile, files)
147
148  # Filter out files for which we have a cached correct result.
149  if cached_results is not None and len(cached_results) != 0:
150    n_input_files = len(files)
151    files = filter(lambda f: ShouldLint(f, cached_results), files)
152    n_skipped_files = n_input_files - len(files)
153    if n_skipped_files != 0:
154      printer.Print(
155        progress_prefix +
156        'Skipping %d correct files that were already processed.' %
157        n_skipped_files)
158
159  pool = multiprocessing.Pool(jobs)
160  # The '.get(9999999)' is workaround to allow killing the test script with
161  # ctrl+C from the shell. This bug is documented at
162  # http://bugs.python.org/issue8296.
163  tasks = [(f, progress_prefix) for f in files]
164  # Run under a try-catch  to avoid flooding the output when the script is
165  # interrupted from the keyboard with ctrl+C.
166  try:
167    results = pool.map_async(LintWrapper, tasks).get(9999999)
168    pool.close()
169    pool.join()
170  except KeyboardInterrupt:
171    pool.terminate()
172    sys.exit(1)
173
174  n_errors = sum(map(lambda (filename, errors): errors, results))
175
176  if cached_results is not None:
177    for filename, errors in results:
178      if errors == 0:
179        with open(filename, 'rb') as f:
180          filename = os.path.realpath(filename)
181          file_hash = hashlib.md5(f.read()).hexdigest()
182          cached_results[filename] = file_hash
183
184
185  printer.PrintOverwritableLine(
186      progress_prefix + 'Total errors found: %d' % n_errors)
187  printer.EnsureNewLine()
188  return n_errors
189
190
191def IsCppLintAvailable():
192    retcode, unused_output = util.getstatusoutput('which cpplint.py')
193    return retcode == 0
194
195
196CPP_EXT_REGEXP = re.compile('\.(cc|h)$')
197def IsLinterInput(filename):
198  # lint all C++ files.
199  return CPP_EXT_REGEXP.search(filename) != None
200
201
202cached_results_pkl_filename = \
203  os.path.join(config.dir_tools, '.cached_lint_results.pkl')
204
205
206def ReadCachedResults():
207  cached_results = {}
208  if os.path.isfile(cached_results_pkl_filename):
209    with open(cached_results_pkl_filename, 'rb') as pkl_file:
210      cached_results = pickle.load(pkl_file)
211  return cached_results
212
213
214def CacheResults(results):
215  with open(cached_results_pkl_filename, 'wb') as pkl_file:
216    pickle.dump(results, pkl_file)
217
218
219def FilterOutTestTraceHeaders(files):
220  def IsTraceHeader(f):
221    relative_aarch32_traces_path = os.path.relpath(config.dir_aarch32_traces,'.')
222    relative_aarch64_traces_path = os.path.relpath(config.dir_aarch64_traces,'.')
223    return \
224      fnmatch.fnmatch(f, os.path.join(relative_aarch32_traces_path, '*.h')) or \
225      fnmatch.fnmatch(f, os.path.join(relative_aarch64_traces_path, '*.h'))
226  return filter(lambda f: not IsTraceHeader(f), files)
227
228
229def RunLinter(files, jobs=1, progress_prefix='', cached=True):
230  results = {} if not cached else ReadCachedResults()
231
232  rc = LintFiles(files,
233                 jobs=jobs,
234                 progress_prefix=progress_prefix,
235                 cached_results=results)
236
237  CacheResults(results)
238  return rc
239
240
241if __name__ == '__main__':
242  # Parse the arguments.
243  args = BuildOptions()
244
245  files = args.files or util.get_source_files()
246
247  cached = not args.no_cache
248  retcode = RunLinter(files, jobs=args.jobs, cached=cached)
249
250  sys.exit(retcode)
251