| #!/usr/bin/env python |
| # Copyright 2017 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Runs clang-tidy on modified files. |
| |
| The tool uses `git diff-index` against the newest parent commit in the upstream |
| branch (or against HEAD if no such commit is found) in order to find the files |
| to be formatted. In result, the tool lints files that are locally modified, |
| staged or touched by any commits introduced on the local branch. |
| """ |
| |
| import argparse |
| import multiprocessing |
| import os |
| import platform |
| import re |
| import subprocess |
| import sys |
| |
| sys.path.append(os.path.dirname(os.path.dirname(__file__))) |
| |
| import git_utils |
| |
| FUCHSIA_ROOT = os.path.dirname( # $root |
| os.path.dirname( # scripts |
| os.path.dirname( # git |
| os.path.abspath(__file__)))) |
| PREBUILT_ROOT = os.path.join(FUCHSIA_ROOT, "prebuilt/third_party") |
| |
| local_os = "linux" |
| if platform.platform().startswith("Darwin"): |
| local_os = "mac" |
| CLANG_TIDY_TOOL = os.path.join(PREBUILT_ROOT, "clang", |
| "%s-x64" % local_os, "bin", |
| "clang-tidy") |
| NINJA_TOOL = os.path.join(PREBUILT_ROOT, "ninja", |
| "%s-x64" % local_os, "ninja") |
| |
| def find_ancestor_with(filepath, relpath): |
| """Returns the lowest ancestor of |filepath| that contains |relpath|.""" |
| cur_dir_path = os.path.abspath(os.path.dirname(filepath)) |
| while True: |
| if os.path.exists(os.path.join(cur_dir_path, relpath)): |
| return cur_dir_path |
| |
| next_dir_path = os.path.dirname(cur_dir_path) |
| if next_dir_path != cur_dir_path: |
| cur_dir_path = next_dir_path |
| else: |
| return None |
| |
| |
| def get_out_dir(args): |
| if args.out_dir: |
| out_dir = args.out_dir |
| |
| if not os.path.isabs(out_dir): |
| out_dir = os.path.join(FUCHSIA_ROOT, out_dir) |
| |
| if not os.path.isdir(out_dir): |
| print out_dir + " is not a directory" |
| sys.exit(-1) |
| return out_dir |
| |
| fuchsia_config_file = os.path.join(FUCHSIA_ROOT, '.fx-build-dir') |
| if os.path.isfile(fuchsia_config_file): |
| fuchsia_config = open(fuchsia_config_file).read() |
| return os.path.join(FUCHSIA_ROOT, fuchsia_config.strip()) |
| |
| print("Couldn't find the output directory, pass --out-dir " + |
| "(absolute or relative to Fuchsia root)") |
| sys.exit(-1) |
| |
| |
| def generate_db(out_dir): |
| cmd = [NINJA_TOOL, "-C", out_dir, "-t", "compdb", "cc", "cxx"] |
| db = subprocess.check_output( |
| cmd, cwd=FUCHSIA_ROOT, universal_newlines=True) |
| |
| # Strip away `gomacc` from the compile commands. This seems to fix problems |
| # with clang-tidy not being able to load system headers. |
| db = re.sub("\"/[\S]+/gomacc ", "\"", db) |
| |
| with open(os.path.join(out_dir, "compile_commands.json"), "w+") as db_file: |
| db_file.write(db) |
| |
| |
| def go(args): |
| out_dir = get_out_dir(args) |
| |
| # generate the compilation database |
| generate_db(out_dir) |
| |
| # Find the files to be checked. |
| if args.all: |
| files = git_utils.get_all_files() |
| else: |
| files = git_utils.get_diff_files() |
| |
| filtered_files = [] |
| for file_path in files: |
| # Skip deleted files. |
| if not os.path.isfile(file_path): |
| if args.verbose: |
| print "skipping " + file_path + " (deleted)" |
| continue |
| |
| # Skip files with parent directories containing .nolint |
| if find_ancestor_with(file_path, ".nolint"): |
| if args.verbose: |
| print "skipping " + file_path + " (.nolint)" |
| continue |
| filtered_files.append(file_path) |
| |
| if args.verbose: |
| print |
| print "Files to be checked:" |
| for file in filtered_files: |
| print " - " + file |
| if not filtered_files: |
| print " (no files)" |
| print |
| |
| # change the working directory to Fuchsia root. |
| os.chdir(FUCHSIA_ROOT) |
| |
| # It's not safe to run in parallel with "--fix", as clang-tidy traverses and |
| # fixes header files, and we might end up with concurrent writes to the same |
| # header file. |
| if args.no_parallel or args.fix: |
| parallel_jobs = 1 |
| else: |
| parallel_jobs = multiprocessing.cpu_count() |
| print("Running " + str(parallel_jobs) + |
| " jobs in parallel, pass --no-parallel to disable") |
| |
| jobs = set() |
| |
| for file_path in filtered_files: |
| _, extension = os.path.splitext(file_path) |
| if extension == ".cc": |
| relpath = os.path.relpath(file_path) |
| cmd = [CLANG_TIDY_TOOL, "-p", out_dir, relpath] |
| if args.checks: |
| cmd.append("-checks=" + args.checks) |
| if args.fix: |
| cmd.append("-fix") |
| if not args.verbose: |
| cmd.append("-quiet") |
| |
| if args.verbose: |
| print "checking " + file_path + ": " + str(cmd) |
| jobs.add(subprocess.Popen(cmd)) |
| if len(jobs) >= parallel_jobs: |
| os.wait() |
| jobs.difference_update( |
| [job for job in jobs if job.poll() is not None]) |
| for job in jobs: |
| if job.poll() is None: |
| job.wait() |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser(description="Lint modified files.") |
| parser.add_argument( |
| "--all", |
| dest="all", |
| action="store_true", |
| default=False, |
| help="process all files in the repo under current working directory") |
| parser.add_argument( |
| "--fix", |
| dest="fix", |
| action="store_true", |
| default=False, |
| help="automatically generate fixes when possible") |
| parser.add_argument("--checks", help="overrides the list of checks to use") |
| parser.add_argument( |
| "--out-dir", |
| help="Output directory, needed to generate compilation db for clang.") |
| parser.add_argument( |
| "--no-parallel", |
| action="store_true", |
| default=False, |
| help="Process one file at a time") |
| parser.add_argument( |
| "--verbose", |
| dest="verbose", |
| action="store_true", |
| default=False, |
| help="tell me what you're doing") |
| args = parser.parse_args() |
| go(args) |
| |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |