scripts/check.py (185 lines of code) (raw):
#!/usr/bin/env python3
# Copyright (c) Facebook, Inc. and its affiliates.
#
# 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
from collections import OrderedDict
import os
import regex
import subprocess
import sys
from util import attrdict
import util
EXTENSIONS = "cpp,h,inc,prolog"
SCRIPTS = util.script_path()
def get_diff(file, formatted):
if not formatted.endswith("\n"):
formatted = formatted + "\n"
status, stdout, stderr = util.run(
f"diff -u {file} --label {file} --label {file} -", input=formatted
)
if stdout != "":
stdout = f"diff a/{file} b/{file}\n" + stdout
return status, stdout, stderr
class CppFormatter(str):
def diff(self, commit):
if commit == "":
return get_diff(self, util.run(f"clang-format --style=file {self}")[1])
else:
return util.run(
f"{SCRIPTS}/git-clang-format -q --extensions='{EXTENSIONS}' --diff --style=file {commit} {self}"
)
def fix(self, commit):
if commit == "":
return util.run(f"clang-format -i --style=file {self}")[0] == 0
else:
return (
util.run(
f"{SCRIPTS}/git-clang-format -q --extensions='{EXTENSIONS}' --style=file {commit} {self}"
)[0]
== 0
)
class CMakeFormatter(str):
def diff(self, commit):
return get_diff(
self, util.run(f"cmake-format --first-comment-is-literal True {self}")[1]
)
def fix(self, commit):
return (
util.run(f"cmake-format --first-comment-is-literal True -i {self}")[0] == 0
)
class PythonFormatter(str):
def diff(self, commit):
return util.run(f"black -q --diff {self}")
def fix(self, commit):
return util.run(f"black -q {self}")[0] == 0
format_file_types = OrderedDict(
{
"CMakeLists.txt": attrdict({"formatter": CMakeFormatter}),
"*.cpp": attrdict({"formatter": CppFormatter}),
"*.h": attrdict({"formatter": CppFormatter}),
"*.inc": attrdict({"formatter": CppFormatter}),
"*.prolog": attrdict({"formatter": CppFormatter}),
"*.py": attrdict({"formatter": PythonFormatter}),
}
)
def get_formatter(filename):
if filename in format_file_types:
return format_file_types[filename]
return format_file_types.get("*" + util.get_fileextn(filename), None)
def format_command(commit, files, fix):
ok = 0
for filepath in files:
filename = util.get_filename(filepath)
filetype = get_formatter(filename)
if filetype is None:
print("Skip : " + filepath, file=sys.stderr)
continue
file = filetype.formatter(filepath)
if fix == "show":
status, diff, stderr = file.diff(commit)
if stderr != "":
ok = 1
print(f"Error: {file}", file=sys.stderr)
continue
if diff != "" and diff != "no modified files to format":
ok = 1
print(f"Fix : {file}", file=sys.stderr)
print(diff)
else:
print(f"Ok : {file}", file=sys.stderr)
else:
print(f"Fix : {file}", file=sys.stderr)
if not file.fix(commit):
ok = 1
print(f"Error: {file}", file=sys.stderr)
return ok
def header_command(commit, files, fix):
options = "-vk" if fix == "show" else "-i"
status, stdout, stderr = util.run(
f"{SCRIPTS}/license-header.py {options} -", input=files
)
if stdout != "":
print(stdout)
return status
def tidy_command(commit, files, fix):
files = [file for file in files if regex.match(r".*\.cpp$", file)]
if not files:
return 0
commit = f"--commit {commit}" if commit != "" else ""
fix = "--fix" if fix == "fix" else ""
status, stdout, stderr = util.run(
f"{SCRIPTS}/run-clang-tidy.py {commit} {fix} -", input=files
)
if stdout != "":
print(stdout)
return status
def get_commit(files):
if files == "commit":
return "HEAD^"
if files == "branch":
return util.run("git merge-base origin/main HEAD")[1]
return ""
def get_files(commit, path):
filelist = []
if commit != "":
status, stdout, stderr = util.run(
f"git diff --name-only --diff-filter='ACM' {commit}"
)
filelist = stdout.splitlines()
else:
for root, dirs, files in os.walk(path):
for name in files:
filelist.append(os.path.join(root, name))
return [
file
for file in filelist
if "/data/" not in file
and "velox/external/" not in file
and "build/fbcode_builder" not in file
and "build/deps" not in file
and "cmake-build-debug" not in file
]
def help(args):
parser.print_help()
return 0
def add_check_options(subparser, name):
parser = subparser.add_parser(name)
parser.add_argument("--fix", action="store_const", default="show", const="fix")
return parser
def add_options(parser):
files = parser.add_subparsers(dest="files")
tree_parser = add_check_options(files, "tree")
tree_parser.add_argument("path", default="")
branch_parser = add_check_options(files, "branch")
commit_parser = add_check_options(files, "commit")
def add_check_command(parser, name):
subparser = parser.add_parser(name)
add_options(subparser)
return subparser
def parse_args():
global parser
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
description="""Check format/header/tidy
check.py {format,header,tidy} {commit,branch} [--fix]
check.py {format,header,tidy} {tree} [--fix] PATH
""",
)
command = parser.add_subparsers(dest="command")
command.add_parser("help")
format_command_parser = add_check_command(command, "format")
header_command_parser = add_check_command(command, "header")
tidy_command_parser = add_check_command(command, "tidy")
parser.set_defaults(path="")
parser.set_defaults(command="help")
return parser.parse_args()
def run_command(args, command):
commit = get_commit(args.files)
files = get_files(commit, args.path)
return command(commit, files, args.fix)
def format(args):
return run_command(args, format_command)
def header(args):
return run_command(args, header_command)
def tidy(args):
return run_command(args, tidy_command)
def main():
args = parse_args()
return globals()[args.command](args)
if __name__ == "__main__":
sys.exit(main())