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