12 ClangTidy Diff Checker
13 ======================
15 This script reads input from a unified diff, runs clang-tidy on all changed
16 files and outputs clang-tidy warnings in changed lines only. This is useful to
17 detect clang-tidy regressions in the lines touched by a specific patch.
18 Example usage for git/svn users:
20 git diff -U0 HEAD^ | clang-tidy-diff.py -p1
21 svn diff --diff-cmd=diff -x-U0 | \
22 clang-tidy-diff.py -fix -checks=-*,modernize-use-override
29 import multiprocessing
44 is_py2 = sys.version[0] ==
'2'
55 command = task_queue.get()
57 proc = subprocess.Popen(command,
58 stdout=subprocess.PIPE,
59 stderr=subprocess.PIPE)
61 if timeout
is not None:
62 watchdog = threading.Timer(timeout, proc.kill)
65 stdout, stderr = proc.communicate()
68 sys.stdout.write(stdout.decode(
'utf-8') +
'\n')
71 sys.stderr.write(stderr.decode(
'utf-8') +
'\n')
73 except Exception
as e:
75 sys.stderr.write(
'Failed: ' + str(e) +
': '.
join(command) +
'\n')
78 if not (timeout
is None or watchdog
is None):
79 if not watchdog.is_alive():
80 sys.stderr.write(
'Terminated by timeout: ' +
81 ' '.
join(command) +
'\n')
83 task_queue.task_done()
87 for _
in range(max_tasks):
88 t = threading.Thread(target=tidy_caller, args=(task_queue, lock, timeout))
94 """Merge all replacement files in a directory into a single file"""
97 mergekey =
"Diagnostics"
99 for replacefile
in glob.iglob(os.path.join(tmpdir,
'*.yaml')):
100 content = yaml.safe_load(open(replacefile,
'r'))
103 merged.extend(content.get(mergekey, []))
110 output = {
'MainSourceFile':
'', mergekey: merged}
111 with open(mergefile,
'w')
as out:
112 yaml.safe_dump(output, out)
115 open(mergefile,
'w').close()
119 parser = argparse.ArgumentParser(description=
120 'Run clang-tidy against changed files, and '
121 'output diagnostics only for modified '
123 parser.add_argument(
'-clang-tidy-binary', metavar=
'PATH',
124 default=
'clang-tidy',
125 help=
'path to clang-tidy binary')
126 parser.add_argument(
'-p', metavar=
'NUM', default=0,
127 help=
'strip the smallest prefix containing P slashes')
128 parser.add_argument(
'-regex', metavar=
'PATTERN', default=
None,
129 help=
'custom pattern selecting file paths to check '
130 '(case sensitive, overrides -iregex)')
131 parser.add_argument(
'-iregex', metavar=
'PATTERN', default=
132 r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)',
133 help=
'custom pattern selecting file paths to check '
134 '(case insensitive, overridden by -regex)')
135 parser.add_argument(
'-j', type=int, default=1,
136 help=
'number of tidy instances to be run in parallel.')
137 parser.add_argument(
'-timeout', type=int, default=
None,
138 help=
'timeout per each file in seconds.')
139 parser.add_argument(
'-fix', action=
'store_true', default=
False,
140 help=
'apply suggested fixes')
141 parser.add_argument(
'-checks',
142 help=
'checks filter, when not specified, use clang-tidy '
145 parser.add_argument(
'-path', dest=
'build_path',
146 help=
'Path used to read a compile command database.')
148 parser.add_argument(
'-export-fixes', metavar=
'FILE', dest=
'export_fixes',
149 help=
'Create a yaml file to store suggested fixes in, '
150 'which can be applied with clang-apply-replacements.')
151 parser.add_argument(
'-extra-arg', dest=
'extra_arg',
152 action=
'append', default=[],
153 help=
'Additional argument to append to the compiler '
155 parser.add_argument(
'-extra-arg-before', dest=
'extra_arg_before',
156 action=
'append', default=[],
157 help=
'Additional argument to prepend to the compiler '
159 parser.add_argument(
'-quiet', action=
'store_true', default=
False,
160 help=
'Run clang-tidy in quiet mode')
164 clang_tidy_args.extend(argv[argv.index(
'--'):])
165 argv = argv[:argv.index(
'--')]
167 args = parser.parse_args(argv)
172 for line
in sys.stdin:
173 match = re.search(
'^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line)
175 filename = match.group(2)
179 if args.regex
is not None:
180 if not re.match(
'^%s$' % args.regex, filename):
183 if not re.match(
'^%s$' % args.iregex, filename, re.IGNORECASE):
186 match = re.search(
'^@@.*\+(\d+)(,(\d+))?', line)
188 start_line = int(match.group(1))
191 line_count = int(match.group(3))
194 end_line = start_line + line_count - 1
195 lines_by_file.setdefault(filename, []).append([start_line, end_line])
197 if not any(lines_by_file):
198 print(
"No relevant changes found.")
201 max_task_count = args.j
202 if max_task_count == 0:
203 max_task_count = multiprocessing.cpu_count()
204 max_task_count = min(len(lines_by_file), max_task_count)
207 if yaml
and args.export_fixes:
208 tmpdir = tempfile.mkdtemp()
211 task_queue = queue.Queue(max_task_count)
213 lock = threading.Lock()
216 start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout)
219 common_clang_tidy_args = []
221 common_clang_tidy_args.append(
'-fix')
222 if args.checks !=
'':
223 common_clang_tidy_args.append(
'-checks=' + args.checks)
225 common_clang_tidy_args.append(
'-quiet')
226 if args.build_path
is not None:
227 common_clang_tidy_args.append(
'-p=%s' % args.build_path)
228 for arg
in args.extra_arg:
229 common_clang_tidy_args.append(
'-extra-arg=%s' % arg)
230 for arg
in args.extra_arg_before:
231 common_clang_tidy_args.append(
'-extra-arg-before=%s' % arg)
233 for name
in lines_by_file:
234 line_filter_json = json.dumps(
235 [{
"name": name,
"lines": lines_by_file[name]}],
236 separators=(
',',
':'))
239 command = [args.clang_tidy_binary]
240 command.append(
'-line-filter=' + line_filter_json)
241 if yaml
and args.export_fixes:
244 (handle, tmp_name) = tempfile.mkstemp(suffix=
'.yaml', dir=tmpdir)
246 command.append(
'-export-fixes=' + tmp_name)
247 command.extend(common_clang_tidy_args)
249 command.extend(clang_tidy_args)
251 task_queue.put(command)
256 if yaml
and args.export_fixes:
257 print(
'Writing fixes to ' + args.export_fixes +
' ...')
261 sys.stderr.write(
'Error exporting fixes.\n')
262 traceback.print_exc()
265 shutil.rmtree(tmpdir)
268 if __name__ ==
'__main__':