alef 0.115.5

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Shared test fixture for the "wildcard element half must not be re-anchored to the result"
//! regression, used by every backend that expands a `container[].field` fixture path into an
//! any-element quantifier.
//!
//! ~keep The IR shape below is the whole point of the fixture and must not be simplified: the
//! defect only appears when the element leaf is UNDECLARED on the call's root type while a
//! `result_fields` entry reaches a type that does declare it. That is what makes
//! `FieldResolver::result_relative_path`'s envelope rescue fire, turning the element half `kind`
//! back into `records[0].kind` — which, rendered against a binding that is already an element,
//! re-applies the container path and addresses a member the element type has no such field for.
//! A flatter fixture (leaf declared on the root) produces a passing accessor either way and
//! would be a test that cannot fail.
//!
//! ~keep Nine backends need this identical shape, so it is built once here rather than copied
//! per backend: a drifted copy would silently stop reproducing the rescue and each backend's
//! regression test would go vacuously green.

use std::collections::{HashMap, HashSet};

use crate::core::ir::{FieldDef, TypeDef, TypeRef};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Assertion;

/// The IR type name the fixture resolver anchors its result-field oracle at.
pub(crate) const ROOT_TYPE: &str = "Report";

/// The IR type name the envelope resolver anchors at.
pub(crate) const ENVELOPE_ROOT_TYPE: &str = "Envelope";

/// The fixture path every backend test renders: `records` is a collection on the root,
/// `kind` is declared only on the element type.
pub(crate) const WILDCARD_FIELD: &str = "records[].kind";

/// A second fixture path whose leaf is likewise element-only — used by the `not_empty` arms,
/// which several backends build through a separate code path from `contains`.
pub(crate) const WILDCARD_NAME_FIELD: &str = "records[].name";

/// `Report { records: Vec<Entry> }`, `Entry { kind, name }` — the minimal shape that leaves the
/// element leaf undeclared on the root while reachable through a `result_fields` container.
fn report_entry_type_defs() -> Vec<TypeDef> {
    vec![
        TypeDef {
            name: ROOT_TYPE.to_string(),
            fields: vec![FieldDef {
                name: "records".to_string(),
                ty: TypeRef::Vec(Box::new(TypeRef::Named("Entry".to_string()))),
                ..FieldDef::default()
            }],
            ..TypeDef::default()
        },
        TypeDef {
            name: "Entry".to_string(),
            fields: vec![
                FieldDef {
                    name: "kind".to_string(),
                    ty: TypeRef::String,
                    ..FieldDef::default()
                },
                FieldDef {
                    name: "name".to_string(),
                    ty: TypeRef::String,
                    ..FieldDef::default()
                },
            ],
            ..TypeDef::default()
        },
    ]
}

/// A resolver wired exactly the way every backend's `call_field_resolver` wires one: IR field
/// facts and collection facts anchored at the call's declared result type, plus the
/// `result_fields` projection entry that the envelope rescue searches.
pub(crate) fn report_resolver(language: &str) -> FieldResolver {
    let type_defs = report_entry_type_defs();
    let result_field_map = FieldResolver::ir_result_field_facts(&type_defs, language);
    let collection_map = FieldResolver::ir_collection_fields(&type_defs);
    let (reachable, excluded, optional) = FieldResolver::ir_field_sets(&type_defs);
    let result_fields = HashSet::from(["records".to_string()]);
    FieldResolver::new(
        &HashMap::new(),
        &HashSet::new(),
        &result_fields,
        &HashSet::new(),
        &HashSet::new(),
    )
    .with_ir_result_fields(result_field_map, Some(ROOT_TYPE.to_string()))
    .with_ir_collection_map(collection_map, Some(ROOT_TYPE.to_string()))
    .with_ir_fields(reachable, excluded, optional)
}

/// `Envelope { results: Vec<Report> }` on top of the shape above — the container `records` is
/// now reachable only THROUGH a `result_fields` projection, so the container half genuinely
/// depends on the result anchoring `element_accessor` must not apply.
fn envelope_report_entry_type_defs() -> Vec<TypeDef> {
    let mut type_defs = vec![TypeDef {
        name: ENVELOPE_ROOT_TYPE.to_string(),
        fields: vec![FieldDef {
            name: "results".to_string(),
            ty: TypeRef::Vec(Box::new(TypeRef::Named(ROOT_TYPE.to_string()))),
            ..FieldDef::default()
        }],
        ..TypeDef::default()
    }];
    type_defs.extend(report_entry_type_defs());
    type_defs
}

/// A resolver whose root is an envelope: `records` is NOT declared on the root, so the container
/// half of `records[].kind` only renders correctly when the result anchoring is applied.
///
/// ~keep This is the control the element-relative fix must not break. The element leaf `kind` is
/// unreachable through the `results` projection here (`Report` does not declare it), so the
/// envelope rescue declines for the element half and this fixture isolates the container half's
/// anchoring on its own: a change that simply stopped anchoring everywhere renders the container
/// as `records` off the root, which is a member the envelope does not declare.
pub(crate) fn envelope_resolver(language: &str) -> FieldResolver {
    let type_defs = envelope_report_entry_type_defs();
    let result_field_map = FieldResolver::ir_result_field_facts(&type_defs, language);
    let collection_map = FieldResolver::ir_collection_fields(&type_defs);
    let (reachable, excluded, optional) = FieldResolver::ir_field_sets(&type_defs);
    let result_fields = HashSet::from(["results".to_string()]);
    FieldResolver::new(
        &HashMap::new(),
        &HashSet::new(),
        &result_fields,
        &HashSet::new(),
        &HashSet::new(),
    )
    .with_ir_result_fields(result_field_map, Some(ENVELOPE_ROOT_TYPE.to_string()))
    .with_ir_collection_map(collection_map, Some(ENVELOPE_ROOT_TYPE.to_string()))
    .with_ir_fields(reachable, excluded, optional)
}

/// Assert the container's rendered accessor fragment appears exactly once in `rendered`.
///
/// ~keep The sharpest statement of the defect that does not depend on a backend's lambda
/// parameter spelling: the container belongs to the iteration expression and nowhere else, so a
/// second occurrence is the container path re-applied inside the closure body. Takes the RENDERED
/// fragment (`.records()`, `->records`, `.Records`) rather than the bare fixture name, because
/// several backends also interpolate the raw fixture path into the failure message.
pub(crate) fn assert_container_accessor_appears_once(rendered: &str, container_accessor: &str) {
    let occurrences = rendered.matches(container_accessor).count();
    assert_eq!(
        occurrences, 1,
        "`{container_accessor}` must appear once (the iteration expression) and not a second time \
         inside the closure body, got {occurrences} in: {rendered}"
    );
}

/// A `contains` assertion on `field` looking for `"Heading"`.
pub(crate) fn contains_assertion(field: &str) -> Assertion {
    Assertion {
        assertion_type: "contains".to_string(),
        field: Some(field.to_string()),
        value: Some(serde_json::json!("Heading")),
        ..Default::default()
    }
}

/// A `not_empty` assertion on `field`.
pub(crate) fn not_empty_assertion(field: &str) -> Assertion {
    Assertion {
        assertion_type: "not_empty".to_string(),
        field: Some(field.to_string()),
        ..Default::default()
    }
}

/// Assert that `rendered` carries an element-relative closure body and no re-applied container.
///
/// `element_body` is the accessor the backend must emit against its own element binding
/// (e.g. `e.Kind` for Go); `container_reapplied` is the fragment the pre-fix envelope rescue
/// produced against that same binding (e.g. `e.Records[0].Kind`).
pub(crate) fn assert_element_relative(rendered: &str, element_body: &str, container_reapplied: &str) {
    assert!(
        !rendered.contains(container_reapplied),
        "closure body must not re-apply the container path to the element binding \
         (`{container_reapplied}` addresses a member the element type does not declare), got: {rendered}"
    );
    assert!(
        rendered.contains(element_body),
        "closure body must address the element binding's own field (`{element_body}`), got: {rendered}"
    );
}

/// ~keep A required root name prevents the global bare-name optional index hiding lost path context.
pub(crate) fn optional_name_resolver(language: &str) -> FieldResolver {
    let mut types = report_entry_type_defs();
    types[0].fields.push(FieldDef {
        name: "name".into(),
        ty: TypeRef::String,
        ..FieldDef::default()
    });
    types[1].fields[1].optional = true;
    let (reachable, excluded, optional) = FieldResolver::ir_field_sets(&types);
    let resolver = FieldResolver::new(
        &HashMap::new(),
        &HashSet::new(),
        &HashSet::new(),
        &HashSet::new(),
        &HashSet::new(),
    )
    .with_ir_result_fields(
        FieldResolver::ir_result_field_facts(&types, language),
        Some(ROOT_TYPE.into()),
    )
    .with_ir_collection_map(FieldResolver::ir_collection_fields(&types), Some(ROOT_TYPE.into()))
    .with_ir_fields(reachable, excluded, optional);
    assert!(!resolver.is_optional("name"));
    assert!(resolver.is_optional(WILDCARD_NAME_FIELD));
    if language == "swift" {
        resolver.with_swift_first_class_map(crate::e2e::field_access::SwiftFirstClassMap {
            first_class_types: HashSet::from([ROOT_TYPE.into(), "Entry".into()]),
            field_types: HashMap::from([(ROOT_TYPE.into(), HashMap::from([("records".into(), "Entry".into())]))]),
            root_type: Some(ROOT_TYPE.into()),
            ..Default::default()
        })
    } else {
        resolver
    }
}

pub(crate) fn compile_and_run_wildcard_program(language: &str, source: &str) {
    let directory = tempfile::tempdir().expect("create compiler test directory");
    let path = directory.path().join(format!("wildcard.{language}"));
    std::fs::write(&path, source).expect("write generated assertion harness");
    let mut command = std::process::Command::new(language);
    if language == "go" {
        command.arg("run");
    }
    let output = command.arg(&path).output().expect("start language toolchain");
    assert!(
        output.status.success(),
        "{}\n{}\n{source}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}

pub(crate) fn wildcard_runtime_cases(language: &str) -> Vec<(Assertion, Option<&'static str>, bool)> {
    let mut cases = Vec::new();
    for name in [None, Some("t"), Some("other"), Some("")] {
        for needle in ["", "t", "missing"] {
            for kind in [
                "contains",
                "not_contains",
                "not_contains_multi",
                "contains_all",
                "not_empty",
            ] {
                // ~keep Swift String.contains rejects empty needles; preserve each backend's existing string semantics.
                let matches =
                    name.is_some_and(|name| name.contains(needle) && (language == "go" || !needle.is_empty()));
                let passes = match kind {
                    "not_contains" => !matches,
                    "not_contains_multi" => !matches && !name.is_some_and(|name| name.contains("t")),
                    "contains_all" => matches && name.is_some_and(|name| name.contains("t")),
                    "not_empty" => name.is_some_and(|name| !name.is_empty()),
                    _ => matches,
                };
                cases.push((
                    Assertion {
                        assertion_type: if kind == "not_contains_multi" {
                            "not_contains"
                        } else {
                            kind
                        }
                        .into(),
                        field: Some(WILDCARD_NAME_FIELD.into()),
                        value: (!["contains_all", "not_contains_multi"].contains(&kind))
                            .then(|| serde_json::json!(needle)),
                        values: ["contains_all", "not_contains_multi"]
                            .contains(&kind)
                            .then(|| vec![serde_json::json!(needle), serde_json::json!("t")]),
                        ..Default::default()
                    },
                    name,
                    passes,
                ));
            }
        }
    }
    assert_eq!(cases.len(), 60);
    cases
}