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 54# Check for comment string indicating an auto-generated file 55AUTOGEN_RE = re.compile(r"Warning[ :-]+This file is (now )?auto[ -]?generated", 56 re.ASCII | re.IGNORECASE) 57def is_file_autogenerated(filename): 58 content = open(filename, encoding="utf-8").read() 59 return AUTOGEN_RE.search(content) is not None 60 61def get_src_files(since: Optional[str]) -> List[str]: 62 """ 63 Use git to get a list of the source files. 64 65 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 69 Only C files are included, and certain files (generated, or 3rdparty) 70 are excluded. 71 """ 72 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) 77 src_files = output.split() 78 if since: 79 # get all files changed in commits since the starting point 80 cmd = ["git", "log", since + "..HEAD", "--name-only", "--pretty=", "--"] + src_files 81 output = subprocess.check_output(cmd, universal_newlines=True) 82 committed_changed_files = output.split() 83 # and also get all files with uncommitted changes 84 cmd = ["git", "diff", "--name-only", "--"] + src_files 85 output = subprocess.check_output(cmd, universal_newlines=True) 86 uncommitted_changed_files = output.split() 87 src_files = list(set(committed_changed_files + uncommitted_changed_files)) 88 89 generated_files = list_generated_files() 90 # Don't correct style for third-party files (and, for simplicity, 91 # companion files in the same subtree), or for automatically 92 # generated files (we're correcting the templates instead). 93 src_files = [filename for filename in src_files 94 if not (filename.startswith("3rdparty/") or 95 filename in generated_files or 96 is_file_autogenerated(filename))] 97 return src_files 98 99def get_uncrustify_version() -> str: 100 """ 101 Get the version string from Uncrustify 102 """ 103 result = subprocess.run([UNCRUSTIFY_EXE, "--version"], 104 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 105 check=False) 106 if result.returncode != 0: 107 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8")) 108 return "" 109 else: 110 return str(result.stdout, "utf-8") 111 112def check_style_is_correct(src_file_list: List[str]) -> bool: 113 """ 114 Check the code style and output a diff for each file whose style is 115 incorrect. 116 """ 117 style_correct = True 118 for src_file in src_file_list: 119 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file] 120 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE, 121 stderr=subprocess.PIPE, check=False) 122 if result.returncode != 0: 123 print_err("Uncrustify returned " + str(result.returncode) + 124 " correcting file " + src_file) 125 return False 126 127 # Uncrustify makes changes to the code and places the result in a new 128 # file with the extension ".uncrustify". To get the changes (if any) 129 # simply diff the 2 files. 130 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"] 131 cp = subprocess.run(diff_cmd, check=False) 132 133 if cp.returncode == 1: 134 print(src_file + " changed - code style is incorrect.") 135 style_correct = False 136 elif cp.returncode != 0: 137 raise subprocess.CalledProcessError(cp.returncode, cp.args, 138 cp.stdout, cp.stderr) 139 140 # Tidy up artifact 141 os.remove(src_file + ".uncrustify") 142 143 return style_correct 144 145def fix_style_single_pass(src_file_list: List[str]) -> bool: 146 """ 147 Run Uncrustify once over the source files. 148 """ 149 code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"] 150 for src_file in src_file_list: 151 uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file] 152 result = subprocess.run(uncrustify_cmd, check=False) 153 if result.returncode != 0: 154 print_err("Uncrustify with file returned: " + 155 str(result.returncode) + " correcting file " + 156 src_file) 157 return False 158 return True 159 160def fix_style(src_file_list: List[str]) -> int: 161 """ 162 Fix the code style. This takes 2 passes of Uncrustify. 163 """ 164 if not fix_style_single_pass(src_file_list): 165 return 1 166 if not fix_style_single_pass(src_file_list): 167 return 1 168 169 # Guard against future changes that cause the codebase to require 170 # more passes. 171 if not check_style_is_correct(src_file_list): 172 print_err("Code style still incorrect after second run of Uncrustify.") 173 return 1 174 else: 175 return 0 176 177def main() -> int: 178 """ 179 Main with command line arguments. 180 """ 181 uncrustify_version = get_uncrustify_version().strip() 182 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version: 183 print("Warning: Using unsupported Uncrustify version '" + 184 uncrustify_version + "'") 185 print("Note: The only supported version is " + 186 UNCRUSTIFY_SUPPORTED_VERSION) 187 188 parser = argparse.ArgumentParser() 189 parser.add_argument('-f', '--fix', action='store_true', 190 help=('modify source files to fix the code style ' 191 '(default: print diff, do not modify files)')) 192 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?', 193 help=('only check files modified since the specified commit' 194 ' (e.g. --since=HEAD~3 or --since=development). If no' 195 ' commit is specified, default to development.')) 196 # --subset is almost useless: it only matters if there are no files 197 # ('code_style.py' without arguments checks all files known to Git, 198 # 'code_style.py --subset' does nothing). In particular, 199 # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain") 200 # way to restyle a possibly empty set of files. 201 parser.add_argument('--subset', action='store_true', 202 help='only check the specified files (default with non-option arguments)') 203 parser.add_argument('operands', nargs='*', metavar='FILE', 204 help='files to check (files MUST be known to git, if none: check all)') 205 206 args = parser.parse_args() 207 208 covered = frozenset(get_src_files(args.since)) 209 # We only check files that are known to git 210 if args.subset or args.operands: 211 src_files = [f for f in args.operands if f in covered] 212 skip_src_files = [f for f in args.operands if f not in covered] 213 if skip_src_files: 214 print_skip(skip_src_files) 215 else: 216 src_files = list(covered) 217 218 if args.fix: 219 # Fix mode 220 return fix_style(src_files) 221 else: 222 # Check mode 223 if check_style_is_correct(src_files): 224 print("Checked {} files, style ok.".format(len(src_files))) 225 return 0 226 else: 227 return 1 228 229if __name__ == '__main__': 230 sys.exit(main()) 231