1#!/usr/bin/env python3
2
3# Copyright 2016, 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 multiprocessing
31import os
32import re
33import subprocess
34import sys
35import tempfile
36import shutil
37
38from threaded_tests import Test, TestQueue
39import printer
40import util
41
42CLANG_TOOL_SUPPORTED_VERSIONS = range(11, 16)
43
44DEFAULT_CLANG_FORMAT = 'clang-format'
45
46CLANG_TOOL_VERSION_MATCH = r"(clang-format|LLVM) version ([\d]+)\.[\d]+\.[\d]+.*$"
47
48is_output_redirected = not sys.stdout.isatty()
49
50def BuildOptions():
51  parser = argparse.ArgumentParser(
52    description = '''This tool runs `clang-format` on C++ files.
53    If no files are provided on the command-line, all C++ source files are
54    processed, except for the test traces.
55    When available, `colordiff` is automatically used to colour the output.''',
56    # Print default values.
57    formatter_class = argparse.ArgumentDefaultsHelpFormatter)
58  parser.add_argument('files', nargs = '*')
59  parser.add_argument('--clang-format', default=DEFAULT_CLANG_FORMAT,
60                      help='Path to clang-format.')
61  parser.add_argument('--in-place', '-i',
62                      action = 'store_true', default = False,
63                      help = 'Edit files in place.')
64  parser.add_argument('--jobs', '-j', metavar = 'N', type = int, nargs = '?',
65                      default = multiprocessing.cpu_count(),
66                      const = multiprocessing.cpu_count(),
67                      help = '''Runs the tests using N jobs. If the option is set
68                      but no value is provided, the script will use as many jobs
69                      as it thinks useful.''')
70  return parser.parse_args()
71
72def is_supported(tool):
73  if not shutil.which(tool):
74    return False
75
76  cmd = '%s -version' % tool
77
78  try:
79    rc, version = util.getstatusoutput(cmd)
80  except OSError:
81    return False
82
83  if rc != 0:
84      util.abort("Failed to execute %s: %s" % (cmd, version))
85  m = re.search(CLANG_TOOL_VERSION_MATCH, version, re.MULTILINE)
86  if not m:
87      util.abort("Failed to get clang tool version: %s" % version)
88  _, major = m.groups()
89
90  if int(major) in CLANG_TOOL_SUPPORTED_VERSIONS:
91    return True
92
93  return False
94
95def detect_clang_tool(tool):
96  supported_tools = [tool] + [tool + '-' + str(ver) for ver in CLANG_TOOL_SUPPORTED_VERSIONS]
97  for tool in supported_tools:
98    if is_supported(tool):
99        return tool
100
101  return None
102
103
104def RunTest(test):
105  filename = test.args['filename']
106  clang_format = test.args['clang_format']
107  in_place = test.args['in_place']
108
109  rc = 0
110
111  cmd_format = [clang_format, filename]
112  temp_file, temp_file_name = tempfile.mkstemp(prefix = 'clang_format_')
113  cmd_format_string = '$ ' + ' '.join(cmd_format) + ' > %s' % temp_file_name
114  p_format = subprocess.Popen(cmd_format,
115                              stdout = temp_file, stderr = subprocess.STDOUT)
116
117  rc += p_format.wait()
118
119  cmd_diff = ['diff', '--unified', filename, temp_file_name]
120  cmd_diff_string = '$ ' + ' '.join(cmd_diff)
121
122  p_diff = subprocess.Popen(cmd_diff,
123                            stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
124
125  if util.IsCommandAvailable('colordiff') and not is_output_redirected:
126    p_colordiff = subprocess.Popen(
127            ['colordiff', '--unified'],
128            stdin = p_diff.stdout,
129            stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
130    out, unused = p_colordiff.communicate()
131    rc += p_colordiff.returncode
132  else:
133    out, unused = p_diff.communicate()
134    rc += p_diff.returncode
135
136
137  if in_place:
138      cmd_format = [clang_format, '-i', filename]
139      subprocess.run(cmd_format, stdout=temp_file, stderr=subprocess.STDOUT)
140
141  if rc != 0:
142    with Test.n_tests_failed.get_lock(): Test.n_tests_failed.value += 1
143  else:
144    with Test.n_tests_passed.get_lock(): Test.n_tests_passed.value += 1
145
146  printer.__print_lock__.acquire()
147
148  printer.UpdateProgress(test.shared.start_time,
149                         Test.n_tests_passed.value,
150                         Test.n_tests_failed.value,
151                         test.shared.n_tests,
152                         Test.n_tests_skipped.value,
153                         test.shared.n_known_failures,
154                         test.name,
155                         prevent_next_overwrite = rc != 0,
156                         has_lock = True,
157                         prefix = test.shared.progress_prefix)
158
159  if rc != 0:
160    printer.Print('Incorrectly formatted file: ' + filename + '\n' + \
161                  cmd_format_string + '\n' + \
162                  cmd_diff_string + '\n' + \
163                  out.decode(), has_lock = True)
164
165  printer.__print_lock__.release()
166
167  os.remove(temp_file_name)
168
169# Returns the total number of files incorrectly formatted.
170def ClangFormatFiles(files, clang_format, in_place = False, jobs = 1,
171                     progress_prefix = ''):
172
173  clang_format = detect_clang_tool("clang-format")
174
175  if not clang_format:
176    error_message = "clang-format not found. Please ensure it " \
177                    "is installed, in your PATH and the correct version."
178    print(printer.COLOUR_RED + error_message + printer.NO_COLOUR)
179    return -1
180
181  queue = TestQueue(prefix = progress_prefix)
182  for f in files:
183    queue.AddTest(f, filename = f, clang_format = clang_format, in_place = in_place)
184
185  rc = queue.Run(jobs, True, RunTest)
186
187  printer.PrintOverwritableLine(
188      progress_prefix + '%d files are incorrectly formatted.' % rc,
189      type = printer.LINE_TYPE_LINTER)
190  printer.EnsureNewLine()
191
192  return rc
193
194if __name__ == '__main__':
195  # Parse the arguments.
196  args = BuildOptions()
197  files = args.files or util.get_source_files(exclude_dirs=['.*', '*/traces/*', '*/aarch32/*'])
198
199  rc = ClangFormatFiles(files, clang_format = args.clang_format,
200                        in_place = args.in_place, jobs = args.jobs)
201
202  sys.exit(rc)
203