foundationdb/contrib/apiversioner.py

405 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
#
# apiversioner.py
#
# This source file is part of the FoundationDB open source project
#
# Copyright 2013-2024 Apple Inc. and the FoundationDB project authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import argparse
import logging
import os
import re
import sys
import traceback
LOG_FORMAT = "%(created)f [%(levelname)s] %(message)s"
EXCLUDED_FILES = list(
map(
re.compile,
[
# Output directories
r"\.git/.*",
r"bin/.*",
r"packages/.*",
r"\.objs/.*",
r"\.deps/.*",
r"bindings/go/build/.*",
r"documentation/sphinx/\.out/.*",
# Generated files
r".*\.g\.cpp$",
r".*\.g\.h$",
r"(^|.*/)generated.mk$",
r".*\.g\.S$",
r".*/MutationType\.java",
r".*/generated\.go",
# Binary files
r".*\.class$",
r".*\.o$",
r".*\.a$",
r".*[\.-]debug",
r".*\.so$",
r".*\.dylib$",
r".*\.dll$",
r".*\.tar[^/]*$",
r".*\.jar$",
r".*pyc$",
r"bindings/flow/bin/.*",
r".*\.pdf$",
r".*\.jp[e]*g",
r".*\.png",
r".*\.ico",
r"packaging/msi/art/.*",
# Project configuration files
r".*foundationdb\.VC\.db$",
r".*foundationdb\.VC\.VC\.opendb$",
r".*iml$",
# Source files from someone else
r"(^|.*/)Hash3\..*",
r"(^|.*/)sqlite.*",
r"bindings/go/godoc-resources/.*",
r"bindings/go/src/fdb/tuple/testdata/tuples.golden",
r"fdbcli/linenoise/.*",
r"contrib/rapidjson/.*",
r"fdbrpc/rapidxml/.*",
r"fdbrpc/zlib/.*",
r"fdbrpc/sha1/.*",
r"fdbrpc/xml2json.hpp$",
r"fdbrpc/libcoroutine/.*",
r"fdbrpc/libeio/.*",
r"fdbrpc/lib64/.*",
r"fdbrpc/generated-constants.cpp$",
# Miscellaneous
r"bindings/nodejs/node_modules/.*",
r"bindings/go/godoc/.*",
r".*trace.*xml$",
r".*log$",
r".*\.DS_Store$",
r"simfdb/\.*",
r".*~$",
r".*.swp$",
],
)
)
SUSPECT_PHRASES = map(
re.compile,
[
r"#define\s+FDB_API_VERSION\s+(\d+)",
r"\.\s*selectApiVersion\s*\(\s*(\d+)\s*\)",
r"\.\s*APIVersion\s*\(\s*(\d+)\s*\)",
r"\.\s*MustAPIVersion\s*\(\s*(\d+)\s*\)",
r"header_version\s+=\s+(\d+)",
r"\.\s*apiVersion\s*\(\s*(\d+)\s*\)",
r"API_VERSION\s*=\s*(\d+)",
r"fdb_select_api_version\s*\((\d+)\)",
],
)
DIM_CODE = "\033[2m"
BOLD_CODE = "\033[1m"
RED_COLOR = "\033[91m"
GREEN_COLOR = "\033[92m"
END_COLOR = "\033[0m"
def positive_response(val):
return val.lower() in {"y", "yes"}
# Returns: new line list + a dirty flag
def rewrite_lines(
lines,
version_re,
new_version,
suspect_only=True,
print_diffs=False,
ask_confirm=False,
grayscale=False,
):
new_lines = []
dirty = False
new_str = str(new_version)
regexes = SUSPECT_PHRASES if suspect_only else [version_re]
group_index = 1 if suspect_only else 2
for line_no, line in enumerate(lines):
new_line = line
offset = 0
for regex in regexes:
for m in regex.finditer(line):
# Replace suspect code with new version.
start = m.start(group_index)
end = m.end(group_index)
new_line = (
new_line[: start + offset] + new_str + new_line[end + offset :]
)
offset += len(new_str) - (end - start)
if (print_diffs or ask_confirm) and line != new_line:
print("Rewrite:")
print(
"\n".join(
map(
lambda pair: " {:4d}: {}".format(
line_no - 1 + pair[0], pair[1]
),
enumerate(lines[line_no - 2 : line_no]),
)
)
)
print(
(DIM_CODE if grayscale else RED_COLOR)
+ "-{:4d}: {}".format(line_no + 1, line)
+ END_COLOR
)
print(
(BOLD_CODE if grayscale else GREEN_COLOR)
+ "+{:4d}: {}".format(line_no + 1, new_line)
+ END_COLOR
)
print(
"\n".join(
map(
lambda pair: " {:4d}: {}".format(
line_no + 2 + pair[0], pair[1]
),
enumerate(lines[line_no + 1 : line_no + 3]),
)
)
)
if ask_confirm:
text = input("Looks good (y/n)? ")
if not positive_response(text):
print("Okay, skipping.")
new_line = line
dirty = dirty or (new_line != line)
new_lines.append(new_line)
return new_lines, dirty
def address_file(
base_path,
file_path,
version,
new_version=None,
suspect_only=False,
show_diffs=False,
rewrite=False,
ask_confirm=True,
grayscale=False,
paths_only=False,
):
if any(map(lambda x: x.match(file_path), EXCLUDED_FILES)):
logging.debug("skipping file %s as matches excluded list", file_path)
return True
# Look for all instances of the version number where it is not part of a larger number
version_re = re.compile("(^|[^\\d])(" + str(version) + ")([^\\d]|$)")
try:
contents = open(os.path.join(base_path, file_path), "r").read()
lines = contents.split("\n")
new_lines = lines
dirty = False
if suspect_only:
# Look for suspect lines (lines that attempt to set a version)
found = False
for line_no, line in enumerate(lines):
for suspect_phrase in SUSPECT_PHRASES:
for match in suspect_phrase.finditer(line):
curr_version = int(match.groups()[0])
if (new_version is None and curr_version < version) or (
new_version is not None and curr_version < new_version
):
found = True
logging.info(
"Old version: %s:%d:%s", file_path, line_no + 1, line
)
if found and new_version is not None and (show_diffs or rewrite):
new_lines, dirty = rewrite_lines(
lines,
version_re,
new_version,
True,
print_diffs=True,
ask_confirm=(rewrite and ask_confirm),
grayscale=grayscale,
)
else:
matching_lines = filter(
lambda pair: version_re.search(pair[1]), enumerate(lines)
)
# Look for lines with the version
if matching_lines:
if paths_only:
logging.info("File %s matches", file_path)
else:
for line_no, line in matching_lines:
logging.info("Match: %s:%d:%s", file_path, line_no + 1, line)
if new_version is not None and (show_diffs or rewrite):
new_lines, dirty = rewrite_lines(
lines,
version_re,
new_version,
False,
print_diffs=True,
ask_confirm=(rewrite and ask_confirm),
grayscale=grayscale,
)
else:
logging.debug("File %s does not match", file_path)
if dirty and rewrite:
logging.info("Rewriting %s", os.path.join(base_path, file_path))
with open(os.path.join(base_path, file_path), "w") as fout:
fout.write("\n".join(new_lines))
return True
except (OSError, UnicodeDecodeError):
logging.exception(
"Unable to read file %s due to OSError", os.path.join(base_path, file_path)
)
return False
def address_path(
path,
version,
new_version=None,
suspect_only=False,
show_diffs=False,
rewrite=False,
ask_confirm=True,
grayscale=False,
paths_only=False,
):
try:
if os.path.exists(path):
if os.path.isdir(path):
status = True
for dir_path, dir_names, file_names in os.walk(path):
for file_name in file_names:
file_path = os.path.relpath(
os.path.join(dir_path, file_name), path
)
status = (
address_file(
path,
file_path,
version,
new_version,
suspect_only,
show_diffs,
rewrite,
ask_confirm,
grayscale,
paths_only,
)
and status
)
return status
else:
base_name, file_name = os.path.split(path)
return address_file(
base_name,
file_name,
version,
new_version,
suspect_only,
show_diffs,
rewrite,
ask_confirm,
grayscale,
)
else:
logging.error("Path %s does not exist", path)
return False
except OSError:
logging.exception("Unable to find all API versions due to OSError")
return False
def run(arg_list):
parser = argparse.ArgumentParser(
description="finds and rewrites the API version in FDB source files"
)
parser.add_argument("path", help="path to search for FDB source files")
parser.add_argument("version", type=int, help="current/old version to search for")
parser.add_argument(
"--new-version", type=int, default=None, help="new version to update to"
)
parser.add_argument(
"--suspect-only",
action="store_true",
default=False,
help="only look for phrases trying to set the API version",
)
parser.add_argument(
"--show-diffs",
action="store_true",
default=False,
help="show suggested diffs for fixing version",
)
parser.add_argument(
"--rewrite", action="store_true", default=False, help="rewrite offending files"
)
parser.add_argument(
"-y",
"--skip-confirm",
action="store_true",
default=False,
help="do not ask for confirmation before rewriting",
)
parser.add_argument(
"--grayscale",
action="store_true",
default=False,
help="print diffs using grayscale output instead of red and green",
)
parser.add_argument(
"--paths-only",
action="store_true",
default=False,
help="display only the path instead of the offending lines",
)
args = parser.parse_args(arg_list)
return address_path(
args.path,
args.version,
args.new_version,
args.suspect_only,
args.show_diffs,
args.rewrite,
not args.skip_confirm,
args.grayscale,
args.paths_only,
)
if __name__ == "__main__":
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
if not run(sys.argv[1:]):
exit(1)