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 in the
56      repository are processed.
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  n_errors_str = res.string[res.start(1):res.end(1)]
97  n_errors = int(n_errors_str)
98
99  return (filename, n_errors)
100
101
102# The multiprocessing map_async function does not allow passing multiple
103# arguments directly, so use a wrapper.
104def LintWrapper(args):
105  # Run under a try-catch  to avoid flooding the output when the script is
106  # interrupted from the keyboard with ctrl+C.
107  try:
108    return Lint(*args)
109  except:
110    sys.exit(1)
111
112
113def ShouldLint(filename, cached_results):
114  filename = os.path.realpath(filename)
115  if filename not in cached_results:
116    return True
117  with open(filename, 'rb') as f:
118    file_hash = hashlib.md5(f.read()).hexdigest()
119  return file_hash != cached_results[filename]
120
121
122# Returns the total number of errors found in the files linted.
123# `cached_results` must be a dictionary, with the format:
124#     { 'filename': file_hash, 'other_filename': other_hash, ... }
125# If not `None`, `cached_results` is used to avoid re-linting files, and new
126# results are stored in it.
127def LintFiles(files,
128              jobs = 1,
129              progress_prefix = '',
130              cached_results = None):
131  if not IsCppLintAvailable():
132    print(
133      printer.COLOUR_RED + \
134      ("cpplint.py not found. Please ensure the depot"
135       " tools are installed and in your PATH. See"
136       " http://dev.chromium.org/developers/how-tos/install-depot-tools for"
137       " details.") + \
138      printer.NO_COLOUR)
139    return -1
140
141  # Filter out directories.
142  files = filter(os.path.isfile, files)
143
144  # Filter out files for which we have a cached correct result.
145  if cached_results is not None and len(cached_results) != 0:
146    n_input_files = len(files)
147    files = filter(lambda f: ShouldLint(f, cached_results), files)
148    n_skipped_files = n_input_files - len(files)
149    if n_skipped_files != 0:
150      printer.Print(
151        progress_prefix +
152        'Skipping %d correct files that were already processed.' %
153        n_skipped_files)
154
155  pool = multiprocessing.Pool(jobs)
156  # The '.get(9999999)' is workaround to allow killing the test script with
157  # ctrl+C from the shell. This bug is documented at
158  # http://bugs.python.org/issue8296.
159  tasks = [(f, progress_prefix) for f in files]
160  # Run under a try-catch  to avoid flooding the output when the script is
161  # interrupted from the keyboard with ctrl+C.
162  try:
163    results = pool.map_async(LintWrapper, tasks).get(9999999)
164    pool.close()
165    pool.join()
166  except KeyboardInterrupt:
167    pool.terminate()
168    sys.exit(1)
169
170  n_errors = sum(map(lambda (filename, errors): errors, results))
171
172  if cached_results is not None:
173    for filename, errors in results:
174      if errors == 0:
175        with open(filename, 'rb') as f:
176          filename = os.path.realpath(filename)
177          file_hash = hashlib.md5(f.read()).hexdigest()
178          cached_results[filename] = file_hash
179
180
181  printer.PrintOverwritableLine(
182      progress_prefix + 'Total errors found: %d' % n_errors)
183  printer.EnsureNewLine()
184  return n_errors
185
186
187def IsCppLintAvailable():
188    retcode, unused_output = util.getstatusoutput('which cpplint.py')
189    return retcode == 0
190
191
192CPP_EXT_REGEXP = re.compile('\.(cc|h)$')
193def IsLinterInput(filename):
194  # lint all C++ files.
195  return CPP_EXT_REGEXP.search(filename) != None
196
197
198def GetDefaultFilesToLint():
199  if git.is_git_repository_root(config.dir_root):
200    files = git.get_tracked_files().split()
201    files = filter(IsLinterInput, files)
202    files = FilterOutTestTraceHeaders(files)
203    return 0, files
204  else:
205    printer.Print(printer.COLOUR_ORANGE + 'WARNING: This script is not run ' \
206                  'from its Git repository. The linter will not run.' + \
207                  printer.NO_COLOUR)
208    return 1, []
209
210
211cached_results_pkl_filename = \
212  os.path.join(config.dir_tools, '.cached_lint_results.pkl')
213
214
215def ReadCachedResults():
216  cached_results = {}
217  if os.path.isfile(cached_results_pkl_filename):
218    with open(cached_results_pkl_filename, 'rb') as pkl_file:
219      cached_results = pickle.load(pkl_file)
220  return cached_results
221
222
223def CacheResults(results):
224  with open(cached_results_pkl_filename, 'wb') as pkl_file:
225    pickle.dump(results, pkl_file)
226
227
228def FilterOutTestTraceHeaders(files):
229  def IsTraceHeader(f):
230    relative_aarch32_traces_path = os.path.relpath(config.dir_aarch32_traces,'.')
231    relative_aarch64_traces_path = os.path.relpath(config.dir_aarch64_traces,'.')
232    return \
233      fnmatch.fnmatch(f, os.path.join(relative_aarch32_traces_path, '*.h')) or \
234      fnmatch.fnmatch(f, os.path.join(relative_aarch64_traces_path, '*.h'))
235  return filter(lambda f: not IsTraceHeader(f), files)
236
237
238def RunLinter(files, jobs=1, progress_prefix='', cached=True):
239  results = {} if not cached else ReadCachedResults()
240
241  rc = LintFiles(files,
242                 jobs=jobs,
243                 progress_prefix=progress_prefix,
244                 cached_results=results)
245
246  CacheResults(results)
247  return rc
248
249
250if __name__ == '__main__':
251  # Parse the arguments.
252  args = BuildOptions()
253
254  files = args.files
255  if not files:
256    retcode, files = GetDefaultFilesToLint()
257    if retcode:
258      sys.exit(retcode)
259
260  cached = not args.no_cache
261  retcode = RunLinter(files, jobs=args.jobs, cached=cached)
262
263  sys.exit(retcode)
264