diff options
Diffstat (limited to 'llvm_tools/check_clang_diags.py')
-rwxr-xr-x | llvm_tools/check_clang_diags.py | 223 |
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:]) |