blob: 71e3edf035b6b381676c15d402b2d0d378529b71 [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
55AUTOGEN_RE = re.compile(r"Warning[ :-]+This file is (now )?auto[ -]generated", re.ASCII | re.IGNORECASE)
56def is_file_autogenerated(filename):
57 content = open(filename, encoding="utf-8").read()
58 return AUTOGEN_RE.search(content) is not None
59
Gilles Peskine43838b82023-06-22 20:29:41 +020060def get_src_files(since: Optional[str]) -> List[str]:
David Horstmannfa928f12022-11-01 15:46:16 +000061 """
Gilles Peskine22eb82c2023-06-22 19:45:01 +020062 Use git to get a list of the source files.
63
Gilles Peskine43838b82023-06-22 20:29:41 +020064 The optional argument since is a commit, indicating to only list files
65 that have changed since that commit. Without this argument, list all
66 files known to git.
67
Gilles Peskine22eb82c2023-06-22 19:45:01 +020068 Only C files are included, and certain files (generated, or 3rdparty)
69 are excluded.
David Horstmannfa928f12022-11-01 15:46:16 +000070 """
Gilles Peskine163ec402023-06-25 22:18:40 +020071 file_patterns = ["*.[hc]",
72 "tests/suites/*.function",
73 "scripts/data_files/*.fmt"]
74 output = subprocess.check_output(["git", "ls-files"] + file_patterns,
75 universal_newlines=True)
Gilles Peskine22eb82c2023-06-22 19:45:01 +020076 src_files = output.split()
Gilles Peskine163ec402023-06-25 22:18:40 +020077 if since:
Dave Rodgman05b60f42023-07-27 14:22:34 +010078 # get all files changed in commits since the starting point
Dave Rodgman82d174a2023-07-27 18:50:50 +010079 cmd = ["git", "log", since + "..HEAD", "--name-only", "--pretty=", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +010080 output = subprocess.check_output(cmd, universal_newlines=True)
81 committed_changed_files = output.split()
82 # and also get all files with uncommitted changes
Dave Rodgmanfccc5f82023-07-27 20:00:41 +010083 cmd = ["git", "diff", "--name-only", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +010084 output = subprocess.check_output(cmd, universal_newlines=True)
85 uncommitted_changed_files = output.split()
Dave Rodgman82d174a2023-07-27 18:50:50 +010086 src_files = list(set(committed_changed_files + uncommitted_changed_files))
David Horstmannfa928f12022-11-01 15:46:16 +000087
Gilles Peskine22eb82c2023-06-22 19:45:01 +020088 generated_files = list_generated_files()
89 # Don't correct style for third-party files (and, for simplicity,
90 # companion files in the same subtree), or for automatically
91 # generated files (we're correcting the templates instead).
92 src_files = [filename for filename in src_files
93 if not (filename.startswith("3rdparty/") or
Dave Rodgman2a9eb222024-03-18 11:15:06 +000094 filename in generated_files or
95 is_file_autogenerated(filename))]
Gilles Peskine22eb82c2023-06-22 19:45:01 +020096 return src_files
David Horstmannfa928f12022-11-01 15:46:16 +000097
98def get_uncrustify_version() -> str:
99 """
100 Get the version string from Uncrustify
101 """
David Horstmann04bdbe32023-01-25 11:39:04 +0000102 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
103 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
104 check=False)
David Horstmannfa928f12022-11-01 15:46:16 +0000105 if result.returncode != 0:
David Horstmannca13c4f2022-12-08 14:33:52 +0000106 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmannfa928f12022-11-01 15:46:16 +0000107 return ""
108 else:
109 return str(result.stdout, "utf-8")
110
111def check_style_is_correct(src_file_list: List[str]) -> bool:
112 """
David Horstmann9711f4e2022-12-08 14:36:10 +0000113 Check the code style and output a diff for each file whose style is
David Horstmannfa928f12022-11-01 15:46:16 +0000114 incorrect.
115 """
116 style_correct = True
117 for src_file in src_file_list:
118 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmann04bdbe32023-01-25 11:39:04 +0000119 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
120 stderr=subprocess.PIPE, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000121 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000122 print_err("Uncrustify returned " + str(result.returncode) +
123 " correcting file " + src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000124 return False
David Horstmannfa928f12022-11-01 15:46:16 +0000125
126 # Uncrustify makes changes to the code and places the result in a new
127 # file with the extension ".uncrustify". To get the changes (if any)
128 # simply diff the 2 files.
David Horstmann0ebc12e2022-12-08 15:04:20 +0000129 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmannce42cc22023-01-24 18:08:49 +0000130 cp = subprocess.run(diff_cmd, check=False)
131
132 if cp.returncode == 1:
David Horstmann6b3ce302023-01-24 18:36:41 +0000133 print(src_file + " changed - code style is incorrect.")
David Horstmannfa928f12022-11-01 15:46:16 +0000134 style_correct = False
David Horstmannce42cc22023-01-24 18:08:49 +0000135 elif cp.returncode != 0:
136 raise subprocess.CalledProcessError(cp.returncode, cp.args,
137 cp.stdout, cp.stderr)
David Horstmannfa928f12022-11-01 15:46:16 +0000138
139 # Tidy up artifact
David Horstmann0ebc12e2022-12-08 15:04:20 +0000140 os.remove(src_file + ".uncrustify")
David Horstmannfa928f12022-11-01 15:46:16 +0000141
142 return style_correct
143
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000144def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmannfa928f12022-11-01 15:46:16 +0000145 """
146 Run Uncrustify once over the source files.
147 """
148 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
149 for src_file in src_file_list:
150 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmann6b3ce302023-01-24 18:36:41 +0000151 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000152 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000153 print_err("Uncrustify with file returned: " +
154 str(result.returncode) + " correcting file " +
155 src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000156 return False
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000157 return True
David Horstmannfa928f12022-11-01 15:46:16 +0000158
159def fix_style(src_file_list: List[str]) -> int:
160 """
161 Fix the code style. This takes 2 passes of Uncrustify.
162 """
David Horstmann78d566b2023-01-05 10:02:09 +0000163 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000164 return 1
David Horstmann78d566b2023-01-05 10:02:09 +0000165 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000166 return 1
David Horstmannfa928f12022-11-01 15:46:16 +0000167
168 # Guard against future changes that cause the codebase to require
169 # more passes.
170 if not check_style_is_correct(src_file_list):
David Horstmann28d21572023-01-16 18:32:56 +0000171 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmannfa928f12022-11-01 15:46:16 +0000172 return 1
173 else:
174 return 0
175
176def main() -> int:
177 """
178 Main with command line arguments.
179 """
David Horstmann2cf779c2022-12-08 14:44:36 +0000180 uncrustify_version = get_uncrustify_version().strip()
181 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100182 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6b3ce302023-01-24 18:36:41 +0000183 uncrustify_version + "'")
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100184 print("Note: The only supported version is " +
David Horstmann6b3ce302023-01-24 18:36:41 +0000185 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmannfa928f12022-11-01 15:46:16 +0000186
187 parser = argparse.ArgumentParser()
Gilles Peskine59803db2022-12-22 16:34:01 +0100188 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100189 help=('modify source files to fix the code style '
190 '(default: print diff, do not modify files)'))
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100191 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?',
Gilles Peskine43838b82023-06-22 20:29:41 +0200192 help=('only check files modified since the specified commit'
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100193 ' (e.g. --since=HEAD~3 or --since=development). If no'
194 ' commit is specified, default to development.'))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800195 # --subset is almost useless: it only matters if there are no files
196 # ('code_style.py' without arguments checks all files known to Git,
197 # 'code_style.py --subset' does nothing). In particular,
198 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
199 # way to restyle a possibly empty set of files.
Pengyu Lv8c6325c2023-02-06 14:29:02 +0800200 parser.add_argument('--subset', action='store_true',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800201 help='only check the specified files (default with non-option arguments)')
Gilles Peskine59803db2022-12-22 16:34:01 +0100202 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800203 help='files to check (files MUST be known to git, if none: check all)')
David Horstmannfa928f12022-11-01 15:46:16 +0000204
205 args = parser.parse_args()
206
Gilles Peskine43838b82023-06-22 20:29:41 +0200207 covered = frozenset(get_src_files(args.since))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800208 # We only check files that are known to git
209 if args.subset or args.operands:
Pengyu Lve19b51b2023-02-14 10:29:53 +0800210 src_files = [f for f in args.operands if f in covered]
211 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lvacbeb7f2023-02-06 14:27:30 +0800212 if skip_src_files:
213 print_skip(skip_src_files)
Pengyu Lvc36743f2023-02-15 10:20:40 +0800214 else:
Pengyu Lv10f41442023-02-15 16:58:09 +0800215 src_files = list(covered)
Gilles Peskine59803db2022-12-22 16:34:01 +0100216
David Horstmannfa928f12022-11-01 15:46:16 +0000217 if args.fix:
218 # Fix mode
219 return fix_style(src_files)
220 else:
221 # Check mode
222 if check_style_is_correct(src_files):
David Horstmann6b3ce302023-01-24 18:36:41 +0000223 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmannfa928f12022-11-01 15:46:16 +0000224 return 0
225 else:
226 return 1
227
228if __name__ == '__main__':
229 sys.exit(main())