-
-
Notifications
You must be signed in to change notification settings - Fork 48
/
checker.py
257 lines (231 loc) · 9.28 KB
/
checker.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
# pylint: disable=C0103,C0114,C0115,C0116,C0301,R0913
from os import walk
from os.path import join
import sys
MACROS_TO_IGNORE = ["ffi_wrap", "wrap_callback", "ffi_wrapper"]
FUNC_NAME_TO_IGNORE = [
"new",
"new_with_init",
"from_slice",
"nm_simplex",
"nm_simplex2",
"nm_simplex2_rand",
]
def read_file(path):
with open(path, 'r') as fd:
return fd.read()
def read_dirs(dir_path, errors, totals, functions_to_call):
for root, _, files in walk(dir_path):
for file in files:
file_path = join(root, file)
content = read_file(file_path)
for func in functions_to_call:
func(file_path, content, errors, totals)
def add_error(file_path, line_nb, errors, err):
errors.append("[{}:{}] => {}".format(file_path, line_nb, err))
def to_rust_name(n):
if n.startswith("_"):
n = n[1:]
# Simple/stupid rule...
if n.startswith("is"):
n = "is_" + n[2:]
return {
"is_nonneg": "is_non_neg",
"swap_rowcol": "swap_row_col",
}.get(n, n)
def validate_name(rust_name, pending_func_name):
if pending_func_name == "drop":
return True
if rust_name not in ["alloc", "calloc"]:
for start in ["from_", "new"]:
if pending_func_name.startswith(start):
return True
if (pending_func_name in "copy" or pending_func_name.startswith("copy_")) and rust_name.endswith("memcpy"):
return True
return rust_name.startswith("gsl_") and pending_func_name in rust_name
def init_check_macro_data(data, totals, ignored):
data["pending_func_name"] = None
data["sys_names"] = []
data["pending_doc_aliases"] = []
data["indent"] = 0
data["func_line"] = 0
added = 0
if data.get("is_test", False):
totals["test_count"] += 1
ignored = True
data["is_test"] = False
if not ignored and data.get("is_in_macro", False):
totals["in_macro_count"] += 1
added += 1
data["is_in_macro"] = False
if not ignored and data.get("is_ffi_wrap", False):
totals["ffi_wrap_count"] += 1
added += 1
data["is_ffi_wrap"] = False
if not ignored and data.get("is_in_func", False):
totals["in_func_count"] += 1
added += 1
data["is_in_func"] = False
if not ignored and data.get("ignore_next", False):
totals["ignored"] += 1
if not ignored and added == 0:
totals["rust_func_count"] += 1
data["ignore_next"] = False
def check_macro_names(file_path, errors, data):
if len(data["sys_names"]) < 1:
# Nothing to check if there is no sys call.
return
# For now, we only check the last sys name.
sys_name = data["sys_names"][-1]
if len(data["pending_doc_aliases"]) == 0:
add_error(
file_path,
data["func_line"],
errors,
"Missing `#[doc(alias = {})]`".format(sys_name))
elif sys_name not in data["pending_doc_aliases"]:
if len(data["pending_doc_aliases"]) == 1:
add_error(
file_path,
data["func_line"],
errors,
"Mismatch between doc alias and sys call: `{}` != `{}`".format(
data["pending_doc_aliases"][0], sys_name))
else:
add_error(
file_path,
data["func_line"],
errors,
"Mismatch between doc alias and sys call: no doc alias named `{}` in {}".format(
sys_name, data["pending_doc_aliases"]))
if data["ignore_next"]:
return
rust_name = to_rust_name(sys_name.split(" ")[-1])
if rust_name != data["pending_func_name"]:
if (data["pending_func_name"] not in FUNC_NAME_TO_IGNORE
and not validate_name(rust_name, data["pending_func_name"])):
add_error(
file_path,
data["func_line"],
errors,
"Mismatch between function name and sys name: should be `{}` (but currently is `{}`)".format(
rust_name, data["pending_func_name"]))
def check_counts(errors, data):
if data["in_macro_count"] < 1:
errors.append("No checks run on macros...")
else:
print("Checked {} items in macros".format(data["in_macro_count"]))
if data["in_func_count"] < 1:
errors.append("No checks run on functions (outside of macros!)...")
else:
print("Checked {} items not in macros".format(data["in_func_count"]))
if data["ffi_wrap_count"] < 1:
errors.append("No checks run on `ffi_wrap` macro...")
else:
print("Checked {} `ffi_wrap` items".format(data["ffi_wrap_count"]))
print("Rust functions: {}".format(data["rust_func_count"]))
print("Rust test functions: {}".format(data["test_count"]))
print("Ignored items: {}".format(data["ignored"]))
# This test is used to ensure that the methods generated through macros are correct (doc alias and
# FFI call conform to what they should be).
# pylint: disable=R0912
def check_macros(file_path, content, errors, totals):
is_in_macro = False
is_in_comment = False
is_in_decl = False
data = {}
init_check_macro_data(data, totals, True)
for line_nb, line in enumerate(content.split('\n')):
if len(line) == 0:
continue
stripped_line = line.strip()
# comment part
if stripped_line.startswith("/*"):
is_in_comment = True
if is_in_comment:
if line.endswith("*/"):
is_in_comment = False
continue
if stripped_line.startswith("//"):
if stripped_line.split("//")[1].strip() == "checker:ignore":
data["ignore_next"] = True
continue
# macro parsing part
if not is_in_macro:
if stripped_line.startswith("macro_rules! "):
macro_name = stripped_line.split("macro_rules! ")[1].split("{")[0]
if not macro_name in MACROS_TO_IGNORE:
is_in_macro = True
continue
if not is_in_decl:
if (stripped_line.startswith("pub trait ") or stripped_line.startswith("trait ")
or stripped_line.startswith("pub struct ") or stripped_line.startswith("struct ")):
is_in_decl = True
continue
if stripped_line == "}":
if line == "}":
is_in_macro = False
is_in_decl = False
current_indent = len(line) - len(line.lstrip())
if current_indent == data["indent"]:
ignored = True
if data["pending_func_name"] is not None and not is_in_decl:
check_macro_names(file_path, errors, data)
ignored = False
init_check_macro_data(data, totals, ignored)
if is_in_decl:
continue
elif " ffi_wrap!(" in line:
sys_name = stripped_line.split("ffi_wrap!(")[1].split(")")[0].strip()
data["sys_names"].append(sys_name)
data["is_ffi_wrap"] = True
else:
if stripped_line.startswith("#[doc(alias"):
alias_name = stripped_line.split("(alias")[1].split("=")[1].split(")]")[0].strip()
data["pending_doc_aliases"].append(alias_name.replace("\"", ""))
if stripped_line == "#[test]":
data["is_test"] = True
elif data["pending_func_name"] is None and (stripped_line.startswith("pub fn ") or stripped_line.startswith("fn ")):
data["pending_func_name"] = line.split("(")[0].split("<")[0].split("fn ")[1].strip()
data["indent"] = len(line) - len(line.lstrip())
data["func_line"] = line_nb + 1
elif data["pending_func_name"] is not None:
if " sys::[<" in stripped_line:
# A sys call in a macro.
sys_name = stripped_line.split(" sys::[<")[1].split(">](")[0].strip()
data["sys_names"].append(sys_name)
data["is_in_macro"] = True
elif "sys::gsl_" in stripped_line:
# A sys call NOT in a macro.
part = "gsl_" + stripped_line.split("sys::gsl_")[1]
if "(" in part:
sys_name = part.split("(")[0].split("{")[0].strip()
data["sys_names"].append(sys_name)
data["is_in_func"] = True
def check_file_header(file_path, content, errors, _totals):
if content.split('\n')[:3] != [
"//",
"// A rust binding for the GSL library by Guillaume Gomez ([email protected])",
"//"
]:
add_error(file_path, 1, errors, "Invalid header (take a look at `lib.rs`)")
def main():
errors = []
totals = {
"in_macro_count": 0,
"ffi_wrap_count": 0,
"in_func_count": 0,
"rust_func_count": 0,
"test_count": 0,
"ignored": 0,
}
read_dirs("src", errors, totals, [check_file_header, check_macros])
check_counts(errors, totals)
if len(errors) > 0:
for err in errors:
print("=> {}".format(err))
print()
print("{} error{} occurred".format(len(errors), "s" if len(errors) > 1 else "" ))
return 0 if len(errors) == 0 else 1
if __name__ == "__main__":
sys.exit(main())