drgn/scripts/iwyu.py
Omar Sandoval 24609a3a2e libdrgn: add autoconf option to enable compiler warnings
This adds an --enable-compiler-warnings flag that:

* Defines a canonical list of warnings that we enforce. For now, this is
  -Wall -Wformat-overflow=2 -Wformat-truncation=2, but we can add to it
  going forward.
* Enables warnings by default.
* Allows erroring on warnings. We recommend that developers use this and
  use it for the CI.

Signed-off-by: Omar Sandoval <osandov@osandov.com>
2022-03-01 15:38:05 -08:00

263 lines
8.3 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
# SPDX-License-Identifier: GPL-3.0-or-later
import argparse
import json
import os
import os.path
import re
import subprocess
import sys
import sysconfig
import tempfile
import yaml
BUILD_BASE = "build/compile_commands"
CDB = BUILD_BASE + "/compile_commands.json"
IWYU_REGEXES = [
("add", r"(.*) should add these lines:"),
("remove", r"(.*) should remove these lines:"),
("include_list", r"The full include-list for (.*):"),
("none", r"---"),
("none", r"\(.* has correct #includes/fwd-decls\)"),
]
# Python.h is the canonical header for the Python C API. The actual definitions
# come from internal header files, so we need an IWYU mapping file. Ideally we
# could do this with include mappings. Unfortunately, Python.h uses ""-style
# includes for those headers, one of which is "object.h". This conflicts with
# libdrgn's "object.h", and IWYU doesn't seem to have a way to distinguish
# between those in the mapping file. So, we generate symbol mappings with the
# find-all-symbols Clang tool.
def gen_python_mapping_file(mapping_path):
# These headers are guaranteed to be included by Python.h. See
# https://docs.python.org/3/c-api/intro.html#include-files.
IMPLIED_HEADERS = (
"<assert.h>",
"<errno.h>",
"<limits.h>",
"<stdio.h>",
"<stdlib.h>",
"<string.h>",
)
include = sysconfig.get_path("include")
platinclude = sysconfig.get_path("platinclude")
with open(
mapping_path + ".tmp", "w"
) as imp, tempfile.TemporaryDirectory() as tmpdir:
imp.write("[\n")
for header in IMPLIED_HEADERS:
imp.write(
f' {{"include": ["{header}", "public", "<Python.h>", "public"]}},\n'
)
build_dir = os.path.join(tmpdir, "build")
os.mkdir(build_dir)
source = os.path.join(build_dir, "python.c")
with open(source, "w") as f:
f.write("#include <Python.h>")
commands = [
{
"arguments": [
"clang",
"-I",
include,
"-I",
platinclude,
"-c",
"python.c",
],
"directory": build_dir,
"file": "python.c",
}
]
with open(os.path.join(build_dir, "compile_commands.json"), "w") as f:
json.dump(commands, f)
symbols_dir = os.path.join(tmpdir, "find_all_symbols")
os.mkdir(symbols_dir)
subprocess.check_call(
[
"find-all-symbols",
"-p=" + build_dir,
"--output-dir=" + symbols_dir,
source,
]
)
find_all_symbols_db = os.path.join(tmpdir, "find_all_symbols_db.yaml")
subprocess.check_call(
[
"find-all-symbols",
"-p=" + build_dir,
"--merge-dir=" + symbols_dir,
find_all_symbols_db,
]
)
with open(find_all_symbols_db, "r") as f:
for document in yaml.safe_load_all(f):
name = document["Name"]
path = document["FilePath"]
if path.startswith(include + "/"):
header = path[len(include) + 1 :]
elif path.startswith(platinclude + "/"):
header = path[len(platinclude) + 1 :]
else:
continue
if header == "pyconfig.h":
# Probably best not to use these.
continue
imp.write(
f' {{"symbol": ["{name}", "private", "<Python.h>", "public"]}}, # From {header}\n'
)
# "cpython/object.h" defines struct _typeobject { ... } PyTypeObject.
# For some reason, include-what-you-mean wants struct _typeobject, but
# find-all-symbols only reports PyTypeObject. Add it manually.
imp.write(
' {"symbol": ["_typeobject", "private", "<Python.h>", "public"]}, # From cpython/object.h\n'
)
imp.write("]\n")
os.rename(mapping_path + ".tmp", mapping_path)
def iwyu_associated_header(path):
with open(path, "r") as f:
match = re.search(
r'^\s*#\s*include\s+"([^"]+)"\s+//\s+IWYU\s+pragma:\s+associated',
f.read(),
re.M,
)
if match:
return os.path.join(os.path.dirname(path), match.group(1))
if path.endswith(".c"):
return path[:-2] + ".h"
return None
def ignore_line(path, state, line):
# include-what-you-use/include-what-you-use#969: iwyu recommends bogus
# forward declarations for the anonymous unions generated by
# BINARY_OP_SIGNED_2C.
if line.endswith("::;"):
return True
# include-what-you-use/include-what-you-use#971: drgn.h "exports" a forward
# declaration of several opaque types, but iwyu doesn't have such a notion.
if re.fullmatch(
r"struct drgn_(language|platform|program|register|stack_trace|symbol);", line
):
paths = [path]
associated_header = iwyu_associated_header(path)
if associated_header is not None:
paths.append(associated_header)
for path in paths:
with open(path, "r") as f:
if re.search(r'^#include "(drgn.h|drgnpy.h)"', f.read(), re.M):
return True
return False
def main():
parser = argparse.ArgumentParser(description="run include-what-you-use on drgn")
parser.add_argument(
"source", nargs="*", help="run on given file instead of all source files"
)
args = parser.parse_args()
if args.source:
sources = {os.path.realpath(source) for source in args.source}
os.makedirs(BUILD_BASE, exist_ok=True)
subprocess.check_call(
[
"bear",
"--output",
CDB,
"--append",
"--",
sys.executable,
"setup.py",
"build",
"-b",
BUILD_BASE,
"build_ext",
]
)
python_mapping_file = os.path.join(
BUILD_BASE,
f"python.{sysconfig.get_platform()}.{sysconfig.get_python_version()}.imp",
)
if not os.path.exists(python_mapping_file):
gen_python_mapping_file(python_mapping_file)
with open(CDB, "r") as f:
commands = json.load(f)
for command in commands:
if (
args.source
and os.path.realpath(os.path.join(command["directory"], command["file"]))
not in sources
):
continue
with subprocess.Popen(
["include-what-you-use"]
+ command["arguments"][1:]
+ [
"-Xiwyu",
"--mapping_file=" + os.path.abspath(python_mapping_file),
"-w", # We don't want warnings from Clang.
],
cwd=command["directory"],
universal_newlines=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as proc:
state = "none"
header = None
lines = []
for line in proc.stdout:
line = line.rstrip("\n")
match = None
for new_state, regex in IWYU_REGEXES:
match = re.fullmatch(regex, line)
if match:
break
if match:
state = new_state
if state != "none":
path = os.path.relpath(
os.path.join(command["directory"], match.group(1))
)
if state in ("add", "remove"):
header = f"{path} should {state} these lines:"
else:
header = None
lines.clear()
elif (
line
and state != "include_list"
and not ignore_line(path, state, line)
):
if header is not None:
print("\n" + header)
header = None
print(line)
if __name__ == "__main__":
main()