alef 0.79.1

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Guard: no kotlin_android e2e test is published green while checking nothing.
//!
//! ~keep A `@Test` whose body runs the call and asserts nothing passes no matter what the
//! binding does, so a suite of them certifies broken bindings as working. That exact shape was
//! generated by this backend once: a removed `render_android_instrumented_test` emitted an
//! `src/androidTest/` source set whose every body was `val result = client.call(/* fixture: id
//! */)` — the fixture spliced in as a COMMENT, no argument passed, nothing asserted. It was
//! deleted in f4a2bb5 when the backend moved to host-JVM tests under `src/test/kotlin/`, but
//! nothing in the repo pins that it cannot come back, and its output is still on disk in
//! consumers generated before that commit.
//!
//! [`assertion_free_tests`] is the detector, and
//! [`the_detector_catches_the_shape_this_guard_exists_for`] proves it fires against that exact
//! historical body — without that control, a detector that matched nothing and a suite that is
//! genuinely clean render identically.
//!
//! Scope: every fixture below declares at least one assertion. A fixture that declares NONE is
//! deliberately published as a bare "just call it" smoke test (see
//! `codegen::inert_example::inert_verdict`'s doc), and this guard does not contradict that.

use super::project;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{FieldDef, FunctionDef, PrimitiveType, TypeDef, TypeRef};
use crate::e2e::config::{CallConfig, E2eConfig};
use crate::e2e::fixture::{Fixture, FixtureGroup};
use std::collections::HashSet;

/// Substrings that make a line a real check. `assert` covers every `kotlin.test` /
/// `org.junit.jupiter` spelling the backend emits (`assertEquals`, `assertTrue`, `assertFalse`,
/// `assertNotNull`, `assertContains`, `assertFailsWith`), including the deliberately FAILING
/// `assertTrue(false, ..)` an unresolved-field-path refusal renders.
const ASSERTION_TOKENS: &[&str] = &["assert", "fail("];

/// Substrings that make a test report as SKIPPED rather than as a pass. These are the sanctioned
/// alternative to an assertion — a body that cannot check anything must say so to the runner
/// instead of going green silently. `assumeTrue(false, ..)` is what
/// `kotlin::test_method::inert_refusal` emits for generator debt; `@Disabled` is what
/// `kotlin_android/excluded_fixtures.kt.jinja` emits for a binding the config excluded.
const EXPLICIT_SKIP_TOKENS: &[&str] = &["@Disabled", "assumeTrue("];

/// Comment openers Kotlin uses. A token inside a comment is not a check — the `not_error`
/// fallback renders the literal line `// not_error: covered by the bare Optional's own
/// assertion`, which contains `assert` and must NOT satisfy this guard. ~keep
const COMMENT_OPENERS: &[&str] = &["//", "/*", "*"];

/// One `@Test` the emitter published, split into the part before its body (annotations and
/// signature) and the body itself.
struct EmittedTest {
    name: String,
    head: String,
    body: String,
}

fn line_is_comment(line: &str) -> bool {
    let trimmed = line.trim();
    COMMENT_OPENERS.iter().any(|opener| trimmed.starts_with(opener))
}

fn executable_lines(body: &str) -> Vec<&str> {
    body.lines()
        .filter(|line| !line.trim().is_empty() && !line_is_comment(line))
        .collect()
}

/// Every `@Test` in one generated Kotlin source file.
///
/// ~keep Region-based rather than brace-matched: a chunk runs from one `@Test` to the next (or
/// to end of file), which is exactly the span a reader attributes to that test. Brace matching
/// would have to model Kotlin string literals, and the generated bodies embed JSON payloads full
/// of braces.
fn emitted_tests(source: &str) -> Vec<EmittedTest> {
    let mut tests = Vec::new();
    for chunk in source.split("@Test").skip(1) {
        let Some(fun_at) = chunk.find("fun ") else {
            continue;
        };
        let Some(brace_at) = chunk[fun_at..].find('{').map(|offset| fun_at + offset) else {
            continue;
        };
        let name = chunk[fun_at + "fun ".len()..]
            .split(|c: char| c == '(' || c.is_whitespace())
            .next()
            .unwrap_or_default()
            .to_string();
        tests.push(EmittedTest {
            name,
            head: chunk[..brace_at].to_string(),
            body: chunk[brace_at..].to_string(),
        });
    }
    tests
}

/// The names of every emitted test that neither asserts anything nor reports itself skipped.
fn assertion_free_tests(files: &[crate::core::backend::GeneratedFile]) -> (usize, Vec<String>) {
    let mut total = 0;
    let mut offenders = Vec::new();
    for file in files.iter().filter(|f| f.path.extension().is_some_and(|e| e == "kt")) {
        for test in emitted_tests(&file.content) {
            total += 1;
            let declares_skip = EXPLICIT_SKIP_TOKENS
                .iter()
                .any(|token| test.head.contains(token) || test.body.contains(token));
            if declares_skip {
                continue;
            }
            let asserts = executable_lines(&test.body)
                .iter()
                .any(|line| ASSERTION_TOKENS.iter().any(|token| line.contains(token)));
            if !asserts {
                offenders.push(format!("{}::{}", file.path.display(), test.name));
            }
        }
    }
    (total, offenders)
}

/// A neutral result type and the free function returning it. No consumer domain names.
fn sample_ir() -> (Vec<TypeDef>, Vec<FunctionDef>) {
    let type_defs = vec![TypeDef {
        name: "SampleReport".into(),
        fields: vec![
            FieldDef {
                name: "label".into(),
                ty: TypeRef::String,
                ..FieldDef::default()
            },
            FieldDef {
                name: "item_count".into(),
                ty: TypeRef::Primitive(PrimitiveType::I64),
                ..FieldDef::default()
            },
        ],
        ..TypeDef::default()
    }];
    let functions = vec![FunctionDef {
        name: "build_report".into(),
        return_type: TypeRef::Named("SampleReport".into()),
        ..FunctionDef::default()
    }];
    (type_defs, functions)
}

fn fixture(id: &str, assertions: serde_json::Value) -> Fixture {
    serde_json::from_value(serde_json::json!({
        "id": id,
        "description": format!("sample fixture {id}"),
        "input": {"source": "alpha"},
        "assertions": assertions,
        "docs": {"topic": "smoke", "stem": id},
    }))
    .expect("fixture must parse")
}

fn sample_groups() -> Vec<FixtureGroup> {
    vec![
        FixtureGroup {
            category: "smoke".into(),
            fixtures: vec![
                fixture(
                    "smoke_label_matches",
                    serde_json::json!([{"type": "equals", "field": "label", "value": "alpha"}]),
                ),
                fixture(
                    "smoke_item_count_positive",
                    serde_json::json!([{"type": "greater_than", "field": "item_count", "value": 0}]),
                ),
            ],
        },
        FixtureGroup {
            category: "reporting".into(),
            // A `not_error`-only fixture: the historical source of vacuous bodies across every
            // backend, and the case `kotlin/not_error.rs` exists to keep visible. ~keep
            fixtures: vec![fixture(
                "reporting_succeeds",
                serde_json::json!([{"type": "not_error"}]),
            )],
        },
    ]
}

fn sample_e2e_config() -> E2eConfig {
    E2eConfig {
        call: CallConfig {
            function: "build_report".into(),
            result_var: "report".into(),
            ..CallConfig::default()
        },
        result_fields: HashSet::from(["label".to_string(), "item_count".to_string()]),
        ..E2eConfig::default()
    }
}

/// The guard. Every `@Test` the kotlin_android e2e emitter publishes for a fixture that declares
/// assertions must either assert something or report itself skipped.
#[test]
fn every_emitted_kotlin_android_test_asserts_or_reports_itself_skipped() {
    let (type_defs, functions) = sample_ir();
    let groups = sample_groups();
    let declared_fixtures: usize = groups.iter().map(|group| group.fixtures.len()).sum();

    let config = ResolvedCrateConfig {
        name: "sample_kit".into(),
        ..ResolvedCrateConfig::default()
    };
    let files = project::generate(&groups, &sample_e2e_config(), &config, &type_defs, &[], &functions)
        .expect("kotlin_android e2e generation succeeds");

    let (total, offenders) = assertion_free_tests(&files);

    // Vacuity guard first: a fixture set that emitted nothing would make every assertion below
    // pass while examining no generated code at all. ~keep
    assert!(
        total >= declared_fixtures,
        "expected at least one emitted @Test per declared fixture ({declared_fixtures}); found \
         {total} across {} generated file(s). A guard that scans zero tests proves nothing.",
        files.len()
    );
    assert!(
        offenders.is_empty(),
        "{} of {total} emitted kotlin_android test(s) run the call and check nothing — such a test \
         passes whatever the binding does. Emit a real assertion, or an explicit skip that reports \
         as skipped:\n  {}",
        offenders.len(),
        offenders.join("\n  ")
    );
}

/// Sabotage control for the guard above: the detector must fire on the exact body shape the
/// deleted `render_android_instrumented_test` emitted. Without this, a detector that silently
/// matched nothing would be indistinguishable from a clean suite. ~keep
#[test]
fn the_detector_catches_the_shape_this_guard_exists_for() {
    let historical = crate::core::backend::GeneratedFile {
        path: std::path::PathBuf::from("src/androidTest/kotlin/dev/sample/e2e/SmokeTest.kt"),
        content: "package dev.sample.e2e\n\n\
             import androidx.test.ext.junit.runners.AndroidJUnit4\n\
             import org.junit.Test\n\
             import org.junit.runner.RunWith\n\n\
             @RunWith(AndroidJUnit4::class)\n\
             class SmokeTest {\n\n\
             \x20   @Test\n\
             \x20   fun test_smoke_label_matches() {\n\
             \x20       val client = SampleFacade()\n\
             \x20       val report = client.buildReport(/* fixture: smoke_label_matches */)\n\
             \x20   }\n\n\
             }\n"
        .to_string(),
        generated_header: true,
    };

    let (total, offenders) = assertion_free_tests(std::slice::from_ref(&historical));

    assert_eq!(total, 1, "the detector must see the one @Test in the sample");
    assert_eq!(
        offenders.len(),
        1,
        "the detector must flag a body that calls and asserts nothing"
    );
    assert!(
        offenders[0].ends_with("::test_smoke_label_matches"),
        "the offender must be named so a failure is actionable, got: {offenders:?}"
    );
}

/// Companion control: an explicitly disabled test is the sanctioned way to publish a fixture
/// that cannot be checked, and must NOT be reported as an offender — otherwise the only way to
/// satisfy the guard would be to delete the skip, which is the silence it exists to prevent.
#[test]
fn an_explicitly_disabled_test_is_not_reported_as_assertion_free() {
    let excluded = crate::core::backend::GeneratedFile {
        path: std::path::PathBuf::from("src/test/kotlin/dev/sample/e2e/ExcludedBindingsTest.kt"),
        content: crate::e2e::template_env::render(
            "kotlin_android/excluded_fixtures.kt.jinja",
            minijinja::context! {
                package_name => "dev.sample",
                entries => vec![minijinja::context! {
                    name => "visitor_round_trip",
                    reason => "visitor is excluded by crates.kotlin_android.exclude_functions",
                }],
            },
        ),
        generated_header: true,
    };

    let (total, offenders) = assertion_free_tests(std::slice::from_ref(&excluded));

    assert_eq!(total, 1, "the detector must see the one @Disabled @Test");
    assert!(
        offenders.is_empty(),
        "@Disabled reports as skipped, not as a pass, and must be accepted: {offenders:?}"
    );
}