code_style.py 8.4 KB

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