blob: 26b691c2f48faf74eea3c641a5992361522d83ef [file] [log] [blame]
David Horstmannfa928f12022-11-01 15:46:16 +00001#!/usr/bin/env python3
2"""Check or fix the code style by running Uncrustify.
David Horstmann8b5a4492023-01-16 18:28:21 +00003
4This script must be run from the root of a Git work tree containing Mbed TLS.
David Horstmannfa928f12022-11-01 15:46:16 +00005"""
6# Copyright The Mbed TLS Contributors
Dave Rodgman16799db2023-11-02 19:47:20 +00007# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
David Horstmannfa928f12022-11-01 15:46:16 +00008import argparse
David Horstmannfa928f12022-11-01 15:46:16 +00009import os
Gilles Peskine9a3771e2022-12-19 00:48:58 +010010import re
David Horstmannfa928f12022-11-01 15:46:16 +000011import subprocess
12import sys
Gilles Peskine43838b82023-06-22 20:29:41 +020013from typing import FrozenSet, List, Optional
David Horstmannfa928f12022-11-01 15:46:16 +000014
David Horstmann2cf779c2022-12-08 14:44:36 +000015UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1"
David Horstmannae93a3f2022-12-08 17:03:01 +000016CONFIG_FILE = ".uncrustify.cfg"
David Horstmannfa928f12022-11-01 15:46:16 +000017UNCRUSTIFY_EXE = "uncrustify"
18UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE]
Gilles Peskine9a3771e2022-12-19 00:48:58 +010019CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh"
David Horstmannfa928f12022-11-01 15:46:16 +000020
David Horstmannca13c4f2022-12-08 14:33:52 +000021def print_err(*args):
David Horstmann6b3ce302023-01-24 18:36:41 +000022 print("Error: ", *args, file=sys.stderr)
David Horstmannca13c4f2022-12-08 14:33:52 +000023
Pengyu Lvacbeb7f2023-02-06 14:27:30 +080024# Print the file names that will be skipped and the help message
25def print_skip(files_to_skip):
26 print()
27 print(*files_to_skip, sep=", SKIP\n", end=", SKIP\n")
Pengyu Lvc36743f2023-02-15 10:20:40 +080028 print("Warning: The listed files will be skipped because\n"
29 "they are not known to git.")
Pengyu Lvacbeb7f2023-02-06 14:27:30 +080030 print()
31
Gilles Peskine9a3771e2022-12-19 00:48:58 +010032# Match FILENAME(s) in "check SCRIPT (FILENAME...)"
33CHECK_CALL_RE = re.compile(r"\n\s*check\s+[^\s#$&*?;|]+([^\n#$&*?;|]+)",
34 re.ASCII)
35def list_generated_files() -> FrozenSet[str]:
36 """Return the names of generated files.
37
38 We don't reformat generated files, since the result might be different
39 from the output of the generator. Ideally the result of the generator
40 would conform to the code style, but this would be difficult, especially
41 with respect to the placement of line breaks in long logical lines.
42 """
43 # Parse check-generated-files.sh to get an up-to-date list of
44 # generated files. Read the file rather than calling it so that
45 # this script only depends on Git, Python and uncrustify, and not other
46 # tools such as sh or grep which might not be available on Windows.
47 # This introduces a limitation: check-generated-files.sh must have
48 # the expected format and must list the files explicitly, not through
49 # wildcards or command substitution.
50 content = open(CHECK_GENERATED_FILES, encoding="utf-8").read()
51 checks = re.findall(CHECK_CALL_RE, content)
52 return frozenset(word for s in checks for word in s.split())
53
Dave Rodgman2a9eb222024-03-18 11:15:06 +000054# Check for comment string indicating an auto-generated file
Dave Rodgman1bd787a2024-03-18 12:32:49 +000055AUTOGEN_RE = re.compile(r"Warning[ :-]+This file is (now )?auto[ -]?generated",
Dave Rodgman4e4540d2024-03-18 11:55:39 +000056 re.ASCII | re.IGNORECASE)
Dave Rodgman2a9eb222024-03-18 11:15:06 +000057def is_file_autogenerated(filename):
58 content = open(filename, encoding="utf-8").read()
59 return AUTOGEN_RE.search(content) is not None
60
Gilles Peskine43838b82023-06-22 20:29:41 +020061def get_src_files(since: Optional[str]) -> List[str]:
David Horstmannfa928f12022-11-01 15:46:16 +000062 """
Gilles Peskine22eb82c2023-06-22 19:45:01 +020063 Use git to get a list of the source files.
64
Gilles Peskine43838b82023-06-22 20:29:41 +020065 The optional argument since is a commit, indicating to only list files
66 that have changed since that commit. Without this argument, list all
67 files known to git.
68
Ronald Cron2fd621e2024-07-02 08:58:21 +020069 Only C files are included, and certain files (generated, or third party)
Gilles Peskine22eb82c2023-06-22 19:45:01 +020070 are excluded.
David Horstmannfa928f12022-11-01 15:46:16 +000071 """
Gilles Peskine163ec402023-06-25 22:18:40 +020072 file_patterns = ["*.[hc]",
73 "tests/suites/*.function",
Ronald Cron9e2ff402024-07-11 19:49:16 +020074 "tf-psa-crypto/tests/suites/*.function",
Gilles Peskine163ec402023-06-25 22:18:40 +020075 "scripts/data_files/*.fmt"]
76 output = subprocess.check_output(["git", "ls-files"] + file_patterns,
77 universal_newlines=True)
Gilles Peskine22eb82c2023-06-22 19:45:01 +020078 src_files = output.split()
David Horstmann330680e2024-06-06 15:25:10 +010079
80 # When this script is called from a git hook, some environment variables
81 # are set by default which force all git commands to use the main repository
82 # (i.e. prevent us from performing commands on the framework repo).
83 # Create an environment without these variables for running commands on the
84 # framework repo.
85 framework_env = os.environ.copy()
86 # Get a list of environment vars that git sets
87 git_env_vars = subprocess.check_output(["git", "rev-parse", "--local-env-vars"],
88 universal_newlines=True)
David Horstmann330680e2024-06-06 15:25:10 +010089 # Remove the vars from the environment
David Horstmannf8bbc2d2024-06-06 16:16:31 +010090 for var in git_env_vars.split():
David Horstmann330680e2024-06-06 15:25:10 +010091 framework_env.pop(var, None)
92
Ronald Cron62a908d2024-04-25 15:46:01 +020093 output = subprocess.check_output(["git", "-C", "framework", "ls-files"]
David Horstmann330680e2024-06-06 15:25:10 +010094 + file_patterns,
95 universal_newlines=True,
96 env=framework_env)
Ronald Cron62a908d2024-04-25 15:46:01 +020097 framework_src_files = output.split()
98
Gilles Peskine163ec402023-06-25 22:18:40 +020099 if since:
Ronald Cron62a908d2024-04-25 15:46:01 +0200100 # get all files changed in commits since the starting point in ...
101 # ... the main repository
102 cmd = ["git", "log", since + "..HEAD", "--ignore-submodules",
103 "--name-only", "--pretty=", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +0100104 output = subprocess.check_output(cmd, universal_newlines=True)
105 committed_changed_files = output.split()
Harry Ramseyda8f42a2024-09-18 14:03:26 +0100106
Ronald Cron62a908d2024-04-25 15:46:01 +0200107 # ... the framework submodule
Harry Ramseyda8f42a2024-09-18 14:03:26 +0100108 framework_since = get_submodule_hash(since, "framework")
109 cmd = ["git", "-C", "framework", "log", framework_since + "..HEAD",
Ronald Cron62a908d2024-04-25 15:46:01 +0200110 "--name-only", "--pretty=", "--"] + framework_src_files
David Horstmann330680e2024-06-06 15:25:10 +0100111 output = subprocess.check_output(cmd, universal_newlines=True,
112 env=framework_env)
Ronald Cron62a908d2024-04-25 15:46:01 +0200113 committed_changed_files += ["framework/" + s for s in output.split()]
114
115 # and also get all files with uncommitted changes in ...
116 # ... the main repository
Dave Rodgmanfccc5f82023-07-27 20:00:41 +0100117 cmd = ["git", "diff", "--name-only", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +0100118 output = subprocess.check_output(cmd, universal_newlines=True)
119 uncommitted_changed_files = output.split()
Ronald Cron62a908d2024-04-25 15:46:01 +0200120 # ... the framework submodule
121 cmd = ["git", "-C", "framework", "diff", "--name-only", "--"] + \
122 framework_src_files
David Horstmann330680e2024-06-06 15:25:10 +0100123 output = subprocess.check_output(cmd, universal_newlines=True,
124 env=framework_env)
Ronald Cron62a908d2024-04-25 15:46:01 +0200125 uncommitted_changed_files += ["framework/" + s for s in output.split()]
126
127 src_files = committed_changed_files + uncommitted_changed_files
128 else:
129 src_files += ["framework/" + s for s in framework_src_files]
David Horstmannfa928f12022-11-01 15:46:16 +0000130
Gilles Peskine22eb82c2023-06-22 19:45:01 +0200131 generated_files = list_generated_files()
132 # Don't correct style for third-party files (and, for simplicity,
133 # companion files in the same subtree), or for automatically
134 # generated files (we're correcting the templates instead).
135 src_files = [filename for filename in src_files
Ronald Cron2fd621e2024-07-02 08:58:21 +0200136 if not (filename.startswith("tf-psa-crypto/drivers/everest/") or
137 filename.startswith("tf-psa-crypto/drivers/p256-m/") or
Dave Rodgman2a9eb222024-03-18 11:15:06 +0000138 filename in generated_files or
139 is_file_autogenerated(filename))]
Gilles Peskine22eb82c2023-06-22 19:45:01 +0200140 return src_files
David Horstmannfa928f12022-11-01 15:46:16 +0000141
Harry Ramseyda8f42a2024-09-18 14:03:26 +0100142def get_submodule_hash(commit: str, submodule: str) -> str:
143 """Get the commit hash of a submodule at a given commit in the Git repository."""
144 cmd = ["git", "ls-tree", commit, submodule]
145 output = subprocess.check_output(cmd, universal_newlines=True)
146 return output.split()[2]
147
David Horstmannfa928f12022-11-01 15:46:16 +0000148def get_uncrustify_version() -> str:
149 """
150 Get the version string from Uncrustify
151 """
David Horstmann04bdbe32023-01-25 11:39:04 +0000152 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
153 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
154 check=False)
David Horstmannfa928f12022-11-01 15:46:16 +0000155 if result.returncode != 0:
David Horstmannca13c4f2022-12-08 14:33:52 +0000156 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmannfa928f12022-11-01 15:46:16 +0000157 return ""
158 else:
159 return str(result.stdout, "utf-8")
160
161def check_style_is_correct(src_file_list: List[str]) -> bool:
162 """
David Horstmann9711f4e2022-12-08 14:36:10 +0000163 Check the code style and output a diff for each file whose style is
David Horstmannfa928f12022-11-01 15:46:16 +0000164 incorrect.
165 """
166 style_correct = True
167 for src_file in src_file_list:
168 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmann04bdbe32023-01-25 11:39:04 +0000169 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
170 stderr=subprocess.PIPE, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000171 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000172 print_err("Uncrustify returned " + str(result.returncode) +
173 " correcting file " + src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000174 return False
David Horstmannfa928f12022-11-01 15:46:16 +0000175
176 # Uncrustify makes changes to the code and places the result in a new
177 # file with the extension ".uncrustify". To get the changes (if any)
178 # simply diff the 2 files.
David Horstmann0ebc12e2022-12-08 15:04:20 +0000179 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmannce42cc22023-01-24 18:08:49 +0000180 cp = subprocess.run(diff_cmd, check=False)
181
182 if cp.returncode == 1:
David Horstmann6b3ce302023-01-24 18:36:41 +0000183 print(src_file + " changed - code style is incorrect.")
David Horstmannfa928f12022-11-01 15:46:16 +0000184 style_correct = False
David Horstmannce42cc22023-01-24 18:08:49 +0000185 elif cp.returncode != 0:
186 raise subprocess.CalledProcessError(cp.returncode, cp.args,
187 cp.stdout, cp.stderr)
David Horstmannfa928f12022-11-01 15:46:16 +0000188
189 # Tidy up artifact
David Horstmann0ebc12e2022-12-08 15:04:20 +0000190 os.remove(src_file + ".uncrustify")
David Horstmannfa928f12022-11-01 15:46:16 +0000191
192 return style_correct
193
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000194def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmannfa928f12022-11-01 15:46:16 +0000195 """
196 Run Uncrustify once over the source files.
197 """
198 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
199 for src_file in src_file_list:
200 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmann6b3ce302023-01-24 18:36:41 +0000201 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000202 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000203 print_err("Uncrustify with file returned: " +
204 str(result.returncode) + " correcting file " +
205 src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000206 return False
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000207 return True
David Horstmannfa928f12022-11-01 15:46:16 +0000208
209def fix_style(src_file_list: List[str]) -> int:
210 """
211 Fix the code style. This takes 2 passes of Uncrustify.
212 """
David Horstmann78d566b2023-01-05 10:02:09 +0000213 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000214 return 1
David Horstmann78d566b2023-01-05 10:02:09 +0000215 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000216 return 1
David Horstmannfa928f12022-11-01 15:46:16 +0000217
218 # Guard against future changes that cause the codebase to require
219 # more passes.
220 if not check_style_is_correct(src_file_list):
David Horstmann28d21572023-01-16 18:32:56 +0000221 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmannfa928f12022-11-01 15:46:16 +0000222 return 1
223 else:
224 return 0
225
226def main() -> int:
227 """
228 Main with command line arguments.
229 """
David Horstmann2cf779c2022-12-08 14:44:36 +0000230 uncrustify_version = get_uncrustify_version().strip()
231 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100232 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6b3ce302023-01-24 18:36:41 +0000233 uncrustify_version + "'")
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100234 print("Note: The only supported version is " +
David Horstmann6b3ce302023-01-24 18:36:41 +0000235 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmannfa928f12022-11-01 15:46:16 +0000236
237 parser = argparse.ArgumentParser()
Gilles Peskine59803db2022-12-22 16:34:01 +0100238 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100239 help=('modify source files to fix the code style '
240 '(default: print diff, do not modify files)'))
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100241 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?',
Gilles Peskine43838b82023-06-22 20:29:41 +0200242 help=('only check files modified since the specified commit'
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100243 ' (e.g. --since=HEAD~3 or --since=development). If no'
244 ' commit is specified, default to development.'))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800245 # --subset is almost useless: it only matters if there are no files
246 # ('code_style.py' without arguments checks all files known to Git,
247 # 'code_style.py --subset' does nothing). In particular,
248 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
249 # way to restyle a possibly empty set of files.
Pengyu Lv8c6325c2023-02-06 14:29:02 +0800250 parser.add_argument('--subset', action='store_true',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800251 help='only check the specified files (default with non-option arguments)')
Gilles Peskine59803db2022-12-22 16:34:01 +0100252 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800253 help='files to check (files MUST be known to git, if none: check all)')
David Horstmannfa928f12022-11-01 15:46:16 +0000254
255 args = parser.parse_args()
256
Gilles Peskine43838b82023-06-22 20:29:41 +0200257 covered = frozenset(get_src_files(args.since))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800258 # We only check files that are known to git
259 if args.subset or args.operands:
Pengyu Lve19b51b2023-02-14 10:29:53 +0800260 src_files = [f for f in args.operands if f in covered]
261 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lvacbeb7f2023-02-06 14:27:30 +0800262 if skip_src_files:
263 print_skip(skip_src_files)
Pengyu Lvc36743f2023-02-15 10:20:40 +0800264 else:
Pengyu Lv10f41442023-02-15 16:58:09 +0800265 src_files = list(covered)
Gilles Peskine59803db2022-12-22 16:34:01 +0100266
David Horstmannfa928f12022-11-01 15:46:16 +0000267 if args.fix:
268 # Fix mode
269 return fix_style(src_files)
270 else:
271 # Check mode
272 if check_style_is_correct(src_files):
David Horstmann6b3ce302023-01-24 18:36:41 +0000273 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmannfa928f12022-11-01 15:46:16 +0000274 return 0
275 else:
276 return 1
277
278if __name__ == '__main__':
279 sys.exit(main())