coding_guidelines_check.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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. import unittest
  15. CLANG_FORMAT_CMD = "clang-format-12"
  16. GIT_CLANG_FORMAT_CMD = "git-clang-format-12"
  17. # glob style patterns
  18. EXCLUDE_PATHS = [
  19. "**/.git/*",
  20. "**/.github/*",
  21. "**/.vscode/*",
  22. "**/assembly-script/*",
  23. "**/build/*",
  24. "**/build-scripts/*",
  25. "**/ci/*",
  26. "**/core/deps/*",
  27. "**/doc/*",
  28. "**/samples/wasm-c-api/src/*.*",
  29. "**/samples/workload/*",
  30. "**/test-tools/wasi-sdk/*",
  31. "**/test-tools/IoT-APP-Store-Demo/*",
  32. "**/tests/wamr-test-suites/workspace/*",
  33. "**/wamr-sdk/*",
  34. ]
  35. C_SUFFIXES = [".c", ".cpp", ".h"]
  36. INVALID_DIR_NAME_SEGMENT = r"([a-zA-Z0-9]+\_[a-zA-Z0-9]+)"
  37. INVALID_FILE_NAME_SEGMENT = r"([a-zA-Z0-9]+\-[a-zA-Z0-9]+)"
  38. def locate_command(command: str) -> bool:
  39. if not shutil.which(command):
  40. print(f"Command '{command}'' not found")
  41. return False
  42. return True
  43. def is_excluded(path: str) -> bool:
  44. path = pathlib.Path(path).resolve()
  45. for exclude_path in EXCLUDE_PATHS:
  46. if path.match(exclude_path):
  47. return True
  48. return False
  49. def pre_flight_check(root: pathlib) -> bool:
  50. def check_aspell(root):
  51. return True
  52. def check_clang_foramt(root: pathlib) -> bool:
  53. if not locate_command(CLANG_FORMAT_CMD):
  54. return False
  55. # Quick syntax check for .clang-format
  56. try:
  57. subprocess.check_output(
  58. shlex.split(f"{CLANG_FORMAT_CMD} --dump-config"), cwd=root
  59. )
  60. except subprocess.CalledProcessError:
  61. print(f"Might have a typo in .clang-format")
  62. return False
  63. return True
  64. def check_git_clang_format() -> bool:
  65. return locate_command(GIT_CLANG_FORMAT_CMD)
  66. return check_aspell(root) and check_clang_foramt(root) and check_git_clang_format()
  67. def run_clang_format(file_path: pathlib, root: pathlib) -> bool:
  68. try:
  69. subprocess.check_call(
  70. shlex.split(
  71. f"{CLANG_FORMAT_CMD} --style=file --Werror --dry-run {file_path}"
  72. ),
  73. cwd=root,
  74. )
  75. return True
  76. except subprocess.CalledProcessError:
  77. print(f"{file_path} failed the check of {CLANG_FORMAT_CMD}")
  78. return False
  79. def run_clang_format_diff(root: pathlib, commits: str) -> bool:
  80. """
  81. Use `clang-format-12` or `git-clang-format-12` to check code format of
  82. the PR, with a commit range specified. It is required to format the
  83. code before committing the PR, or it might fail to pass the CI check:
  84. 1. Install clang-format-12.0.0
  85. Normally we can install it by `sudo apt-get install clang-format-12`,
  86. or download the `clang+llvm-12.0.0-xxx-tar.xz` package from
  87. https://github.com/llvm/llvm-project/releases/tag/llvmorg-12.0.0
  88. and install it
  89. 2. Format the C/C++ source file
  90. ``` shell
  91. cd path/to/wamr/root
  92. clang-format-12 --style file -i path/to/file
  93. ```
  94. The code wrapped by `/* clang-format off */` and `/* clang-format on */`
  95. will not be formatted, you shall use them when the formatted code is not
  96. readable or friendly:
  97. ``` cc
  98. /* clang-format off */
  99. code snippets
  100. /* clang-format on */
  101. ```
  102. """
  103. try:
  104. before, after = commits.split("..")
  105. after = after if after else "HEAD"
  106. COMMAND = (
  107. f"{GIT_CLANG_FORMAT_CMD} -v --binary "
  108. f"{shutil.which(CLANG_FORMAT_CMD)} --style file "
  109. f"--extensions c,cpp,h --diff {before} {after}"
  110. )
  111. p = subprocess.Popen(
  112. shlex.split(COMMAND),
  113. stdout=subprocess.PIPE,
  114. stderr=None,
  115. stdin=None,
  116. universal_newlines=True,
  117. )
  118. stdout, _ = p.communicate()
  119. if not stdout.startswith("diff --git"):
  120. return True
  121. diff_content = stdout.split("\n")
  122. found = False
  123. for summary in [x for x in diff_content if x.startswith("diff --git")]:
  124. # b/path/to/file -> path/to/file
  125. with_invalid_format = re.split("\s+", summary)[-1][2:]
  126. if not is_excluded(with_invalid_format):
  127. print(f"--- {with_invalid_format} failed on code style checking.")
  128. found = True
  129. else:
  130. return not found
  131. except subprocess.subprocess.CalledProcessError:
  132. return False
  133. def run_aspell(file_path: pathlib, root: pathlib) -> bool:
  134. return True
  135. def check_dir_name(path: pathlib, root: pathlib) -> bool:
  136. m = re.search(INVALID_DIR_NAME_SEGMENT, str(path.relative_to(root).parent))
  137. if m:
  138. print(f"--- found a character '_' in {m.groups()} in {path}")
  139. return not m
  140. def check_file_name(path: pathlib) -> bool:
  141. m = re.search(INVALID_FILE_NAME_SEGMENT, path.stem)
  142. if m:
  143. print(f"--- found a character '-' in {m.groups()} in {path}")
  144. return not m
  145. def parse_commits_range(root: pathlib, commits: str) -> list:
  146. GIT_LOG_CMD = f"git log --pretty='%H' {commits}"
  147. try:
  148. ret = subprocess.check_output(
  149. shlex.split(GIT_LOG_CMD), cwd=root, universal_newlines=True
  150. )
  151. return [x for x in ret.split("\n") if x]
  152. except subprocess.CalledProcessError:
  153. print(f"can not parse any commit from the range {commits}")
  154. return []
  155. def analysis_new_item_name(root: pathlib, commit: str) -> bool:
  156. """
  157. For any file name in the repo, it is required to use '_' to replace '-'.
  158. For any directory name in the repo, it is required to use '-' to replace '_'.
  159. """
  160. GIT_SHOW_CMD = f"git show --oneline --name-status --diff-filter A {commit}"
  161. try:
  162. invalid_items = True
  163. output = subprocess.check_output(
  164. shlex.split(GIT_SHOW_CMD), cwd=root, universal_newlines=True
  165. )
  166. if not output:
  167. return True
  168. NEW_FILE_PATTERN = "^A\s+(\S+)"
  169. for line_no, line in enumerate(output.split("\n")):
  170. # bypass the first line, usually it is the commit description
  171. if line_no == 0:
  172. continue
  173. if not line:
  174. continue
  175. match = re.match(NEW_FILE_PATTERN, line)
  176. if not match:
  177. continue
  178. new_item = match.group(1)
  179. new_item = pathlib.Path(new_item).resolve()
  180. if new_item.is_file():
  181. if not check_file_name(new_item):
  182. invalid_items = False
  183. continue
  184. new_item = new_item.parent
  185. if not check_dir_name(new_item, root):
  186. invalid_items = False
  187. continue
  188. else:
  189. return invalid_items
  190. except subprocess.CalledProcessError:
  191. return False
  192. def process_entire_pr(root: pathlib, commits: str) -> bool:
  193. if not commits:
  194. print("Please provide a commits range")
  195. return False
  196. commit_list = parse_commits_range(root, commits)
  197. if not commit_list:
  198. print(f"Quit since there is no commit to check with")
  199. return True
  200. print(f"there are {len(commit_list)} commits in the PR")
  201. found = False
  202. if not analysis_new_item_name(root, commits):
  203. print(f"{analysis_new_item_name.__doc__}")
  204. found = True
  205. if not run_clang_format_diff(root, commits):
  206. print(f"{run_clang_format_diff.__doc__}")
  207. found = True
  208. return not found
  209. def main() -> int:
  210. parser = argparse.ArgumentParser(
  211. description="Check if change meets all coding guideline requirements"
  212. )
  213. parser.add_argument(
  214. "-c", "--commits", default=None, help="Commit range in the form: a..b"
  215. )
  216. options = parser.parse_args()
  217. wamr_root = pathlib.Path(__file__).parent.joinpath("..").resolve()
  218. if not pre_flight_check(wamr_root):
  219. return False
  220. return process_entire_pr(wamr_root, options.commits)
  221. # run with python3 -m unitest ci/coding_guidelines_check.py
  222. class TestCheck(unittest.TestCase):
  223. def test_check_dir_name_failed(self):
  224. root = pathlib.Path("/root/Workspace/")
  225. new_file_path = root.joinpath("core/shared/platform/esp_idf/espid_memmap.c")
  226. self.assertFalse(check_dir_name(new_file_path, root))
  227. def test_check_dir_name_pass(self):
  228. root = pathlib.Path("/root/Workspace/")
  229. new_file_path = root.joinpath("core/shared/platform/esp-idf/espid_memmap.c")
  230. self.assertTrue(check_dir_name(new_file_path, root))
  231. def test_check_file_name_failed(self):
  232. new_file_path = pathlib.Path(
  233. "/root/Workspace/core/shared/platform/esp-idf/espid-memmap.c"
  234. )
  235. self.assertFalse(check_file_name(new_file_path))
  236. def test_check_file_name_pass(self):
  237. new_file_path = pathlib.Path(
  238. "/root/Workspace/core/shared/platform/esp-idf/espid_memmap.c"
  239. )
  240. self.assertTrue(check_file_name(new_file_path))
  241. if __name__ == "__main__":
  242. sys.exit(0 if main() else 1)