GSL 7.0.0

A rust binding for the GSL (the GNU scientific library)
Documentation
# 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"]


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 (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())