xref: /llvm-project/clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py (revision 475440703238ca32adab6c3fe5e0039c3f96d1a5)
1#!/usr/bin/env python3
2#
3# ===- clang-tidy-diff.py - ClangTidy Diff Checker -----------*- python -*--===#
4#
5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6# See https://llvm.org/LICENSE.txt for license information.
7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8#
9# ===-----------------------------------------------------------------------===#
10
11r"""
12ClangTidy Diff Checker
13======================
14
15This script reads input from a unified diff, runs clang-tidy on all changed
16files and outputs clang-tidy warnings in changed lines only. This is useful to
17detect clang-tidy regressions in the lines touched by a specific patch.
18Example usage for git/svn users:
19
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
23
24"""
25
26import argparse
27import glob
28import json
29import multiprocessing
30import os
31import re
32import shutil
33import subprocess
34import sys
35import tempfile
36import threading
37import traceback
38
39try:
40    import yaml
41except ImportError:
42    yaml = None
43
44is_py2 = sys.version[0] == "2"
45
46if is_py2:
47    import Queue as queue
48else:
49    import queue as queue
50
51
52def run_tidy(task_queue, lock, timeout):
53    watchdog = None
54    while True:
55        command = task_queue.get()
56        try:
57            proc = subprocess.Popen(
58                command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
59            )
60
61            if timeout is not None:
62                watchdog = threading.Timer(timeout, proc.kill)
63                watchdog.start()
64
65            stdout, stderr = proc.communicate()
66
67            with lock:
68                sys.stdout.write(stdout.decode("utf-8") + "\n")
69                sys.stdout.flush()
70                if stderr:
71                    sys.stderr.write(stderr.decode("utf-8") + "\n")
72                    sys.stderr.flush()
73        except Exception as e:
74            with lock:
75                sys.stderr.write("Failed: " + str(e) + ": ".join(command) + "\n")
76        finally:
77            with lock:
78                if not (timeout is None or watchdog is None):
79                    if not watchdog.is_alive():
80                        sys.stderr.write(
81                            "Terminated by timeout: " + " ".join(command) + "\n"
82                        )
83                    watchdog.cancel()
84            task_queue.task_done()
85
86
87def start_workers(max_tasks, tidy_caller, task_queue, lock, timeout):
88    for _ in range(max_tasks):
89        t = threading.Thread(target=tidy_caller, args=(task_queue, lock, timeout))
90        t.daemon = True
91        t.start()
92
93
94def merge_replacement_files(tmpdir, mergefile):
95    """Merge all replacement files in a directory into a single file"""
96    # The fixes suggested by clang-tidy >= 4.0.0 are given under
97    # the top level key 'Diagnostics' in the output yaml files
98    mergekey = "Diagnostics"
99    merged = []
100    for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
101        content = yaml.safe_load(open(replacefile, "r"))
102        if not content:
103            continue  # Skip empty files.
104        merged.extend(content.get(mergekey, []))
105
106    if merged:
107        # MainSourceFile: The key is required by the definition inside
108        # include/clang/Tooling/ReplacementsYaml.h, but the value
109        # is actually never used inside clang-apply-replacements,
110        # so we set it to '' here.
111        output = {"MainSourceFile": "", mergekey: merged}
112        with open(mergefile, "w") as out:
113            yaml.safe_dump(output, out)
114    else:
115        # Empty the file:
116        open(mergefile, "w").close()
117
118
119def main():
120    parser = argparse.ArgumentParser(
121        description="Run clang-tidy against changed files, and "
122        "output diagnostics only for modified "
123        "lines."
124    )
125    parser.add_argument(
126        "-clang-tidy-binary",
127        metavar="PATH",
128        default="clang-tidy",
129        help="path to clang-tidy binary",
130    )
131    parser.add_argument(
132        "-p",
133        metavar="NUM",
134        default=0,
135        help="strip the smallest prefix containing P slashes",
136    )
137    parser.add_argument(
138        "-regex",
139        metavar="PATTERN",
140        default=None,
141        help="custom pattern selecting file paths to check "
142        "(case sensitive, overrides -iregex)",
143    )
144    parser.add_argument(
145        "-iregex",
146        metavar="PATTERN",
147        default=r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)",
148        help="custom pattern selecting file paths to check "
149        "(case insensitive, overridden by -regex)",
150    )
151    parser.add_argument(
152        "-j",
153        type=int,
154        default=1,
155        help="number of tidy instances to be run in parallel.",
156    )
157    parser.add_argument(
158        "-timeout", type=int, default=None, help="timeout per each file in seconds."
159    )
160    parser.add_argument(
161        "-fix", action="store_true", default=False, help="apply suggested fixes"
162    )
163    parser.add_argument(
164        "-checks",
165        help="checks filter, when not specified, use clang-tidy " "default",
166        default="",
167    )
168    parser.add_argument("-use-color", action="store_true", help="Use colors in output")
169    parser.add_argument(
170        "-path", dest="build_path", help="Path used to read a compile command database."
171    )
172    if yaml:
173        parser.add_argument(
174            "-export-fixes",
175            metavar="FILE",
176            dest="export_fixes",
177            help="Create a yaml file to store suggested fixes in, "
178            "which can be applied with clang-apply-replacements.",
179        )
180    parser.add_argument(
181        "-extra-arg",
182        dest="extra_arg",
183        action="append",
184        default=[],
185        help="Additional argument to append to the compiler " "command line.",
186    )
187    parser.add_argument(
188        "-extra-arg-before",
189        dest="extra_arg_before",
190        action="append",
191        default=[],
192        help="Additional argument to prepend to the compiler " "command line.",
193    )
194    parser.add_argument(
195        "-quiet",
196        action="store_true",
197        default=False,
198        help="Run clang-tidy in quiet mode",
199    )
200    parser.add_argument(
201        "-load",
202        dest="plugins",
203        action="append",
204        default=[],
205        help="Load the specified plugin in clang-tidy.",
206    )
207
208    clang_tidy_args = []
209    argv = sys.argv[1:]
210    if "--" in argv:
211        clang_tidy_args.extend(argv[argv.index("--") :])
212        argv = argv[: argv.index("--")]
213
214    args = parser.parse_args(argv)
215
216    # Extract changed lines for each file.
217    filename = None
218    lines_by_file = {}
219    for line in sys.stdin:
220        match = re.search('^\+\+\+\ "?(.*?/){%s}([^ \t\n"]*)' % args.p, line)
221        if match:
222            filename = match.group(2)
223        if filename is None:
224            continue
225
226        if args.regex is not None:
227            if not re.match("^%s$" % args.regex, filename):
228                continue
229        else:
230            if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE):
231                continue
232
233        match = re.search("^@@.*\+(\d+)(,(\d+))?", line)
234        if match:
235            start_line = int(match.group(1))
236            line_count = 1
237            if match.group(3):
238                line_count = int(match.group(3))
239            if line_count == 0:
240                continue
241            end_line = start_line + line_count - 1
242            lines_by_file.setdefault(filename, []).append([start_line, end_line])
243
244    if not any(lines_by_file):
245        print("No relevant changes found.")
246        sys.exit(0)
247
248    max_task_count = args.j
249    if max_task_count == 0:
250        max_task_count = multiprocessing.cpu_count()
251    max_task_count = min(len(lines_by_file), max_task_count)
252
253    tmpdir = None
254    if yaml and args.export_fixes:
255        tmpdir = tempfile.mkdtemp()
256
257    # Tasks for clang-tidy.
258    task_queue = queue.Queue(max_task_count)
259    # A lock for console output.
260    lock = threading.Lock()
261
262    # Run a pool of clang-tidy workers.
263    start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout)
264
265    # Form the common args list.
266    common_clang_tidy_args = []
267    if args.fix:
268        common_clang_tidy_args.append("-fix")
269    if args.checks != "":
270        common_clang_tidy_args.append("-checks=" + args.checks)
271    if args.quiet:
272        common_clang_tidy_args.append("-quiet")
273    if args.build_path is not None:
274        common_clang_tidy_args.append("-p=%s" % args.build_path)
275    if args.use_color:
276        common_clang_tidy_args.append("--use-color")
277    for arg in args.extra_arg:
278        common_clang_tidy_args.append("-extra-arg=%s" % arg)
279    for arg in args.extra_arg_before:
280        common_clang_tidy_args.append("-extra-arg-before=%s" % arg)
281    for plugin in args.plugins:
282        common_clang_tidy_args.append("-load=%s" % plugin)
283
284    for name in lines_by_file:
285        line_filter_json = json.dumps(
286            [{"name": name, "lines": lines_by_file[name]}], separators=(",", ":")
287        )
288
289        # Run clang-tidy on files containing changes.
290        command = [args.clang_tidy_binary]
291        command.append("-line-filter=" + line_filter_json)
292        if yaml and args.export_fixes:
293            # Get a temporary file. We immediately close the handle so clang-tidy can
294            # overwrite it.
295            (handle, tmp_name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir)
296            os.close(handle)
297            command.append("-export-fixes=" + tmp_name)
298        command.extend(common_clang_tidy_args)
299        command.append(name)
300        command.extend(clang_tidy_args)
301
302        task_queue.put(command)
303
304    # Wait for all threads to be done.
305    task_queue.join()
306
307    if yaml and args.export_fixes:
308        print("Writing fixes to " + args.export_fixes + " ...")
309        try:
310            merge_replacement_files(tmpdir, args.export_fixes)
311        except:
312            sys.stderr.write("Error exporting fixes.\n")
313            traceback.print_exc()
314
315    if tmpdir:
316        shutil.rmtree(tmpdir)
317
318
319if __name__ == "__main__":
320    main()
321