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"]
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:]
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:
return
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"]))
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()
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
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:
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:
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 (guillaume1.gomez@gmail.com)",
"//"
]:
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())