|  | #!/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() |