coding_guidelines_check.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright (C) 2019 Intel Corporation. All rights reserved.
  4. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  5. #
  6. import argparse
  7. import re
  8. import pathlib
  9. import re
  10. import shlex
  11. import shutil
  12. import subprocess
  13. import sys
  14. CLANG_FORMAT_CMD = "clang-format-12"
  15. GIT_CLANG_FORMAT_CMD = "git-clang-format-12"
  16. # glob style patterns
  17. EXCLUDE_PATHS = [
  18. "**/.git/*",
  19. "**/.github/*",
  20. "**/.vscode/*",
  21. "**/assembly-script/*",
  22. "**/build/*",
  23. "**/build-scripts/*",
  24. "**/ci/*",
  25. "**/core/deps/*",
  26. "**/doc/*",
  27. "**/samples/wasm-c-api/src/*.*",
  28. "**/samples/workload/*",
  29. "**/test-tools/wasi-sdk/*",
  30. "**/tests/wamr-test-suites/workspace/*",
  31. "**/wamr-sdk/*",
  32. ]
  33. C_SUFFIXES = [".c", ".cpp", ".h"]
  34. INVALID_DIR_NAME_SEGMENT = r"([a-zA-Z0-9]+\_[a-zA-Z0-9]+)"
  35. INVALID_FILE_NAME_SEGMENT = r"([a-zA-Z0-9]+\-[a-zA-Z0-9]+)"
  36. def locate_command(command: str) -> bool:
  37. if not shutil.which(command):
  38. print(f"Command '{command}'' not found")
  39. return False
  40. return True
  41. def is_excluded(path: str) -> bool:
  42. path = pathlib.Path(path).resolve()
  43. for exclude_path in EXCLUDE_PATHS:
  44. if path.match(exclude_path):
  45. return True
  46. return False
  47. def pre_flight_check(root: pathlib) -> bool:
  48. def check_aspell(root):
  49. return True
  50. def check_clang_foramt(root: pathlib) -> bool:
  51. if not locate_command(CLANG_FORMAT_CMD):
  52. return False
  53. # Quick syntax check for .clang-format
  54. try:
  55. subprocess.check_output(
  56. shlex.split(f"{CLANG_FORMAT_CMD} --dump-config"), cwd=root
  57. )
  58. except subprocess.CalledProcessError:
  59. print(f"Might have a typo in .clang-format")
  60. return False
  61. return True
  62. def check_git_clang_format() -> bool:
  63. return locate_command(GIT_CLANG_FORMAT_CMD)
  64. return check_aspell(root) and check_clang_foramt(root) and check_git_clang_format()
  65. def run_clang_format(file_path: pathlib, root: pathlib) -> bool:
  66. try:
  67. subprocess.check_call(
  68. shlex.split(
  69. f"{CLANG_FORMAT_CMD} --style=file --Werror --dry-run {file_path}"
  70. ),
  71. cwd=root,
  72. )
  73. return True
  74. except subprocess.CalledProcessError:
  75. print(f"{file_path} failed the check of {CLANG_FORMAT_CMD}")
  76. return False
  77. def run_clang_format_diff(root: pathlib, commits: str) -> bool:
  78. """
  79. Use `clang-format-12` and `git-clang-format-12` to check code
  80. format of the PR, which specificed a commit range. It is required to
  81. format code before `git commit` or when failed the PR check:
  82. ``` shell
  83. cd path/to/wamr/root
  84. clang-format-12 --style file -i path/to/file
  85. ```
  86. The code wrapped by `/* clang-format off */` and `/* clang-format on */`
  87. will not be formatted, you shall use them when the formatted code is not
  88. readable or friendly:
  89. ``` cc
  90. /* clang-format off */
  91. code snippets
  92. /* clang-format on */
  93. ```
  94. """
  95. try:
  96. before, after = commits.split("..")
  97. after = after if after else "HEAD"
  98. COMMAND = (
  99. f"{GIT_CLANG_FORMAT_CMD} -v --binary "
  100. f"{shutil.which(CLANG_FORMAT_CMD)} --style file "
  101. f"--extensions c,cpp,h --diff {before} {after}"
  102. )
  103. p = subprocess.Popen(
  104. shlex.split(COMMAND),
  105. stdout=subprocess.PIPE,
  106. stderr=None,
  107. stdin=None,
  108. universal_newlines=True,
  109. )
  110. stdout, _ = p.communicate()
  111. if not stdout.startswith("diff --git"):
  112. return True
  113. diff_content = stdout.split("\n")
  114. found = False
  115. for summary in [x for x in diff_content if x.startswith("diff --git")]:
  116. # b/path/to/file -> path/to/file
  117. with_invalid_format = re.split("\s+", summary)[-1][2:]
  118. if not is_excluded(with_invalid_format):
  119. print(f"--- {with_invalid_format} failed on code style checking.")
  120. found = True
  121. else:
  122. return not found
  123. except subprocess.subprocess.CalledProcessError:
  124. return False
  125. def run_aspell(file_path: pathlib, root: pathlib) -> bool:
  126. return True
  127. def check_dir_name(path: pathlib, root: pathlib) -> bool:
  128. m = re.search(INVALID_DIR_NAME_SEGMENT, str(path.relative_to(root)))
  129. if m:
  130. print(f"--- found a character '_' in {m.groups()} in {path}")
  131. return not m
  132. def check_file_name(path: pathlib) -> bool:
  133. m = re.search(INVALID_FILE_NAME_SEGMENT, path.stem)
  134. if m:
  135. print(f"--- found a character '-' in {m.groups()} in {path}")
  136. return not m
  137. def parse_commits_range(root: pathlib, commits: str) -> list:
  138. GIT_LOG_CMD = f"git log --pretty='%H' {commits}"
  139. try:
  140. ret = subprocess.check_output(
  141. shlex.split(GIT_LOG_CMD), cwd=root, universal_newlines=True
  142. )
  143. return [x for x in ret.split("\n") if x]
  144. except subprocess.CalledProcessError:
  145. print(f"can not parse any commit from the range {commits}")
  146. return []
  147. def analysis_new_item_name(root: pathlib, commit: str) -> bool:
  148. """
  149. For any file name in the repo, it is required to use '_' to replace '-'.
  150. For any directory name in the repo, it is required to use '-' to replace '_'.
  151. """
  152. GIT_SHOW_CMD = f"git show --oneline --name-status --diff-filter A {commit}"
  153. try:
  154. invalid_items = True
  155. output = subprocess.check_output(
  156. shlex.split(GIT_SHOW_CMD), cwd=root, universal_newlines=True
  157. )
  158. if not output:
  159. return True
  160. NEW_FILE_PATTERN = "^A\s+(\S+)"
  161. for line_no, line in enumerate(output.split("\n")):
  162. # bypass the first line, usually it is the commit description
  163. if line_no == 0:
  164. continue
  165. if not line:
  166. continue
  167. match = re.match(NEW_FILE_PATTERN, line)
  168. if not match:
  169. continue
  170. new_item = match.group(1)
  171. new_item = pathlib.Path(new_item).resolve()
  172. if new_item.is_file():
  173. if not check_file_name(new_item):
  174. invalid_items = False
  175. continue
  176. new_item = new_item.parent
  177. if not check_dir_name(new_item, root):
  178. invalid_items = False
  179. continue
  180. else:
  181. return invalid_items
  182. except subprocess.CalledProcessError:
  183. return False
  184. def process_entire_pr(root: pathlib, commits: str) -> bool:
  185. if not commits:
  186. print("Please provide a commits range")
  187. return False
  188. commit_list = parse_commits_range(root, commits)
  189. if not commit_list:
  190. print(f"Quit since there is no commit to check with")
  191. return True
  192. print(f"there are {len(commit_list)} commits in the PR")
  193. found = False
  194. if not analysis_new_item_name(root, commits):
  195. print(f"{analysis_new_item_name.__doc__}")
  196. found = True
  197. if not run_clang_format_diff(root, commits):
  198. print(f"{run_clang_format_diff.__doc__}")
  199. found = True
  200. return not found
  201. def main() -> int:
  202. parser = argparse.ArgumentParser(
  203. description="Check if change meets all coding guideline requirements"
  204. )
  205. parser.add_argument(
  206. "-c", "--commits", default=None, help="Commit range in the form: a..b"
  207. )
  208. options = parser.parse_args()
  209. wamr_root = pathlib.Path(__file__).parent.joinpath("..").resolve()
  210. if not pre_flight_check(wamr_root):
  211. return False
  212. return process_entire_pr(wamr_root, options.commits)
  213. if __name__ == "__main__":
  214. sys.exit(0 if main() else 1)