1#!/usr/bin/env python3 2"""Check or fix the code style by running Uncrustify. 3 4This script must be run from the root of a Git work tree containing Mbed TLS. 5""" 6# Copyright The Mbed TLS Contributors 7# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later 8import argparse 9import os 10import re 11import subprocess 12import sys 13from typing import FrozenSet, List, Optional 14 15UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1" 16CONFIG_FILE = ".uncrustify.cfg" 17UNCRUSTIFY_EXE = "uncrustify" 18UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE] 19CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh" 20 21def print_err(*args): 22 print("Error: ", *args, file=sys.stderr) 23 24# 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") 28 print("Warning: The listed files will be skipped because\n" 29 "they are not known to git.") 30 print() 31 32# 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 54def get_src_files(since: Optional[str]) -> List[str]: 55 """ 56 Use git to get a list of the source files. 57 58 The optional argument since is a commit, indicating to only list files 59 that have changed since that commit. Without this argument, list all 60 files known to git. 61 62 Only C files are included, and certain files (generated, or 3rdparty) 63 are excluded. 64 """ 65 file_patterns = ["*.[hc]", 66 "tests/suites/*.function", 67 "scripts/data_files/*.fmt"] 68 output = subprocess.check_output(["git", "ls-files"] + file_patterns, 69 universal_newlines=True) 70 src_files = output.split() 71 if since: 72 # get all files changed in commits since the starting point 73 cmd = ["git", "log", since + "..HEAD", "--name-only", "--pretty=", "--"] + src_files 74 output = subprocess.check_output(cmd, universal_newlines=True) 75 committed_changed_files = output.split() 76 # and also get all files with uncommitted changes 77 cmd = ["git", "diff", "--name-only", "--"] + src_files 78 output = subprocess.check_output(cmd, universal_newlines=True) 79 uncommitted_changed_files = output.split() 80 src_files = list(set(committed_changed_files + uncommitted_changed_files)) 81 82 generated_files = list_generated_files() 83 # Don't correct style for third-party files (and, for simplicity, 84 # companion files in the same subtree), or for automatically 85 # generated files (we're correcting the templates instead). 86 src_files = [filename for filename in src_files 87 if not (filename.startswith("3rdparty/") or 88 filename in generated_files)] 89 return src_files 90 91def get_uncrustify_version() -> str: 92 """ 93 Get the version string from Uncrustify 94 """ 95 result = subprocess.run([UNCRUSTIFY_EXE, "--version"], 96 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 97 check=False) 98 if result.returncode != 0: 99 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8")) 100 return "" 101 else: 102 return str(result.stdout, "utf-8") 103 104def check_style_is_correct(src_file_list: List[str]) -> bool: 105 """ 106 Check the code style and output a diff for each file whose style is 107 incorrect. 108 """ 109 style_correct = True 110 for src_file in src_file_list: 111 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file] 112 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE, 113 stderr=subprocess.PIPE, check=False) 114 if result.returncode != 0: 115 print_err("Uncrustify returned " + str(result.returncode) + 116 " correcting file " + src_file) 117 return False 118 119 # Uncrustify makes changes to the code and places the result in a new 120 # file with the extension ".uncrustify". To get the changes (if any) 121 # simply diff the 2 files. 122 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"] 123 cp = subprocess.run(diff_cmd, check=False) 124 125 if cp.returncode == 1: 126 print(src_file + " changed - code style is incorrect.") 127 style_correct = False 128 elif cp.returncode != 0: 129 raise subprocess.CalledProcessError(cp.returncode, cp.args, 130 cp.stdout, cp.stderr) 131 132 # Tidy up artifact 133 os.remove(src_file + ".uncrustify") 134 135 return style_correct 136 137def fix_style_single_pass(src_file_list: List[str]) -> bool: 138 """ 139 Run Uncrustify once over the source files. 140 """ 141 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"] 142 for src_file in src_file_list: 143 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file] 144 result = subprocess.run(uncrustify_cmd, check=False) 145 if result.returncode != 0: 146 print_err("Uncrustify with file returned: " + 147 str(result.returncode) + " correcting file " + 148 src_file) 149 return False 150 return True 151 152def fix_style(src_file_list: List[str]) -> int: 153 """ 154 Fix the code style. This takes 2 passes of Uncrustify. 155 """ 156 if not fix_style_single_pass(src_file_list): 157 return 1 158 if not fix_style_single_pass(src_file_list): 159 return 1 160 161 # Guard against future changes that cause the codebase to require 162 # more passes. 163 if not check_style_is_correct(src_file_list): 164 print_err("Code style still incorrect after second run of Uncrustify.") 165 return 1 166 else: 167 return 0 168 169def main() -> int: 170 """ 171 Main with command line arguments. 172 """ 173 uncrustify_version = get_uncrustify_version().strip() 174 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version: 175 print("Warning: Using unsupported Uncrustify version '" + 176 uncrustify_version + "'") 177 print("Note: The only supported version is " + 178 UNCRUSTIFY_SUPPORTED_VERSION) 179 180 parser = argparse.ArgumentParser() 181 parser.add_argument('-f', '--fix', action='store_true', 182 help=('modify source files to fix the code style ' 183 '(default: print diff, do not modify files)')) 184 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?', 185 help=('only check files modified since the specified commit' 186 ' (e.g. --since=HEAD~3 or --since=development). If no' 187 ' commit is specified, default to development.')) 188 # --subset is almost useless: it only matters if there are no files 189 # ('code_style.py' without arguments checks all files known to Git, 190 # 'code_style.py --subset' does nothing). In particular, 191 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain") 192 # way to restyle a possibly empty set of files. 193 parser.add_argument('--subset', action='store_true', 194 help='only check the specified files (default with non-option arguments)') 195 parser.add_argument('operands', nargs='*', metavar='FILE', 196 help='files to check (files MUST be known to git, if none: check all)') 197 198 args = parser.parse_args() 199 200 covered = frozenset(get_src_files(args.since)) 201 # We only check files that are known to git 202 if args.subset or args.operands: 203 src_files = [f for f in args.operands if f in covered] 204 skip_src_files = [f for f in args.operands if f not in covered] 205 if skip_src_files: 206 print_skip(skip_src_files) 207 else: 208 src_files = list(covered) 209 210 if args.fix: 211 # Fix mode 212 return fix_style(src_files) 213 else: 214 # Check mode 215 if check_style_is_correct(src_files): 216 print("Checked {} files, style ok.".format(len(src_files))) 217 return 0 218 else: 219 return 1 220 221if __name__ == '__main__': 222 sys.exit(main()) 223