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