blob: 2f6fd4ffba903431ff34fe2647672b0f6fd2bc76 [file] [log] [blame] [edit]
#!/usr/bin/env python3
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Tool to add `#pragma`s to libchrome files, so warnings can be disabled.
Some ChromeOS projects require libchrome headers, and want to use more strict
warnings than Chrome cares to use. This results in the need for `#pragma
GCC warning disable` blocks around libchrome headers, which get repetitive and
can be a bit error-prone to work with if used at `#include` sites.
Rather than forcing ChromeOS projects to scatter these `#pragma`s around their
codebases, this script lets us centralize them in libchrome without having to
worry about merge conflicts.
"""
import argparse
import logging
import os
import re
from pathlib import Path
import stat
from typing import Dict, Iterable, List, NamedTuple, Optional, Sequence
# A dict of header paths mapped to the series of warnings to disable for the
# given header.
WARNINGS_TO_SILENCE: Dict[str, Iterable[str]] = {
"base/memory/scoped_refptr.h": ("-Wsign-conversion",),
"base/numerics/clamped_math_impl.h": ("-Wimplicit-int-float-conversion",),
"base/time/time.h": ("-Wimplicit-int-float-conversion",),
}
def header_path_to_define(header_path: str) -> str:
"""Turns a header name into a include-guard-like string.
The returned value is safe for use in a regexp without escaping.
>>> header_name_to_define("foo.h")
"FOO_H"
>>> header_name_to_define("foo/bar.h")
"FOO_BAR_H"
"""
return re.sub(r"[^a-zA-Z0-9_]", "_", header_path).upper()
def add_into_header_guards(
add_after_ifndef: str,
add_before_endif: str,
header_contents: str,
header_path: str,
) -> str:
"""Adds content between header guards in a C or C++ header.
Most C and C++ header files have header guards, like:
```
#ifndef _FOO_H
#define _FOO_H
/* Meaningful header contents go here. */
#endif
```
For style and build speed reasons, it's best to put all header content
between these guards. This function exists to identify these guards, and
place text between them.
Args:
add_after_ifndef: Text to add a line after the `#ifdef ... #define`
lines in the header.
add_before_endif: Text to add on the line before the `#endif` in the
header.
header_contents: The text of the header to modify.
header_path: The path to the header, rooted at libchrome's source.
Returns:
The text of the modified header.
Raises:
ValueError if include guards could not be detected in
`header_contents`.
"""
# The heuristc here was selected after discussion on
# http://crrev.com/c/4859386.
header_define = header_path_to_define(header_path)
ifndef_regex_text = (
r"^\#ifndef\s+("
+ header_define
+ r"_?)\b.*?\n"
+ r"\#define\s+\1\b.*?$"
)
ifndef_regex = re.compile(ifndef_regex_text, re.MULTILINE)
found = ifndef_regex.search(header_contents)
if not found:
raise ValueError("No `#ifndef ... #define` guard found in header")
# Add 1 so we get past the newline at the end of ifndef_regex.
after_ifndef = found.end() + 1
endif_regex = re.compile(
r"^#endif\s+//\s*" + re.escape(found.group(1)),
re.MULTILINE,
)
found = endif_regex.search(header_contents, after_ifndef)
if not found:
raise ValueError("No `#endif` found after include guard in header")
before_endif = found.start()
return "".join(
(
header_contents[:after_ifndef],
add_after_ifndef,
header_contents[after_ifndef:before_endif],
add_before_endif,
header_contents[before_endif:],
)
)
def add_warnings_pragmas(
header_path: str, file_contents: str, warnings: List[str]
) -> str:
"""Adds pragmas to ignore `warnings` to the given file."""
assert warnings, "warnings lists shouldn't be empty"
add_after_ifndef_lines = ["#pragma GCC diagnostic push"]
for warning in warnings:
# No actual warning contains these, and if `warning` has neither
# backslashes nor quotes, we can skip quoting the warning.
if '"' in warning or "\\" in warning:
raise ValueError(f"invalid warning: {repr(warning)}")
add_after_ifndef_lines.append(
f'#pragma GCC diagnostic ignored "{warning}"'
)
# Ensure we append a trailing '\n' to `add_after_ifndef_lines`.
add_after_ifndef_lines.append("")
return add_into_header_guards(
add_after_ifndef="\n".join(add_after_ifndef_lines),
add_before_endif="#pragma GCC diagnostic pop\n",
header_contents=file_contents,
header_path=header_path,
)
def main() -> None:
parser = argparse.ArgumentParser(
description="Disable warnings for select parts of libchrome."
)
parser.add_argument(
"--libchrome-path",
default=Path(__file__).resolve().parent.parent,
type=Path,
help=(
"Path of libchrome to apply warnings patches. "
"Defaults to parent of this file's directory."
),
)
parser.add_argument(
"--dry-run",
action="store_true",
help="If specified, patched files won't actually be written.",
)
args = parser.parse_args()
logging.basicConfig(level=logging.INFO)
libchrome_path = args.libchrome_path
dry_run = args.dry_run
for file_path, all_warnings in sorted(WARNINGS_TO_SILENCE.items()):
path = libchrome_path / file_path
if path.suffix != ".h":
raise ValueError("WARNINGS_TO_SILENCE should only contain headers")
warnings = sorted(all_warnings)
logging.info("Patching %s to ignore %s...", path, warnings)
file_contents = path.read_text(encoding="utf-8")
new_contents = add_warnings_pragmas(file_path, file_contents, warnings)
if dry_run:
logging.info("--dry-run passed; skipping write to %s", path)
else:
path.write_text(new_contents, encoding="utf-8")
if __name__ == "__main__":
main()