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