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 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) 88 # Remove the vars from the environment 89 for var in git_env_vars.split(): 90 framework_env.pop(var, None) 91 92 output = subprocess.check_output(["git", "-C", "framework", "ls-files"] 93 + file_patterns, 94 universal_newlines=True, 95 env=framework_env) 96 framework_src_files = output.split() 97 98 if since: 99 # 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 103 output = subprocess.check_output(cmd, universal_newlines=True) 104 committed_changed_files = output.split() 105 # ... the framework submodule 106 cmd = ["git", "-C", "framework", "log", since + "..HEAD", 107 "--name-only", "--pretty=", "--"] + framework_src_files 108 output = subprocess.check_output(cmd, universal_newlines=True, 109 env=framework_env) 110 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 114 cmd = ["git", "diff", "--name-only", "--"] + src_files 115 output = subprocess.check_output(cmd, universal_newlines=True) 116 uncommitted_changed_files = output.split() 117 # ... the framework submodule 118 cmd = ["git", "-C", "framework", "diff", "--name-only", "--"] + \ 119 framework_src_files 120 output = subprocess.check_output(cmd, universal_newlines=True, 121 env=framework_env) 122 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] 127 128 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 134 filename in generated_files or 135 is_file_autogenerated(filename))] 136 return src_files 137 138def get_uncrustify_version() -> str: 139 """ 140 Get the version string from Uncrustify 141 """ 142 result = subprocess.run([UNCRUSTIFY_EXE, "--version"], 143 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 144 check=False) 145 if result.returncode != 0: 146 print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8")) 147 return "" 148 else: 149 return str(result.stdout, "utf-8") 150 151def check_style_is_correct(src_file_list: List[str]) -> bool: 152 """ 153 Check the code style and output a diff for each file whose style is 154 incorrect. 155 """ 156 style_correct = True 157 for src_file in src_file_list: 158 uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file] 159 result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE, 160 stderr=subprocess.PIPE, check=False) 161 if result.returncode != 0: 162 print_err("Uncrustify returned " + str(result.returncode) + 163 " correcting file " + src_file) 164 return False 165 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. 169 diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"] 170 cp = subprocess.run(diff_cmd, check=False) 171 172 if cp.returncode == 1: 173 print(src_file + " changed - code style is incorrect.") 174 style_correct = False 175 elif cp.returncode != 0: 176 raise subprocess.CalledProcessError(cp.returncode, cp.args, 177 cp.stdout, cp.stderr) 178 179 # Tidy up artifact 180 os.remove(src_file + ".uncrustify") 181 182 return style_correct 183 184def fix_style_single_pass(src_file_list: List[str]) -> bool: 185 """ 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] 191 result = subprocess.run(uncrustify_cmd, check=False) 192 if result.returncode != 0: 193 print_err("Uncrustify with file returned: " + 194 str(result.returncode) + " correcting file " + 195 src_file) 196 return False 197 return True 198 199def fix_style(src_file_list: List[str]) -> int: 200 """ 201 Fix the code style. This takes 2 passes of Uncrustify. 202 """ 203 if not fix_style_single_pass(src_file_list): 204 return 1 205 if not fix_style_single_pass(src_file_list): 206 return 1 207 208 # Guard against future changes that cause the codebase to require 209 # more passes. 210 if not check_style_is_correct(src_file_list): 211 print_err("Code style still incorrect after second run of Uncrustify.") 212 return 1 213 else: 214 return 0 215 216def main() -> int: 217 """ 218 Main with command line arguments. 219 """ 220 uncrustify_version = get_uncrustify_version().strip() 221 if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version: 222 print("Warning: Using unsupported Uncrustify version '" + 223 uncrustify_version + "'") 224 print("Note: The only supported version is " + 225 UNCRUSTIFY_SUPPORTED_VERSION) 226 227 parser = argparse.ArgumentParser() 228 parser.add_argument('-f', '--fix', action='store_true', 229 help=('modify source files to fix the code style ' 230 '(default: print diff, do not modify files)')) 231 parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?', 232 help=('only check files modified since the specified commit' 233 ' (e.g. --since=HEAD~3 or --since=development). If no' 234 ' commit is specified, default to development.')) 235 # --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. 240 parser.add_argument('--subset', action='store_true', 241 help='only check the specified files (default with non-option arguments)') 242 parser.add_argument('operands', nargs='*', metavar='FILE', 243 help='files to check (files MUST be known to git, if none: check all)') 244 245 args = parser.parse_args() 246 247 covered = frozenset(get_src_files(args.since)) 248 # We only check files that are known to git 249 if args.subset or args.operands: 250 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] 252 if skip_src_files: 253 print_skip(skip_src_files) 254 else: 255 src_files = list(covered) 256 257 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): 263 print("Checked {} files, style ok.".format(len(src_files))) 264 return 0 265 else: 266 return 1 267 268if __name__ == '__main__': 269 sys.exit(main()) 270