blob: ddd0a9800b4cf2bf03e81749175819dbf1971307 [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
7# SPDX-License-Identifier: Apache-2.0
8#
9# Licensed under the Apache License, Version 2.0 (the "License"); you may
10# not use this file except in compliance with the License.
11# You may obtain a copy of the License at
12#
13# http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18# See the License for the specific language governing permissions and
19# limitations under the License.
20import argparse
David Horstmannfa928f12022-11-01 15:46:16 +000021import os
Gilles Peskine9a3771e2022-12-19 00:48:58 +010022import re
David Horstmannfa928f12022-11-01 15:46:16 +000023import subprocess
24import sys
Gilles Peskine43838b82023-06-22 20:29:41 +020025from typing import FrozenSet, List, Optional
David Horstmannfa928f12022-11-01 15:46:16 +000026
David Horstmann2cf779c2022-12-08 14:44:36 +000027UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1"
David Horstmannae93a3f2022-12-08 17:03:01 +000028CONFIG_FILE = ".uncrustify.cfg"
David Horstmannfa928f12022-11-01 15:46:16 +000029UNCRUSTIFY_EXE = "uncrustify"
30UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE]
Gilles Peskine9a3771e2022-12-19 00:48:58 +010031CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh"
David Horstmannfa928f12022-11-01 15:46:16 +000032
David Horstmannca13c4f2022-12-08 14:33:52 +000033def print_err(*args):
David Horstmann6b3ce302023-01-24 18:36:41 +000034 print("Error: ", *args, file=sys.stderr)
David Horstmannca13c4f2022-12-08 14:33:52 +000035
Pengyu Lvacbeb7f2023-02-06 14:27:30 +080036# Print the file names that will be skipped and the help message
37def print_skip(files_to_skip):
38 print()
39 print(*files_to_skip, sep=", SKIP\n", end=", SKIP\n")
Pengyu Lvc36743f2023-02-15 10:20:40 +080040 print("Warning: The listed files will be skipped because\n"
41 "they are not known to git.")
Pengyu Lvacbeb7f2023-02-06 14:27:30 +080042 print()
43
Gilles Peskine9a3771e2022-12-19 00:48:58 +010044# Match FILENAME(s) in "check SCRIPT (FILENAME...)"
45CHECK_CALL_RE = re.compile(r"\n\s*check\s+[^\s#$&*?;|]+([^\n#$&*?;|]+)",
46 re.ASCII)
47def list_generated_files() -> FrozenSet[str]:
48 """Return the names of generated files.
49
50 We don't reformat generated files, since the result might be different
51 from the output of the generator. Ideally the result of the generator
52 would conform to the code style, but this would be difficult, especially
53 with respect to the placement of line breaks in long logical lines.
54 """
55 # Parse check-generated-files.sh to get an up-to-date list of
56 # generated files. Read the file rather than calling it so that
57 # this script only depends on Git, Python and uncrustify, and not other
58 # tools such as sh or grep which might not be available on Windows.
59 # This introduces a limitation: check-generated-files.sh must have
60 # the expected format and must list the files explicitly, not through
61 # wildcards or command substitution.
62 content = open(CHECK_GENERATED_FILES, encoding="utf-8").read()
63 checks = re.findall(CHECK_CALL_RE, content)
64 return frozenset(word for s in checks for word in s.split())
65
Gilles Peskine43838b82023-06-22 20:29:41 +020066def get_src_files(since: Optional[str]) -> List[str]:
David Horstmannfa928f12022-11-01 15:46:16 +000067 """
Gilles Peskine22eb82c2023-06-22 19:45:01 +020068 Use git to get a list of the source files.
69
Gilles Peskine43838b82023-06-22 20:29:41 +020070 The optional argument since is a commit, indicating to only list files
71 that have changed since that commit. Without this argument, list all
72 files known to git.
73
Gilles Peskine22eb82c2023-06-22 19:45:01 +020074 Only C files are included, and certain files (generated, or 3rdparty)
75 are excluded.
David Horstmannfa928f12022-11-01 15:46:16 +000076 """
Gilles Peskine163ec402023-06-25 22:18:40 +020077 file_patterns = ["*.[hc]",
78 "tests/suites/*.function",
79 "scripts/data_files/*.fmt"]
80 output = subprocess.check_output(["git", "ls-files"] + file_patterns,
81 universal_newlines=True)
Gilles Peskine22eb82c2023-06-22 19:45:01 +020082 src_files = output.split()
Gilles Peskine163ec402023-06-25 22:18:40 +020083 if since:
Dave Rodgman05b60f42023-07-27 14:22:34 +010084 # get all files changed in commits since the starting point
Dave Rodgman82d174a2023-07-27 18:50:50 +010085 cmd = ["git", "log", since + "..HEAD", "--name-only", "--pretty=", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +010086 output = subprocess.check_output(cmd, universal_newlines=True)
87 committed_changed_files = output.split()
88 # and also get all files with uncommitted changes
Dave Rodgmanfccc5f82023-07-27 20:00:41 +010089 cmd = ["git", "diff", "--name-only", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +010090 output = subprocess.check_output(cmd, universal_newlines=True)
91 uncommitted_changed_files = output.split()
Dave Rodgman82d174a2023-07-27 18:50:50 +010092 src_files = list(set(committed_changed_files + uncommitted_changed_files))
David Horstmannfa928f12022-11-01 15:46:16 +000093
Gilles Peskine22eb82c2023-06-22 19:45:01 +020094 generated_files = list_generated_files()
95 # Don't correct style for third-party files (and, for simplicity,
96 # companion files in the same subtree), or for automatically
97 # generated files (we're correcting the templates instead).
98 src_files = [filename for filename in src_files
99 if not (filename.startswith("3rdparty/") or
100 filename in generated_files)]
101 return src_files
David Horstmannfa928f12022-11-01 15:46:16 +0000102
103def get_uncrustify_version() -> str:
104 """
105 Get the version string from Uncrustify
106 """
David Horstmann04bdbe32023-01-25 11:39:04 +0000107 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
108 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
109 check=False)
David Horstmannfa928f12022-11-01 15:46:16 +0000110 if result.returncode != 0:
David Horstmannca13c4f2022-12-08 14:33:52 +0000111 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmannfa928f12022-11-01 15:46:16 +0000112 return ""
113 else:
114 return str(result.stdout, "utf-8")
115
116def check_style_is_correct(src_file_list: List[str]) -> bool:
117 """
David Horstmann9711f4e2022-12-08 14:36:10 +0000118 Check the code style and output a diff for each file whose style is
David Horstmannfa928f12022-11-01 15:46:16 +0000119 incorrect.
120 """
121 style_correct = True
122 for src_file in src_file_list:
123 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmann04bdbe32023-01-25 11:39:04 +0000124 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
125 stderr=subprocess.PIPE, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000126 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000127 print_err("Uncrustify returned " + str(result.returncode) +
128 " correcting file " + src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000129 return False
David Horstmannfa928f12022-11-01 15:46:16 +0000130
131 # Uncrustify makes changes to the code and places the result in a new
132 # file with the extension ".uncrustify". To get the changes (if any)
133 # simply diff the 2 files.
David Horstmann0ebc12e2022-12-08 15:04:20 +0000134 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmannce42cc22023-01-24 18:08:49 +0000135 cp = subprocess.run(diff_cmd, check=False)
136
137 if cp.returncode == 1:
David Horstmann6b3ce302023-01-24 18:36:41 +0000138 print(src_file + " changed - code style is incorrect.")
David Horstmannfa928f12022-11-01 15:46:16 +0000139 style_correct = False
David Horstmannce42cc22023-01-24 18:08:49 +0000140 elif cp.returncode != 0:
141 raise subprocess.CalledProcessError(cp.returncode, cp.args,
142 cp.stdout, cp.stderr)
David Horstmannfa928f12022-11-01 15:46:16 +0000143
144 # Tidy up artifact
David Horstmann0ebc12e2022-12-08 15:04:20 +0000145 os.remove(src_file + ".uncrustify")
David Horstmannfa928f12022-11-01 15:46:16 +0000146
147 return style_correct
148
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000149def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmannfa928f12022-11-01 15:46:16 +0000150 """
151 Run Uncrustify once over the source files.
152 """
153 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
154 for src_file in src_file_list:
155 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmann6b3ce302023-01-24 18:36:41 +0000156 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000157 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000158 print_err("Uncrustify with file returned: " +
159 str(result.returncode) + " correcting file " +
160 src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000161 return False
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000162 return True
David Horstmannfa928f12022-11-01 15:46:16 +0000163
164def fix_style(src_file_list: List[str]) -> int:
165 """
166 Fix the code style. This takes 2 passes of Uncrustify.
167 """
David Horstmann78d566b2023-01-05 10:02:09 +0000168 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000169 return 1
David Horstmann78d566b2023-01-05 10:02:09 +0000170 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000171 return 1
David Horstmannfa928f12022-11-01 15:46:16 +0000172
173 # Guard against future changes that cause the codebase to require
174 # more passes.
175 if not check_style_is_correct(src_file_list):
David Horstmann28d21572023-01-16 18:32:56 +0000176 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmannfa928f12022-11-01 15:46:16 +0000177 return 1
178 else:
179 return 0
180
181def main() -> int:
182 """
183 Main with command line arguments.
184 """
David Horstmann2cf779c2022-12-08 14:44:36 +0000185 uncrustify_version = get_uncrustify_version().strip()
186 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100187 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6b3ce302023-01-24 18:36:41 +0000188 uncrustify_version + "'")
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100189 print("Note: The only supported version is " +
David Horstmann6b3ce302023-01-24 18:36:41 +0000190 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmannfa928f12022-11-01 15:46:16 +0000191
192 parser = argparse.ArgumentParser()
Gilles Peskine59803db2022-12-22 16:34:01 +0100193 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100194 help=('modify source files to fix the code style '
195 '(default: print diff, do not modify files)'))
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100196 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?',
Gilles Peskine43838b82023-06-22 20:29:41 +0200197 help=('only check files modified since the specified commit'
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100198 ' (e.g. --since=HEAD~3 or --since=development). If no'
199 ' commit is specified, default to development.'))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800200 # --subset is almost useless: it only matters if there are no files
201 # ('code_style.py' without arguments checks all files known to Git,
202 # 'code_style.py --subset' does nothing). In particular,
203 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
204 # way to restyle a possibly empty set of files.
Pengyu Lv8c6325c2023-02-06 14:29:02 +0800205 parser.add_argument('--subset', action='store_true',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800206 help='only check the specified files (default with non-option arguments)')
Gilles Peskine59803db2022-12-22 16:34:01 +0100207 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800208 help='files to check (files MUST be known to git, if none: check all)')
David Horstmannfa928f12022-11-01 15:46:16 +0000209
210 args = parser.parse_args()
211
Gilles Peskine43838b82023-06-22 20:29:41 +0200212 covered = frozenset(get_src_files(args.since))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800213 # We only check files that are known to git
214 if args.subset or args.operands:
Pengyu Lve19b51b2023-02-14 10:29:53 +0800215 src_files = [f for f in args.operands if f in covered]
216 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lvacbeb7f2023-02-06 14:27:30 +0800217 if skip_src_files:
218 print_skip(skip_src_files)
Pengyu Lvc36743f2023-02-15 10:20:40 +0800219 else:
Pengyu Lv10f41442023-02-15 16:58:09 +0800220 src_files = list(covered)
Gilles Peskine59803db2022-12-22 16:34:01 +0100221
David Horstmannfa928f12022-11-01 15:46:16 +0000222 if args.fix:
223 # Fix mode
224 return fix_style(src_files)
225 else:
226 # Check mode
227 if check_style_is_correct(src_files):
David Horstmann6b3ce302023-01-24 18:36:41 +0000228 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmannfa928f12022-11-01 15:46:16 +0000229 return 0
230 else:
231 return 1
232
233if __name__ == '__main__':
234 sys.exit(main())