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