blob: 04bc72f415bff389edd3d541c5f60f2ec448bbf8 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright (c) 2019-2021, Arm Limited. All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
#
import argparse
import codecs
import collections
import os
import re
import subprocess
import sys
import utils
import yaml
import logging
# File extensions to check
VALID_FILE_EXTENSIONS = (".c", ".S", ".h")
# Paths inside the tree to ignore. Hidden folders and files are always ignored.
# They mustn't end in '/'.
IGNORED_FOLDERS = (
'platform/ext',
'bl2/ext',
'docs',
'lib',
'tools'
)
# List of ignored files in folders that aren't ignored
IGNORED_FILES = ()
INCLUDE_RE = re.compile(r"^\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
INCLUDE_RE_DIFF = re.compile(r"^\+?\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
def include_paths(lines, diff_mode=False):
"""List all include paths in a file. Ignore starting `+` in diff mode."""
pattern = INCLUDE_RE_DIFF if diff_mode else INCLUDE_RE
matches = (pattern.match(line) for line in lines)
return [m.group("path") for m in matches if m]
def file_include_list(path):
"""Return a list of all include paths in a file or None on failure."""
try:
with codecs.open(path, encoding="utf-8") as f:
return include_paths(f)
except Exception:
logging.exception(path + ":error while parsing.")
return None
def inc_order_is_correct(inc_list, path, commit_hash=""):
"""Returns true if the provided list is in order. If not, output error
messages to stdout."""
# If there are less than 2 includes there's no need to check.
if len(inc_list) < 2:
return True
if commit_hash != "":
commit_hash = commit_hash + ":"
# Get list of system includes from libc include directory.
# No libc from TF-M secure_fw
libc_incs = []
# First, check if all includes are in the appropriate group.
inc_group = "Public"
incs = collections.defaultdict(list)
error_msgs = []
for inc in inc_list:
if inc.startswith('"'):
inc_group = "Private"
elif inc_group == "Private":
error_msgs.append(
inc[1:-1] + " should be in public group, before private group"
)
incs[inc_group].append(inc[1:-1])
# Then, check alphabetic order (system, project and user separately).
if not error_msgs:
for name, inc_list in incs.items():
if sorted(inc_list) != inc_list:
error_msgs.append("{} includes not in order.".format(name))
# Output error messages.
if error_msgs:
print(yaml.dump({commit_hash + path: error_msgs}))
return False
else:
return True
def file_is_correct(path):
"""Checks whether the order of includes in the file specified in the path
is correct or not."""
inc_list = file_include_list(path)
return inc_list is not None and inc_order_is_correct(inc_list, path)
def directory_tree_is_correct():
"""Checks all tracked files in the current git repository, except the ones
explicitly ignored by this script.
Returns True if all files are correct."""
(rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
if rc != 0:
return False
all_files_correct = True
for f in stdout.splitlines():
if not utils.file_is_ignored(
f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
):
all_files_correct &= file_is_correct(f)
return all_files_correct
def group_lines(patchlines, starting_with):
"""Generator of (name, lines) almost the same as itertools.groupby
This function's control flow is non-trivial. In particular, the clearing
of the lines variable, marked with [1], is intentional and must come
after the yield. That's because we must yield the (name, lines) tuple
after we have found the name of the next section but before we assign the
name and start collecting lines. Further, [2] is required to yield the
last block as there will not be a block start delimiter at the end of
the stream.
"""
lines = []
name = None
for line in patchlines:
if line.startswith(starting_with):
if name:
yield name, lines
name = line[len(starting_with) :]
lines = [] # [1]
else:
lines.append(line)
yield name, lines # [2]
def group_files(commitlines):
"""Generator of (commit hash, lines) almost the same as itertools.groupby"""
return group_lines(commitlines, "+++ b/")
def group_commits(commitlines):
"""Generator of (file name, lines) almost the same as itertools.groupby"""
return group_lines(commitlines, "commit ")
def patch_is_correct(base_commit, end_commit):
"""Get the output of a git diff and analyse each modified file."""
# Get patches of the affected commits with one line of context.
gitlog = subprocess.run(
[
"git",
"log",
"--unified=1",
"--pretty=commit %h",
base_commit + ".." + end_commit,
],
stdout=subprocess.PIPE,
)
if gitlog.returncode != 0:
return False
gitlines = gitlog.stdout.decode("utf-8", "replace").splitlines()
all_files_correct = True
for commit, comlines in group_commits(gitlines):
for path, lines in group_files(comlines):
all_files_correct &= inc_order_is_correct(
include_paths(lines, diff_mode=True), path, commit
)
return all_files_correct
def parse_cmd_line(argv, prog_name):
parser = argparse.ArgumentParser(
prog=prog_name,
formatter_class=argparse.RawTextHelpFormatter,
description="Check alphabetical order of #includes",
epilog="""
For each source file in the tree, checks that #include's C preprocessor
directives are ordered alphabetically (as mandated by the Trusted
Firmware coding style). System header includes must come before user
header includes.
""",
)
parser.add_argument(
"--tree",
"-t",
help="Path to the source tree to check (default: %(default)s)",
default=os.curdir,
)
parser.add_argument(
"--patch",
"-p",
help="""
Patch mode.
Instead of checking all files in the source tree, the script will consider
only files that are modified by the latest patch(es).""",
action="store_true",
)
parser.add_argument(
"--from-ref",
help="Base commit in patch mode (default: %(default)s)",
default="origin/master",
)
parser.add_argument(
"--to-ref",
help="Final commit in patch mode (default: %(default)s)",
default="HEAD",
)
args = parser.parse_args(argv)
return args
if __name__ == "__main__":
args = parse_cmd_line(sys.argv[1:], sys.argv[0])
os.chdir(args.tree)
if args.patch:
print(
"Checking files modified between patches "
+ args.from_ref
+ " and "
+ args.to_ref
+ "..."
)
if not patch_is_correct(args.from_ref, args.to_ref):
sys.exit(1)
else:
print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
if not directory_tree_is_correct():
sys.exit(1)
# All source code files are correct.
sys.exit(0)