blob: 95ef45998d34dbecfbe2da7ce4bd822192c6adf0 [file] [log] [blame]
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import json
from recipe_engine import post_process
DEPS = [
'chromium',
'chromium_checkout',
'goma',
'depot_tools/depot_tools',
'depot_tools/gclient',
'depot_tools/gerrit',
'depot_tools/tryserver',
'recipe_engine/context',
'recipe_engine/file',
'recipe_engine/json',
'recipe_engine/path',
'recipe_engine/platform',
'recipe_engine/properties',
'recipe_engine/python',
'recipe_engine/raw_io',
'recipe_engine/step',
'recipe_engine/tricium',
]
_chromium_tidy_path = ('third_party', 'llvm-build', 'Release+Asserts', 'bin',
'clang-tidy')
def _canonicalize_check_name(name):
"""Fixes `name` up for the user, and returns whether it's a clang diag."""
if not name.startswith('clang-diagnostic-'):
return name, False
# Either we get:
# - clang-diagnostic-warning
# - clang-diagnostic-error
# - clang-diagnostic-`${foo}`
#
# where `${foo}` is a -W flag; -Woverflow == clang-diagnostic-overflow, etc.
# so anything that isn't "error" is a warning.
if name == 'clang-diagnostic-error':
return 'compile-time error', True
return 'compile-time warning', True
_TidyDiagnosticID = collections.namedtuple(
'_TidyDiagnosticID', ['message', 'line_number', 'check_name'])
class _SourceFileComments(object):
def __init__(self):
self._source_comments = []
# _source_comments which have notes that map them back to macros. We have
# to group these, since a single CL might have multiple diagnostics that
# ultimately point to this same macro.
self._macro_comments = collections.defaultdict(set)
self._build_failed = False
self._tidy_failed = False
self._tidy_timed_out = False
def is_emission_useful(self):
# Always tell the user if clang-tidy timed out.
if self._tidy_timed_out:
return True
is_tidy_nit = lambda check_name: not _canonicalize_check_name(check_name)[1]
return (any(is_tidy_nit(x.check_name) for x in self._macro_comments) or
any(is_tidy_nit(x.check_name) for x, _ in self._source_comments))
def note_tidy_timed_out(self):
self._tidy_timed_out = True
def note_build_failed(self):
self._build_failed = True
def note_tidy_failed(self):
self._tidy_failed = True
def add_macro_expanded_tidy_diagnostic(self, message, line_number, check_name,
suggestions, file_of_expansion,
line_of_expansion):
key = _TidyDiagnosticID(message, line_number, check_name)
# FIXME(gbiv): Macro suggestions may be tricky. Don't mind them for now.
_ = suggestions
self._macro_comments[key].add((file_of_expansion, line_of_expansion))
def add_tidy_diagnostic(self, message, line_number, check_name, suggestions):
self._source_comments.append((_TidyDiagnosticID(message, line_number,
check_name), suggestions))
def __iter__(self):
"""Yields comments as (category, message, line_num, suggestions) tuples."""
category = 'ClangTidy'
if self._tidy_timed_out:
message = ('warning: clang-tidy timed out on this file; issuing '
'diagnostics is impossible.')
yield category, message, 0, ()
def fix_message(message, check_name):
name, is_diagnostic = _canonicalize_check_name(check_name)
# FIXME(crbug.com/1059096): Simplify this code if we want to no longer
# handle diags here.
assert not is_diagnostic, check_name
suffix = 'https://clang.llvm.org/extra/clang-tidy/checks/%s.html' % name
return message + ' (' + suffix + ')'
if self._build_failed:
failure_suffix = ('\n\n(Note: building this file or its dependencies '
'failed; this diagnostic might be incorrect as a '
'result.)')
elif self._tidy_failed:
failure_suffix = ('\n\n(Note: running clang-tidy on this file failed; '
'this diagnostic might be incorrect as a result.)')
else:
failure_suffix = ''
for (message, line_number,
check_name), suggestions in self._source_comments:
subcategory = '/'.join([category, check_name])
message = fix_message(message, check_name) + failure_suffix
yield subcategory, message, line_number, suggestions
macro_comments = sorted(self._macro_comments.items())
for (message, line_number, check_name), expansions in macro_comments:
assert len(expansions) > 0, message
message = fix_message(message, check_name)
expansions = sorted(expansions)
expansion_file, expansion_line = expansions[0]
suffix = '\n\nExpanded from %s:%d' % (expansion_file, expansion_line)
# Listing one expansion location should be enough for now.
if len(expansions) == 1:
suffix += '.'
elif len(expansions) == 2:
suffix += ', and 1 other place.'
else:
suffix += ', and %d other places.' % (len(expansions) - 1)
subcategory = '/'.join([category, check_name])
yield subcategory, message + suffix + failure_suffix, line_number, ()
def _generate_clang_tidy_comments(api, file_paths):
clang_tidy_location = api.context.cwd.join(*_chromium_tidy_path)
per_file_comments = collections.defaultdict(_SourceFileComments)
# CLs based before Chromium's src@a55e6bed3d40262fad227ae7fb68ee1fea0e32af
# won't sync clang-tidy, and so will show up as red if we try to run
# tricium_clang_tidy on them. Avoid that.
#
# FIXME(crbug.com/1035217): Remove this once M80 is a distant memory.
if not api.path.exists(clang_tidy_location):
api.step.active_result.presentation.status = 'WARNING'
api.step.active_result.presentation.logs['skipped'] = [
'No clang-tidy binary found; skipping linting (crbug.com/1035217)'
]
return per_file_comments
warnings_file = api.path['cleanup'].join('clang_tidy_complaints.yaml')
tricium_clang_tidy_args = [
'--out_dir=%s' % api.chromium.output_dir,
'--findings_file=%s' % warnings_file,
'--clang_tidy_binary=%s' % clang_tidy_location,
'--base_path=%s' % api.context.cwd,
'--ninja_jobs=%s' % api.goma.recommended_goma_jobs,
'--verbose',
'--',
]
tricium_clang_tidy_args += file_paths
ninja_path = {'PATH': [api.path.dirname(api.depot_tools.ninja_path)]}
with api.context(env_suffixes=ninja_path):
api.python(
name='tricium_clang_tidy.py',
script=api.resource('tricium_clang_tidy.py'),
args=tricium_clang_tidy_args)
# Please see tricium_clang_tidy.py for full docs on what this contains.
clang_tidy_output = api.file.read_json('read tidy output', warnings_file)
if clang_tidy_output.get('failed_src_files') or clang_tidy_output.get(
'failed_tidy_files') or clang_tidy_output.get('timed_out_src_files'):
api.step.active_result.presentation.status = 'WARNING'
for file_path in clang_tidy_output.get('failed_src_files', []):
per_file_comments[file_path].note_build_failed()
for file_path in clang_tidy_output.get('failed_tidy_files', []):
per_file_comments[file_path].note_tidy_failed()
for file_path in clang_tidy_output.get('timed_out_src_files', []):
per_file_comments[file_path].note_tidy_timed_out()
for diagnostic in clang_tidy_output.get('diagnostics', []):
file_path = diagnostic['file_path']
assert file_path, ("Empty paths should've been filtered "
"by tricium_clang_tidy: %s" % diagnostic)
# FIXME(crbug.com/1059096): Reporting this has caused more confusion than
# not. Let's disable it for a bit and see if anyone complains.
diag_name = diagnostic['diag_name']
_, is_diagnostic = _canonicalize_check_name(diag_name)
if is_diagnostic:
continue
tidy_replacements = diagnostic['replacements']
if tidy_replacements:
suggestions = [{
'replacements': [{
'path': file_path,
'replacement': x['new_text'],
'start_line': x['start_line'],
'end_line': x['end_line'],
'start_char': x['start_char'],
'end_char': x['end_char'],
} for x in tidy_replacements],
}]
else:
suggestions = ()
message = diagnostic['message']
report_line = diagnostic['line_number']
report_file = file_path
expansions = diagnostic['expansion_locs']
if expansions:
# Expansions are emitted by clang-tidy (thus tricium_clang_tidy) such
# that item [i] "invokes" the expansion of [i+1]. So the last item in
# this list should tell us where the original macro definition is.
e = expansions[-1]
per_file_comments[e['file_path']].add_macro_expanded_tidy_diagnostic(
message, e['line_number'], diag_name, suggestions, report_file,
report_line)
else:
per_file_comments[report_file].add_tidy_diagnostic(
message, report_line, diag_name, suggestions)
return per_file_comments
def _should_skip_linting(api):
revision_info = api.gerrit.get_revision_info(
'https://%s' % api.tryserver.gerrit_change.host,
api.tryserver.gerrit_change.change, api.tryserver.gerrit_change.patchset)
commit_message = revision_info['commit']['message']
return commit_message.startswith('Revert')
def RunSteps(api):
assert api.tryserver.is_tryserver
if _should_skip_linting(api):
return
with api.chromium.chromium_layout():
api.gclient.set_config('chromium')
api.gclient.apply_config('use_clang_tidy')
api.chromium.set_config('chromium')
api.chromium.apply_config('mb')
# Do not rebase the patch, so that the Tricium analyzer observes the correct
# line numbers. Otherwise, line numbers would be relative to origin/master,
# which may be synced to include changes subsequent to the actual patch.
api.chromium_checkout.ensure_checkout(gerrit_no_rebase_patch_ref=True)
api.chromium.runhooks(name='runhooks (with patch)')
src_dir = api.chromium_checkout.checkout_dir.join('src')
with api.context(cwd=src_dir):
# If files were removed by the CL, they'll be listed by
# get_files_affected_by_patch. We probably don't want to try to lint
# them. :)
#
# Similarly, linting non-source files is out of scope.
src_file_suffixes = {'.cc', '.cpp', '.cxx', '.c', '.h', '.hpp'}
affected = [
f for f in api.chromium_checkout.get_files_affected_by_patch()
if api.path.exists(src_dir.join(f)) and
api.path.splitext(f)[1] in src_file_suffixes
]
if affected:
api.chromium.ensure_goma()
api.goma.start()
# `gn gen` can take up to a minute, and the script we call out to
# already does that for us, so set up a minimal build dir.
gn_args = api.chromium.mb_lookup(api.chromium.get_builder_id())
api.file.ensure_directory('ensure out dir', api.chromium.output_dir)
api.file.write_text('write args.gn',
api.chromium.output_dir.join('args.gn'), gn_args)
with api.step.nest('clang-tidy'):
with api.step.nest('generate-warnings'):
per_file_comments = _generate_clang_tidy_comments(api, affected)
for file_path, comments in per_file_comments.items():
if comments.is_emission_useful():
for category, message, line_number, suggestions in comments:
# Clang-tidy only gives us one file offset, so we use line
# comments.
api.tricium.add_comment(
category,
message,
file_path,
start_line=line_number,
suggestions=suggestions)
api.tricium.write_comments()
def _get_tricium_comments(steps):
write_results = steps['write results']
tricium_json = write_results.output_properties['tricium']
return json.loads(tricium_json).get('comments')
def tricium_has_no_messages(check, steps):
comments = _get_tricium_comments(steps)
check(not comments)
def tricium_has_message(check, steps, message):
comments = _get_tricium_comments(steps)
check(comments)
if comments:
check(message in [x['message'] for x in comments])
def tricium_has_replacements(check, steps, *expected_replacements):
replacement_messages = set()
for comment in _get_tricium_comments(steps):
for suggestion in comment.get('suggestions', ()):
for replacement in suggestion['replacements']:
replacement_messages.add(replacement['replacement'])
check(set(expected_replacements) == replacement_messages)
def tricium_outputs_json(check, steps, json_obj):
comments = _get_tricium_comments(steps)
check(comments == json_obj)
def GenTests(api):
def test_with_patch(name,
affected_files,
is_revert=False,
include_diff=True,
auto_exist_files=True,
clang_tidy_exists=True,
author='gbiv@google.com'):
commit_message = 'Revert foo' if is_revert else 'foo'
commit_message += '\nTriciumTest'
test = (
api.test(name) + api.properties.tryserver(
build_config='Release',
mastername='tryserver.chromium.linux',
buildername='linux_chromium_compile_rel_ng',
buildnumber='1234',
patch_set=1) + api.platform('linux', 64) + api.override_step_data(
'gerrit changes',
api.json.output([{
'revisions': {
'a' * 40: {
'_number': 1,
'commit': {
'author': {
'email': author,
},
'message': commit_message,
}
}
}
}])))
# If this would otherwise be skipped, we'll never analyze the patch.
if include_diff:
test += api.step_data('git diff to analyze patch',
api.raw_io.stream_output('\n'.join(affected_files)))
existing_files = []
if auto_exist_files:
existing_files += [
api.path['cache'].join('builder', 'src', x) for x in affected_files
]
if clang_tidy_exists:
existing_files.append(api.path['cache'].join('builder', 'src',
*_chromium_tidy_path))
if existing_files:
test += api.path.exists(*existing_files)
return test
yield (test_with_patch('no_files', affected_files=[]) + api.post_process(
post_process.DoesNotRun, 'clang-tidy') + api.post_process(
post_process.StatusSuccess) + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'no_analysis_non_cpp', affected_files=['some/cc/file.txt']) +
api.post_process(post_process.DoesNotRun, 'clang-tidy') +
api.post_process(post_process.StatusSuccess) + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'removed_file',
affected_files=['path/to/some/cc/file.cpp'],
auto_exist_files=False) + api.post_process(post_process.DoesNotRun,
'clang-tidy') +
api.post_process(tricium_has_no_messages) + api.post_process(
post_process.StatusSuccess) + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'analyze_cpp_timed_out_files',
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'timed_out_src_files': ['oh/no.cpp']
})) + api.post_process(post_process.StepWarning,
'clang-tidy.generate-warnings') +
api.post_process(
tricium_has_message, 'warning: clang-tidy timed out on this '
'file; issuing diagnostics is impossible.') + api.post_process(
post_process.StatusSuccess) + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'analyze_cpp_failed_files',
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'failed_src_files': ['path/to/some/cc/file.cpp']
})) + api.post_process(post_process.StepWarning,
'clang-tidy.generate-warnings') +
api.post_process(post_process.StatusSuccess) + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'analyze_cpp_failed_tidy_files',
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'failed_tidy_files': ['path/to/some/cc/file.cpp']
})) + api.post_process(post_process.StepWarning,
'clang-tidy.generate-warnings') +
api.post_process(post_process.StatusSuccess) + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'analyze_cpp',
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'diagnostics': [
{
'file_path': 'path/to/some/cc/file.cpp',
'line_number': 2,
'diag_name': 'super-cool-diag',
'message': 'hello, world 1',
'replacements': [],
'expansion_locs': [],
},
{
'file_path': 'path/to/some/cc/file.cpp',
'line_number': 50,
'diag_name': 'moderately-cool-diag',
'message': 'hello, world',
'replacements': [],
'expansion_locs': [],
},
]
})) + api.post_process(post_process.StepSuccess,
'clang-tidy.generate-warnings') +
api.post_process(post_process.StatusSuccess) + api.post_process(
tricium_has_message, 'hello, world 1 (https://clang.llvm.org/'
'extra/clang-tidy/checks/super-cool-diag.html)') +
api.post_process(post_process.DropExpectation))
yield (test_with_patch(
'only_warnings_and_errors_are_silenced',
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'failed_src_files': ['path/to/some/cc/file.cpp'],
'diagnostics': [
{
'file_path': 'path/to/some/cc/file.cpp',
'line_number': 2,
'diag_name': 'clang-diagnostic-warning',
'message': 'hello, world',
'replacements': [],
'expansion_locs': [],
},
{
'file_path': 'path/to/some/cc/file.cpp',
'line_number': 2,
'diag_name': 'clang-diagnostic-error',
'message': 'hello, world',
'replacements': [],
'expansion_locs': [],
},
],
})) + api.post_process(post_process.StepWarning,
'clang-tidy.generate-warnings') +
api.post_process(post_process.StatusSuccess) +
api.post_process(tricium_has_no_messages) + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'append_complaint_on_failure',
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'failed_src_files': ['path/to/some/cc/file.cpp'],
'diagnostics': [{
'file_path': 'path/to/some/cc/file.cpp',
'line_number': 2,
'diag_name': 'b',
'message': 'a',
'replacements': [],
'expansion_locs': [],
},],
})) + api.post_process(post_process.StepWarning,
'clang-tidy.generate-warnings') +
api.post_process(post_process.StatusSuccess) + api.post_process(
tricium_has_message,
'a (https://clang.llvm.org/extra/clang-tidy/checks/b.html)\n\n'
'(Note: building this file or its dependencies failed; this '
'diagnostic might be incorrect as a result.)') + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'prefer_complaints_about_build_failures_over_tidy_ones',
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'failed_tidy_files': ['path/to/some/cc/file.cpp'],
'failed_src_files': ['path/to/some/cc/file.cpp'],
'diagnostics': [{
'file_path': 'path/to/some/cc/file.cpp',
'line_number': 2,
'diag_name': 'b',
'message': 'a',
'replacements': [],
'expansion_locs': [],
},],
})) + api.post_process(post_process.StepWarning,
'clang-tidy.generate-warnings') +
api.post_process(post_process.StatusSuccess) + api.post_process(
tricium_has_message,
'a (https://clang.llvm.org/extra/clang-tidy/checks/b.html)\n\n'
'(Note: building this file or its dependencies failed; this '
'diagnostic might be incorrect as a result.)') + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'append_complaint_on_tidy_failure',
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'failed_tidy_files': ['path/to/some/cc/file.cpp'],
'diagnostics': [{
'file_path': 'path/to/some/cc/file.cpp',
'line_number': 2,
'diag_name': 'b',
'message': 'a',
'replacements': [],
'expansion_locs': [],
},],
})) + api.post_process(post_process.StepWarning,
'clang-tidy.generate-warnings') +
api.post_process(post_process.StatusSuccess) + api.post_process(
tricium_has_message,
'a (https://clang.llvm.org/extra/clang-tidy/checks/b.html)\n\n'
'(Note: running clang-tidy on this file failed; this '
'diagnostic might be incorrect as a result.)') + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'diagnostic_suggestions',
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'diagnostics': [{
'file_path':
'path/to/some/cc/file.cpp',
'line_number':
2,
'diag_name':
'tidy-is-angry',
'message':
'hello, world',
'replacements': [
{
'new_text': 'foo',
'start_line': 1,
'end_line': 2,
'start_char': 0,
'end_char': 1,
},
{
'new_text': 'bar',
'start_line': 3,
'end_line': 4,
'start_char': 5,
'end_char': 5,
},
],
'expansion_locs': [],
},]
})) + api.post_process(post_process.StepSuccess,
'clang-tidy.generate-warnings') +
api.post_process(post_process.StatusSuccess) + api.post_process(
tricium_has_replacements, 'foo', 'bar') + api.post_process(
post_process.DropExpectation))
expansions_tests = [
(1, 1, '.'),
(2, 1, ', and 1 other place.'),
(3, 1, ', and 2 other places.'),
(4, 2, ', and 3 other places.'),
]
for num_expansions, expansion_duplication, suffix in expansions_tests:
diags = []
for i in range(num_expansions):
for _ in range(expansion_duplication):
diags.append({
'file_path':
'path/to/some/cc/file%d.cpp' % i,
'line_number':
2,
'diag_name':
'tidy-is-angry',
'message':
'grrr',
'replacements': [],
'expansion_locs': [
{
'file_path': 'path/to/some/cc/file%d.cpp' % i,
'line_number': 1,
},
{
'file_path': 'path/to/some/cc/file.h',
'line_number': 3,
},
],
})
yield (
test_with_patch(
'expansion_%d' % num_expansions,
affected_files=['path/to/some/cc/file.cpp']) + api.step_data(
'clang-tidy.generate-warnings.read tidy output',
api.file.read_json({
'diagnostics': diags
})) + api.post_process(post_process.StepSuccess,
'clang-tidy.generate-warnings') +
api.post_process(post_process.StatusSuccess) +
api.post_process(tricium_outputs_json, [{
'category': 'ClangTidy/tidy-is-angry',
'path': 'path/to/some/cc/file.h',
'message':
'grrr '
'(https://clang.llvm.org/extra/clang-tidy/checks/tidy-is-angry'
'.html)'
'\n\nExpanded from path/to/some/cc/file0.cpp:2' + suffix,
'startLine': 3,
}]) + api.post_process(post_process.DropExpectation))
yield (test_with_patch(
'skip_if_no_clang_tidy',
affected_files=['path/to/some/cc/file.cpp'],
clang_tidy_exists=False,
) + api.post_process(post_process.StepWarning, 'clang-tidy') +
api.post_process(post_process.StatusSuccess) + api.post_process(
post_process.DropExpectation))
yield (test_with_patch(
'skip_reverted_cl',
affected_files=['path/to/some/cc/file.cpp'],
is_revert=True,
include_diff=False) + api.post_process(
post_process.DoesNotRun, 'bot_update') + api.post_process(
post_process.StatusSuccess) + api.post_process(
post_process.DropExpectation))