blob: 4f605f33bdd53f194c30d20d823a7df4253604ef [file] [log] [blame]
Fathi Boudra422bf772019-12-02 11:10:16 +02001#!/usr/bin/env python3
2#
Zelalem219df412020-05-17 19:21:20 -05003# Copyright (c) 2019-2020, Arm Limited. All rights reserved.
Fathi Boudra422bf772019-12-02 11:10:16 +02004#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7
8import argparse
9import codecs
Zelalem219df412020-05-17 19:21:20 -050010import collections
Fathi Boudra422bf772019-12-02 11:10:16 +020011import os
12import re
Zelalem219df412020-05-17 19:21:20 -050013import subprocess
Fathi Boudra422bf772019-12-02 11:10:16 +020014import sys
15import utils
Zelalem219df412020-05-17 19:21:20 -050016import yaml
17import logging
Fathi Boudra422bf772019-12-02 11:10:16 +020018
19
20# File extensions to check
Zelalem219df412020-05-17 19:21:20 -050021VALID_FILE_EXTENSIONS = (".c", ".S", ".h")
Fathi Boudra422bf772019-12-02 11:10:16 +020022
23
24# Paths inside the tree to ignore. Hidden folders and files are always ignored.
25# They mustn't end in '/'.
Zelalem219df412020-05-17 19:21:20 -050026IGNORED_FOLDERS = (
27 "include/lib/stdlib",
28 "include/lib/libc",
29 "include/lib/libfdt",
30 "lib/libfdt",
31 "lib/libc",
32 "lib/stdlib",
Fathi Boudra422bf772019-12-02 11:10:16 +020033)
34
Zelalem219df412020-05-17 19:21:20 -050035# List of ignored files in folders that aren't ignored
36IGNORED_FILES = ()
Fathi Boudra422bf772019-12-02 11:10:16 +020037
Zelalem219df412020-05-17 19:21:20 -050038INCLUDE_RE = re.compile(r"^\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
39INCLUDE_RE_DIFF = re.compile(r"^\+?\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
Fathi Boudra422bf772019-12-02 11:10:16 +020040
41
Zelalem219df412020-05-17 19:21:20 -050042def include_paths(lines, diff_mode=False):
43 """List all include paths in a file. Ignore starting `+` in diff mode."""
44 pattern = INCLUDE_RE_DIFF if diff_mode else INCLUDE_RE
45 matches = (pattern.match(line) for line in lines)
46 return [m.group("path") for m in matches if m]
Fathi Boudra422bf772019-12-02 11:10:16 +020047
48
Zelalem219df412020-05-17 19:21:20 -050049def file_include_list(path):
50 """Return a list of all include paths in a file or None on failure."""
Fathi Boudra422bf772019-12-02 11:10:16 +020051 try:
Zelalem219df412020-05-17 19:21:20 -050052 with codecs.open(path, encoding="utf-8") as f:
53 return include_paths(f)
54 except Exception:
55 logging.exception(path + ":error while parsing.")
Fathi Boudra422bf772019-12-02 11:10:16 +020056 return None
57
Fathi Boudra422bf772019-12-02 11:10:16 +020058
59def inc_order_is_correct(inc_list, path, commit_hash=""):
Zelalem219df412020-05-17 19:21:20 -050060 """Returns true if the provided list is in order. If not, output error
61 messages to stdout."""
Fathi Boudra422bf772019-12-02 11:10:16 +020062
63 # If there are less than 2 includes there's no need to check.
64 if len(inc_list) < 2:
65 return True
66
67 if commit_hash != "":
Zelalem219df412020-05-17 19:21:20 -050068 commit_hash = commit_hash + ":"
Fathi Boudra422bf772019-12-02 11:10:16 +020069
Zelalem219df412020-05-17 19:21:20 -050070 # Get list of system includes from libc include directory.
71 libc_incs = [f for f in os.listdir("include/lib/libc") if f.endswith(".h")]
Fathi Boudra422bf772019-12-02 11:10:16 +020072
Zelalem219df412020-05-17 19:21:20 -050073 # First, check if all includes are in the appropriate group.
74 inc_group = "System"
75 incs = collections.defaultdict(list)
76 error_msgs = []
Fathi Boudra422bf772019-12-02 11:10:16 +020077
78 for inc in inc_list:
Zelalem219df412020-05-17 19:21:20 -050079 if inc[1:-1] in libc_incs:
80 if inc_group != "System":
81 error_msgs.append(inc[1:-1] + " should be in system group, at the top")
82 elif (
83 "plat/" in inc
84 or "platform" in inc
85 or (inc.startswith('"') and "plat" in path)
86 ):
87 inc_group = "Platform"
88 elif inc_group in ("Project", "System"):
89 inc_group = "Project"
90 else:
91 error_msgs.append(
92 inc[1:-1] + " should be in project group, after system group"
93 )
94 incs[inc_group].append(inc[1:-1])
Fathi Boudra422bf772019-12-02 11:10:16 +020095
Zelalem219df412020-05-17 19:21:20 -050096 # Then, check alphabetic order (system, project and user separately).
97 if not error_msgs:
98 for name, inc_list in incs.items():
99 if sorted(inc_list) != inc_list:
100 error_msgs.append("{} includes not in order.".format(name))
Fathi Boudra422bf772019-12-02 11:10:16 +0200101
102 # Output error messages.
Zelalem219df412020-05-17 19:21:20 -0500103 if error_msgs:
104 print(yaml.dump({commit_hash + path: error_msgs}))
105 return False
106 else:
107 return True
Fathi Boudra422bf772019-12-02 11:10:16 +0200108
109
110def file_is_correct(path):
Zelalem219df412020-05-17 19:21:20 -0500111 """Checks whether the order of includes in the file specified in the path
112 is correct or not."""
113 inc_list = file_include_list(path)
114 return inc_list is not None and inc_order_is_correct(inc_list, path)
Fathi Boudra422bf772019-12-02 11:10:16 +0200115
116
117def directory_tree_is_correct():
Zelalem219df412020-05-17 19:21:20 -0500118 """Checks all tracked files in the current git repository, except the ones
Fathi Boudra422bf772019-12-02 11:10:16 +0200119 explicitly ignored by this script.
Zelalem219df412020-05-17 19:21:20 -0500120 Returns True if all files are correct."""
121 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
Fathi Boudra422bf772019-12-02 11:10:16 +0200122 if rc != 0:
123 return False
Fathi Boudra422bf772019-12-02 11:10:16 +0200124 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500125 for f in stdout.splitlines():
126 if not utils.file_is_ignored(
127 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
128 ):
129 all_files_correct &= file_is_correct(f)
Fathi Boudra422bf772019-12-02 11:10:16 +0200130 return all_files_correct
131
132
Zelalem219df412020-05-17 19:21:20 -0500133def group_lines(patchlines, starting_with):
134 """Generator of (name, lines) almost the same as itertools.groupby
135
136 This function's control flow is non-trivial. In particular, the clearing
137 of the lines variable, marked with [1], is intentional and must come
138 after the yield. That's because we must yield the (name, lines) tuple
139 after we have found the name of the next section but before we assign the
140 name and start collecting lines. Further, [2] is required to yeild the
141 last block as there will not be a block start delimeter at the end of
142 the stream.
143 """
144 lines = []
145 name = None
146 for line in patchlines:
147 if line.startswith(starting_with):
148 if name:
149 yield name, lines
150 name = line[len(starting_with) :]
151 lines = [] # [1]
152 else:
153 lines.append(line)
154 yield name, lines # [2]
155
156
157def group_files(commitlines):
158 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
159 return group_lines(commitlines, "+++ b/")
160
161
162def group_commits(commitlines):
163 """Generator of (file name, lines) almost the same as itertools.groupby"""
164 return group_lines(commitlines, "commit ")
165
166
Fathi Boudra422bf772019-12-02 11:10:16 +0200167def patch_is_correct(base_commit, end_commit):
Zelalem219df412020-05-17 19:21:20 -0500168 """Get the output of a git diff and analyse each modified file."""
Fathi Boudra422bf772019-12-02 11:10:16 +0200169
170 # Get patches of the affected commits with one line of context.
Zelalem219df412020-05-17 19:21:20 -0500171 gitlog = subprocess.run(
172 [
173 "git",
174 "log",
175 "--unified=1",
176 "--pretty=commit %h",
177 base_commit + ".." + end_commit,
178 ],
179 stdout=subprocess.PIPE,
180 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200181
Zelalem219df412020-05-17 19:21:20 -0500182 if gitlog.returncode != 0:
Fathi Boudra422bf772019-12-02 11:10:16 +0200183 return False
184
Zelalem219df412020-05-17 19:21:20 -0500185 gitlines = gitlog.stdout.decode("utf-8").splitlines()
Fathi Boudra422bf772019-12-02 11:10:16 +0200186 all_files_correct = True
Zelalem219df412020-05-17 19:21:20 -0500187 for commit, comlines in group_commits(gitlines):
188 for path, lines in group_files(comlines):
189 all_files_correct &= inc_order_is_correct(
190 include_paths(lines, diff_mode=True), path, commit
191 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200192 return all_files_correct
193
194
Fathi Boudra422bf772019-12-02 11:10:16 +0200195def parse_cmd_line(argv, prog_name):
196 parser = argparse.ArgumentParser(
197 prog=prog_name,
198 formatter_class=argparse.RawTextHelpFormatter,
199 description="Check alphabetical order of #includes",
200 epilog="""
201For each source file in the tree, checks that #include's C preprocessor
202directives are ordered alphabetically (as mandated by the Trusted
203Firmware coding style). System header includes must come before user
204header includes.
Zelalem219df412020-05-17 19:21:20 -0500205""",
206 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200207
Zelalem219df412020-05-17 19:21:20 -0500208 parser.add_argument(
209 "--tree",
210 "-t",
211 help="Path to the source tree to check (default: %(default)s)",
212 default=os.curdir,
213 )
214 parser.add_argument(
215 "--patch",
216 "-p",
217 help="""
Fathi Boudra422bf772019-12-02 11:10:16 +0200218Patch mode.
219Instead of checking all files in the source tree, the script will consider
220only files that are modified by the latest patch(es).""",
Zelalem219df412020-05-17 19:21:20 -0500221 action="store_true",
222 )
223 parser.add_argument(
224 "--from-ref",
225 help="Base commit in patch mode (default: %(default)s)",
226 default="master",
227 )
228 parser.add_argument(
229 "--to-ref",
230 help="Final commit in patch mode (default: %(default)s)",
231 default="HEAD",
232 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200233 args = parser.parse_args(argv)
234 return args
235
236
237if __name__ == "__main__":
238 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
239
240 os.chdir(args.tree)
241
242 if args.patch:
Zelalem219df412020-05-17 19:21:20 -0500243 print(
244 "Checking files modified between patches "
245 + args.from_ref
246 + " and "
247 + args.to_ref
248 + "..."
249 )
Fathi Boudra422bf772019-12-02 11:10:16 +0200250 if not patch_is_correct(args.from_ref, args.to_ref):
251 sys.exit(1)
252 else:
253 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
254 if not directory_tree_is_correct():
255 sys.exit(1)
256
257 # All source code files are correct.
258 sys.exit(0)