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 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 os 22import re 23import subprocess 24import sys 25from typing import FrozenSet, List 26 27UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1" 28CONFIG_FILE = ".uncrustify.cfg" 29UNCRUSTIFY_EXE = "uncrustify" 30UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE] 31CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh" 32 33def print_err(*args): 34 print("Error: ", *args, file=sys.stderr) 35 36# Print the file names that will be skipped and the help message 37def print_skip(files_to_skip): 38 print() 39 print(*files_to_skip, sep=", SKIP\n", end=", SKIP\n") 40 print("Warning: The listed files will be skipped because\n" 41 "they are not known to git.") 42 print() 43 44# Match FILENAME(s) in "check SCRIPT (FILENAME...)" 45CHECK_CALL_RE = re.compile(r"\n\s*check\s+[^\s#$&*?;|]+([^\n#$&*?;|]+)", 46 re.ASCII) 47def list_generated_files() -> FrozenSet[str]: 48 """Return the names of generated files. 49 50 We don't reformat generated files, since the result might be different 51 from the output of the generator. Ideally the result of the generator 52 would conform to the code style, but this would be difficult, especially 53 with respect to the placement of line breaks in long logical lines. 54 """ 55 # Parse check-generated-files.sh to get an up-to-date list of 56 # generated files. Read the file rather than calling it so that 57 # this script only depends on Git, Python and uncrustify, and not other 58 # tools such as sh or grep which might not be available on Windows. 59 # This introduces a limitation: check-generated-files.sh must have 60 # the expected format and must list the files explicitly, not through 61 # wildcards or command substitution. 62 content = open(CHECK_GENERATED_FILES, encoding="utf-8").read() 63 checks = re.findall(CHECK_CALL_RE, content) 64 return frozenset(word for s in checks for word in s.split()) 65 66def get_src_files() -> List[str]: 67 """ 68 Use git ls-files to get a list of the source files 69 """ 70 git_ls_files_cmd = ["git", "ls-files", 71 "*.[hc]", 72 "tests/suites/*.function", 73 "scripts/data_files/*.fmt"] 74 75 result = subprocess.run(git_ls_files_cmd, stdout=subprocess.PIPE, 76 check=False) 77 78 if result.returncode != 0: 79 print_err("git ls-files returned: " + str(result.returncode)) 80 return [] 81 else: 82 generated_files = list_generated_files() 83 src_files = str(result.stdout, "utf-8").split() 84 # Don't correct style for third-party files (and, for simplicity, 85 # companion files in the same subtree), or for automatically 86 # generated files (we're correcting the templates instead). 87 src_files = [filename for filename in src_files 88 if not (filename.startswith("3rdparty/") or 89 filename in generated_files)] 90 return src_files 91 92def get_uncrustify_version() -> str: 93 """ 94 Get the version string from Uncrustify 95 """ 96 result = subprocess.run([UNCRUSTIFY_EXE, "--version"], 97 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 98 check=False) 99 if result.returncode != 0: 100 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8")) 101 return "" 102 else: 103 return str(result.stdout, "utf-8") 104 105def check_style_is_correct(src_file_list: List[str]) -> bool: 106 """ 107 Check the code style and output a diff for each file whose style is 108 incorrect. 109 """ 110 style_correct = True 111 for src_file in src_file_list: 112 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file] 113 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE, 114 stderr=subprocess.PIPE, check=False) 115 if result.returncode != 0: 116 print_err("Uncrustify returned " + str(result.returncode) + 117 " correcting file " + src_file) 118 return False 119 120 # Uncrustify makes changes to the code and places the result in a new 121 # file with the extension ".uncrustify". To get the changes (if any) 122 # simply diff the 2 files. 123 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"] 124 cp = subprocess.run(diff_cmd, check=False) 125 126 if cp.returncode == 1: 127 print(src_file + " changed - code style is incorrect.") 128 style_correct = False 129 elif cp.returncode != 0: 130 raise subprocess.CalledProcessError(cp.returncode, cp.args, 131 cp.stdout, cp.stderr) 132 133 # Tidy up artifact 134 os.remove(src_file + ".uncrustify") 135 136 return style_correct 137 138def fix_style_single_pass(src_file_list: List[str]) -> bool: 139 """ 140 Run Uncrustify once over the source files. 141 """ 142 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"] 143 for src_file in src_file_list: 144 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file] 145 result = subprocess.run(uncrustify_cmd, check=False) 146 if result.returncode != 0: 147 print_err("Uncrustify with file returned: " + 148 str(result.returncode) + " correcting file " + 149 src_file) 150 return False 151 return True 152 153def fix_style(src_file_list: List[str]) -> int: 154 """ 155 Fix the code style. This takes 2 passes of Uncrustify. 156 """ 157 if not fix_style_single_pass(src_file_list): 158 return 1 159 if not fix_style_single_pass(src_file_list): 160 return 1 161 162 # Guard against future changes that cause the codebase to require 163 # more passes. 164 if not check_style_is_correct(src_file_list): 165 print_err("Code style still incorrect after second run of Uncrustify.") 166 return 1 167 else: 168 return 0 169 170def main() -> int: 171 """ 172 Main with command line arguments. 173 """ 174 uncrustify_version = get_uncrustify_version().strip() 175 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version: 176 print("Warning: Using unsupported Uncrustify version '" + 177 uncrustify_version + "'") 178 print("Note: The only supported version is " + 179 UNCRUSTIFY_SUPPORTED_VERSION) 180 181 parser = argparse.ArgumentParser() 182 parser.add_argument('-f', '--fix', action='store_true', 183 help=('modify source files to fix the code style ' 184 '(default: print diff, do not modify files)')) 185 # --subset is almost useless: it only matters if there are no files 186 # ('code_style.py' without arguments checks all files known to Git, 187 # 'code_style.py --subset' does nothing). In particular, 188 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain") 189 # way to restyle a possibly empty set of files. 190 parser.add_argument('--subset', action='store_true', 191 help='only check the specified files (default with non-option arguments)') 192 parser.add_argument('operands', nargs='*', metavar='FILE', 193 help='files to check (files MUST be known to git, if none: check all)') 194 195 args = parser.parse_args() 196 197 covered = frozenset(get_src_files()) 198 # We only check files that are known to git 199 if args.subset or args.operands: 200 src_files = [f for f in args.operands if f in covered] 201 skip_src_files = [f for f in args.operands if f not in covered] 202 if skip_src_files: 203 print_skip(skip_src_files) 204 else: 205 src_files = list(covered) 206 207 if args.fix: 208 # Fix mode 209 return fix_style(src_files) 210 else: 211 # Check mode 212 if check_style_is_correct(src_files): 213 print("Checked {} files, style ok.".format(len(src_files))) 214 return 0 215 else: 216 return 1 217 218if __name__ == '__main__': 219 sys.exit(main()) 220