xref: /third_party/vixl/tools/lint.py (revision b8021494)
1#!/usr/bin/env python3
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 printer
42import util
43
44
45# Catch SIGINT to gracefully exit when ctrl+C is pressed.
46def sigint_handler(signal, frame):
47  sys.exit(1)
48signal.signal(signal.SIGINT, sigint_handler)
49
50def BuildOptions():
51  parser = argparse.ArgumentParser(
52      description =
53      '''This tool lints C++ files and produces a summary of the errors found.
54      If no files are provided on the command-line, all C++ source files are
55      processed, except for the test traces.
56      Results are cached to speed up the process.
57      ''',
58      # Print default values.
59      formatter_class=argparse.ArgumentDefaultsHelpFormatter)
60  parser.add_argument('files', nargs = '*')
61  parser.add_argument('--jobs', '-j', metavar='N', type=int, nargs='?',
62                      default=multiprocessing.cpu_count(),
63                      const=multiprocessing.cpu_count(),
64                      help='''Runs the tests using N jobs. If the option is set
65                      but no value is provided, the script will use as many jobs
66                      as it thinks useful.''')
67  parser.add_argument('--no-cache',
68                      action='store_true', default=False,
69                      help='Do not use cached lint results.')
70  return parser.parse_args()
71
72
73
74# Returns a tuple (filename, number of lint errors).
75def Lint(filename, progress_prefix = ''):
76  command = ['cpplint.py', filename]
77  process = subprocess.Popen(command,
78                             stdout=subprocess.PIPE,
79                             stderr=subprocess.STDOUT)
80
81  outerr, _ = process.communicate()
82
83  if process.returncode == 0:
84    printer.PrintOverwritableLine(
85      progress_prefix + "Done processing %s" % filename,
86      type = printer.LINE_TYPE_LINTER)
87    return (filename, 0)
88
89  if progress_prefix:
90    outerr = re.sub('^', progress_prefix, outerr, flags=re.MULTILINE)
91  printer.Print(outerr)
92
93  # Find the number of errors in this file.
94  res = re.search('Total errors found: (\d+)', outerr)
95  if res:
96    n_errors_str = res.string[res.start(1):res.end(1)]
97    n_errors = int(n_errors_str)
98  else:
99    print("Couldn't parse cpplint.py output.")
100    n_errors = -1
101
102  return (filename, n_errors)
103
104
105# The multiprocessing map_async function does not allow passing multiple
106# arguments directly, so use a wrapper.
107def LintWrapper(args):
108  # Run under a try-catch  to avoid flooding the output when the script is
109  # interrupted from the keyboard with ctrl+C.
110  try:
111    return Lint(*args)
112  except:
113    sys.exit(1)
114
115
116def ShouldLint(filename, cached_results):
117  filename = os.path.realpath(filename)
118  if filename not in cached_results:
119    return True
120  with open(filename, 'rb') as f:
121    file_hash = hashlib.md5(f.read()).hexdigest()
122  return file_hash != cached_results[filename]
123
124
125# Returns the total number of errors found in the files linted.
126# `cached_results` must be a dictionary, with the format:
127#     { 'filename': file_hash, 'other_filename': other_hash, ... }
128# If not `None`, `cached_results` is used to avoid re-linting files, and new
129# results are stored in it.
130def LintFiles(files,
131              jobs = 1,
132              progress_prefix = '',
133              cached_results = None):
134  if not IsCppLintAvailable():
135    print(
136      printer.COLOUR_RED + \
137      ("cpplint.py not found. Please ensure the depot"
138       " tools are installed and in your PATH. See"
139       " http://dev.chromium.org/developers/how-tos/install-depot-tools for"
140       " details.") + \
141      printer.NO_COLOUR)
142    return -1
143
144  # Filter out directories.
145  files = list(filter(os.path.isfile, files))
146
147  # Filter out files for which we have a cached correct result.
148  if cached_results is not None and len(cached_results) != 0:
149    n_input_files = len(files)
150    files = [f for f in files if ShouldLint(f, cached_results)]
151    n_skipped_files = n_input_files - len(files)
152    if n_skipped_files != 0:
153      printer.Print(
154        progress_prefix +
155        'Skipping %d correct files that were already processed.' %
156        n_skipped_files)
157
158  pool = multiprocessing.Pool(jobs)
159  # The '.get(9999999)' is workaround to allow killing the test script with
160  # ctrl+C from the shell. This bug is documented at
161  # http://bugs.python.org/issue8296.
162  tasks = [(f, progress_prefix) for f in files]
163  # Run under a try-catch  to avoid flooding the output when the script is
164  # interrupted from the keyboard with ctrl+C.
165  try:
166    results = pool.map_async(LintWrapper, tasks).get(9999999)
167    pool.close()
168    pool.join()
169  except KeyboardInterrupt:
170    pool.terminate()
171    sys.exit(1)
172
173  n_errors = sum([filename_errors[1] for filename_errors in results])
174
175  if cached_results is not None:
176    for filename, errors in results:
177      if errors == 0:
178        with open(filename, 'rb') as f:
179          filename = os.path.realpath(filename)
180          file_hash = hashlib.md5(f.read()).hexdigest()
181          cached_results[filename] = file_hash
182
183
184  printer.PrintOverwritableLine(
185      progress_prefix + 'Total errors found: %d' % n_errors)
186  printer.EnsureNewLine()
187  return n_errors
188
189
190def IsCppLintAvailable():
191    retcode, unused_output = util.getstatusoutput('which cpplint.py')
192    return retcode == 0
193
194
195CPP_EXT_REGEXP = re.compile('\.(cc|h)$')
196def IsLinterInput(filename):
197  # lint all C++ files.
198  return CPP_EXT_REGEXP.search(filename) != None
199
200
201cached_results_pkl_filename = \
202  os.path.join(config.dir_tools, '.cached_lint_results.pkl')
203
204
205def ReadCachedResults():
206  cached_results = {}
207  if os.path.isfile(cached_results_pkl_filename):
208    with open(cached_results_pkl_filename, 'rb') as pkl_file:
209      cached_results = pickle.load(pkl_file)
210  return cached_results
211
212
213def CacheResults(results):
214  with open(cached_results_pkl_filename, 'wb') as pkl_file:
215    pickle.dump(results, pkl_file)
216
217
218def FilterOutTestTraceHeaders(files):
219  def IsTraceHeader(f):
220    relative_aarch32_traces_path = os.path.relpath(config.dir_aarch32_traces,'.')
221    relative_aarch64_traces_path = os.path.relpath(config.dir_aarch64_traces,'.')
222    return \
223      fnmatch.fnmatch(f, os.path.join(relative_aarch32_traces_path, '*.h')) or \
224      fnmatch.fnmatch(f, os.path.join(relative_aarch64_traces_path, '*.h'))
225  return [f for f in files if not IsTraceHeader(f)]
226
227
228def RunLinter(files, jobs=1, progress_prefix='', cached=True):
229  results = {} if not cached else ReadCachedResults()
230
231  rc = LintFiles(files,
232                 jobs=jobs,
233                 progress_prefix=progress_prefix,
234                 cached_results=results)
235
236  CacheResults(results)
237  return rc
238
239
240if __name__ == '__main__':
241  # Parse the arguments.
242  args = BuildOptions()
243
244  files = args.files or util.get_source_files()
245
246  cached = not args.no_cache
247  retcode = RunLinter(files, jobs=args.jobs, cached=cached)
248
249  sys.exit(retcode)
250