quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

code_style.py (13013B)


      1 #!/usr/bin/env python3
      2 """Check or fix the code style by running Uncrustify.
      3 
      4 This 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
      8 import argparse
      9 import os
     10 import re
     11 import subprocess
     12 import sys
     13 from typing import FrozenSet, List, Optional
     14 from mbedtls_framework import build_tree
     15 
     16 UNCRUSTIFY_SUPPORTED_VERSION = "0.75.1"
     17 CONFIG_FILE = ".uncrustify.cfg"
     18 UNCRUSTIFY_EXE = "uncrustify"
     19 UNCRUSTIFY_ARGS = ["-c", CONFIG_FILE]
     20 CHECK_GENERATED_FILES = "tests/scripts/check-generated-files.sh"
     21 
     22 def print_err(*args):
     23     print("Error: ", *args, file=sys.stderr)
     24 
     25 # Print the file names that will be skipped and the help message
     26 def print_skip(files_to_skip):
     27     print()
     28     print(*files_to_skip, sep=", SKIP\n", end=", SKIP\n")
     29     print("Warning: The listed files will be skipped because\n"
     30           "they are not known to git.")
     31     print()
     32 
     33 # Match FILENAME(s) in "check SCRIPT (FILENAME...)"
     34 CHECK_CALL_RE = re.compile(r"\n\s*check\s+[^\s#$&*?;|]+([^\n#$&*?;|]+)",
     35                            re.ASCII)
     36 def list_generated_files() -> FrozenSet[str]:
     37     """Return the names of generated files.
     38 
     39     We don't reformat generated files, since the result might be different
     40     from the output of the generator. Ideally the result of the generator
     41     would conform to the code style, but this would be difficult, especially
     42     with respect to the placement of line breaks in long logical lines.
     43     """
     44     if build_tree.is_mbedtls_3_6():
     45         # Parse check-generated-files.sh to get an up-to-date list of
     46         # generated files. Read the file rather than calling it so that
     47         # this script only depends on Git, Python and uncrustify, and not other
     48         # tools such as sh or grep which might not be available on Windows.
     49         # This introduces a limitation: check-generated-files.sh must have
     50         # the expected format and must list the files explicitly, not through
     51         # wildcards or command substitution.
     52         content = open(CHECK_GENERATED_FILES, encoding="utf-8").read()
     53         checks = re.findall(CHECK_CALL_RE, content)
     54         return frozenset(word for s in checks for word in s.split())
     55     else:
     56         output = subprocess.check_output(["framework/scripts/make_generated_files.py",
     57                                           "--list"], universal_newlines=True)
     58         # psa_test_wrappers.[hc], generated by generate_psa_wrappers.py, are
     59         # currently committed and unknown to make_generated_files.py. Add them
     60         # here to the list of generated file as we do not want to check their
     61         # coding style.
     62         if build_tree.looks_like_tf_psa_crypto_root("."):
     63             output += "tests/include/test/psa_test_wrappers.h\n"
     64             output += "tests/src/psa_test_wrappers.c"
     65 
     66         return frozenset(line for line in output.splitlines())
     67 
     68 # Check for comment string indicating an auto-generated file
     69 AUTOGEN_RE = re.compile(r"Warning[ :-]+This file is (now )?auto[ -]?generated",
     70                         re.ASCII | re.IGNORECASE)
     71 def is_file_autogenerated(filename):
     72     content = open(filename, encoding="utf-8").read()
     73     return AUTOGEN_RE.search(content) is not None
     74 
     75 def get_src_files(since: Optional[str]) -> List[str]:
     76     """
     77     Use git to get a list of the source files.
     78 
     79     The optional argument since is a commit, indicating to only list files
     80     that have changed since that commit. Without this argument, list all
     81     files known to git.
     82 
     83     Only C files are included, and certain files (generated, or third party)
     84     are excluded.
     85     """
     86     file_patterns = ["*.[hc]",
     87                      "tests/suites/*.function",
     88                      "scripts/data_files/*.fmt"]
     89     output = subprocess.check_output(["git", "ls-files"] + file_patterns,
     90                                      universal_newlines=True)
     91     src_files = output.split()
     92 
     93     # When this script is called from a git hook, some environment variables
     94     # are set by default which force all git commands to use the main repository
     95     # (i.e. prevent us from performing commands on the framework repo).
     96     # Create an environment without these variables for running commands on the
     97     # framework repo.
     98     framework_env = os.environ.copy()
     99     # Get a list of environment vars that git sets
    100     git_env_vars = subprocess.check_output(["git", "rev-parse", "--local-env-vars"],
    101                                            universal_newlines=True)
    102     # Remove the vars from the environment
    103     for var in git_env_vars.split():
    104         framework_env.pop(var, None)
    105 
    106     output = subprocess.check_output(["git", "-C", "framework", "ls-files"]
    107                                      + file_patterns,
    108                                      universal_newlines=True,
    109                                      env=framework_env)
    110     framework_src_files = output.split()
    111 
    112     if since:
    113         # get all files changed in commits since the starting point in ...
    114         # ... the main repository
    115         cmd = ["git", "log", since + "..HEAD", "--ignore-submodules",
    116                "--name-only", "--pretty=", "--"] + src_files
    117         output = subprocess.check_output(cmd, universal_newlines=True)
    118         committed_changed_files = output.split()
    119 
    120         # ... the framework submodule
    121         framework_since = get_submodule_hash(since, "framework")
    122         cmd = ["git", "-C", "framework", "log", framework_since + "..HEAD",
    123                "--name-only", "--pretty=", "--"] + framework_src_files
    124         output = subprocess.check_output(cmd, universal_newlines=True,
    125                                          env=framework_env)
    126         committed_changed_files += ["framework/" + s for s in output.split()]
    127 
    128         # and also get all files with uncommitted changes in ...
    129         # ... the main repository
    130         cmd = ["git", "diff", "--name-only", "--"] + src_files
    131         output = subprocess.check_output(cmd, universal_newlines=True)
    132         uncommitted_changed_files = output.split()
    133         # ... the framework submodule
    134         cmd = ["git", "-C", "framework", "diff", "--name-only", "--"] + \
    135               framework_src_files
    136         output = subprocess.check_output(cmd, universal_newlines=True,
    137                                          env=framework_env)
    138         uncommitted_changed_files += ["framework/" + s for s in output.split()]
    139 
    140         src_files = committed_changed_files + uncommitted_changed_files
    141     else:
    142         src_files += ["framework/" + s for s in framework_src_files]
    143 
    144     generated_files = list_generated_files()
    145     # Don't correct style for third-party files (and, for simplicity,
    146     # companion files in the same subtree), or for automatically
    147     # generated files (we're correcting the templates instead).
    148     if build_tree.is_mbedtls_3_6():
    149         src_files = [filename for filename in src_files
    150                      if not (filename.startswith("3rdparty/") or
    151                              filename in generated_files or
    152                              is_file_autogenerated(filename))]
    153     else:
    154         src_files = [filename for filename in src_files
    155                      if not (filename.startswith("drivers/everest/") or
    156                              filename.startswith("drivers/p256-m/") or
    157                              filename in generated_files or
    158                              is_file_autogenerated(filename))]
    159     return src_files
    160 
    161 def get_submodule_hash(commit: str, submodule: str) -> str:
    162     """Get the commit hash of a submodule at a given commit in the Git repository."""
    163     cmd = ["git", "ls-tree", commit, submodule]
    164     output = subprocess.check_output(cmd, universal_newlines=True)
    165     return output.split()[2]
    166 
    167 def get_uncrustify_version() -> str:
    168     """
    169     Get the version string from Uncrustify
    170     """
    171     result = subprocess.run([UNCRUSTIFY_EXE, "--version"],
    172                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
    173                             check=False)
    174     if result.returncode != 0:
    175         print_err("Could not get Uncrustify version:", str(result.stderr, "utf-8"))
    176         return ""
    177     else:
    178         return str(result.stdout, "utf-8")
    179 
    180 def check_style_is_correct(src_file_list: List[str]) -> bool:
    181     """
    182     Check the code style and output a diff for each file whose style is
    183     incorrect.
    184     """
    185     style_correct = True
    186     for src_file in src_file_list:
    187         uncrustify_cmd = [UNCRUSTIFY_EXE] + UNCRUSTIFY_ARGS + [src_file]
    188         result = subprocess.run(uncrustify_cmd, stdout=subprocess.PIPE,
    189                                 stderr=subprocess.PIPE, check=False)
    190         if result.returncode != 0:
    191             print_err("Uncrustify returned " + str(result.returncode) +
    192                       " correcting file " + src_file)
    193             return False
    194 
    195         # Uncrustify makes changes to the code and places the result in a new
    196         # file with the extension ".uncrustify". To get the changes (if any)
    197         # simply diff the 2 files.
    198         diff_cmd = ["diff", "-u", src_file, src_file + ".uncrustify"]
    199         cp = subprocess.run(diff_cmd, check=False)
    200 
    201         if cp.returncode == 1:
    202             print(src_file + " changed - code style is incorrect.")
    203             style_correct = False
    204         elif cp.returncode != 0:
    205             raise subprocess.CalledProcessError(cp.returncode, cp.args,
    206                                                 cp.stdout, cp.stderr)
    207 
    208         # Tidy up artifact
    209         os.remove(src_file + ".uncrustify")
    210 
    211     return style_correct
    212 
    213 def fix_style_single_pass(src_file_list: List[str]) -> bool:
    214     """
    215     Run Uncrustify once over the source files.
    216     """
    217     code_change_args = UNCRUSTIFY_ARGS + ["--no-backup"]
    218     for src_file in src_file_list:
    219         uncrustify_cmd = [UNCRUSTIFY_EXE] + code_change_args + [src_file]
    220         result = subprocess.run(uncrustify_cmd, check=False)
    221         if result.returncode != 0:
    222             print_err("Uncrustify with file returned: " +
    223                       str(result.returncode) + " correcting file " +
    224                       src_file)
    225             return False
    226     return True
    227 
    228 def fix_style(src_file_list: List[str]) -> int:
    229     """
    230     Fix the code style. This takes 2 passes of Uncrustify.
    231     """
    232     if not fix_style_single_pass(src_file_list):
    233         return 1
    234     if not fix_style_single_pass(src_file_list):
    235         return 1
    236 
    237     # Guard against future changes that cause the codebase to require
    238     # more passes.
    239     if not check_style_is_correct(src_file_list):
    240         print_err("Code style still incorrect after second run of Uncrustify.")
    241         return 1
    242     else:
    243         return 0
    244 
    245 def main() -> int:
    246     """
    247     Main with command line arguments.
    248     """
    249     uncrustify_version = get_uncrustify_version().strip()
    250     if UNCRUSTIFY_SUPPORTED_VERSION not in uncrustify_version:
    251         print("Warning: Using unsupported Uncrustify version '" +
    252               uncrustify_version + "'")
    253         print("Note: The only supported version is " +
    254               UNCRUSTIFY_SUPPORTED_VERSION)
    255 
    256     parser = argparse.ArgumentParser()
    257     parser.add_argument('-f', '--fix', action='store_true',
    258                         help=('modify source files to fix the code style '
    259                               '(default: print diff, do not modify files)'))
    260     parser.add_argument('-s', '--since', metavar='COMMIT', const='development', nargs='?',
    261                         help=('only check files modified since the specified commit'
    262                               ' (e.g. --since=HEAD~3 or --since=development). If no'
    263                               ' commit is specified, default to development.'))
    264     # --subset is almost useless: it only matters if there are no files
    265     # ('code_style.py' without arguments checks all files known to Git,
    266     # 'code_style.py --subset' does nothing). In particular,
    267     # 'code_style.py --fix --subset ...' is intended as a stable ("porcelain")
    268     # way to restyle a possibly empty set of files.
    269     parser.add_argument('--subset', action='store_true',
    270                         help='only check the specified files (default with non-option arguments)')
    271     parser.add_argument('operands', nargs='*', metavar='FILE',
    272                         help='files to check (files MUST be known to git, if none: check all)')
    273 
    274     args = parser.parse_args()
    275 
    276     covered = frozenset(get_src_files(args.since))
    277     # We only check files that are known to git
    278     if args.subset or args.operands:
    279         src_files = [f for f in args.operands if f in covered]
    280         skip_src_files = [f for f in args.operands if f not in covered]
    281         if skip_src_files:
    282             print_skip(skip_src_files)
    283     else:
    284         src_files = list(covered)
    285 
    286     if args.fix:
    287         # Fix mode
    288         return fix_style(src_files)
    289     else:
    290         # Check mode
    291         if check_style_is_correct(src_files):
    292             print("Checked {} files, style ok.".format(len(src_files)))
    293             return 0
    294         else:
    295             return 1
    296 
    297 if __name__ == '__main__':
    298     sys.exit(main())