Skip to content

Commit 225f5d0

Browse files
authored
Enable to trigger code guideline checks when PR is created (#790)
Enable to trigger code guideline checks for C/C++ source codes when PR is created
1 parent 68d72c3 commit 225f5d0

3 files changed

Lines changed: 313 additions & 263 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright (C) 2019 Intel Corporation. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
3+
name: Coding Guidelines
4+
5+
on:
6+
# will be triggered on PR events
7+
pull_request:
8+
# allow to be triggered manually
9+
workflow_dispatch:
10+
11+
# Cancel any in-flight jobs for the same PR/branch so there's only one active
12+
# at a time
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.ref }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
# Cancel any in-flight jobs for the same PR/branch so there's only one active
19+
# at a time
20+
cancel_previous:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- name: Cancel Workflow Action
24+
uses: styfle/cancel-workflow-action@0.6.0
25+
with:
26+
access_token: ${{ github.token }}
27+
28+
complinace_job:
29+
needs: cancel_previous
30+
runs-on: ubuntu-latest
31+
steps:
32+
- name: checkout
33+
uses: actions/checkout@v2
34+
with:
35+
fetch-depth: 0
36+
37+
# github.event.pull_request.base.label = ${{github.repository}}/${{github.base_ref}}
38+
- name: Run Coding Guidelines Checks
39+
run: /usr/bin/env python3 ./ci/coding_guidelines_check.py --commits ${{ github.event.pull_request.base.sha }}..HEAD

ci/coding_guidelines_check.py

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

0 commit comments

Comments
 (0)