aboutsummaryrefslogtreecommitdiff
path: root/llvm_tools/check_clang_diags.py
diff options
context:
space:
mode:
Diffstat (limited to 'llvm_tools/check_clang_diags.py')
-rwxr-xr-xllvm_tools/check_clang_diags.py223
1 files changed, 223 insertions, 0 deletions
diff --git a/llvm_tools/check_clang_diags.py b/llvm_tools/check_clang_diags.py
new file mode 100755
index 00000000..7beb958f
--- /dev/null
+++ b/llvm_tools/check_clang_diags.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+# Copyright 2022 The ChromiumOS Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""check_clang_diags monitors for new diagnostics in LLVM
+
+This looks at projects we care about (currently only clang-tidy, though
+hopefully clang in the future, too?) and files bugs whenever a new check or
+warning appears. These bugs are intended to keep us up-to-date with new
+diagnostics, so we can enable them as they land.
+"""
+
+import argparse
+import json
+import logging
+import os
+import shutil
+import subprocess
+import sys
+from typing import Dict, List, Tuple
+
+from cros_utils import bugs
+
+
+_DEFAULT_ASSIGNEE = "mage"
+_DEFAULT_CCS = ["cjdb@google.com"]
+
+
+# FIXME: clang would be cool to check, too? Doesn't seem to have a super stable
+# way of listing all warnings, unfortunately.
+def _build_llvm(llvm_dir: str, build_dir: str):
+ """Builds everything that _collect_available_diagnostics depends on."""
+ targets = ["clang-tidy"]
+ # use `-C $llvm_dir` so the failure is easier to handle if llvm_dir DNE.
+ ninja_result = subprocess.run(
+ ["ninja", "-C", build_dir] + targets,
+ check=False,
+ )
+ if not ninja_result.returncode:
+ return
+
+ # Sometimes the directory doesn't exist, sometimes incremental cmake
+ # breaks, sometimes something random happens. Start fresh since that fixes
+ # the issue most of the time.
+ logging.warning("Initial build failed; trying to build from scratch.")
+ shutil.rmtree(build_dir, ignore_errors=True)
+ os.makedirs(build_dir)
+ subprocess.run(
+ [
+ "cmake",
+ "-G",
+ "Ninja",
+ "-DCMAKE_BUILD_TYPE=MinSizeRel",
+ "-DLLVM_USE_LINKER=lld",
+ "-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra",
+ "-DLLVM_TARGETS_TO_BUILD=X86",
+ f"{os.path.abspath(llvm_dir)}/llvm",
+ ],
+ cwd=build_dir,
+ check=True,
+ )
+ subprocess.run(["ninja"] + targets, check=True, cwd=build_dir)
+
+
+def _collect_available_diagnostics(
+ llvm_dir: str, build_dir: str
+) -> Dict[str, List[str]]:
+ _build_llvm(llvm_dir, build_dir)
+
+ clang_tidy = os.path.join(os.path.abspath(build_dir), "bin", "clang-tidy")
+ clang_tidy_checks = subprocess.run(
+ [clang_tidy, "-checks=*", "-list-checks"],
+ # Use cwd='/' to ensure no .clang-tidy files are picked up. It
+ # _shouldn't_ matter, but it's also ~free, so...
+ check=True,
+ cwd="/",
+ stdout=subprocess.PIPE,
+ encoding="utf-8",
+ )
+ clang_tidy_checks_stdout = [
+ x.strip() for x in clang_tidy_checks.stdout.strip().splitlines()
+ ]
+
+ # The first line should always be this, then each line thereafter is a check
+ # name.
+ assert (
+ clang_tidy_checks_stdout[0] == "Enabled checks:"
+ ), clang_tidy_checks_stdout
+ clang_tidy_checks = clang_tidy_checks_stdout[1:]
+ assert not any(
+ check.isspace() for check in clang_tidy_checks
+ ), clang_tidy_checks
+ return {"clang-tidy": clang_tidy_checks}
+
+
+def _process_new_diagnostics(
+ old: Dict[str, List[str]], new: Dict[str, List[str]]
+) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]:
+ """Determines the set of new diagnostics that we should file bugs for.
+
+ old: The previous state that this function returned as `new_state_file`, or
+ `{}`
+ new: The diagnostics that we've most recently found. This is a dict in the
+ form {tool: [diag]}
+
+ Returns a `new_state_file` to pass into this function as `old` in the
+ future, and a dict of diags to file bugs about.
+ """
+ new_diagnostics = {}
+ new_state_file = {}
+ for tool, diags in new.items():
+ if tool not in old:
+ logging.info(
+ "New tool with diagnostics: %s; pretending none are new", tool
+ )
+ new_state_file[tool] = diags
+ else:
+ old_diags = set(old[tool])
+ newly_added_diags = [x for x in diags if x not in old_diags]
+ if newly_added_diags:
+ new_diagnostics[tool] = newly_added_diags
+ # This specifically tries to make diags sticky: if one is landed, then
+ # reverted, then relanded, we ignore the reland. This might not be
+ # desirable? I don't know.
+ new_state_file[tool] = old[tool] + newly_added_diags
+
+ # Sort things so we have more predictable output.
+ for v in new_diagnostics.values():
+ v.sort()
+
+ return new_state_file, new_diagnostics
+
+
+def _file_bugs_for_new_diags(new_diags: Dict[str, List[str]]):
+ for tool, diags in sorted(new_diags.items()):
+ for diag in diags:
+ bugs.CreateNewBug(
+ component_id=bugs.WellKnownComponents.CrOSToolchainPublic,
+ title=f"Investigate {tool} check `{diag}`",
+ body="\n".join(
+ (
+ f"It seems that the `{diag}` check was recently added to {tool}.",
+ "It's probably good to TAL at whether this check would be good",
+ "for us to enable in e.g., platform2, or across ChromeOS.",
+ )
+ ),
+ assignee=_DEFAULT_ASSIGNEE,
+ cc=_DEFAULT_CCS,
+ )
+
+
+def main(argv: List[str]):
+ logging.basicConfig(
+ format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: "
+ "%(message)s",
+ level=logging.INFO,
+ )
+
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ "--llvm_dir", required=True, help="LLVM directory to check. Required."
+ )
+ parser.add_argument(
+ "--llvm_build_dir",
+ required=True,
+ help="Build directory for LLVM. Required & autocreated.",
+ )
+ parser.add_argument(
+ "--state_file",
+ required=True,
+ help="State file to use to suppress duplicate complaints. Required.",
+ )
+ parser.add_argument(
+ "--dry_run",
+ action="store_true",
+ help="Skip filing bugs & writing to the state file; just log "
+ "differences.",
+ )
+ opts = parser.parse_args(argv)
+
+ build_dir = opts.llvm_build_dir
+ dry_run = opts.dry_run
+ llvm_dir = opts.llvm_dir
+ state_file = opts.state_file
+
+ try:
+ with open(state_file, encoding="utf-8") as f:
+ prior_diagnostics = json.load(f)
+ except FileNotFoundError:
+ # If the state file didn't exist, just create it without complaining this
+ # time.
+ prior_diagnostics = {}
+
+ available_diagnostics = _collect_available_diagnostics(llvm_dir, build_dir)
+ logging.info("Available diagnostics are %s", available_diagnostics)
+ if available_diagnostics == prior_diagnostics:
+ logging.info("Current diagnostics are identical to previous ones; quit")
+ return
+
+ new_state_file, new_diagnostics = _process_new_diagnostics(
+ prior_diagnostics, available_diagnostics
+ )
+ logging.info("New diagnostics in existing tool(s): %s", new_diagnostics)
+
+ if dry_run:
+ logging.info(
+ "Skipping new state file writing and bug filing; dry-run "
+ "mode wins"
+ )
+ else:
+ _file_bugs_for_new_diags(new_diagnostics)
+ new_state_file_path = state_file + ".new"
+ with open(new_state_file_path, "w", encoding="utf-8") as f:
+ json.dump(new_state_file, f)
+ os.rename(new_state_file_path, state_file)
+
+
+if __name__ == "__main__":
+ main(sys.argv[1:])