rulewright 0.1.1

Executable engineering standards for Rust workspaces
Documentation
#[cfg(test)]
use googletest::prelude::*;

use crate::{Example, FileCtx, Violation, infra::scanner, violation};

#[rustfmt::skip]
const EXAMPLES: &[Example] = &[
    Example {
        label: "no_mangle export in non-ffi crate",
        code: "#[no_mangle]\npub extern \"C\" fn service_tick() {}",
        pass: false,
    },
    Example {
        label: "unsafe no_mangle export in non-ffi crate",
        code: "#[unsafe(no_mangle)]\npub extern \"C\" fn service_tick() {}",
        pass: false,
    },
    Example {
        label: "foreign block in non-sys crate",
        code: "extern \"C\" {\n    fn native_call();\n}",
        pass: false,
    },
    Example {
        label: "plain rust fn",
        code: "pub fn tick() {}",
        pass: true,
    },
    Example {
        label: "no_mangle without extern C fn",
        code: "#[no_mangle]\npub static VERSION: u32 = 1;",
        pass: true,
    },
    Example {
        label: "extern C fn without no_mangle",
        code: "pub extern \"C\" fn callback() {}",
        pass: true,
    },
    Example {
        label: "wasm_bindgen crate uses its own convention",
        code: "use wasm_bindgen::prelude::JsValue;\n#[no_mangle]\npub extern \"C\" fn service_tick() {}",
        pass: true,
    },
];

crate::line_rule!(
    ffi_crate_naming,
    "Require `-ffi` naming for crates exporting C symbols and `-sys` naming for crates linking foreign C items.",
    "The `-ffi` (export) and `-sys` (import) suffixes make a crate's FFI role immediately recognizable across projects.",
    Low,
);

#[derive(Default)]
struct FfiUsage {
    no_mangle: bool,
    extern_fn_line: Option<usize>,
    foreign_block_line: Option<usize>,
}

fn scan_ffi_usage(lines: &[&str]) -> FfiUsage {
    let mut usage = FfiUsage::default();

    for (index, line) in lines.iter().enumerate() {
        let code = scanner::code_only(line);

        if code.contains("#[no_mangle]") || code.contains("#[unsafe(no_mangle)]") {
            usage.no_mangle = true;
        }

        if !code.contains("extern") {
            continue;
        }

        if usage.extern_fn_line.is_none() && line.contains("extern \"C\" fn") {
            usage.extern_fn_line = Some(index + 1);
        }

        if usage.foreign_block_line.is_none() && line.contains("extern \"C\" {") {
            usage.foreign_block_line = Some(index + 1);
        }
    }

    usage
}

fn check_ffi_crate_naming(ctx: &FileCtx<'_>) -> Vec<Violation> {
    let Some(name) = ctx.package_name else {
        return Vec::new();
    };

    if super::wasm_exempt(ctx.contents) {
        return Vec::new();
    }

    let usage = scan_ffi_usage(ctx.lines);
    let mut out = Vec::new();

    if usage.no_mangle
        && !super::is_ffi_crate(name)
        && let Some(line) = usage.extern_fn_line
    {
        out.push(violation(
                ctx.rel,
                line,
                format!("crate `{name}` exports C symbols but is not named `*-ffi` — FFI export crates follow the `-ffi` naming convention"),
            ));
    }

    if !super::is_sys_crate(name)
        && let Some(line) = usage.foreign_block_line
    {
        out.push(violation(
                ctx.rel,
                line,
                format!("crate `{name}` links foreign C items but is not named `*-sys` — FFI import crates follow the `-sys` naming convention"),
            ));
    }

    out
}

#[cfg(test)]
mod tests {
    use super::*;

    const CORE_REL: &str = "packages/core/src/lib.rs";

    fn run_at(rel: &str, source: &str) -> Vec<Violation> {
        crate::test_support::check_source_at(rel, source, check_ffi_crate_naming)
    }

    #[gtest]
    fn examples() -> Result<()> {
        for ex in EXAMPLES {
            let violations = run_at(CORE_REL, ex.code);

            verify_eq!(violations.is_empty(), ex.pass)?;
        }

        Ok(())
    }

    #[gtest]
    fn export_from_ffi_crate_passes() -> Result<()> {
        let source = "#[no_mangle]\npub extern \"C\" fn service_tick() {}";

        verify_true!(run_at("nested/core-ffi/src/lib.rs", source).is_empty())?;
        verify_true!(run_at("nested/core_ffi/src/lib.rs", source).is_empty())?;

        Ok(())
    }

    #[gtest]
    fn foreign_block_in_sys_crate_passes() -> Result<()> {
        let source = "extern \"C\" {\n    fn native_call();\n}";

        verify_true!(run_at("nested/native-sys/src/lib.rs", source).is_empty())?;
        verify_true!(run_at("nested/native_sys/src/lib.rs", source).is_empty())?;

        Ok(())
    }

    #[gtest]
    fn export_direction_flags_ffi_naming_only() -> Result<()> {
        let violations = run_at(
            "nested/core-sys/src/lib.rs",
            "#[no_mangle]\npub extern \"C\" fn f() {}",
        );

        verify_eq!(violations.len(), 1)?;

        Ok(())
    }

    #[gtest]
    fn files_without_an_owning_package_are_skipped() -> Result<()> {
        verify_true!(run_at("clean.rs", "extern \"C\" {\n    fn native_call();\n}").is_empty())?;

        Ok(())
    }

    #[gtest]
    fn patterns_inside_strings_do_not_count() -> Result<()> {
        let source = "const SNIPPET: &str = \"#[no_mangle] pub extern \\\"C\\\" fn f() {}\";";

        verify_true!(run_at(CORE_REL, source).is_empty())?;

        Ok(())
    }
}