coding_guidelines_check.py 9.2 KB

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