blob: 89263cec061f2270483b99aa3f4e9626d4e471bd [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 Peskine7b780492023-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 Peskine0f1053c2023-06-22 19:45:01 +020082 src_files = output.split()
Gilles Peskine7b780492023-06-25 22:18:40 +020083 if since:
Dave Rodgmanb96dbc62023-07-27 14:22:34 +010084 # get all files changed in commits since the starting point
85 cmd = ["git", "log", since + "..HEAD", "--name-only", "--pretty=", "--" ] + src_files
86 output = subprocess.check_output(cmd, universal_newlines=True)
87 committed_changed_files = output.split()
88 # and also get all files with uncommitted changes
89 cmd = ["git", "diff", "--name-only", "--" ] + src_files
90 output = subprocess.check_output(cmd, universal_newlines=True)
91 uncommitted_changed_files = output.split()
92 src_files = set(committed_changed_files + uncommitted_changed_files)
David Horstmann20d6bfa2022-11-01 15:46:16 +000093
Gilles Peskine0f1053c2023-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 Horstmann20d6bfa2022-11-01 15:46:16 +0000102
103def get_uncrustify_version() -> str:
104 """
105 Get the version string from Uncrustify
106 """
David Horstmann04aaa452023-01-25 11:39:04 +0000107 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
108 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
109 check=False)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000110 if result.returncode != 0:
David Horstmann448cfec2022-12-08 14:33:52 +0000111 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmann20d6bfa2022-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 Horstmann99a669a2022-12-08 14:36:10 +0000118 Check the code style and output a diff for each file whose style is
David Horstmann20d6bfa2022-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 Horstmann04aaa452023-01-25 11:39:04 +0000124 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
125 stderr=subprocess.PIPE, check=False)
David Horstmannb92d30f2023-01-04 18:33:25 +0000126 if result.returncode != 0:
David Horstmann04aaa452023-01-25 11:39:04 +0000127 print_err("Uncrustify returned " + str(result.returncode) +
128 " correcting file " + src_file)
David Horstmannb92d30f2023-01-04 18:33:25 +0000129 return False
David Horstmann20d6bfa2022-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 Horstmann1f8b4d92022-12-08 15:04:20 +0000134 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmann5682e802023-01-24 18:08:49 +0000135 cp = subprocess.run(diff_cmd, check=False)
136
137 if cp.returncode == 1:
David Horstmann6956cb52023-01-24 18:36:41 +0000138 print(src_file + " changed - code style is incorrect.")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000139 style_correct = False
David Horstmann5682e802023-01-24 18:08:49 +0000140 elif cp.returncode != 0:
141 raise subprocess.CalledProcessError(cp.returncode, cp.args,
142 cp.stdout, cp.stderr)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000143
144 # Tidy up artifact
David Horstmann1f8b4d92022-12-08 15:04:20 +0000145 os.remove(src_file + ".uncrustify")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000146
147 return style_correct
148
David Horstmannfa69def2023-01-05 09:59:35 +0000149def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmann20d6bfa2022-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 Horstmann6956cb52023-01-24 18:36:41 +0000156 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannb92d30f2023-01-04 18:33:25 +0000157 if result.returncode != 0:
David Horstmann04aaa452023-01-25 11:39:04 +0000158 print_err("Uncrustify with file returned: " +
159 str(result.returncode) + " correcting file " +
160 src_file)
David Horstmannb92d30f2023-01-04 18:33:25 +0000161 return False
David Horstmannfa69def2023-01-05 09:59:35 +0000162 return True
David Horstmann20d6bfa2022-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 Horstmann242df482023-01-05 10:02:09 +0000168 if not fix_style_single_pass(src_file_list):
David Horstmannb92d30f2023-01-04 18:33:25 +0000169 return 1
David Horstmann242df482023-01-05 10:02:09 +0000170 if not fix_style_single_pass(src_file_list):
David Horstmannb92d30f2023-01-04 18:33:25 +0000171 return 1
David Horstmann20d6bfa2022-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 Horstmann64827e42023-01-16 18:32:56 +0000176 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmann20d6bfa2022-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 Horstmann3a6f9f92022-12-08 14:44:36 +0000185 uncrustify_version = get_uncrustify_version().strip()
186 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine75289862022-12-23 18:15:19 +0100187 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6956cb52023-01-24 18:36:41 +0000188 uncrustify_version + "'")
Gilles Peskine75289862022-12-23 18:15:19 +0100189 print("Note: The only supported version is " +
David Horstmann6956cb52023-01-24 18:36:41 +0000190 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000191
192 parser = argparse.ArgumentParser()
Gilles Peskine38f514d2022-12-22 16:34:01 +0100193 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine75289862022-12-23 18:15:19 +0100194 help=('modify source files to fix the code style '
195 '(default: print diff, do not modify files)'))
Gilles Peskine8ceeba42023-06-22 20:29:41 +0200196 parser.add_argument('-s', '--since', metavar='COMMIT',
197 help=('only check files modified since the specified commit'
198 ' (e.g. --since=HEAD~3 or --since=development)'))
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800199 # --subset is almost useless: it only matters if there are no files
200 # ('code_style.py' without arguments checks all files known to Git,
201 # 'code_style.py --subset' does nothing). In particular,
202 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
203 # way to restyle a possibly empty set of files.
Pengyu Lvb1c9cc32023-02-06 14:29:02 +0800204 parser.add_argument('--subset', action='store_true',
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800205 help='only check the specified files (default with non-option arguments)')
Gilles Peskine38f514d2022-12-22 16:34:01 +0100206 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800207 help='files to check (files MUST be known to git, if none: check all)')
David Horstmann20d6bfa2022-11-01 15:46:16 +0000208
209 args = parser.parse_args()
210
Gilles Peskine8ceeba42023-06-22 20:29:41 +0200211 covered = frozenset(get_src_files(args.since))
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800212 # We only check files that are known to git
213 if args.subset or args.operands:
Pengyu Lvbae83d22023-02-14 10:29:53 +0800214 src_files = [f for f in args.operands if f in covered]
215 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lva4b9b772023-02-06 14:27:30 +0800216 if skip_src_files:
217 print_skip(skip_src_files)
Pengyu Lv4a37eef2023-02-15 10:20:40 +0800218 else:
Pengyu Lve95df0b2023-02-15 16:58:09 +0800219 src_files = list(covered)
Gilles Peskine38f514d2022-12-22 16:34:01 +0100220
David Horstmann20d6bfa2022-11-01 15:46:16 +0000221 if args.fix:
222 # Fix mode
223 return fix_style(src_files)
224 else:
225 # Check mode
226 if check_style_is_correct(src_files):
David Horstmann6956cb52023-01-24 18:36:41 +0000227 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmann20d6bfa2022-11-01 15:46:16 +0000228 return 0
229 else:
230 return 1
231
232if __name__ == '__main__':
233 sys.exit(main())