blob: e98fb2b450dd4e5a9b39593d5c588ff715502bfc [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
Gilles Peskine22eb82c2023-06-22 19:45:01 +020069 Only C files are included, and certain files (generated, or 3rdparty)
70 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",
74 "scripts/data_files/*.fmt"]
75 output = subprocess.check_output(["git", "ls-files"] + file_patterns,
76 universal_newlines=True)
Gilles Peskine22eb82c2023-06-22 19:45:01 +020077 src_files = output.split()
David Horstmann8eaeb382024-06-06 15:25:10 +010078
79 # When this script is called from a git hook, some environment variables
80 # are set by default which force all git commands to use the main repository
81 # (i.e. prevent us from performing commands on the framework repo).
82 # Create an environment without these variables for running commands on the
83 # framework repo.
84 framework_env = os.environ.copy()
85 # Get a list of environment vars that git sets
86 git_env_vars = subprocess.check_output(["git", "rev-parse", "--local-env-vars"],
87 universal_newlines=True)
David Horstmann8eaeb382024-06-06 15:25:10 +010088 # Remove the vars from the environment
David Horstmann93ee0162024-06-06 16:16:31 +010089 for var in git_env_vars.split():
David Horstmann8eaeb382024-06-06 15:25:10 +010090 framework_env.pop(var, None)
91
Ronald Cronbc93d0e2024-04-25 15:46:01 +020092 output = subprocess.check_output(["git", "-C", "framework", "ls-files"]
David Horstmann8eaeb382024-06-06 15:25:10 +010093 + file_patterns,
94 universal_newlines=True,
95 env=framework_env)
Ronald Cronbc93d0e2024-04-25 15:46:01 +020096 framework_src_files = output.split()
97
Gilles Peskine163ec402023-06-25 22:18:40 +020098 if since:
Ronald Cronbc93d0e2024-04-25 15:46:01 +020099 # get all files changed in commits since the starting point in ...
100 # ... the main repository
101 cmd = ["git", "log", since + "..HEAD", "--ignore-submodules",
102 "--name-only", "--pretty=", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +0100103 output = subprocess.check_output(cmd, universal_newlines=True)
104 committed_changed_files = output.split()
Harry Ramsey5098d042024-09-18 14:03:26 +0100105
Ronald Cronbc93d0e2024-04-25 15:46:01 +0200106 # ... the framework submodule
Harry Ramsey5098d042024-09-18 14:03:26 +0100107 framework_since = get_submodule_hash(since, "framework")
108 cmd = ["git", "-C", "framework", "log", framework_since + "..HEAD",
Ronald Cronbc93d0e2024-04-25 15:46:01 +0200109 "--name-only", "--pretty=", "--"] + framework_src_files
David Horstmann8eaeb382024-06-06 15:25:10 +0100110 output = subprocess.check_output(cmd, universal_newlines=True,
111 env=framework_env)
Ronald Cronbc93d0e2024-04-25 15:46:01 +0200112 committed_changed_files += ["framework/" + s for s in output.split()]
113
114 # and also get all files with uncommitted changes in ...
115 # ... the main repository
Dave Rodgmanfccc5f82023-07-27 20:00:41 +0100116 cmd = ["git", "diff", "--name-only", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +0100117 output = subprocess.check_output(cmd, universal_newlines=True)
118 uncommitted_changed_files = output.split()
Ronald Cronbc93d0e2024-04-25 15:46:01 +0200119 # ... the framework submodule
120 cmd = ["git", "-C", "framework", "diff", "--name-only", "--"] + \
121 framework_src_files
David Horstmann8eaeb382024-06-06 15:25:10 +0100122 output = subprocess.check_output(cmd, universal_newlines=True,
123 env=framework_env)
Ronald Cronbc93d0e2024-04-25 15:46:01 +0200124 uncommitted_changed_files += ["framework/" + s for s in output.split()]
125
126 src_files = committed_changed_files + uncommitted_changed_files
127 else:
128 src_files += ["framework/" + s for s in framework_src_files]
David Horstmannfa928f12022-11-01 15:46:16 +0000129
Gilles Peskine22eb82c2023-06-22 19:45:01 +0200130 generated_files = list_generated_files()
131 # Don't correct style for third-party files (and, for simplicity,
132 # companion files in the same subtree), or for automatically
133 # generated files (we're correcting the templates instead).
134 src_files = [filename for filename in src_files
135 if not (filename.startswith("3rdparty/") 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
Harry Ramsey5098d042024-09-18 14:03:26 +0100140def get_submodule_hash(commit: str, submodule: str) -> str:
141 """Get the commit hash of a submodule at a given commit in the Git repository."""
142 cmd = ["git", "ls-tree", commit, submodule]
143 output = subprocess.check_output(cmd, universal_newlines=True)
144 return output.split()[2]
145
David Horstmannfa928f12022-11-01 15:46:16 +0000146def get_uncrustify_version() -> str:
147 """
148 Get the version string from Uncrustify
149 """
David Horstmann04bdbe32023-01-25 11:39:04 +0000150 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
151 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
152 check=False)
David Horstmannfa928f12022-11-01 15:46:16 +0000153 if result.returncode != 0:
David Horstmannca13c4f2022-12-08 14:33:52 +0000154 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmannfa928f12022-11-01 15:46:16 +0000155 return ""
156 else:
157 return str(result.stdout, "utf-8")
158
159def check_style_is_correct(src_file_list: List[str]) -> bool:
160 """
David Horstmann9711f4e2022-12-08 14:36:10 +0000161 Check the code style and output a diff for each file whose style is
David Horstmannfa928f12022-11-01 15:46:16 +0000162 incorrect.
163 """
164 style_correct = True
165 for src_file in src_file_list:
166 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmann04bdbe32023-01-25 11:39:04 +0000167 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
168 stderr=subprocess.PIPE, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000169 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000170 print_err("Uncrustify returned " + str(result.returncode) +
171 " correcting file " + src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000172 return False
David Horstmannfa928f12022-11-01 15:46:16 +0000173
174 # Uncrustify makes changes to the code and places the result in a new
175 # file with the extension ".uncrustify". To get the changes (if any)
176 # simply diff the 2 files.
David Horstmann0ebc12e2022-12-08 15:04:20 +0000177 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmannce42cc22023-01-24 18:08:49 +0000178 cp = subprocess.run(diff_cmd, check=False)
179
180 if cp.returncode == 1:
David Horstmann6b3ce302023-01-24 18:36:41 +0000181 print(src_file + " changed - code style is incorrect.")
David Horstmannfa928f12022-11-01 15:46:16 +0000182 style_correct = False
David Horstmannce42cc22023-01-24 18:08:49 +0000183 elif cp.returncode != 0:
184 raise subprocess.CalledProcessError(cp.returncode, cp.args,
185 cp.stdout, cp.stderr)
David Horstmannfa928f12022-11-01 15:46:16 +0000186
187 # Tidy up artifact
David Horstmann0ebc12e2022-12-08 15:04:20 +0000188 os.remove(src_file + ".uncrustify")
David Horstmannfa928f12022-11-01 15:46:16 +0000189
190 return style_correct
191
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000192def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmannfa928f12022-11-01 15:46:16 +0000193 """
194 Run Uncrustify once over the source files.
195 """
196 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
197 for src_file in src_file_list:
198 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmann6b3ce302023-01-24 18:36:41 +0000199 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000200 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000201 print_err("Uncrustify with file returned: " +
202 str(result.returncode) + " correcting file " +
203 src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000204 return False
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000205 return True
David Horstmannfa928f12022-11-01 15:46:16 +0000206
207def fix_style(src_file_list: List[str]) -> int:
208 """
209 Fix the code style. This takes 2 passes of Uncrustify.
210 """
David Horstmann78d566b2023-01-05 10:02:09 +0000211 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000212 return 1
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 Horstmannfa928f12022-11-01 15:46:16 +0000215
216 # Guard against future changes that cause the codebase to require
217 # more passes.
218 if not check_style_is_correct(src_file_list):
David Horstmann28d21572023-01-16 18:32:56 +0000219 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmannfa928f12022-11-01 15:46:16 +0000220 return 1
221 else:
222 return 0
223
224def main() -> int:
225 """
226 Main with command line arguments.
227 """
David Horstmann2cf779c2022-12-08 14:44:36 +0000228 uncrustify_version = get_uncrustify_version().strip()
229 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100230 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6b3ce302023-01-24 18:36:41 +0000231 uncrustify_version + "'")
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100232 print("Note: The only supported version is " +
David Horstmann6b3ce302023-01-24 18:36:41 +0000233 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmannfa928f12022-11-01 15:46:16 +0000234
235 parser = argparse.ArgumentParser()
Gilles Peskine59803db2022-12-22 16:34:01 +0100236 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100237 help=('modify source files to fix the code style '
238 '(default: print diff, do not modify files)'))
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100239 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?',
Gilles Peskine43838b82023-06-22 20:29:41 +0200240 help=('only check files modified since the specified commit'
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100241 ' (e.g. --since=HEAD~3 or --since=development). If no'
242 ' commit is specified, default to development.'))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800243 # --subset is almost useless: it only matters if there are no files
244 # ('code_style.py' without arguments checks all files known to Git,
245 # 'code_style.py --subset' does nothing). In particular,
246 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
247 # way to restyle a possibly empty set of files.
Pengyu Lv8c6325c2023-02-06 14:29:02 +0800248 parser.add_argument('--subset', action='store_true',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800249 help='only check the specified files (default with non-option arguments)')
Gilles Peskine59803db2022-12-22 16:34:01 +0100250 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800251 help='files to check (files MUST be known to git, if none: check all)')
David Horstmannfa928f12022-11-01 15:46:16 +0000252
253 args = parser.parse_args()
254
Gilles Peskine43838b82023-06-22 20:29:41 +0200255 covered = frozenset(get_src_files(args.since))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800256 # We only check files that are known to git
257 if args.subset or args.operands:
Pengyu Lve19b51b2023-02-14 10:29:53 +0800258 src_files = [f for f in args.operands if f in covered]
259 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lvacbeb7f2023-02-06 14:27:30 +0800260 if skip_src_files:
261 print_skip(skip_src_files)
Pengyu Lvc36743f2023-02-15 10:20:40 +0800262 else:
Pengyu Lv10f41442023-02-15 16:58:09 +0800263 src_files = list(covered)
Gilles Peskine59803db2022-12-22 16:34:01 +0100264
David Horstmannfa928f12022-11-01 15:46:16 +0000265 if args.fix:
266 # Fix mode
267 return fix_style(src_files)
268 else:
269 # Check mode
270 if check_style_is_correct(src_files):
David Horstmann6b3ce302023-01-24 18:36:41 +0000271 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmannfa928f12022-11-01 15:46:16 +0000272 return 0
273 else:
274 return 1
275
276if __name__ == '__main__':
277 sys.exit(main())