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