copy script/static-checks folder from tf-a-ci-scripts
This is a complete copy of the folder [1], based on working dir
from commit [2]. Some checks DO NOT apply to TF-M project, but we
introduce the code as it is from TF-A project for completeness and
then further commits will remove/change relevant tests.
[1] https://git.trustedfirmware.org/ci/tf-a-ci-scripts.git/tree/script/static-checks
[2] https://git.trustedfirmware.org/ci/tf-a-ci-scripts.git/commit/?id=a16c8e2b37e2a8446e586f45c3e9b406496ad7b8
Change-Id: I024c30dc59caa6e635fcd43b1a30086231b29bc3
Signed-off-by: Leonardo Sandoval <leonardo.sandoval@linaro.org>
diff --git a/script/static-checks/check-include-order.py b/script/static-checks/check-include-order.py
new file mode 100755
index 0000000..4f605f3
--- /dev/null
+++ b/script/static-checks/check-include-order.py
@@ -0,0 +1,258 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2019-2020, 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 = (
+ "include/lib/stdlib",
+ "include/lib/libc",
+ "include/lib/libfdt",
+ "lib/libfdt",
+ "lib/libc",
+ "lib/stdlib",
+)
+
+# 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.
+ libc_incs = [f for f in os.listdir("include/lib/libc") if f.endswith(".h")]
+
+ # First, check if all includes are in the appropriate group.
+ inc_group = "System"
+ incs = collections.defaultdict(list)
+ error_msgs = []
+
+ for inc in inc_list:
+ if inc[1:-1] in libc_incs:
+ if inc_group != "System":
+ error_msgs.append(inc[1:-1] + " should be in system group, at the top")
+ elif (
+ "plat/" in inc
+ or "platform" in inc
+ or (inc.startswith('"') and "plat" in path)
+ ):
+ inc_group = "Platform"
+ elif inc_group in ("Project", "System"):
+ inc_group = "Project"
+ else:
+ error_msgs.append(
+ inc[1:-1] + " should be in project group, after system 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 yeild the
+ last block as there will not be a block start delimeter 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").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="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)