blob: 463a349ff601d9934552ce92529454047523430c [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.
3"""
4# Copyright The Mbed TLS Contributors
5# SPDX-License-Identifier: Apache-2.0
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18import argparse
19import io
20import os
Gilles Peskine4ca54d42022-12-19 00:48:58 +010021import re
David Horstmann20d6bfa2022-11-01 15:46:16 +000022import subprocess
23import sys
Gilles Peskine4ca54d42022-12-19 00:48:58 +010024from typing import FrozenSet, List
David Horstmann20d6bfa2022-11-01 15:46:16 +000025
David Horstmann3a6f9f92022-12-08 14:44:36 +000026UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1"
David Horstmannc747fdf2022-12-08 17:03:01 +000027CONFIG_FILE = ".uncrustify.cfg"
David Horstmann20d6bfa2022-11-01 15:46:16 +000028UNCRUSTIFY_EXE = "uncrustify"
29UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE]
30STDOUT_UTF8 = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
31STDERR_UTF8 = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
Gilles Peskine4ca54d42022-12-19 00:48:58 +010032CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh"
David Horstmann20d6bfa2022-11-01 15:46:16 +000033
David Horstmann448cfec2022-12-08 14:33:52 +000034def print_err(*args):
35 print("Error: ", *args, file=STDERR_UTF8)
36
Gilles Peskine4ca54d42022-12-19 00:48:58 +010037# Match FILENAME(s) in "check SCRIPT (FILENAME...)"
38CHECK_CALL_RE = re.compile(r"\n\s*check\s+[^\s#$&*?;|]+([^\n#$&*?;|]+)",
39 re.ASCII)
40def list_generated_files() -> FrozenSet[str]:
41 """Return the names of generated files.
42
43 We don't reformat generated files, since the result might be different
44 from the output of the generator. Ideally the result of the generator
45 would conform to the code style, but this would be difficult, especially
46 with respect to the placement of line breaks in long logical lines.
47 """
48 # Parse check-generated-files.sh to get an up-to-date list of
49 # generated files. Read the file rather than calling it so that
50 # this script only depends on Git, Python and uncrustify, and not other
51 # tools such as sh or grep which might not be available on Windows.
52 # This introduces a limitation: check-generated-files.sh must have
53 # the expected format and must list the files explicitly, not through
54 # wildcards or command substitution.
55 content = open(CHECK_GENERATED_FILES, encoding="utf-8").read()
56 checks = re.findall(CHECK_CALL_RE, content)
57 return frozenset(word for s in checks for word in s.split())
58
David Horstmann20d6bfa2022-11-01 15:46:16 +000059def get_src_files() -> List[str]:
60 """
61 Use git ls-files to get a list of the source files
62 """
David Horstmann27b37042022-12-08 13:12:21 +000063 git_ls_files_cmd = ["git", "ls-files",
David Horstmanneead72e2022-12-08 17:38:27 +000064 "*.[hc]",
65 "tests/suites/*.function",
66 "scripts/data_files/*.fmt"]
David Horstmann20d6bfa2022-11-01 15:46:16 +000067
68 result = subprocess.run(git_ls_files_cmd, stdout=subprocess.PIPE, \
69 stderr=STDERR_UTF8, check=False)
70
71 if result.returncode != 0:
David Horstmann1f8b4d92022-12-08 15:04:20 +000072 print_err("git ls-files returned: " + str(result.returncode))
David Horstmann20d6bfa2022-11-01 15:46:16 +000073 return []
74 else:
Gilles Peskine4ca54d42022-12-19 00:48:58 +010075 generated_files = list_generated_files()
David Horstmann20d6bfa2022-11-01 15:46:16 +000076 src_files = str(result.stdout, "utf-8").split()
Gilles Peskine4ca54d42022-12-19 00:48:58 +010077 # Don't correct style for third-party files (and, for simplicity,
78 # companion files in the same subtree), or for automatically
79 # generated files (we're correcting the templates instead).
80 src_files = [filename for filename in src_files
81 if not (filename.startswith("3rdparty/") or
82 filename in generated_files)]
David Horstmann20d6bfa2022-11-01 15:46:16 +000083 return src_files
84
85def get_uncrustify_version() -> str:
86 """
87 Get the version string from Uncrustify
88 """
David Horstmann27b37042022-12-08 13:12:21 +000089 result = subprocess.run([UNCRUSTIFY_EXE, "--version"], \
David Horstmann20d6bfa2022-11-01 15:46:16 +000090 stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
91 if result.returncode != 0:
David Horstmann448cfec2022-12-08 14:33:52 +000092 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmann20d6bfa2022-11-01 15:46:16 +000093 return ""
94 else:
95 return str(result.stdout, "utf-8")
96
97def check_style_is_correct(src_file_list: List[str]) -> bool:
98 """
David Horstmann99a669a2022-12-08 14:36:10 +000099 Check the code style and output a diff for each file whose style is
David Horstmann20d6bfa2022-11-01 15:46:16 +0000100 incorrect.
101 """
102 style_correct = True
103 for src_file in src_file_list:
104 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmannb92d30f2023-01-04 18:33:25 +0000105 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE, \
David Horstmann20d6bfa2022-11-01 15:46:16 +0000106 stderr=subprocess.PIPE, check=False)
David Horstmannb92d30f2023-01-04 18:33:25 +0000107 if result.returncode != 0:
108 print_err("Uncrustify returned " + str(result.returncode) + \
109 " correcting file " + src_file)
110 return False
David Horstmann20d6bfa2022-11-01 15:46:16 +0000111
112 # Uncrustify makes changes to the code and places the result in a new
113 # file with the extension ".uncrustify". To get the changes (if any)
114 # simply diff the 2 files.
David Horstmann1f8b4d92022-12-08 15:04:20 +0000115 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmann20d6bfa2022-11-01 15:46:16 +0000116 result = subprocess.run(diff_cmd, stdout=subprocess.PIPE, \
117 stderr=STDERR_UTF8, check=False)
118 if len(result.stdout) > 0:
David Horstmann1f8b4d92022-12-08 15:04:20 +0000119 print(src_file + " - Incorrect code style.", file=STDOUT_UTF8)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000120 print("File changed - diff:", file=STDOUT_UTF8)
121 print(str(result.stdout, "utf-8"), file=STDOUT_UTF8)
122 style_correct = False
123 else:
David Horstmann1f8b4d92022-12-08 15:04:20 +0000124 print(src_file + " - OK.", file=STDOUT_UTF8)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000125
126 # Tidy up artifact
David Horstmann1f8b4d92022-12-08 15:04:20 +0000127 os.remove(src_file + ".uncrustify")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000128
129 return style_correct
130
David Horstmannfa69def2023-01-05 09:59:35 +0000131def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmann20d6bfa2022-11-01 15:46:16 +0000132 """
133 Run Uncrustify once over the source files.
134 """
135 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
136 for src_file in src_file_list:
137 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmannb92d30f2023-01-04 18:33:25 +0000138 result = subprocess.run(uncrustify_cmd, check=False, \
139 stdout=STDOUT_UTF8, stderr=STDERR_UTF8)
140 if result.returncode != 0:
141 print_err("Uncrustify with file returned: " + \
142 str(result.returncode) + " correcting file " + \
143 src_file)
144 return False
David Horstmannfa69def2023-01-05 09:59:35 +0000145 return True
David Horstmann20d6bfa2022-11-01 15:46:16 +0000146
147def fix_style(src_file_list: List[str]) -> int:
148 """
149 Fix the code style. This takes 2 passes of Uncrustify.
150 """
David Horstmann242df482023-01-05 10:02:09 +0000151 if not fix_style_single_pass(src_file_list):
David Horstmannb92d30f2023-01-04 18:33:25 +0000152 return 1
David Horstmann242df482023-01-05 10:02:09 +0000153 if not fix_style_single_pass(src_file_list):
David Horstmannb92d30f2023-01-04 18:33:25 +0000154 return 1
David Horstmann20d6bfa2022-11-01 15:46:16 +0000155
156 # Guard against future changes that cause the codebase to require
157 # more passes.
158 if not check_style_is_correct(src_file_list):
David Horstmann448cfec2022-12-08 14:33:52 +0000159 print("Code style still incorrect after second run of Uncrustify.")
David Horstmann20d6bfa2022-11-01 15:46:16 +0000160 return 1
161 else:
162 return 0
163
164def main() -> int:
165 """
166 Main with command line arguments.
167 """
David Horstmann3a6f9f92022-12-08 14:44:36 +0000168 uncrustify_version = get_uncrustify_version().strip()
169 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine75289862022-12-23 18:15:19 +0100170 print("Warning: Using unsupported Uncrustify version '" +
171 uncrustify_version + "'", file=STDOUT_UTF8)
172 print("Note: The only supported version is " +
173 UNCRUSTIFY_SUPPORTED_VERSION, file=STDOUT_UTF8)
David Horstmann20d6bfa2022-11-01 15:46:16 +0000174
175 parser = argparse.ArgumentParser()
Gilles Peskine38f514d2022-12-22 16:34:01 +0100176 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine75289862022-12-23 18:15:19 +0100177 help=('modify source files to fix the code style '
178 '(default: print diff, do not modify files)'))
Gilles Peskine38f514d2022-12-22 16:34:01 +0100179 # --files is almost useless: it only matters if there are no files
180 # ('code_style.py' without arguments checks all files known to Git,
Gilles Peskine75289862022-12-23 18:15:19 +0100181 # 'code_style.py --files' does nothing). In particular,
Gilles Peskineb71d28b2023-01-12 15:45:32 +0100182 # 'code_style.py --fix --files ...' is intended as a stable ("porcelain")
183 # way to restyle a possibly empty set of files.
Gilles Peskine38f514d2022-12-22 16:34:01 +0100184 parser.add_argument('--files', action='store_true',
185 help='only check the specified files (default with non-option arguments)')
186 parser.add_argument('operands', nargs='*', metavar='FILE',
187 help='files to check (if none: check files that are known to git)')
David Horstmann20d6bfa2022-11-01 15:46:16 +0000188
189 args = parser.parse_args()
190
Gilles Peskine38f514d2022-12-22 16:34:01 +0100191 if args.files or args.operands:
192 src_files = args.operands
193 else:
194 src_files = get_src_files()
195
David Horstmann20d6bfa2022-11-01 15:46:16 +0000196 if args.fix:
197 # Fix mode
198 return fix_style(src_files)
199 else:
200 # Check mode
201 if check_style_is_correct(src_files):
202 return 0
203 else:
204 return 1
205
206if __name__ == '__main__':
207 sys.exit(main())