blob: 9749af5d11b79329a7d432a8dc7e4db00a7d5d55 [file] [log] [blame] [edit]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2024 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Attempts to replace all instances of the `NEXT` pseudo-API-level with a
concrete value.
This is done heuristically, based on text. Always do a subsequent codebase-wide
regular expression search for `\bNEXT\b` after running this script, and ensure
it got everything. If you find any coverage gaps, please amend this script to
fix them.
Usage:
./scripts/versioning/replace_next.py --fuchsia-dir $PWD --new-api-level 24
"""
import argparse
import concurrent.futures
import dataclasses
import pathlib
import re
import subprocess
from typing import Generator
IGNORED_RELATIVE_PATHS: set[pathlib.Path] = {
pathlib.Path("tools/fidl/fidlc/testdata/versions.test.fidl"),
pathlib.Path("zircon/system/availability_test.c"),
pathlib.Path("tools/fidl/fidlc/tests/versioning_attribute_tests.cc"),
}
@dataclasses.dataclass
class Pattern:
# `sub_regex` will only be applied to files whose path (relative to
# FUCHSIA_DIR) matches this regex.
path_regex: re.Pattern[str]
# Substitution to apply to the file. Must have a capture group named
# "prefix" and another named "suffix". Any matches will be replaced with
# <prefix>new_api_level<suffix>.
sub_regex: re.Pattern[str]
def path_matches(self, rel_path: pathlib.Path) -> bool:
"""Returns true if this pattern should apply to the given file."""
return bool(self.path_regex.search(str(rel_path)))
def substitute(self, text: str, new_api_level: str) -> str:
"""Performs the substitution and returns the result."""
def sub_fn(m: re.Match[str]) -> str:
return m.group("prefix") + new_api_level + m.group("suffix")
return self.sub_regex.sub(sub_fn, text)
# `json` is included because magma.json is used to generate a header and thus can
# contain the macros.
CPP_PATH_REGEX: re.Pattern[str] = re.compile(r"\.(h|c|cc|json)$")
PATTERNS = [
Pattern(
path_regex=CPP_PATH_REGEX,
# Matches macros like `FUCHSIA_API_LEVEL_AT_LEAST(NEXT)`, at the end of
# lines.
#
# Possible coverage gaps:
# - FUCHSIA_API_LEVEL_AT_LEAST(NEXT) && FUCHSIA_API_LEVEL_LESS_THAN(HEAD)
# - FUCHSIA_API_LEVEL_AT_LEAST( NEXT )
sub_regex=re.compile(r"(?P<prefix>\()NEXT(?P<suffix>\)(\n|$))"),
),
Pattern(
path_regex=CPP_PATH_REGEX,
# Matches annotations like `ZX_AVAILABLE_SINCE(NEXT)` or
# `ZX_REMOVED_SINCE(1, 19,
# NEXT,
# "Use DoNewThing() instead")`
#
# Possible coverage gaps:
# - ZX_REMOVED_SINCE((1), 19, NEXT)
sub_regex=re.compile(
r"(?P<prefix>_SINCE\([^)]*)NEXT(?P<suffix>)", re.MULTILINE
),
),
Pattern(
path_regex=re.compile(r"\.rs$"),
# Matches `cfg` predicates like `fuchsia_api_level_at_least = "NEXT"`.
# Possible coverage gaps:
# - Multiple instances on the same line?
sub_regex=re.compile(
r"(?P<prefix>fuchsia_api_level_.*\")NEXT(?P<suffix>\")",
re.MULTILINE,
),
),
Pattern(
path_regex=re.compile(r"\.fidl$"),
# Matches annotations like `@available(added=NEXT)` or
# `@available(added=15, deprecated=22, removed=NEXT)`.
#
# Possible coverage gaps:
# - The exclusion of `[^_;(A-Z]` is somewhat arbitrary.
sub_regex=re.compile(
r"(?P<prefix>= *)NEXT(?P<suffix>[^_;(A-Z])", re.MULTILINE
),
),
]
def get_git_files_for_substitution(
fuchsia_dir: pathlib.Path,
) -> Generator[pathlib.Path, None, None]:
"""Calls `git ls-files` in the given directory and filters out files that
cannot have relevant matches."""
git_ls_files_out = subprocess.check_output(
["git", "-C", str(fuchsia_dir), "ls-files"], text=True
)
for line in git_ls_files_out.splitlines():
path = pathlib.Path(line)
if all(
[
path not in IGNORED_RELATIVE_PATHS,
not path.is_dir(),
any(p.path_matches(path) for p in PATTERNS),
]
):
yield path
def do_subsitituion(path: pathlib.Path, new_api_level: str) -> None:
"""Replaces `NEXT` with new_api_level in the given file."""
try:
with path.open() as f:
contents = f.read()
except UnicodeDecodeError:
print("Encountered binary file: " + str(path))
return
new_contents = contents
for p in PATTERNS:
if p.path_matches(path):
new_contents = p.substitute(new_contents, new_api_level)
if new_contents != contents:
with path.open("w") as f:
f.write(new_contents)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--fuchsia-dir", type=pathlib.Path, required=True)
parser.add_argument("--new-api-level", type=str, required=True)
args = parser.parse_args()
with concurrent.futures.ThreadPoolExecutor() as exe:
# Use `list` to drain the iterator.
list(
exe.map(
lambda path: do_subsitituion(path, args.new_api_level),
get_git_files_for_substitution(args.fuchsia_dir),
)
)
if __name__ == "__main__":
main()