blob: e26d42e9c39436cab208a21013997155292f7f51 [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
21import io
22import os
Gilles Peskine9a3771e2022-12-19 00:48:58 +010023import re
David Horstmannfa928f12022-11-01 15:46:16 +000024import subprocess
25import sys
Gilles Peskine9a3771e2022-12-19 00:48:58 +010026from typing import FrozenSet, List
David Horstmannfa928f12022-11-01 15:46:16 +000027
David Horstmann2cf779c2022-12-08 14:44:36 +000028UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1"
David Horstmannae93a3f2022-12-08 17:03:01 +000029CONFIG_FILE = ".uncrustify.cfg"
David Horstmannfa928f12022-11-01 15:46:16 +000030UNCRUSTIFY_EXE = "uncrustify"
31UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE]
32STDOUT_UTF8 = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
33STDERR_UTF8 = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
Gilles Peskine9a3771e2022-12-19 00:48:58 +010034CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh"
David Horstmannfa928f12022-11-01 15:46:16 +000035
David Horstmannca13c4f2022-12-08 14:33:52 +000036def print_err(*args):
37 print("Error: ", *args, file=STDERR_UTF8)
38
Gilles Peskine9a3771e2022-12-19 00:48:58 +010039# Match FILENAME(s) in "check SCRIPT (FILENAME...)"
40CHECK_CALL_RE = re.compile(r"\n\s*check\s+[^\s#$&*?;|]+([^\n#$&*?;|]+)",
41 re.ASCII)
42def list_generated_files() -> FrozenSet[str]:
43 """Return the names of generated files.
44
45 We don't reformat generated files, since the result might be different
46 from the output of the generator. Ideally the result of the generator
47 would conform to the code style, but this would be difficult, especially
48 with respect to the placement of line breaks in long logical lines.
49 """
50 # Parse check-generated-files.sh to get an up-to-date list of
51 # generated files. Read the file rather than calling it so that
52 # this script only depends on Git, Python and uncrustify, and not other
53 # tools such as sh or grep which might not be available on Windows.
54 # This introduces a limitation: check-generated-files.sh must have
55 # the expected format and must list the files explicitly, not through
56 # wildcards or command substitution.
57 content = open(CHECK_GENERATED_FILES, encoding="utf-8").read()
58 checks = re.findall(CHECK_CALL_RE, content)
59 return frozenset(word for s in checks for word in s.split())
60
David Horstmannfa928f12022-11-01 15:46:16 +000061def get_src_files() -> List[str]:
62 """
63 Use git ls-files to get a list of the source files
64 """
David Horstmannb7dab412022-12-08 13:12:21 +000065 git_ls_files_cmd = ["git", "ls-files",
David Horstmannc6b604e2022-12-08 17:38:27 +000066 "*.[hc]",
67 "tests/suites/*.function",
68 "scripts/data_files/*.fmt"]
David Horstmannfa928f12022-11-01 15:46:16 +000069
70 result = subprocess.run(git_ls_files_cmd, stdout=subprocess.PIPE, \
71 stderr=STDERR_UTF8, check=False)
72
73 if result.returncode != 0:
David Horstmann0ebc12e2022-12-08 15:04:20 +000074 print_err("git ls-files returned: " + str(result.returncode))
David Horstmannfa928f12022-11-01 15:46:16 +000075 return []
76 else:
Gilles Peskine9a3771e2022-12-19 00:48:58 +010077 generated_files = list_generated_files()
David Horstmannfa928f12022-11-01 15:46:16 +000078 src_files = str(result.stdout, "utf-8").split()
Gilles Peskine9a3771e2022-12-19 00:48:58 +010079 # Don't correct style for third-party files (and, for simplicity,
80 # companion files in the same subtree), or for automatically
81 # generated files (we're correcting the templates instead).
82 src_files = [filename for filename in src_files
83 if not (filename.startswith("3rdparty/") or
84 filename in generated_files)]
David Horstmannfa928f12022-11-01 15:46:16 +000085 return src_files
86
87def get_uncrustify_version() -> str:
88 """
89 Get the version string from Uncrustify
90 """
David Horstmannb7dab412022-12-08 13:12:21 +000091 result = subprocess.run([UNCRUSTIFY_EXE, "--version"], \
David Horstmannfa928f12022-11-01 15:46:16 +000092 stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
93 if result.returncode != 0:
David Horstmannca13c4f2022-12-08 14:33:52 +000094 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmannfa928f12022-11-01 15:46:16 +000095 return ""
96 else:
97 return str(result.stdout, "utf-8")
98
99def check_style_is_correct(src_file_list: List[str]) -> bool:
100 """
David Horstmann9711f4e2022-12-08 14:36:10 +0000101 Check the code style and output a diff for each file whose style is
David Horstmannfa928f12022-11-01 15:46:16 +0000102 incorrect.
103 """
104 style_correct = True
105 for src_file in src_file_list:
106 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmannc571c5b2023-01-04 18:33:25 +0000107 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE, \
David Horstmannfa928f12022-11-01 15:46:16 +0000108 stderr=subprocess.PIPE, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000109 if result.returncode != 0:
110 print_err("Uncrustify returned " + str(result.returncode) + \
111 " correcting file " + src_file)
112 return False
David Horstmannfa928f12022-11-01 15:46:16 +0000113
114 # Uncrustify makes changes to the code and places the result in a new
115 # file with the extension ".uncrustify". To get the changes (if any)
116 # simply diff the 2 files.
David Horstmann0ebc12e2022-12-08 15:04:20 +0000117 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmannfa928f12022-11-01 15:46:16 +0000118 result = subprocess.run(diff_cmd, stdout=subprocess.PIPE, \
119 stderr=STDERR_UTF8, check=False)
120 if len(result.stdout) > 0:
David Horstmann0ebc12e2022-12-08 15:04:20 +0000121 print(src_file + " - Incorrect code style.", file=STDOUT_UTF8)
David Horstmannfa928f12022-11-01 15:46:16 +0000122 print("File changed - diff:", file=STDOUT_UTF8)
123 print(str(result.stdout, "utf-8"), file=STDOUT_UTF8)
124 style_correct = False
125 else:
David Horstmann0ebc12e2022-12-08 15:04:20 +0000126 print(src_file + " - OK.", file=STDOUT_UTF8)
David Horstmannfa928f12022-11-01 15:46:16 +0000127
128 # Tidy up artifact
David Horstmann0ebc12e2022-12-08 15:04:20 +0000129 os.remove(src_file + ".uncrustify")
David Horstmannfa928f12022-11-01 15:46:16 +0000130
131 return style_correct
132
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000133def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmannfa928f12022-11-01 15:46:16 +0000134 """
135 Run Uncrustify once over the source files.
136 """
137 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
138 for src_file in src_file_list:
139 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmannc571c5b2023-01-04 18:33:25 +0000140 result = subprocess.run(uncrustify_cmd, check=False, \
141 stdout=STDOUT_UTF8, stderr=STDERR_UTF8)
142 if result.returncode != 0:
143 print_err("Uncrustify with file returned: " + \
144 str(result.returncode) + " correcting file " + \
145 src_file)
146 return False
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000147 return True
David Horstmannfa928f12022-11-01 15:46:16 +0000148
149def fix_style(src_file_list: List[str]) -> int:
150 """
151 Fix the code style. This takes 2 passes of Uncrustify.
152 """
David Horstmann78d566b2023-01-05 10:02:09 +0000153 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000154 return 1
David Horstmann78d566b2023-01-05 10:02:09 +0000155 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000156 return 1
David Horstmannfa928f12022-11-01 15:46:16 +0000157
158 # Guard against future changes that cause the codebase to require
159 # more passes.
160 if not check_style_is_correct(src_file_list):
David Horstmannca13c4f2022-12-08 14:33:52 +0000161 print("Code style still incorrect after second run of Uncrustify.")
David Horstmannfa928f12022-11-01 15:46:16 +0000162 return 1
163 else:
164 return 0
165
166def main() -> int:
167 """
168 Main with command line arguments.
169 """
David Horstmann2cf779c2022-12-08 14:44:36 +0000170 uncrustify_version = get_uncrustify_version().strip()
171 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100172 print("Warning: Using unsupported Uncrustify version '" +
173 uncrustify_version + "'", file=STDOUT_UTF8)
174 print("Note: The only supported version is " +
175 UNCRUSTIFY_SUPPORTED_VERSION, file=STDOUT_UTF8)
David Horstmannfa928f12022-11-01 15:46:16 +0000176
177 parser = argparse.ArgumentParser()
Gilles Peskine59803db2022-12-22 16:34:01 +0100178 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100179 help=('modify source files to fix the code style '
180 '(default: print diff, do not modify files)'))
Gilles Peskine59803db2022-12-22 16:34:01 +0100181 # --files is almost useless: it only matters if there are no files
182 # ('code_style.py' without arguments checks all files known to Git,
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100183 # 'code_style.py --files' does nothing). In particular,
Gilles Peskined449ced2023-01-12 15:45:32 +0100184 # 'code_style.py --fix --files ...' is intended as a stable ("porcelain")
185 # way to restyle a possibly empty set of files.
Gilles Peskine59803db2022-12-22 16:34:01 +0100186 parser.add_argument('--files', action='store_true',
187 help='only check the specified files (default with non-option arguments)')
188 parser.add_argument('operands', nargs='*', metavar='FILE',
189 help='files to check (if none: check files that are known to git)')
David Horstmannfa928f12022-11-01 15:46:16 +0000190
191 args = parser.parse_args()
192
Gilles Peskine59803db2022-12-22 16:34:01 +0100193 if args.files or args.operands:
194 src_files = args.operands
195 else:
196 src_files = get_src_files()
197
David Horstmannfa928f12022-11-01 15:46:16 +0000198 if args.fix:
199 # Fix mode
200 return fix_style(src_files)
201 else:
202 # Check mode
203 if check_style_is_correct(src_files):
204 return 0
205 else:
206 return 1
207
208if __name__ == '__main__':
209 sys.exit(main())