blob: 9d36e299b10a00b3572581ddd5a3ae22c03c8098 [file] [log] [blame]
David Horstmann20d6bfa2022-11-01 15:46:16 +00001#!/usr/bin/env python3
2"""Check or fix the code style by running Uncrustify.
David Horstmanna27d8722023-01-16 18:28:21 +00003
4This script must be run from the root of a Git work tree containing Mbed TLS.
David Horstmann20d6bfa2022-11-01 15:46:16 +00005"""
6# Copyright The Mbed TLS Contributors
Dave Rodgman7ff79652023-11-03 12:04:52 +00007# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
David Horstmann20d6bfa2022-11-01 15:46:16 +00008import argparse
David Horstmann20d6bfa2022-11-01 15:46:16 +00009import os
Gilles Peskine4ca54d42022-12-19 00:48:58 +010010import re
David Horstmann20d6bfa2022-11-01 15:46:16 +000011import subprocess
12import sys
Gilles Peskine8ceeba42023-06-22 20:29:41 +020013from typing import FrozenSet, List, Optional
David Horstmann20d6bfa2022-11-01 15:46:16 +000014
David Horstmann3a6f9f92022-12-08 14:44:36 +000015UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1"
David Horstmannc747fdf2022-12-08 17:03:01 +000016CONFIG_FILE = ".uncrustify.cfg"
David Horstmann20d6bfa2022-11-01 15:46:16 +000017UNCRUSTIFY_EXE = "uncrustify"
18UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE]
Gilles Peskine4ca54d42022-12-19 00:48:58 +010019CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh"
David Horstmann20d6bfa2022-11-01 15:46:16 +000020
David Horstmann448cfec2022-12-08 14:33:52 +000021def print_err(*args):
David Horstmann6956cb52023-01-24 18:36:41 +000022 print("Error: ", *args, file=sys.stderr)
David Horstmann448cfec2022-12-08 14:33:52 +000023
Pengyu Lva4b9b772023-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 Lv4a37eef2023-02-15 10:20:40 +080028 print("Warning: The listed files will be skipped because\n"
29 "they are not known to git.")
Pengyu Lva4b9b772023-02-06 14:27:30 +080030 print()
31
Gilles Peskine4ca54d42022-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
Gilles Peskine8ceeba42023-06-22 20:29:41 +020054def get_src_files(since: Optional[str]) -> List[str]:
David Horstmann20d6bfa2022-11-01 15:46:16 +000055 """
Gilles Peskine0f1053c2023-06-22 19:45:01 +020056 Use git to get a list of the source files.
57
Gilles Peskine8ceeba42023-06-22 20:29:41 +020058 The optional argument since is a commit, indicating to only list files
59 that have changed since that commit. Without this argument, list all
60 files known to git.
61
Gilles Peskine0f1053c2023-06-22 19:45:01 +020062 Only C files are included, and certain files (generated, or 3rdparty)
63 are excluded.
David Horstmann20d6bfa2022-11-01 15:46:16 +000064 """
Gilles Peskine7b780492023-06-25 22:18:40 +020065 file_patterns = ["*.[hc]",
66 "tests/suites/*.function",
67 "scripts/data_files/*.fmt"]
68 output = subprocess.check_output(["git", "ls-files"] + file_patterns,
69 universal_newlines=True)
Gilles Peskine0f1053c2023-06-22 19:45:01 +020070 src_files = output.split()
Gilles Peskine7b780492023-06-25 22:18:40 +020071 if since:
Dave Rodgmanb96dbc62023-07-27 14:22:34 +010072 # get all files changed in commits since the starting point
Dave Rodgmane67ffd62023-07-27 18:50:50 +010073 cmd = ["git", "log", since + "..HEAD", "--name-only", "--pretty=", "--"] + src_files
Dave Rodgmanb96dbc62023-07-27 14:22:34 +010074 output = subprocess.check_output(cmd, universal_newlines=True)
75 committed_changed_files = output.split()
76 # and also get all files with uncommitted changes
Dave Rodgmandd7b24c2023-07-27 20:00:41 +010077 cmd = ["git", "diff", "--name-only", "--"] + src_files
Dave Rodgmanb96dbc62023-07-27 14:22:34 +010078 output = subprocess.check_output(cmd, universal_newlines=True)
79 uncommitted_changed_files = output.split()
Dave Rodgmane67ffd62023-07-27 18:50:50 +010080 src_files = list(set(committed_changed_files + uncommitted_changed_files))
David Horstmann20d6bfa2022-11-01 15:46:16 +000081
Gilles Peskine0f1053c2023-06-22 19:45:01 +020082 generated_files = list_generated_files()
83 # Don't correct style for third-party files (and, for simplicity,
84 # companion files in the same subtree), or for automatically
85 # generated files (we're correcting the templates instead).
86 src_files = [filename for filename in src_files
87 if not (filename.startswith("3rdparty/") or
88 filename in generated_files)]
89 return src_files
David Horstmann20d6bfa2022-11-01 15:46:16 +000090
91def get_uncrustify_version() -> str:
92 """
93 Get the version string from Uncrustify
94 """
David Horstmann04aaa452023-01-25 11:39:04 +000095 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
96 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
97 check=False)
David Horstmann20d6bfa2022-11-01 15:46:16 +000098 if result.returncode != 0:
David Horstmann448cfec2022-12-08 14:33:52 +000099 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmann20d6bfa2022-11-01 15:46:16 +0000100 return ""
101 else:
102 return str(result.stdout, "utf-8")
103
104def check_style_is_correct(src_file_list: List[str]) -> bool:
105 """
David Horstmann99a669a2022-12-08 14:36:10 +0000106 Check the code style and output a diff for each file whose style is
David Horstmann20d6bfa2022-11-01 15:46:16 +0000107 incorrect.
108 """
109 style_correct = True
110 for src_file in src_file_list:
111 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmann04aaa452023-01-25 11:39:04 +0000112 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
113 stderr=subprocess.PIPE, check=False)
David Horstmannb92d30f2023-01-04 18:33:25 +0000114 if result.returncode != 0:
David Horstmann04aaa452023-01-25 11:39:04 +0000115 print_err("Uncrustify returned " + str(result.returncode) +
116 " correcting file " + src_file)
David Horstmannb92d30f2023-01-04 18:33:25 +0000117 return False
David Horstmann20d6bfa2022-11-01 15:46:16 +0000118
119 # Uncrustify makes changes to the code and places the result in a new
120 # file with the extension ".uncrustify". To get the changes (if any)
121 # simply diff the 2 files.
David Horstmann1f8b4d92022-12-08 15:04:20 +0000122 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmann5682e802023-01-24 18:08:49 +0000123 cp = subprocess.run(diff_cmd, check=False)
124
125 if cp.returncode == 1:
David Horstmann6956cb52023-01-24 18:36:41 +0000126 print(src_file + " changed - code style is incorrect.")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000127 style_correct = False
David Horstmann5682e802023-01-24 18:08:49 +0000128 elif cp.returncode != 0:
129 raise subprocess.CalledProcessError(cp.returncode, cp.args,
130 cp.stdout, cp.stderr)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000131
132 # Tidy up artifact
David Horstmann1f8b4d92022-12-08 15:04:20 +0000133 os.remove(src_file + ".uncrustify")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000134
135 return style_correct
136
David Horstmannfa69def2023-01-05 09:59:35 +0000137def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmann20d6bfa2022-11-01 15:46:16 +0000138 """
139 Run Uncrustify once over the source files.
140 """
141 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
142 for src_file in src_file_list:
143 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmann6956cb52023-01-24 18:36:41 +0000144 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannb92d30f2023-01-04 18:33:25 +0000145 if result.returncode != 0:
David Horstmann04aaa452023-01-25 11:39:04 +0000146 print_err("Uncrustify with file returned: " +
147 str(result.returncode) + " correcting file " +
148 src_file)
David Horstmannb92d30f2023-01-04 18:33:25 +0000149 return False
David Horstmannfa69def2023-01-05 09:59:35 +0000150 return True
David Horstmann20d6bfa2022-11-01 15:46:16 +0000151
152def fix_style(src_file_list: List[str]) -> int:
153 """
154 Fix the code style. This takes 2 passes of Uncrustify.
155 """
David Horstmann242df482023-01-05 10:02:09 +0000156 if not fix_style_single_pass(src_file_list):
David Horstmannb92d30f2023-01-04 18:33:25 +0000157 return 1
David Horstmann242df482023-01-05 10:02:09 +0000158 if not fix_style_single_pass(src_file_list):
David Horstmannb92d30f2023-01-04 18:33:25 +0000159 return 1
David Horstmann20d6bfa2022-11-01 15:46:16 +0000160
161 # Guard against future changes that cause the codebase to require
162 # more passes.
163 if not check_style_is_correct(src_file_list):
David Horstmann64827e42023-01-16 18:32:56 +0000164 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000165 return 1
166 else:
167 return 0
168
169def main() -> int:
170 """
171 Main with command line arguments.
172 """
David Horstmann3a6f9f92022-12-08 14:44:36 +0000173 uncrustify_version = get_uncrustify_version().strip()
174 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine75289862022-12-23 18:15:19 +0100175 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6956cb52023-01-24 18:36:41 +0000176 uncrustify_version + "'")
Gilles Peskine75289862022-12-23 18:15:19 +0100177 print("Note: The only supported version is " +
David Horstmann6956cb52023-01-24 18:36:41 +0000178 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000179
180 parser = argparse.ArgumentParser()
Gilles Peskine38f514d2022-12-22 16:34:01 +0100181 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine75289862022-12-23 18:15:19 +0100182 help=('modify source files to fix the code style '
183 '(default: print diff, do not modify files)'))
Dave Rodgman73b87e32023-07-27 14:22:55 +0100184 parser.add_argument('-s', '--since', metavar='COMMIT', const='mbedtls-2.28', nargs='?',
Gilles Peskine8ceeba42023-06-22 20:29:41 +0200185 help=('only check files modified since the specified commit'
Dave Rodgman73b87e32023-07-27 14:22:55 +0100186 ' (e.g. --since=HEAD~3 or --since=mbedtls-2.28). If no'
187 ' commit is specified, default to mbedtls-2.28.'))
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800188 # --subset is almost useless: it only matters if there are no files
189 # ('code_style.py' without arguments checks all files known to Git,
190 # 'code_style.py --subset' does nothing). In particular,
191 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
192 # way to restyle a possibly empty set of files.
Pengyu Lvb1c9cc32023-02-06 14:29:02 +0800193 parser.add_argument('--subset', action='store_true',
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800194 help='only check the specified files (default with non-option arguments)')
Gilles Peskine38f514d2022-12-22 16:34:01 +0100195 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800196 help='files to check (files MUST be known to git, if none: check all)')
David Horstmann20d6bfa2022-11-01 15:46:16 +0000197
198 args = parser.parse_args()
199
Gilles Peskine8ceeba42023-06-22 20:29:41 +0200200 covered = frozenset(get_src_files(args.since))
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800201 # We only check files that are known to git
202 if args.subset or args.operands:
Pengyu Lvbae83d22023-02-14 10:29:53 +0800203 src_files = [f for f in args.operands if f in covered]
204 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lva4b9b772023-02-06 14:27:30 +0800205 if skip_src_files:
206 print_skip(skip_src_files)
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800207 else:
Pengyu Lve95df0b2023-02-15 16:58:09 +0800208 src_files = list(covered)
Gilles Peskine38f514d2022-12-22 16:34:01 +0100209
David Horstmann20d6bfa2022-11-01 15:46:16 +0000210 if args.fix:
211 # Fix mode
212 return fix_style(src_files)
213 else:
214 # Check mode
215 if check_style_is_correct(src_files):
David Horstmann6956cb52023-01-24 18:36:41 +0000216 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmann20d6bfa2022-11-01 15:46:16 +0000217 return 0
218 else:
219 return 1
220
221if __name__ == '__main__':
222 sys.exit(main())