blob: ed9f7bc0bf20ab460b6abd6c54401120d8809e12 [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()
Ronald Cron62a908d2024-04-25 15:46:01 +0200106 # ... the framework submodule
107 cmd = ["git", "-C", "framework", "log", since + "..HEAD",
108 "--name-only", "--pretty=", "--"] + framework_src_files
David Horstmann330680e2024-06-06 15:25:10 +0100109 output = subprocess.check_output(cmd, universal_newlines=True,
110 env=framework_env)
Ronald Cron62a908d2024-04-25 15:46:01 +0200111 committed_changed_files += ["framework/" + s for s in output.split()]
112
113 # and also get all files with uncommitted changes in ...
114 # ... the main repository
Dave Rodgmanfccc5f82023-07-27 20:00:41 +0100115 cmd = ["git", "diff", "--name-only", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +0100116 output = subprocess.check_output(cmd, universal_newlines=True)
117 uncommitted_changed_files = output.split()
Ronald Cron62a908d2024-04-25 15:46:01 +0200118 # ... the framework submodule
119 cmd = ["git", "-C", "framework", "diff", "--name-only", "--"] + \
120 framework_src_files
David Horstmann330680e2024-06-06 15:25:10 +0100121 output = subprocess.check_output(cmd, universal_newlines=True,
122 env=framework_env)
Ronald Cron62a908d2024-04-25 15:46:01 +0200123 uncommitted_changed_files += ["framework/" + s for s in output.split()]
124
125 src_files = committed_changed_files + uncommitted_changed_files
126 else:
127 src_files += ["framework/" + s for s in framework_src_files]
David Horstmannfa928f12022-11-01 15:46:16 +0000128
Gilles Peskine22eb82c2023-06-22 19:45:01 +0200129 generated_files = list_generated_files()
130 # Don't correct style for third-party files (and, for simplicity,
131 # companion files in the same subtree), or for automatically
132 # generated files (we're correcting the templates instead).
133 src_files = [filename for filename in src_files
Ronald Cron2fd621e2024-07-02 08:58:21 +0200134 if not (filename.startswith("tf-psa-crypto/drivers/everest/") or
135 filename.startswith("tf-psa-crypto/drivers/p256-m/") or
Dave Rodgman2a9eb222024-03-18 11:15:06 +0000136 filename in generated_files or
137 is_file_autogenerated(filename))]
Gilles Peskine22eb82c2023-06-22 19:45:01 +0200138 return src_files
David Horstmannfa928f12022-11-01 15:46:16 +0000139
140def get_uncrustify_version() -> str:
141 """
142 Get the version string from Uncrustify
143 """
David Horstmann04bdbe32023-01-25 11:39:04 +0000144 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
145 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
146 check=False)
David Horstmannfa928f12022-11-01 15:46:16 +0000147 if result.returncode != 0:
David Horstmannca13c4f2022-12-08 14:33:52 +0000148 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmannfa928f12022-11-01 15:46:16 +0000149 return ""
150 else:
151 return str(result.stdout, "utf-8")
152
153def check_style_is_correct(src_file_list: List[str]) -> bool:
154 """
David Horstmann9711f4e2022-12-08 14:36:10 +0000155 Check the code style and output a diff for each file whose style is
David Horstmannfa928f12022-11-01 15:46:16 +0000156 incorrect.
157 """
158 style_correct = True
159 for src_file in src_file_list:
160 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmann04bdbe32023-01-25 11:39:04 +0000161 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
162 stderr=subprocess.PIPE, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000163 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000164 print_err("Uncrustify returned " + str(result.returncode) +
165 " correcting file " + src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000166 return False
David Horstmannfa928f12022-11-01 15:46:16 +0000167
168 # Uncrustify makes changes to the code and places the result in a new
169 # file with the extension ".uncrustify". To get the changes (if any)
170 # simply diff the 2 files.
David Horstmann0ebc12e2022-12-08 15:04:20 +0000171 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmannce42cc22023-01-24 18:08:49 +0000172 cp = subprocess.run(diff_cmd, check=False)
173
174 if cp.returncode == 1:
David Horstmann6b3ce302023-01-24 18:36:41 +0000175 print(src_file + " changed - code style is incorrect.")
David Horstmannfa928f12022-11-01 15:46:16 +0000176 style_correct = False
David Horstmannce42cc22023-01-24 18:08:49 +0000177 elif cp.returncode != 0:
178 raise subprocess.CalledProcessError(cp.returncode, cp.args,
179 cp.stdout, cp.stderr)
David Horstmannfa928f12022-11-01 15:46:16 +0000180
181 # Tidy up artifact
David Horstmann0ebc12e2022-12-08 15:04:20 +0000182 os.remove(src_file + ".uncrustify")
David Horstmannfa928f12022-11-01 15:46:16 +0000183
184 return style_correct
185
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000186def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmannfa928f12022-11-01 15:46:16 +0000187 """
188 Run Uncrustify once over the source files.
189 """
190 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
191 for src_file in src_file_list:
192 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmann6b3ce302023-01-24 18:36:41 +0000193 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000194 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000195 print_err("Uncrustify with file returned: " +
196 str(result.returncode) + " correcting file " +
197 src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000198 return False
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000199 return True
David Horstmannfa928f12022-11-01 15:46:16 +0000200
201def fix_style(src_file_list: List[str]) -> int:
202 """
203 Fix the code style. This takes 2 passes of Uncrustify.
204 """
David Horstmann78d566b2023-01-05 10:02:09 +0000205 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000206 return 1
David Horstmann78d566b2023-01-05 10:02:09 +0000207 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000208 return 1
David Horstmannfa928f12022-11-01 15:46:16 +0000209
210 # Guard against future changes that cause the codebase to require
211 # more passes.
212 if not check_style_is_correct(src_file_list):
David Horstmann28d21572023-01-16 18:32:56 +0000213 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmannfa928f12022-11-01 15:46:16 +0000214 return 1
215 else:
216 return 0
217
218def main() -> int:
219 """
220 Main with command line arguments.
221 """
David Horstmann2cf779c2022-12-08 14:44:36 +0000222 uncrustify_version = get_uncrustify_version().strip()
223 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100224 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6b3ce302023-01-24 18:36:41 +0000225 uncrustify_version + "'")
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100226 print("Note: The only supported version is " +
David Horstmann6b3ce302023-01-24 18:36:41 +0000227 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmannfa928f12022-11-01 15:46:16 +0000228
229 parser = argparse.ArgumentParser()
Gilles Peskine59803db2022-12-22 16:34:01 +0100230 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100231 help=('modify source files to fix the code style '
232 '(default: print diff, do not modify files)'))
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100233 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?',
Gilles Peskine43838b82023-06-22 20:29:41 +0200234 help=('only check files modified since the specified commit'
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100235 ' (e.g. --since=HEAD~3 or --since=development). If no'
236 ' commit is specified, default to development.'))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800237 # --subset is almost useless: it only matters if there are no files
238 # ('code_style.py' without arguments checks all files known to Git,
239 # 'code_style.py --subset' does nothing). In particular,
240 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
241 # way to restyle a possibly empty set of files.
Pengyu Lv8c6325c2023-02-06 14:29:02 +0800242 parser.add_argument('--subset', action='store_true',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800243 help='only check the specified files (default with non-option arguments)')
Gilles Peskine59803db2022-12-22 16:34:01 +0100244 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800245 help='files to check (files MUST be known to git, if none: check all)')
David Horstmannfa928f12022-11-01 15:46:16 +0000246
247 args = parser.parse_args()
248
Gilles Peskine43838b82023-06-22 20:29:41 +0200249 covered = frozenset(get_src_files(args.since))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800250 # We only check files that are known to git
251 if args.subset or args.operands:
Pengyu Lve19b51b2023-02-14 10:29:53 +0800252 src_files = [f for f in args.operands if f in covered]
253 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lvacbeb7f2023-02-06 14:27:30 +0800254 if skip_src_files:
255 print_skip(skip_src_files)
Pengyu Lvc36743f2023-02-15 10:20:40 +0800256 else:
Pengyu Lv10f41442023-02-15 16:58:09 +0800257 src_files = list(covered)
Gilles Peskine59803db2022-12-22 16:34:01 +0100258
David Horstmannfa928f12022-11-01 15:46:16 +0000259 if args.fix:
260 # Fix mode
261 return fix_style(src_files)
262 else:
263 # Check mode
264 if check_style_is_correct(src_files):
David Horstmann6b3ce302023-01-24 18:36:41 +0000265 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmannfa928f12022-11-01 15:46:16 +0000266 return 0
267 else:
268 return 1
269
270if __name__ == '__main__':
271 sys.exit(main())