blob: 84eff5b417e956db57de06722987a6d12f0c24e3 [file]
#!/usr/bin/env python3
# Copyright 2026 The Dawn & Tint Authors
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# For info about this script, see docs/clang-tidy.md.
import argparse
import datetime
import json
import shlex
import shutil
import subprocess
import sys
import time
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
DAWN_ROOT = SCRIPT_DIR.parent
def fail(message: str) -> None:
print(message, file=sys.stderr)
sys.exit(1)
def run_command(args: list, capture_stdout: bool = False) -> str:
str_args = [str(arg) for arg in args]
try:
result = subprocess.run(
str_args,
stdout=subprocess.PIPE if capture_stdout else None,
text=True,
check=True,
)
return result.stdout if capture_stdout else ''
except subprocess.CalledProcessError as e:
fail(f'Command failed: {" ".join(str_args)}\nError: {e}')
def has_source_file_extension(path: Path) -> bool:
extensions = {
'.c',
'.cc',
'.cpp',
'.cxx',
'.h',
'.hh',
'.hpp',
'.hxx',
'.m',
'.mm',
}
ignore_patterns = {
# Ignore files generated by protobuf, we have no control over them.
'**/*.pb.cc',
'**/*.pb.h',
}
return path.suffix.lower() in extensions and \
not any(path.full_match(pattern) for pattern in ignore_patterns)
def find_source_files(paths: list[Path]) -> list[str]:
files = []
for path in paths:
p = Path(path).resolve()
if p.is_file():
if has_source_file_extension(p):
files.append(str(p.relative_to(DAWN_ROOT)))
else:
print(
f"Warning: Skipped explicitly-specified file because its filename doesn't look like a C/C++ source file: {path}",
file=sys.stderr)
elif p.is_dir():
for item in p.rglob('*'):
if item.is_file() and has_source_file_extension(item):
files.append(str(item.relative_to(DAWN_ROOT)))
else:
fail(f'Not found: {path}')
return files
def main():
if Path.cwd().resolve() != DAWN_ROOT.resolve():
fail(f'Error: must be run from the Dawn root directory.')
parser = argparse.ArgumentParser(
description='Run clang-tidy on Dawn source files and directories.',
formatter_class=argparse.RawDescriptionHelpFormatter,
)
# Options
parser.add_argument(
'--clean',
action=argparse.BooleanOptionalAction,
required=True,
help=
'Pass --clean to clean the build before building and running clang-tidy. Pass --no-clean to skip that, and promise there are no leftover no-longer-used generated files from old Dawn revisions.'
)
parser.add_argument(
'--stdout-only',
action='store_true',
help=
'Print the summary to stdout instead of saving the JSON and summary files to disk. Useful when iterating on a specific small directory of files.'
)
parser.add_argument('outdir', help='The build directory, e.g., out/debug')
# Source files
source_group = parser.add_mutually_exclusive_group(required=True)
source_group.add_argument(
'--default',
action='store_true',
help='Use default sources (all of our source files).')
source_group.add_argument(
'--all',
action='store_true',
help='Run across all C/C++ files known to ninja (not recommended).')
source_group.add_argument(
'sources',
nargs='*',
help=
'Specific source files and/or directories to run clang-tidy on. Any files which are not part of the build will be ignored.'
)
args = parser.parse_args()
outdir = Path(args.outdir)
# Locate required binaries/scripts
clang_tidy = Path('third_party/llvm-build/Release+Asserts/bin/clang-tidy')
if sys.platform == 'win32':
clang_tidy = clang_tidy.with_suffix('.exe')
if not clang_tidy.is_file():
fail(f'{clang_tidy} missing. See docs/clang-tidy.md.')
recipe_script = Path(
'third_party/chromium-tools-build/src/recipes/recipe_modules/tricium_clang_tidy/resources/tricium_clang_tidy_script.py'
)
if not recipe_script.is_file():
fail(f'{recipe_script} missing. See docs/clang-tidy.md.')
run_command(['gn', 'gen', outdir])
# Check that Wasm is disabled, because clang-tidy doesn't understand em++
# commands (it doesn't know where the sysroot is because it's implicit).
#
# TODO(crbug.com/501491694): Fix this by setting the sysroot via additional
# flag in .clang-tidy, but is there a way to do this only for Emscripten
# commands? Also add third_party/emdawnwebgpu and outdir/wasm/gen
# (or outdir/*/gen) to --default if this is fixed.
gn_has_wasm = run_command(
['gn', 'args', outdir, '--short', '--list=dawn_build_emdawnwebgpu'],
capture_stdout=True,
).strip() != 'dawn_build_emdawnwebgpu = false'
if gn_has_wasm:
fail('Wasm build must be disabled for clang-tidy. Set '
'dawn_build_emdawnwebgpu = false in args.gn.')
# Clean if requested.
if args.clean:
run_command(['gn', 'clean', outdir])
# Make sure the build is up to date. Really we only need to regenerate
# generated files, but building everything doesn't take that long.
run_command(['autoninja', '-C', outdir])
# Get the source paths from the command line, or from the defaults.
source_paths = []
if args.default:
source_paths = [
DAWN_ROOT / 'include',
DAWN_ROOT / 'src',
outdir / 'gen' / 'include',
outdir / 'gen' / 'src',
]
elif args.sources:
source_paths = [Path(s) for s in args.sources]
# Find source files
source_files = []
if not args.all:
source_files = find_source_files(source_paths)
if not source_files:
fail('No matching source files found to run clang-tidy on.')
timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')
findings_file = Path(f'clang-tidy-{timestamp}-findings.json')
summary_file = Path(f'clang-tidy-{timestamp}-summary.txt')
script_args = [
f'--base_path={DAWN_ROOT}',
f'--out_dir={outdir}',
f'--clang_tidy_binary={clang_tidy}',
f'--findings_file={findings_file}',
'--no_clean', # We handled cleaning/building already, if requested
'--windows', # Also recognize windows-style compile commands if there are any
]
print('Running on:')
if args.all:
script_args.append('--all')
print(' --all')
print(f'Clang-tidy will run on ALL files used in the build.')
else:
for d in source_paths:
print(f' {d}')
print(f'Found {len(source_files)} source files to lint.')
script_args.extend(source_files)
run_command([recipe_script] + script_args)
# Lists of diagnostics. Each item is a comparable tuple of strings+numbers,
# allowing the array to be sorted. Then the tuple is concatenated to print.
summary_unknowns = []
summary_compiler = []
summary_tidy = []
with open(findings_file) as f:
findings = json.load(f)
seen_file_paths = set()
for diagnostic in findings['diagnostics']:
file_path = diagnostic['file_path']
line_number = diagnostic['line_number']
diag_name = diagnostic['diag_name']
message = diagnostic['message']
line = (f'{file_path}:', line_number, f': {message} [{diag_name}]')
if diag_name.startswith('clang-diagnostic-'):
summary_compiler.append(line)
else:
summary_tidy.append(line)
seen_file_paths.add(file_path)
for expansion_loc in diagnostic['expansion_locs']:
seen_file_paths.add(expansion_loc['file_path'])
for file_path in findings['failed_tidy_files']:
if file_path not in seen_file_paths:
summary_unknowns.append(
(file_path, ': found in failed_tidy_files'))
for file_path in findings['failed_src_files']:
if file_path not in seen_file_paths:
summary_unknowns.append(
(file_path, ': found in failed_src_files'))
for file_path in findings['timed_out_src_files']:
if file_path not in seen_file_paths:
summary_unknowns.append(
(file_path, ': found in timed_out_src_files'))
summary_unknowns.sort()
summary_compiler.sort()
summary_tidy.sort()
print('\n------------------------------------------------------------\n')
with (sys.stdout if args.stdout_only else open(summary_file, 'w')) as f:
f.write('Command line:\n')
f.write(' ' + shlex.join(sys.argv[:]) + '\n')
f.write('\nFiles that failed with no associated diagnostics:\n')
for line in summary_unknowns:
f.write(' ' + ''.join(map(str, line)) + '\n')
f.write('\nCompiler diagnostics (clang-diagnostic-*):\n')
for line in summary_compiler:
f.write(' ' + ''.join(map(str, line)) + '\n')
f.write('\nClang-Tidy diagnostics:\n')
for line in summary_tidy:
f.write(' ' + ''.join(map(str, line)) + '\n')
if args.stdout_only:
findings_file.unlink()
else:
print(f'Full findings JSON saved to {findings_file}')
print(f'Readable summary saved to {summary_file}')
if __name__ == '__main__':
main()