blob: d3f89d91301399c1d7d0ad584e2b937aba7c5650 [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
Dave Rodgman1bd787a2024-03-18 12:32:49 +000055AUTOGEN_RE = re.compile(r"Warning[ :-]+This file is (now )?auto[ -]?generated",
Dave Rodgman4e4540d2024-03-18 11:55:39 +000056 re.ASCII | re.IGNORECASE)
Dave Rodgman2a9eb222024-03-18 11:15:06 +000057def is_file_autogenerated(filename):
58 content = open(filename, encoding="utf-8").read()
59 return AUTOGEN_RE.search(content) is not None
60
Gilles Peskine43838b82023-06-22 20:29:41 +020061def get_src_files(since: Optional[str]) -> List[str]:
David Horstmannfa928f12022-11-01 15:46:16 +000062 """
Gilles Peskine22eb82c2023-06-22 19:45:01 +020063 Use git to get a list of the source files.
64
Gilles Peskine43838b82023-06-22 20:29:41 +020065 The optional argument since is a commit, indicating to only list files
66 that have changed since that commit. Without this argument, list all
67 files known to git.
68
Gilles Peskine22eb82c2023-06-22 19:45:01 +020069 Only C files are included, and certain files (generated, or 3rdparty)
70 are excluded.
David Horstmannfa928f12022-11-01 15:46:16 +000071 """
Gilles Peskine163ec402023-06-25 22:18:40 +020072 file_patterns = ["*.[hc]",
73 "tests/suites/*.function",
74 "scripts/data_files/*.fmt"]
75 output = subprocess.check_output(["git", "ls-files"] + file_patterns,
76 universal_newlines=True)
Gilles Peskine22eb82c2023-06-22 19:45:01 +020077 src_files = output.split()
David Horstmann8eaeb382024-06-06 15:25:10 +010078
79 # When this script is called from a git hook, some environment variables
80 # are set by default which force all git commands to use the main repository
81 # (i.e. prevent us from performing commands on the framework repo).
82 # Create an environment without these variables for running commands on the
83 # framework repo.
84 framework_env = os.environ.copy()
85 # Get a list of environment vars that git sets
86 git_env_vars = subprocess.check_output(["git", "rev-parse", "--local-env-vars"],
87 universal_newlines=True)
David Horstmann8eaeb382024-06-06 15:25:10 +010088 # Remove the vars from the environment
David Horstmann93ee0162024-06-06 16:16:31 +010089 for var in git_env_vars.split():
David Horstmann8eaeb382024-06-06 15:25:10 +010090 framework_env.pop(var, None)
91
Ronald Cronbc93d0e2024-04-25 15:46:01 +020092 output = subprocess.check_output(["git", "-C", "framework", "ls-files"]
David Horstmann8eaeb382024-06-06 15:25:10 +010093 + file_patterns,
94 universal_newlines=True,
95 env=framework_env)
Ronald Cronbc93d0e2024-04-25 15:46:01 +020096 framework_src_files = output.split()
97
Gilles Peskine163ec402023-06-25 22:18:40 +020098 if since:
Ronald Cronbc93d0e2024-04-25 15:46:01 +020099 # get all files changed in commits since the starting point in ...
100 # ... the main repository
101 cmd = ["git", "log", since + "..HEAD", "--ignore-submodules",
102 "--name-only", "--pretty=", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +0100103 output = subprocess.check_output(cmd, universal_newlines=True)
104 committed_changed_files = output.split()
Ronald Cronbc93d0e2024-04-25 15:46:01 +0200105 # ... the framework submodule
106 cmd = ["git", "-C", "framework", "log", since + "..HEAD",
107 "--name-only", "--pretty=", "--"] + framework_src_files
David Horstmann8eaeb382024-06-06 15:25:10 +0100108 output = subprocess.check_output(cmd, universal_newlines=True,
109 env=framework_env)
Ronald Cronbc93d0e2024-04-25 15:46:01 +0200110 committed_changed_files += ["framework/" + s for s in output.split()]
111
112 # and also get all files with uncommitted changes in ...
113 # ... the main repository
Dave Rodgmanfccc5f82023-07-27 20:00:41 +0100114 cmd = ["git", "diff", "--name-only", "--"] + src_files
Dave Rodgman05b60f42023-07-27 14:22:34 +0100115 output = subprocess.check_output(cmd, universal_newlines=True)
116 uncommitted_changed_files = output.split()
Ronald Cronbc93d0e2024-04-25 15:46:01 +0200117 # ... the framework submodule
118 cmd = ["git", "-C", "framework", "diff", "--name-only", "--"] + \
119 framework_src_files
David Horstmann8eaeb382024-06-06 15:25:10 +0100120 output = subprocess.check_output(cmd, universal_newlines=True,
121 env=framework_env)
Ronald Cronbc93d0e2024-04-25 15:46:01 +0200122 uncommitted_changed_files += ["framework/" + s for s in output.split()]
123
124 src_files = committed_changed_files + uncommitted_changed_files
125 else:
126 src_files += ["framework/" + s for s in framework_src_files]
David Horstmannfa928f12022-11-01 15:46:16 +0000127
Gilles Peskine22eb82c2023-06-22 19:45:01 +0200128 generated_files = list_generated_files()
129 # Don't correct style for third-party files (and, for simplicity,
130 # companion files in the same subtree), or for automatically
131 # generated files (we're correcting the templates instead).
132 src_files = [filename for filename in src_files
133 if not (filename.startswith("3rdparty/") or
Dave Rodgman2a9eb222024-03-18 11:15:06 +0000134 filename in generated_files or
135 is_file_autogenerated(filename))]
Gilles Peskine22eb82c2023-06-22 19:45:01 +0200136 return src_files
David Horstmannfa928f12022-11-01 15:46:16 +0000137
138def get_uncrustify_version() -> str:
139 """
140 Get the version string from Uncrustify
141 """
David Horstmann04bdbe32023-01-25 11:39:04 +0000142 result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
143 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
144 check=False)
David Horstmannfa928f12022-11-01 15:46:16 +0000145 if result.returncode != 0:
David Horstmannca13c4f2022-12-08 14:33:52 +0000146 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
David Horstmannfa928f12022-11-01 15:46:16 +0000147 return ""
148 else:
149 return str(result.stdout, "utf-8")
150
151def check_style_is_correct(src_file_list: List[str]) -> bool:
152 """
David Horstmann9711f4e2022-12-08 14:36:10 +0000153 Check the code style and output a diff for each file whose style is
David Horstmannfa928f12022-11-01 15:46:16 +0000154 incorrect.
155 """
156 style_correct = True
157 for src_file in src_file_list:
158 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
David Horstmann04bdbe32023-01-25 11:39:04 +0000159 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
160 stderr=subprocess.PIPE, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000161 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000162 print_err("Uncrustify returned " + str(result.returncode) +
163 " correcting file " + src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000164 return False
David Horstmannfa928f12022-11-01 15:46:16 +0000165
166 # Uncrustify makes changes to the code and places the result in a new
167 # file with the extension ".uncrustify". To get the changes (if any)
168 # simply diff the 2 files.
David Horstmann0ebc12e2022-12-08 15:04:20 +0000169 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
David Horstmannce42cc22023-01-24 18:08:49 +0000170 cp = subprocess.run(diff_cmd, check=False)
171
172 if cp.returncode == 1:
David Horstmann6b3ce302023-01-24 18:36:41 +0000173 print(src_file + " changed - code style is incorrect.")
David Horstmannfa928f12022-11-01 15:46:16 +0000174 style_correct = False
David Horstmannce42cc22023-01-24 18:08:49 +0000175 elif cp.returncode != 0:
176 raise subprocess.CalledProcessError(cp.returncode, cp.args,
177 cp.stdout, cp.stderr)
David Horstmannfa928f12022-11-01 15:46:16 +0000178
179 # Tidy up artifact
David Horstmann0ebc12e2022-12-08 15:04:20 +0000180 os.remove(src_file + ".uncrustify")
David Horstmannfa928f12022-11-01 15:46:16 +0000181
182 return style_correct
183
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000184def fix_style_single_pass(src_file_list: List[str]) -> bool:
David Horstmannfa928f12022-11-01 15:46:16 +0000185 """
186 Run Uncrustify once over the source files.
187 """
188 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
189 for src_file in src_file_list:
190 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
David Horstmann6b3ce302023-01-24 18:36:41 +0000191 result = subprocess.run(uncrustify_cmd, check=False)
David Horstmannc571c5b2023-01-04 18:33:25 +0000192 if result.returncode != 0:
David Horstmann04bdbe32023-01-25 11:39:04 +0000193 print_err("Uncrustify with file returned: " +
194 str(result.returncode) + " correcting file " +
195 src_file)
David Horstmannc571c5b2023-01-04 18:33:25 +0000196 return False
David Horstmann8d1d6ed2023-01-05 09:59:35 +0000197 return True
David Horstmannfa928f12022-11-01 15:46:16 +0000198
199def fix_style(src_file_list: List[str]) -> int:
200 """
201 Fix the code style. This takes 2 passes of Uncrustify.
202 """
David Horstmann78d566b2023-01-05 10:02:09 +0000203 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000204 return 1
David Horstmann78d566b2023-01-05 10:02:09 +0000205 if not fix_style_single_pass(src_file_list):
David Horstmannc571c5b2023-01-04 18:33:25 +0000206 return 1
David Horstmannfa928f12022-11-01 15:46:16 +0000207
208 # Guard against future changes that cause the codebase to require
209 # more passes.
210 if not check_style_is_correct(src_file_list):
David Horstmann28d21572023-01-16 18:32:56 +0000211 print_err("Code style still incorrect after second run of Uncrustify.")
David Horstmannfa928f12022-11-01 15:46:16 +0000212 return 1
213 else:
214 return 0
215
216def main() -> int:
217 """
218 Main with command line arguments.
219 """
David Horstmann2cf779c2022-12-08 14:44:36 +0000220 uncrustify_version = get_uncrustify_version().strip()
221 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100222 print("Warning: Using unsupported Uncrustify version '" +
David Horstmann6b3ce302023-01-24 18:36:41 +0000223 uncrustify_version + "'")
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100224 print("Note: The only supported version is " +
David Horstmann6b3ce302023-01-24 18:36:41 +0000225 UNCRUSTIFY_SUPPORTED_VERSION)
David Horstmannfa928f12022-11-01 15:46:16 +0000226
227 parser = argparse.ArgumentParser()
Gilles Peskine59803db2022-12-22 16:34:01 +0100228 parser.add_argument('-f', '--fix', action='store_true',
Gilles Peskine9d34cf32022-12-23 18:15:19 +0100229 help=('modify source files to fix the code style '
230 '(default: print diff, do not modify files)'))
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100231 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?',
Gilles Peskine43838b82023-06-22 20:29:41 +0200232 help=('only check files modified since the specified commit'
Dave Rodgmaneaf27612023-07-27 14:22:55 +0100233 ' (e.g. --since=HEAD~3 or --since=development). If no'
234 ' commit is specified, default to development.'))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800235 # --subset is almost useless: it only matters if there are no files
236 # ('code_style.py' without arguments checks all files known to Git,
237 # 'code_style.py --subset' does nothing). In particular,
238 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
239 # way to restyle a possibly empty set of files.
Pengyu Lv8c6325c2023-02-06 14:29:02 +0800240 parser.add_argument('--subset', action='store_true',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800241 help='only check the specified files (default with non-option arguments)')
Gilles Peskine59803db2022-12-22 16:34:01 +0100242 parser.add_argument('operands', nargs='*', metavar='FILE',
Pengyu Lvc36743f2023-02-15 10:20:40 +0800243 help='files to check (files MUST be known to git, if none: check all)')
David Horstmannfa928f12022-11-01 15:46:16 +0000244
245 args = parser.parse_args()
246
Gilles Peskine43838b82023-06-22 20:29:41 +0200247 covered = frozenset(get_src_files(args.since))
Pengyu Lvc36743f2023-02-15 10:20:40 +0800248 # We only check files that are known to git
249 if args.subset or args.operands:
Pengyu Lve19b51b2023-02-14 10:29:53 +0800250 src_files = [f for f in args.operands if f in covered]
251 skip_src_files = [f for f in args.operands if f not in covered]
Pengyu Lvacbeb7f2023-02-06 14:27:30 +0800252 if skip_src_files:
253 print_skip(skip_src_files)
Pengyu Lvc36743f2023-02-15 10:20:40 +0800254 else:
Pengyu Lv10f41442023-02-15 16:58:09 +0800255 src_files = list(covered)
Gilles Peskine59803db2022-12-22 16:34:01 +0100256
David Horstmannfa928f12022-11-01 15:46:16 +0000257 if args.fix:
258 # Fix mode
259 return fix_style(src_files)
260 else:
261 # Check mode
262 if check_style_is_correct(src_files):
David Horstmann6b3ce302023-01-24 18:36:41 +0000263 print("Checked {} files, style ok.".format(len(src_files)))
David Horstmannfa928f12022-11-01 15:46:16 +0000264 return 0
265 else:
266 return 1
267
268if __name__ == '__main__':
269 sys.exit(main())