blob: 4cb58babbea2011789ebb644e546ffcdecda9663 [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
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 Horstmann20d6bfa2022-11-01 15:46:16 +000021import os
Gilles Peskine4ca54d42022-12-19 00:48:58 +010022import re
David Horstmann20d6bfa2022-11-01 15:46:16 +000023import subprocess
24import sys
Gilles Peskine8ceeba42023-06-22 20:29:41 +020025from typing import FrozenSet, List, Optional
David Horstmann20d6bfa2022-11-01 15:46:16 +000026
David Horstmann3a6f9f92022-12-08 14:44:36 +000027UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1"
David Horstmannc747fdf2022-12-08 17:03:01 +000028CONFIG_FILE = ".uncrustify.cfg"
David Horstmann20d6bfa2022-11-01 15:46:16 +000029UNCRUSTIFY_EXE = "uncrustify"
30UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE]
Gilles Peskine4ca54d42022-12-19 00:48:58 +010031CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh"
David Horstmann20d6bfa2022-11-01 15:46:16 +000032
David Horstmann448cfec2022-12-08 14:33:52 +000033def print_err(*args):
David Horstmann6956cb52023-01-24 18:36:41 +000034 print("Error: ", *args, file=sys.stderr)
David Horstmann448cfec2022-12-08 14:33:52 +000035
Pengyu Lva4b9b772023-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 Lv4a37eef2023-02-15 10:20:40 +080040 print("Warning: The listed files will be skipped because\n"
41 "they are not known to git.")
Pengyu Lva4b9b772023-02-06 14:27:30 +080042 print()
43
Gilles Peskine4ca54d42022-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 Peskine8ceeba42023-06-22 20:29:41 +020066def get_src_files(since: Optional[str]) -> List[str]:
David Horstmann20d6bfa2022-11-01 15:46:16 +000067 """
Gilles Peskine0f1053c2023-06-22 19:45:01 +020068 Use git to get a list of the source files.
69
Gilles Peskine8ceeba42023-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 Peskine0f1053c2023-06-22 19:45:01 +020074 Only C files are included, and certain files (generated, or 3rdparty)
75 are excluded.
David Horstmann20d6bfa2022-11-01 15:46:16 +000076 """
Gilles Peskine8ceeba42023-06-22 20:29:41 +020077 if since is None:
78 git_ls_files_cmd = ["git", "ls-files",
79 "*.[hc]",
80 "tests/suites/*.function",
81 "scripts/data_files/*.fmt"]
82 output = subprocess.check_output(git_ls_files_cmd,
83 universal_newlines=True)
84 else:
85 git_ls_files_cmd = ["git", "diff", "--name-only", since, "--",
86 "*.[hc]",
87 "tests/suites/*.function",
88 "scripts/data_files/*.fmt"]
89 output = subprocess.check_output(git_ls_files_cmd,
90 universal_newlines=True)
Gilles Peskine0f1053c2023-06-22 19:45:01 +020091 src_files = output.split()
David Horstmann20d6bfa2022-11-01 15:46:16 +000092
Gilles Peskine0f1053c2023-06-22 19:45:01 +020093 generated_files = list_generated_files()
94 # Don't correct style for third-party files (and, for simplicity,
95 # companion files in the same subtree), or for automatically
96 # generated files (we're correcting the templates instead).
97 src_files = [filename for filename in src_files
98 if not (filename.startswith("3rdparty/") or
99 filename in generated_files)]
100 return src_files
David Horstmann20d6bfa2022-11-01 15:46:16 +0000101
102def get_uncrustify_version() -> str:
103 """
104 Get the version string from Uncrustify
105 """
David Horstmann04aaa452023-01-25 11:39:04 +0000106 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
107 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
108 check=False)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000109 if result.returncode != 0:
David Horstmann448cfec2022-12-08 14:33:52 +0000110 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmann20d6bfa2022-11-01 15:46:16 +0000111 return ""
112 else:
113 return str(result.stdout, "utf-8")
114
115def check_style_is_correct(src_file_list: List[str]) -> bool:
116 """
David Horstmann99a669a2022-12-08 14:36:10 +0000117 Check the code style and output a diff for each file whose style is
David Horstmann20d6bfa2022-11-01 15:46:16 +0000118 incorrect.
119 """
120 style_correct = True
121 for src_file in src_file_list:
122 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmann04aaa452023-01-25 11:39:04 +0000123 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
124 stderr=subprocess.PIPE, check=False)
David Horstmannb92d30f2023-01-04 18:33:25 +0000125 if result.returncode != 0:
David Horstmann04aaa452023-01-25 11:39:04 +0000126 print_err("Uncrustify returned " + str(result.returncode) +
127 " correcting file " + src_file)
David Horstmannb92d30f2023-01-04 18:33:25 +0000128 return False
David Horstmann20d6bfa2022-11-01 15:46:16 +0000129
130 # Uncrustify makes changes to the code and places the result in a new
131 # file with the extension ".uncrustify". To get the changes (if any)
132 # simply diff the 2 files.
David Horstmann1f8b4d92022-12-08 15:04:20 +0000133 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmann5682e802023-01-24 18:08:49 +0000134 cp = subprocess.run(diff_cmd, check=False)
135
136 if cp.returncode == 1:
David Horstmann6956cb52023-01-24 18:36:41 +0000137 print(src_file + " changed - code style is incorrect.")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000138 style_correct = False
David Horstmann5682e802023-01-24 18:08:49 +0000139 elif cp.returncode != 0:
140 raise subprocess.CalledProcessError(cp.returncode, cp.args,
141 cp.stdout, cp.stderr)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000142
143 # Tidy up artifact
David Horstmann1f8b4d92022-12-08 15:04:20 +0000144 os.remove(src_file + ".uncrustify")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000145
146 return style_correct
147
David Horstmannfa69def2023-01-05 09:59:35 +0000148def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmann20d6bfa2022-11-01 15:46:16 +0000149 """
150 Run Uncrustify once over the source files.
151 """
152 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
153 for src_file in src_file_list:
154 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmann6956cb52023-01-24 18:36:41 +0000155 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannb92d30f2023-01-04 18:33:25 +0000156 if result.returncode != 0:
David Horstmann04aaa452023-01-25 11:39:04 +0000157 print_err("Uncrustify with file returned: " +
158 str(result.returncode) + " correcting file " +
159 src_file)
David Horstmannb92d30f2023-01-04 18:33:25 +0000160 return False
David Horstmannfa69def2023-01-05 09:59:35 +0000161 return True
David Horstmann20d6bfa2022-11-01 15:46:16 +0000162
163def fix_style(src_file_list: List[str]) -> int:
164 """
165 Fix the code style. This takes 2 passes of Uncrustify.
166 """
David Horstmann242df482023-01-05 10:02:09 +0000167 if not fix_style_single_pass(src_file_list):
David Horstmannb92d30f2023-01-04 18:33:25 +0000168 return 1
David Horstmann242df482023-01-05 10:02:09 +0000169 if not fix_style_single_pass(src_file_list):
David Horstmannb92d30f2023-01-04 18:33:25 +0000170 return 1
David Horstmann20d6bfa2022-11-01 15:46:16 +0000171
172 # Guard against future changes that cause the codebase to require
173 # more passes.
174 if not check_style_is_correct(src_file_list):
David Horstmann64827e42023-01-16 18:32:56 +0000175 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000176 return 1
177 else:
178 return 0
179
180def main() -> int:
181 """
182 Main with command line arguments.
183 """
David Horstmann3a6f9f92022-12-08 14:44:36 +0000184 uncrustify_version = get_uncrustify_version().strip()
185 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine75289862022-12-23 18:15:19 +0100186 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6956cb52023-01-24 18:36:41 +0000187 uncrustify_version + "'")
Gilles Peskine75289862022-12-23 18:15:19 +0100188 print("Note: The only supported version is " +
David Horstmann6956cb52023-01-24 18:36:41 +0000189 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000190
191 parser = argparse.ArgumentParser()
Gilles Peskine38f514d2022-12-22 16:34:01 +0100192 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine75289862022-12-23 18:15:19 +0100193 help=('modify source files to fix the code style '
194 '(default: print diff, do not modify files)'))
Gilles Peskine8ceeba42023-06-22 20:29:41 +0200195 parser.add_argument('-s', '--since', metavar='COMMIT',
196 help=('only check files modified since the specified commit'
197 ' (e.g. --since=HEAD~3 or --since=development)'))
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800198 # --subset is almost useless: it only matters if there are no files
199 # ('code_style.py' without arguments checks all files known to Git,
200 # 'code_style.py --subset' does nothing). In particular,
201 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
202 # way to restyle a possibly empty set of files.
Pengyu Lvb1c9cc32023-02-06 14:29:02 +0800203 parser.add_argument('--subset', action='store_true',
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800204 help='only check the specified files (default with non-option arguments)')
Gilles Peskine38f514d2022-12-22 16:34:01 +0100205 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800206 help='files to check (files MUST be known to git, if none: check all)')
David Horstmann20d6bfa2022-11-01 15:46:16 +0000207
208 args = parser.parse_args()
209
Gilles Peskine8ceeba42023-06-22 20:29:41 +0200210 covered = frozenset(get_src_files(args.since))
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800211 # We only check files that are known to git
212 if args.subset or args.operands:
Pengyu Lvbae83d22023-02-14 10:29:53 +0800213 src_files = [f for f in args.operands if f in covered]
214 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lva4b9b772023-02-06 14:27:30 +0800215 if skip_src_files:
216 print_skip(skip_src_files)
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800217 else:
Pengyu Lve95df0b2023-02-15 16:58:09 +0800218 src_files = list(covered)
Gilles Peskine38f514d2022-12-22 16:34:01 +0100219
David Horstmann20d6bfa2022-11-01 15:46:16 +0000220 if args.fix:
221 # Fix mode
222 return fix_style(src_files)
223 else:
224 # Check mode
225 if check_style_is_correct(src_files):
David Horstmann6956cb52023-01-24 18:36:41 +0000226 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmann20d6bfa2022-11-01 15:46:16 +0000227 return 0
228 else:
229 return 1
230
231if __name__ == '__main__':
232 sys.exit(main())