3 A driver script to run clang-tidy on changes detected via git. 5 By default, clang-tidy runs on all files you point it at. This means that even 6 if you changed only parts of that file, you will get warnings for the whole 7 file. This script has the ability to ask git for the exact lines that have 8 changed since a particular git revision, and makes clang-tidy only lint those. 9 This makes it much less overhead to integrate in CI and much more relevant to 10 developers. This git-enabled mode is optional, and full scans of a directory 11 tree are also possible. In both cases, the script allows filtering files via 12 glob or regular expressions. 27 from shlex
import quote
29 from pipes
import quote
31 Patterns = collections.namedtuple(
"Patterns",
"positive, negative")
37 DEFAULT_FILE_PATTERN = re.compile(
r".*\.c(c|pp)?")
40 CHUNK_PATTERN =
r"^@@\s+-\d+,\d+\s+\+(\d+)(?:,(\d+))?\s+@@" 47 """Executes a shell command.""" 49 print(
" ".join(arguments))
51 output = subprocess.check_output(arguments).decode().strip()
52 except subprocess.CalledProcessError:
53 _, error, _ = sys.exc_info()
54 error_output = error.output.decode().strip()
55 raise RuntimeError(
"Error executing {}: {}".format(
" ".join(arguments), error_output))
61 """Separates negative patterns (that start with a dash) from positive patterns""" 62 positive, negative = [], []
63 for pattern
in patterns:
64 if pattern.startswith(
"-"):
65 negative.append(pattern[1:])
67 positive.append(pattern)
69 return Patterns(positive, negative)
73 """Returns a list of compiled regex objects from globs and regex pattern strings.""" 79 positive_regexes = regexes.positive + [fnmatch.translate(g)
for g
in glob.positive]
80 negative_regexes = regexes.negative + [fnmatch.translate(g)
for g
in glob.negative]
82 positive_patterns = [re.compile(regex)
for regex
in positive_regexes]
or [
85 negative_patterns = [re.compile(regex)
for regex
in negative_regexes]
87 return Patterns(positive_patterns, negative_patterns)
91 """Returns all files that match any of the patterns.""" 93 print(
"Filtering with these file patterns: {}".format(file_patterns))
95 if not any(n.match(file)
for n
in file_patterns.negative):
96 if any(p.match(file)
for p
in file_patterns.positive):
100 print(
"{} ommitted due to file filters".format(file))
104 """Runs git diff to get the paths of all changed files.""" 107 command =
"git diff-index --diff-filter=AMU --ignore-all-space --name-only" 109 return output.split(
"\n")
113 """Returns all files that are tracked by git in the given paths.""" 115 return output.split(
"\n")
119 """Runs git diff to get the line ranges of all file changes.""" 120 command = shlex.split(
"git diff-index --unified=0") + [revision, filename]
123 for chunk
in re.finditer(CHUNK_PATTERN, output, re.MULTILINE):
124 start = int(chunk.group(1))
125 count = int(chunk.group(2)
or 1)
126 changed_lines.append([start, start + count])
128 return {
"name": filename,
"lines": changed_lines}
133 description = Running clang-tidy 145 """runs all the commands in parallel with ninja, commands is a List[List[str]]""" 146 build_entries = [build_template.format(i=i, cmd=
' '.join([quote(s)
for s
in command]))
147 for i, command
in enumerate(commands)]
149 file_contents = ninja_template.format(build_rules=
'\n'.join(build_entries)).encode()
150 f = tempfile.NamedTemporaryFile(delete=
False)
152 f.write(file_contents)
160 """Executes the actual clang-tidy command in the shell.""" 161 command = [options.clang_tidy_exe,
"-p", options.compile_commands_dir]
162 if not options.config_file
and os.path.exists(
".clang-tidy"):
163 options.config_file =
".clang-tidy" 164 if options.config_file:
167 with open(options.config_file)
as config:
169 command += [
"-config", json.dumps(yaml.load(config))]
170 command += options.extra_args
173 command += [
"-line-filter", json.dumps(line_filters)]
176 commands = [list(command) + [f]
for f
in files]
181 command = [re.sub(
r"^([{[].*[]}])$",
r"'\1'", arg)
for arg
in command]
182 return " ".join(command)
186 if not options.keep_going
and "[clang-diagnostic-error]" in output:
187 message =
"Found clang-diagnostic-errors in clang-tidy output: {}" 188 raise RuntimeError(message.format(output))
194 """Parses the command line options.""" 195 parser = argparse.ArgumentParser(description=
"Run Clang-Tidy (on your Git changes)")
199 default=
"clang-tidy",
200 help=
"Path to clang-tidy executable",
207 help=
"Only lint files that match these glob patterns " 208 "(see documentation for `fnmatch` for supported syntax)." 209 "If a pattern starts with a - the search is negated for that pattern.",
216 help=
"Only lint files that match these regular expressions (from the start of the filename). " 217 "If a pattern starts with a - the search is negated for that pattern.",
221 "--compile-commands-dir",
223 help=
"Path to the folder containing compile_commands.json",
226 "-d",
"--diff", help=
"Git revision to diff against to get changes" 233 help=
"Lint only the given paths (recursively)",
239 help=
"Only show the command to be executed, without running it",
241 parser.add_argument(
"-v",
"--verbose", action=
"store_true", help=
"Verbose output")
244 help=
"Path to a clang-tidy config file. Defaults to '.clang-tidy'.",
250 help=
"Don't error on compiler errors (clang-diagnostic-error)",
256 help=
"Run clang tidy in parallel per-file (requires ninja to be installed).",
259 "extra_args", nargs=
"*", help=
"Extra arguments to forward to clang-tidy" 261 return parser.parse_args()
270 VERBOSE = options.verbose
273 paths = [path.rstrip(
"/")
for path
in options.paths]
283 print(
"No files detected.")
293 if __name__ ==
"__main__":