| #!/usr/bin/env python |
| # |
| # Copyright 2018 the V8 project authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # for py2/py3 compatibility |
| from __future__ import print_function |
| |
| import json |
| import multiprocessing |
| import optparse |
| import os |
| import re |
| import subprocess |
| import sys |
| |
| CLANG_TIDY_WARNING = re.compile(r'(\/.*?)\ .*\[(.*)\]$') |
| CLANG_TIDY_CMDLINE_OUT = re.compile(r'^clang-tidy.*\ .*|^\./\.\*') |
| FILE_REGEXS = ['../src/*', '../test/*'] |
| HEADER_REGEX = ['\.\.\/src\/.*|\.\.\/include\/.*|\.\.\/test\/.*'] |
| |
| THREADS = multiprocessing.cpu_count() |
| |
| |
| class ClangTidyWarning(object): |
| """ |
| Wraps up a clang-tidy warning to present aggregated information. |
| """ |
| |
| def __init__(self, warning_type): |
| self.warning_type = warning_type |
| self.occurrences = set() |
| |
| def add_occurrence(self, file_path): |
| self.occurrences.add(file_path.lstrip()) |
| |
| def __hash__(self): |
| return hash(self.warning_type) |
| |
| def to_string(self, file_loc): |
| s = '[%s] #%d\n' % (self.warning_type, len(self.occurrences)) |
| if file_loc: |
| s += ' ' + '\n '.join(self.occurrences) |
| s += '\n' |
| return s |
| |
| def __str__(self): |
| return self.to_string(False) |
| |
| def __lt__(self, other): |
| return len(self.occurrences) < len(other.occurrences) |
| |
| |
| def GenerateCompileCommands(build_folder): |
| """ |
| Generate a compilation database. |
| |
| Currently clang-tidy-4 does not understand all flags that are passed |
| by the build system, therefore, we remove them from the generated file. |
| """ |
| ninja_ps = subprocess.Popen( |
| ['ninja', '-t', 'compdb', 'cxx', 'cc'], |
| stdout=subprocess.PIPE, |
| cwd=build_folder) |
| |
| out_filepath = os.path.join(build_folder, 'compile_commands.json') |
| with open(out_filepath, 'w') as cc_file: |
| while True: |
| line = ninja_ps.stdout.readline() |
| |
| if line == '': |
| break |
| |
| line = line.replace('-fcomplete-member-pointers', '') |
| line = line.replace('-Wno-enum-compare-switch', '') |
| line = line.replace('-Wno-ignored-pragma-optimize', '') |
| line = line.replace('-Wno-null-pointer-arithmetic', '') |
| line = line.replace('-Wno-unused-lambda-capture', '') |
| line = line.replace('-Wno-defaulted-function-deleted', '') |
| cc_file.write(line) |
| |
| |
| def skip_line(line): |
| """ |
| Check if a clang-tidy output line should be skipped. |
| """ |
| return bool(CLANG_TIDY_CMDLINE_OUT.search(line)) |
| |
| |
| def ClangTidyRunFull(build_folder, skip_output_filter, checks, auto_fix): |
| """ |
| Run clang-tidy on the full codebase and print warnings. |
| """ |
| extra_args = [] |
| if auto_fix: |
| extra_args.append('-fix') |
| |
| if checks is not None: |
| extra_args.append('-checks') |
| extra_args.append('-*, ' + checks) |
| |
| with open(os.devnull, 'w') as DEVNULL: |
| ct_process = subprocess.Popen( |
| ['run-clang-tidy', '-j' + str(THREADS), '-p', '.'] |
| + ['-header-filter'] + HEADER_REGEX + extra_args |
| + FILE_REGEXS, |
| cwd=build_folder, |
| stdout=subprocess.PIPE, |
| stderr=DEVNULL) |
| removing_check_header = False |
| empty_lines = 0 |
| |
| while True: |
| line = ct_process.stdout.readline() |
| if line == '': |
| break |
| |
| # Skip all lines after Enbale checks and before two newlines, |
| # i.e., skip clang-tidy check list. |
| if line.startswith('Enabled checks'): |
| removing_check_header = True |
| if removing_check_header and not skip_output_filter: |
| if line == '\n': |
| empty_lines += 1 |
| if empty_lines == 2: |
| removing_check_header = False |
| continue |
| |
| # Different lines get removed to ease output reading. |
| if not skip_output_filter and skip_line(line): |
| continue |
| |
| # Print line, because no filter was matched. |
| if line != '\n': |
| sys.stdout.write(line) |
| |
| |
| def ClangTidyRunAggregate(build_folder, print_files): |
| """ |
| Run clang-tidy on the full codebase and aggregate warnings into categories. |
| """ |
| with open(os.devnull, 'w') as DEVNULL: |
| ct_process = subprocess.Popen( |
| ['run-clang-tidy', '-j' + str(THREADS), '-p', '.'] + |
| ['-header-filter'] + HEADER_REGEX + |
| FILE_REGEXS, |
| cwd=build_folder, |
| stdout=subprocess.PIPE, |
| stderr=DEVNULL) |
| warnings = dict() |
| while True: |
| line = ct_process.stdout.readline() |
| if line == '': |
| break |
| |
| res = CLANG_TIDY_WARNING.search(line) |
| if res is not None: |
| warnings.setdefault( |
| res.group(2), |
| ClangTidyWarning(res.group(2))).add_occurrence(res.group(1)) |
| |
| for warning in sorted(warnings.values(), reverse=True): |
| sys.stdout.write(warning.to_string(print_files)) |
| |
| |
| def ClangTidyRunDiff(build_folder, diff_branch, auto_fix): |
| """ |
| Run clang-tidy on the diff between current and the diff_branch. |
| """ |
| if diff_branch is None: |
| diff_branch = subprocess.check_output(['git', 'merge-base', |
| 'HEAD', 'origin/master']).strip() |
| |
| git_ps = subprocess.Popen( |
| ['git', 'diff', '-U0', diff_branch], stdout=subprocess.PIPE) |
| |
| extra_args = [] |
| if auto_fix: |
| extra_args.append('-fix') |
| |
| with open(os.devnull, 'w') as DEVNULL: |
| """ |
| The script `clang-tidy-diff` does not provide support to add header- |
| filters. To still analyze headers we use the build path option `-path` to |
| inject our header-filter option. This works because the script just adds |
| the passed path string to the commandline of clang-tidy. |
| """ |
| modified_build_folder = build_folder |
| modified_build_folder += ' -header-filter=' |
| modified_build_folder += '\'' + ''.join(HEADER_REGEX) + '\'' |
| |
| ct_ps = subprocess.Popen( |
| ['clang-tidy-diff.py', '-path', modified_build_folder, '-p1'] + |
| extra_args, |
| stdin=git_ps.stdout, |
| stdout=subprocess.PIPE, |
| stderr=DEVNULL) |
| git_ps.wait() |
| while True: |
| line = ct_ps.stdout.readline() |
| if line == '': |
| break |
| |
| if skip_line(line): |
| continue |
| |
| sys.stdout.write(line) |
| |
| |
| def rm_prefix(string, prefix): |
| """ |
| Removes prefix from a string until the new string |
| no longer starts with the prefix. |
| """ |
| while string.startswith(prefix): |
| string = string[len(prefix):] |
| return string |
| |
| |
| def ClangTidyRunSingleFile(build_folder, filename_to_check, auto_fix, |
| line_ranges=[]): |
| """ |
| Run clang-tidy on a single file. |
| """ |
| files_with_relative_path = [] |
| |
| compdb_filepath = os.path.join(build_folder, 'compile_commands.json') |
| with open(compdb_filepath) as raw_json_file: |
| compdb = json.load(raw_json_file) |
| |
| for db_entry in compdb: |
| if db_entry['file'].endswith(filename_to_check): |
| files_with_relative_path.append(db_entry['file']) |
| |
| with open(os.devnull, 'w') as DEVNULL: |
| for file_with_relative_path in files_with_relative_path: |
| line_filter = None |
| if len(line_ranges) != 0: |
| line_filter = '[' |
| line_filter += '{ \"lines\":[' + ', '.join(line_ranges) |
| line_filter += '], \"name\":\"' |
| line_filter += rm_prefix(file_with_relative_path, |
| '../') + '\"}' |
| line_filter += ']' |
| |
| extra_args = ['-line-filter=' + line_filter] if line_filter else [] |
| |
| if auto_fix: |
| extra_args.append('-fix') |
| |
| subprocess.call(['clang-tidy', '-p', '.'] + |
| extra_args + |
| [file_with_relative_path], |
| cwd=build_folder, |
| stderr=DEVNULL) |
| |
| |
| def CheckClangTidy(): |
| """ |
| Checks if a clang-tidy binary exists. |
| """ |
| with open(os.devnull, 'w') as DEVNULL: |
| return subprocess.call(['which', 'clang-tidy'], stdout=DEVNULL) == 0 |
| |
| |
| def CheckCompDB(build_folder): |
| """ |
| Checks if a compilation database exists in the build_folder. |
| """ |
| return os.path.isfile(os.path.join(build_folder, 'compile_commands.json')) |
| |
| |
| def DetectBuildFolder(): |
| """ |
| Tries to auto detect the last used build folder in out/ |
| """ |
| outdirs_folder = 'out/' |
| last_used = None |
| last_timestamp = -1 |
| for outdir in [outdirs_folder + folder_name |
| for folder_name in os.listdir(outdirs_folder) |
| if os.path.isdir(outdirs_folder + folder_name)]: |
| outdir_modified_timestamp = os.path.getmtime(outdir) |
| if outdir_modified_timestamp > last_timestamp: |
| last_timestamp = outdir_modified_timestamp |
| last_used = outdir |
| |
| return last_used |
| |
| |
| def GetOptions(): |
| """ |
| Generate the option parser for this script. |
| """ |
| result = optparse.OptionParser() |
| result.add_option( |
| '-b', |
| '--build-folder', |
| help='Set V8 build folder', |
| dest='build_folder', |
| default=None) |
| result.add_option( |
| '-j', |
| help='Set the amount of threads that should be used', |
| dest='threads', |
| default=None) |
| result.add_option( |
| '--gen-compdb', |
| help='Generate a compilation database for clang-tidy', |
| default=False, |
| action='store_true') |
| result.add_option( |
| '--no-output-filter', |
| help='Done use any output filterning', |
| default=False, |
| action='store_true') |
| result.add_option( |
| '--fix', |
| help='Fix auto fixable issues', |
| default=False, |
| dest='auto_fix', |
| action='store_true' |
| ) |
| |
| # Full clang-tidy. |
| full_run_g = optparse.OptionGroup(result, 'Clang-tidy full', '') |
| full_run_g.add_option( |
| '--full', |
| help='Run clang-tidy on the whole codebase', |
| default=False, |
| action='store_true') |
| full_run_g.add_option('--checks', |
| help='Clang-tidy checks to use.', |
| default=None) |
| result.add_option_group(full_run_g) |
| |
| # Aggregate clang-tidy. |
| agg_run_g = optparse.OptionGroup(result, 'Clang-tidy aggregate', '') |
| agg_run_g.add_option('--aggregate', help='Run clang-tidy on the whole '\ |
| 'codebase and aggregate the warnings', |
| default=False, action='store_true') |
| agg_run_g.add_option('--show-loc', help='Show file locations when running '\ |
| 'in aggregate mode', default=False, |
| action='store_true') |
| result.add_option_group(agg_run_g) |
| |
| # Diff clang-tidy. |
| diff_run_g = optparse.OptionGroup(result, 'Clang-tidy diff', '') |
| diff_run_g.add_option('--branch', help='Run clang-tidy on the diff '\ |
| 'between HEAD and the merge-base between HEAD '\ |
| 'and DIFF_BRANCH (origin/master by default).', |
| default=None, dest='diff_branch') |
| result.add_option_group(diff_run_g) |
| |
| # Single clang-tidy. |
| single_run_g = optparse.OptionGroup(result, 'Clang-tidy single', '') |
| single_run_g.add_option( |
| '--single', help='', default=False, action='store_true') |
| single_run_g.add_option( |
| '--file', help='File name to check', default=None, dest='file_name') |
| single_run_g.add_option('--lines', help='Limit checks to a line range. '\ |
| 'For example: --lines="[2,4], [5,6]"', |
| default=[], dest='line_ranges') |
| |
| result.add_option_group(single_run_g) |
| return result |
| |
| |
| def main(): |
| parser = GetOptions() |
| (options, _) = parser.parse_args() |
| |
| if options.threads is not None: |
| global THREADS |
| THREADS = options.threads |
| |
| if options.build_folder is None: |
| options.build_folder = DetectBuildFolder() |
| |
| if not CheckClangTidy(): |
| print('Could not find clang-tidy') |
| elif options.build_folder is None or not os.path.isdir(options.build_folder): |
| print('Please provide a build folder with -b') |
| elif options.gen_compdb: |
| GenerateCompileCommands(options.build_folder) |
| elif not CheckCompDB(options.build_folder): |
| print('Could not find compilation database, ' \ |
| 'please generate it with --gen-compdb') |
| else: |
| print('Using build folder:', options.build_folder) |
| if options.full: |
| print('Running clang-tidy - full') |
| ClangTidyRunFull(options.build_folder, |
| options.no_output_filter, |
| options.checks, |
| options.auto_fix) |
| elif options.aggregate: |
| print('Running clang-tidy - aggregating warnings') |
| if options.auto_fix: |
| print('Auto fix not working in aggregate mode, running without.') |
| ClangTidyRunAggregate(options.build_folder, options.show_loc) |
| elif options.single: |
| print('Running clang-tidy - single on ' + options.file_name) |
| if options.file_name is not None: |
| line_ranges = [] |
| for match in re.findall(r'(\[.*?\])', options.line_ranges): |
| if match is not []: |
| line_ranges.append(match) |
| ClangTidyRunSingleFile(options.build_folder, |
| options.file_name, |
| options.auto_fix, |
| line_ranges) |
| else: |
| print('Filename provided, please specify a filename with --file') |
| else: |
| print('Running clang-tidy') |
| ClangTidyRunDiff(options.build_folder, |
| options.diff_branch, |
| options.auto_fix) |
| |
| |
| if __name__ == '__main__': |
| main() |