blob: 937e4648683d665c20a1019d1d267e7af0a3b789 [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)
88 git_env_vars = git_env_vars.split()
89 # Remove the vars from the environment
90 for var in git_env_vars:
91 framework_env.pop(var, None)
92
Ronald Cronbc93d0e2024-04-25 15:46:01 +020093 output = subprocess.check_output(["git", "-C", "framework", "ls-files"]
David Horstmann8eaeb382024-06-06 15:25:10 +010094 + file_patterns,
95 universal_newlines=True,
96 env=framework_env)
Ronald Cronbc93d0e2024-04-25 15:46:01 +020097 framework_src_files = output.split()
98
Gilles Peskine163ec402023-06-25 22:18:40 +020099 if since:
Ronald Cronbc93d0e2024-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 Cronbc93d0e2024-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 Horstmann8eaeb382024-06-06 15:25:10 +0100109 output = subprocess.check_output(cmd, universal_newlines=True,
110 env=framework_env)
Ronald Cronbc93d0e2024-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 Cronbc93d0e2024-04-25 15:46:01 +0200118 # ... the framework submodule
119 cmd = ["git", "-C", "framework", "diff", "--name-only", "--"] + \
120 framework_src_files
David Horstmann8eaeb382024-06-06 15:25:10 +0100121 output = subprocess.check_output(cmd, universal_newlines=True,
122 env=framework_env)
Ronald Cronbc93d0e2024-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
134 if not (filename.startswith("3rdparty/") or
Dave Rodgman2a9eb222024-03-18 11:15:06 +0000135 filename in generated_files or
136 is_file_autogenerated(filename))]
Gilles Peskine22eb82c2023-06-22 19:45:01 +0200137 return src_files
David Horstmannfa928f12022-11-01 15:46:16 +0000138
139def get_uncrustify_version() -> str:
140 """
141 Get the version string from Uncrustify
142 """
David Horstmann04bdbe32023-01-25 11:39:04 +0000143 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
144 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
145 check=False)
David Horstmannfa928f12022-11-01 15:46:16 +0000146 if result.returncode != 0:
David Horstmannca13c4f2022-12-08 14:33:52 +0000147 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmannfa928f12022-11-01 15:46:16 +0000148 return ""
149 else:
150 return str(result.stdout, "utf-8")
151
152def check_style_is_correct(src_file_list: List[str]) -> bool:
153 """
David Horstmann9711f4e2022-12-08 14:36:10 +0000154 Check the code style and output a diff for each file whose style is
David Horstmannfa928f12022-11-01 15:46:16 +0000155 incorrect.
156 """
157 style_correct = True
158 for src_file in src_file_list:
159 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmann04bdbe32023-01-25 11:39:04 +0000160 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
161 stderr=subprocess.PIPE, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000162 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000163 print_err("Uncrustify returned " + str(result.returncode) +
164 " correcting file " + src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000165 return False
David Horstmannfa928f12022-11-01 15:46:16 +0000166
167 # Uncrustify makes changes to the code and places the result in a new
168 # file with the extension ".uncrustify". To get the changes (if any)
169 # simply diff the 2 files.
David Horstmann0ebc12e2022-12-08 15:04:20 +0000170 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmannce42cc22023-01-24 18:08:49 +0000171 cp = subprocess.run(diff_cmd, check=False)
172
173 if cp.returncode == 1:
David Horstmann6b3ce302023-01-24 18:36:41 +0000174 print(src_file + " changed - code style is incorrect.")
David Horstmannfa928f12022-11-01 15:46:16 +0000175 style_correct = False
David Horstmannce42cc22023-01-24 18:08:49 +0000176 elif cp.returncode != 0:
177 raise subprocess.CalledProcessError(cp.returncode, cp.args,
178 cp.stdout, cp.stderr)
David Horstmannfa928f12022-11-01 15:46:16 +0000179
180 # Tidy up artifact
David Horstmann0ebc12e2022-12-08 15:04:20 +0000181 os.remove(src_file + ".uncrustify")
David Horstmannfa928f12022-11-01 15:46:16 +0000182
183 return style_correct
184
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000185def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmannfa928f12022-11-01 15:46:16 +0000186 """
187 Run Uncrustify once over the source files.
188 """
189 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
190 for src_file in src_file_list:
191 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmann6b3ce302023-01-24 18:36:41 +0000192 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000193 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000194 print_err("Uncrustify with file returned: " +
195 str(result.returncode) + " correcting file " +
196 src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000197 return False
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000198 return True
David Horstmannfa928f12022-11-01 15:46:16 +0000199
200def fix_style(src_file_list: List[str]) -> int:
201 """
202 Fix the code style. This takes 2 passes of Uncrustify.
203 """
David Horstmann78d566b2023-01-05 10:02:09 +0000204 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000205 return 1
David Horstmann78d566b2023-01-05 10:02:09 +0000206 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000207 return 1
David Horstmannfa928f12022-11-01 15:46:16 +0000208
209 # Guard against future changes that cause the codebase to require
210 # more passes.
211 if not check_style_is_correct(src_file_list):
David Horstmann28d21572023-01-16 18:32:56 +0000212 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmannfa928f12022-11-01 15:46:16 +0000213 return 1
214 else:
215 return 0
216
217def main() -> int:
218 """
219 Main with command line arguments.
220 """
David Horstmann2cf779c2022-12-08 14:44:36 +0000221 uncrustify_version = get_uncrustify_version().strip()
222 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100223 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6b3ce302023-01-24 18:36:41 +0000224 uncrustify_version + "'")
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100225 print("Note: The only supported version is " +
David Horstmann6b3ce302023-01-24 18:36:41 +0000226 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmannfa928f12022-11-01 15:46:16 +0000227
228 parser = argparse.ArgumentParser()
Gilles Peskine59803db2022-12-22 16:34:01 +0100229 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100230 help=('modify source files to fix the code style '
231 '(default: print diff, do not modify files)'))
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100232 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?',
Gilles Peskine43838b82023-06-22 20:29:41 +0200233 help=('only check files modified since the specified commit'
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100234 ' (e.g. --since=HEAD~3 or --since=development). If no'
235 ' commit is specified, default to development.'))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800236 # --subset is almost useless: it only matters if there are no files
237 # ('code_style.py' without arguments checks all files known to Git,
238 # 'code_style.py --subset' does nothing). In particular,
239 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
240 # way to restyle a possibly empty set of files.
Pengyu Lv8c6325c2023-02-06 14:29:02 +0800241 parser.add_argument('--subset', action='store_true',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800242 help='only check the specified files (default with non-option arguments)')
Gilles Peskine59803db2022-12-22 16:34:01 +0100243 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800244 help='files to check (files MUST be known to git, if none: check all)')
David Horstmannfa928f12022-11-01 15:46:16 +0000245
246 args = parser.parse_args()
247
Gilles Peskine43838b82023-06-22 20:29:41 +0200248 covered = frozenset(get_src_files(args.since))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800249 # We only check files that are known to git
250 if args.subset or args.operands:
Pengyu Lve19b51b2023-02-14 10:29:53 +0800251 src_files = [f for f in args.operands if f in covered]
252 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lvacbeb7f2023-02-06 14:27:30 +0800253 if skip_src_files:
254 print_skip(skip_src_files)
Pengyu Lvc36743f2023-02-15 10:20:40 +0800255 else:
Pengyu Lv10f41442023-02-15 16:58:09 +0800256 src_files = list(covered)
Gilles Peskine59803db2022-12-22 16:34:01 +0100257
David Horstmannfa928f12022-11-01 15:46:16 +0000258 if args.fix:
259 # Fix mode
260 return fix_style(src_files)
261 else:
262 # Check mode
263 if check_style_is_correct(src_files):
David Horstmann6b3ce302023-01-24 18:36:41 +0000264 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmannfa928f12022-11-01 15:46:16 +0000265 return 0
266 else:
267 return 1
268
269if __name__ == '__main__':
270 sys.exit(main())