blob: 04bc72f415bff389edd3d541c5f60f2ec448bbf8 [file] [log] [blame]
Leonardo Sandoval314eed82020-08-05 13:32:04 -05001#!/usr/bin/env python3
2#
Xinyu Zhang235d5ae2021-02-07 10:42:38 +08003# Copyright (c) 2019-2021, Arm Limited. All rights reserved.
Leonardo Sandoval314eed82020-08-05 13:32:04 -05004#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7
8import argparse
9import codecs
10import collections
11import os
12import re
13import subprocess
14import sys
15import utils
16import yaml
17import logging
18
19
20# File extensions to check
21VALID_FILE_EXTENSIONS = (".c", ".S", ".h")
22
23
24# Paths inside the tree to ignore. Hidden folders and files are always ignored.
25# They mustn't end in '/'.
26IGNORED_FOLDERS = (
Xinyu Zhang235d5ae2021-02-07 10:42:38 +080027 'platform/ext',
28 'bl2/ext',
29 'docs',
30 'lib',
31 'tools'
Leonardo Sandoval314eed82020-08-05 13:32:04 -050032)
33
34# List of ignored files in folders that aren't ignored
35IGNORED_FILES = ()
36
37INCLUDE_RE = re.compile(r"^\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
38INCLUDE_RE_DIFF = re.compile(r"^\+?\s*#\s*include\s\s*(?P<path>[\"<].+[\">])")
39
40
41def include_paths(lines, diff_mode=False):
42 """List all include paths in a file. Ignore starting `+` in diff mode."""
43 pattern = INCLUDE_RE_DIFF if diff_mode else INCLUDE_RE
44 matches = (pattern.match(line) for line in lines)
45 return [m.group("path") for m in matches if m]
46
47
48def file_include_list(path):
49 """Return a list of all include paths in a file or None on failure."""
50 try:
51 with codecs.open(path, encoding="utf-8") as f:
52 return include_paths(f)
53 except Exception:
54 logging.exception(path + ":error while parsing.")
55 return None
56
57
58def inc_order_is_correct(inc_list, path, commit_hash=""):
59 """Returns true if the provided list is in order. If not, output error
60 messages to stdout."""
61
62 # If there are less than 2 includes there's no need to check.
63 if len(inc_list) < 2:
64 return True
65
66 if commit_hash != "":
67 commit_hash = commit_hash + ":"
68
69 # Get list of system includes from libc include directory.
Xinyu Zhang235d5ae2021-02-07 10:42:38 +080070 # No libc from TF-M secure_fw
71 libc_incs = []
Leonardo Sandoval314eed82020-08-05 13:32:04 -050072
73 # First, check if all includes are in the appropriate group.
Xinyu Zhange8015942021-02-22 16:06:00 +080074 inc_group = "Public"
Leonardo Sandoval314eed82020-08-05 13:32:04 -050075 incs = collections.defaultdict(list)
76 error_msgs = []
77
78 for inc in inc_list:
Xinyu Zhange8015942021-02-22 16:06:00 +080079 if inc.startswith('"'):
80 inc_group = "Private"
81 elif inc_group == "Private":
Leonardo Sandoval314eed82020-08-05 13:32:04 -050082 error_msgs.append(
Xinyu Zhange8015942021-02-22 16:06:00 +080083 inc[1:-1] + " should be in public group, before private group"
Leonardo Sandoval314eed82020-08-05 13:32:04 -050084 )
85 incs[inc_group].append(inc[1:-1])
86
87 # Then, check alphabetic order (system, project and user separately).
88 if not error_msgs:
89 for name, inc_list in incs.items():
90 if sorted(inc_list) != inc_list:
91 error_msgs.append("{} includes not in order.".format(name))
92
93 # Output error messages.
94 if error_msgs:
95 print(yaml.dump({commit_hash + path: error_msgs}))
96 return False
97 else:
98 return True
99
100
101def file_is_correct(path):
102 """Checks whether the order of includes in the file specified in the path
103 is correct or not."""
104 inc_list = file_include_list(path)
105 return inc_list is not None and inc_order_is_correct(inc_list, path)
106
107
108def directory_tree_is_correct():
109 """Checks all tracked files in the current git repository, except the ones
110 explicitly ignored by this script.
111 Returns True if all files are correct."""
112 (rc, stdout, stderr) = utils.shell_command(["git", "ls-files"])
113 if rc != 0:
114 return False
115 all_files_correct = True
116 for f in stdout.splitlines():
117 if not utils.file_is_ignored(
118 f, VALID_FILE_EXTENSIONS, IGNORED_FILES, IGNORED_FOLDERS
119 ):
120 all_files_correct &= file_is_correct(f)
121 return all_files_correct
122
123
124def group_lines(patchlines, starting_with):
125 """Generator of (name, lines) almost the same as itertools.groupby
126
127 This function's control flow is non-trivial. In particular, the clearing
128 of the lines variable, marked with [1], is intentional and must come
129 after the yield. That's because we must yield the (name, lines) tuple
130 after we have found the name of the next section but before we assign the
Nicola Mazzucato935f9cb2025-05-16 17:21:07 +0100131 name and start collecting lines. Further, [2] is required to yield the
132 last block as there will not be a block start delimiter at the end of
Leonardo Sandoval314eed82020-08-05 13:32:04 -0500133 the stream.
134 """
135 lines = []
136 name = None
137 for line in patchlines:
138 if line.startswith(starting_with):
139 if name:
140 yield name, lines
141 name = line[len(starting_with) :]
142 lines = [] # [1]
143 else:
144 lines.append(line)
145 yield name, lines # [2]
146
147
148def group_files(commitlines):
149 """Generator of (commit hash, lines) almost the same as itertools.groupby"""
150 return group_lines(commitlines, "+++ b/")
151
152
153def group_commits(commitlines):
154 """Generator of (file name, lines) almost the same as itertools.groupby"""
155 return group_lines(commitlines, "commit ")
156
157
158def patch_is_correct(base_commit, end_commit):
159 """Get the output of a git diff and analyse each modified file."""
160
161 # Get patches of the affected commits with one line of context.
162 gitlog = subprocess.run(
163 [
164 "git",
165 "log",
166 "--unified=1",
167 "--pretty=commit %h",
168 base_commit + ".." + end_commit,
169 ],
170 stdout=subprocess.PIPE,
171 )
172
173 if gitlog.returncode != 0:
174 return False
175
Paul Sokolovskyd89d7e72024-03-12 17:11:04 +0700176 gitlines = gitlog.stdout.decode("utf-8", "replace").splitlines()
Leonardo Sandoval314eed82020-08-05 13:32:04 -0500177 all_files_correct = True
178 for commit, comlines in group_commits(gitlines):
179 for path, lines in group_files(comlines):
180 all_files_correct &= inc_order_is_correct(
181 include_paths(lines, diff_mode=True), path, commit
182 )
183 return all_files_correct
184
185
186def parse_cmd_line(argv, prog_name):
187 parser = argparse.ArgumentParser(
188 prog=prog_name,
189 formatter_class=argparse.RawTextHelpFormatter,
190 description="Check alphabetical order of #includes",
191 epilog="""
192For each source file in the tree, checks that #include's C preprocessor
193directives are ordered alphabetically (as mandated by the Trusted
194Firmware coding style). System header includes must come before user
195header includes.
196""",
197 )
198
199 parser.add_argument(
200 "--tree",
201 "-t",
202 help="Path to the source tree to check (default: %(default)s)",
203 default=os.curdir,
204 )
205 parser.add_argument(
206 "--patch",
207 "-p",
208 help="""
209Patch mode.
210Instead of checking all files in the source tree, the script will consider
211only files that are modified by the latest patch(es).""",
212 action="store_true",
213 )
214 parser.add_argument(
215 "--from-ref",
216 help="Base commit in patch mode (default: %(default)s)",
Leonardo Sandoval900de582020-09-07 18:34:57 -0500217 default="origin/master",
Leonardo Sandoval314eed82020-08-05 13:32:04 -0500218 )
219 parser.add_argument(
220 "--to-ref",
221 help="Final commit in patch mode (default: %(default)s)",
222 default="HEAD",
223 )
224 args = parser.parse_args(argv)
225 return args
226
227
228if __name__ == "__main__":
229 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
230
231 os.chdir(args.tree)
232
233 if args.patch:
234 print(
235 "Checking files modified between patches "
236 + args.from_ref
237 + " and "
238 + args.to_ref
239 + "..."
240 )
241 if not patch_is_correct(args.from_ref, args.to_ref):
242 sys.exit(1)
243 else:
244 print("Checking all files in directory '%s'..." % os.path.abspath(args.tree))
245 if not directory_tree_is_correct():
246 sys.exit(1)
247
248 # All source code files are correct.
249 sys.exit(0)