blob: 36216b8ce2e00771b97eb31785f06a7f73f31de4 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Script to use clang complier errors to enclose UNSAFE_TODO() regions.
import argparse
import os
import re
import subprocess
import sys
import tempfile
def main():
parser = argparse.ArgumentParser(
description="Rewrite UNSAFE_TODO regions using clang compiler errors.",
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-C",
dest="build_dir",
default="out/Debug",
help="Specify the build directory, defaults to out/Debug")
parser.add_argument("-f",
dest="force",
action="store_true",
help="skip conditional compilation checks.")
parser.add_argument("-v",
dest="verbose",
action="store_true",
help="Enable verbose logging")
parser.add_argument("directory",
help="Directory under src/ to checkout and modify.")
args = parser.parse_args()
build_dir = args.build_dir
directory = args.directory
force = args.force
verbose = args.verbose
print("Checking GN build arg configuration ...")
try:
dcheck_cmd = [
"gn", "args", "-C", build_dir, "--short", "--list=dcheck_always_on"
]
dcheck = subprocess.check_output(dcheck_cmd, text=True)
if "true" not in dcheck:
print("Set GN arg dcheck_always_on = true", file=sys.stderr)
sys.exit(1)
diag_cmd = [
"gn", "args", "-C", build_dir, "--short",
"--list=diagnostics_print_source_range_info"
]
diag = subprocess.check_output(diag_cmd, text=True)
if "true" not in diag:
print("Set GN arg diagnostics_print_source_range_info = true",
file=sys.stderr)
sys.exit(1)
warn_cmd = [
"gn", "args", "-C", build_dir, "--short",
"--list=treat_warnings_as_errors"
]
warn = subprocess.check_output(warn_cmd, text=True)
if "false" not in warn:
print("Set GN arg treat_warnings_as_errors = false", file=sys.stderr)
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"Error checking GN args: {e.stderr}", file=sys.stderr)
sys.exit(1)
tmpdir = tempfile.mkdtemp(None, "unsafe_pragma_rewriter.")
print(f"Temporary files will be written to {tmpdir}\n")
try:
grep_cmd = ["git", "grep", "-l", "^#pragma allow_unsafe_", directory]
grep = subprocess.check_output(grep_cmd, text=True).strip()
except Exception as e:
print("No candidates found")
sys.exit(1)
grep_lines = grep.splitlines() if grep else []
source_files = [x for x in grep_lines if re.match(r".*\.cc$", x)]
if not source_files:
print("No files met criteria")
sys.exit(1)
if verbose:
print("Files containing unsafe pragmas:")
print("\n".join(source_files), "\n")
if not force:
iffy_cmd = ["grep", "-Pc", "^#if(?! DCHECK_IS_ON\\(\\))"] + source_files
iffy = subprocess.check_output(iffy_cmd, text=True).strip()
iffy_lines = iffy.splitlines() if iffy else []
iffy_files = [x.split(":")[0] for x in iffy_lines if x.split(":")[1] != "1"]
if iffy_files:
if verbose:
print("Skipping conditionally-compiled files:")
print("\n".join(iffy_files), "\n")
source_files = [x for x in source_files if not x in set(iffy_files)]
if not source_files:
print("No remaining files")
sys.exit(1)
if verbose:
print("Remaining files after excluding #ifdefs:")
print("\n".join(source_files), "\n")
# Starting with all files in the directory, find the ones that are
# able to be compiled on this platform/configurarion by asking ninja
# to build them all, and then removing the ones that aren't known.
obj_targets = ["../../" + x + "^" for x in source_files]
ninja_command = ["autoninja", "-C", build_dir] + obj_targets
ninja = subprocess.run(ninja_command, text=True, capture_output=True)
if ninja.stderr:
source_files = [
x for x in source_files
if not 'unknown target "../../' + x in ninja.stderr
]
if verbose:
print("Remaining files after excluding unbuildable:")
print("\n".join(source_files), "\n")
subprocess.run(
["tools/clang/unsafe_pragma_rewriter/remove_unsafe_pragma.py"] +
source_files,
check=True)
print("Compile to find unsafe errors ...")
targets = ["../../" + x + "^" for x in source_files]
buildlog0 = os.path.join(tmpdir, "buildlog0")
with open(buildlog0, "w") as f_log:
subprocess.run(["autoninja", "-k", "1000", "-C", build_dir, "-v"] + targets,
stdout=f_log,
stderr=subprocess.STDOUT)
with open(buildlog0) as f_in:
compiled = subprocess.check_output(
["tools/clang/unsafe_pragma_rewriter/extract_sources.py"],
stdin=f_in,
text=True).strip()
compiled_files = compiled.splitlines() if compiled else []
if not compiled_files:
print("No modified files were compiled")
sys.exit(1)
if verbose:
print("Set of files compiled")
print("\n".join(compiled_files), "\n")
source_files = [x for x in source_files if x in set(compiled_files)]
if verbose:
print("Set of modified files compiled")
print("\n".join(compiled_files), "\n")
with open(buildlog0) as f_in:
fail = subprocess.check_output(
["tools/clang/unsafe_pragma_rewriter/extract_failures.py"],
stdin=f_in,
text=True).strip()
fail_files = fail.splitlines() if fail else []
if verbose and fail_files:
print("Set of files with detected warnings")
print("\n".join(fail_files), "\n")
print("Resetting to clean state ...")
subprocess.run(["git", "checkout", "--", directory], check=True)
subprocess.run(
["tools/clang/unsafe_pragma_rewriter/remove_unsafe_pragma.py"] +
source_files,
check=True)
print("Adding UNSAFE_TODO() ...")
with open(buildlog0) as f_in:
subprocess.run(["tools/clang/unsafe_pragma_rewriter/fix_unsafe.py"],
stdin=f_in,
check=True)
if source_files:
try:
needs_header_cmd = ["git", "grep", "-l", "UNSAFE_TODO"] + source_files
needs_header = subprocess.check_output(needs_header_cmd,
text=True).strip()
needs_header_files = needs_header.splitlines() if needs_header else []
except Exception as e:
needs_header_files = []
if needs_header_files:
subprocess.run(
["tools/add_header.py", "--header", '"base/compiler_specific.h"'] +
needs_header_files,
check=True)
for i in range(1, 5):
print(f"Compile to find bad rewrites (Pass {i}) ...")
buildlog_i = os.path.join(tmpdir, f"buildlog{i}")
with open(buildlog_i, "w") as f_log:
subprocess.run(["autoninja", "-k", "1000", "-C", build_dir] + targets,
stdout=f_log,
stderr=subprocess.STDOUT)
with open(buildlog_i) as f_in:
fail = subprocess.check_output(
["tools/clang/unsafe_pragma_rewriter/extract_failures.py"],
stdin=f_in,
text=True).strip()
failures = fail.splitlines() if fail else []
if not failures:
break
if verbose:
print("Failed to compile, reverting:")
print("\n".join(failures), "\n")
for failure in failures:
subprocess.run(["git", "checkout", "--", failure], check=True)
print("Formatting changes")
subprocess.run(["git", "cl", "format"], check=True)
print("Finished.")
if __name__ == "__main__":
main()