omena-query 0.5.0

Omena query boundary over CME producer query fragments
Documentation
use std::collections::{BTreeMap, BTreeSet};

use serde::Deserialize;

use super::super::{
    OmenaQueryClassSitePlaneV0, OmenaQueryClassSiteTypeFactInputV0, OmenaQueryClassSiteValueV0,
    StringTypeFactsV2, build_omena_query_guarded_token_map_for_site,
    resolve_omena_query_class_site_values_for_source,
    resolve_omena_query_class_site_values_for_source_with_type_facts,
};

const FIXTURE_SOURCE: &str =
    include_str!("../../../../../test/_fixtures/guarded-token-map/ClassSitePlane.tsx");
const EXPECTATION_SOURCE: &str = include_str!("../../data/class-site-expectations-v0.json");

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ExpectationTableV0 {
    rows: Vec<ExpectationRowV0>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ExpectationRowV0 {
    site_text: String,
    must: Vec<String>,
    may: Vec<String>,
    unknown_cause: Option<String>,
    #[serde(default)]
    type_fact_values: Vec<String>,
    type_fact_refusal_cause: Option<String>,
    #[serde(default)]
    must_with_type_fact: Vec<String>,
    #[serde(default)]
    may_with_type_fact: Vec<String>,
}

#[test]
fn class_site_plane_matches_authored_sites_with_and_without_type_facts()
-> Result<(), Box<dyn std::error::Error>> {
    let expectations = serde_json::from_str::<ExpectationTableV0>(EXPECTATION_SOURCE)?;
    let cfg_only = resolve_omena_query_class_site_values_for_source(
        "test/_fixtures/guarded-token-map/ClassSitePlane.tsx",
        FIXTURE_SOURCE,
        Some("typescriptreact"),
    );
    let expected_sites = expectations
        .rows
        .iter()
        .map(|row| row.site_text.as_str())
        .collect::<BTreeSet<_>>();
    let actual_by_site = sites_by_source_text(FIXTURE_SOURCE, &cfg_only)?;
    let actual_sites = actual_by_site.keys().copied().collect::<BTreeSet<_>>();
    assert_eq!(
        expected_sites
            .difference(&actual_sites)
            .copied()
            .collect::<Vec<_>>(),
        Vec::<&str>::new(),
        "authored class sites missing from product enumerator"
    );
    assert_eq!(
        actual_sites
            .difference(&expected_sites)
            .copied()
            .collect::<Vec<_>>(),
        Vec::<&str>::new(),
        "product enumerator returned unauthored class sites"
    );

    for row in &expectations.rows {
        let site = actual_by_site[&row.site_text.as_str()];
        assert_support(site, &row.must, &row.may, row.site_text.as_str());
        assert_eq!(
            site.unknown_cause
                .as_ref()
                .map(serde_json::to_value)
                .transpose()?
                .and_then(|value| value.as_str().map(str::to_string)),
            row.unknown_cause,
            "unknown cause for {}",
            row.site_text
        );
        assert_eq!(site.type_fact_cause.as_deref(), Some("typeFactNotProvided"));
        if !row.may.is_empty() {
            assert!(
                site.token_provenance
                    .iter()
                    .all(|token| token.planes.contains(&OmenaQueryClassSitePlaneV0::Cfg)),
                "CFG provenance for {}",
                row.site_text
            );
        }
    }

    let guarded = actual_by_site[&"className={clsx({ active: flag })}"];
    let guarded_map = build_omena_query_guarded_token_map_for_site(guarded)?;
    let active = super::super::GuardedTokenLanguageV0::concrete("active");
    assert!(guarded_map.is_may(&active));
    assert!(!guarded_map.is_must(&active));

    for (site_text, unconditional, conditional, guard) in [
        (
            "className={clsx(\"a\", flag && \"b\")}",
            Some("a"),
            "b",
            "flag",
        ),
        ("className={flag && \"x\"}", None, "x", "flag"),
        (
            "className={clsx(\"d\", flag ? \"e\" : undefined)}",
            Some("d"),
            "e",
            "flag",
        ),
    ] {
        let site = actual_by_site[site_text];
        let provenance = site
            .token_provenance
            .iter()
            .find(|provenance| provenance.token.as_str() == conditional)
            .ok_or_else(|| std::io::Error::other("conditional token provenance"))?;
        assert_eq!(
            provenance.guard_conditions,
            [guard],
            "S4 guard atom for {site_text}"
        );
        let map = build_omena_query_guarded_token_map_for_site(site)?;
        let conditional = super::super::GuardedTokenLanguageV0::concrete(conditional);
        assert!(
            map.is_may(&conditional),
            "conditional token for {site_text}"
        );
        assert!(!map.is_must(&conditional), "guarded token for {site_text}");
        if let Some(unconditional) = unconditional {
            assert!(
                map.is_must(&super::super::GuardedTokenLanguageV0::concrete(
                    unconditional
                )),
                "unconditional sibling for {site_text}"
            );
        }
    }

    let static_site = actual_by_site[&"className=\"root root\""];
    let static_map = build_omena_query_guarded_token_map_for_site(static_site)?;
    let root = super::super::GuardedTokenLanguageV0::concrete("root");
    assert!(static_map.is_must(&root));

    let symbolic_site = actual_by_site[&"className={`tone-${computeClassName()}`}"];
    let symbolic_map = build_omena_query_guarded_token_map_for_site(symbolic_site)?;
    let symbolic = super::super::GuardedTokenLanguageV0::symbolic("`tone-${computeClassName()}`");
    assert!(symbolic_map.is_must(&symbolic));

    let type_facts = expectations
        .rows
        .iter()
        .map(|row| {
            let site = actual_by_site[&row.site_text.as_str()];
            OmenaQueryClassSiteTypeFactInputV0 {
                site_byte_span: site.site_byte_span,
                facts: (!row.type_fact_values.is_empty()).then(|| StringTypeFactsV2 {
                    kind: if row.type_fact_values.len() == 1 {
                        "exact".to_string()
                    } else {
                        "finiteSet".to_string()
                    },
                    values: Some(row.type_fact_values.clone()),
                    constraint_kind: None,
                    prefix: None,
                    suffix: None,
                    min_len: None,
                    max_len: None,
                    char_must: None,
                    char_may: None,
                    may_include_other_chars: None,
                    provenance: Some("authoredTypeFactFixture".to_string()),
                }),
                refusal_cause: row.type_fact_refusal_cause.clone(),
            }
        })
        .collect::<Vec<_>>();
    let joined = resolve_omena_query_class_site_values_for_source_with_type_facts(
        "test/_fixtures/guarded-token-map/ClassSitePlane.tsx",
        FIXTURE_SOURCE,
        Some("typescriptreact"),
        &type_facts,
    );
    let joined_by_site = sites_by_source_text(FIXTURE_SOURCE, &joined)?;
    for row in &expectations.rows {
        let site = joined_by_site[&row.site_text.as_str()];
        assert_support(
            site,
            &row.must_with_type_fact,
            &row.may_with_type_fact,
            row.site_text.as_str(),
        );
        assert_eq!(
            site.type_fact_cause, row.type_fact_refusal_cause,
            "type-fact cause for {}",
            row.site_text
        );
        if !row.type_fact_values.is_empty() && !row.may.is_empty() {
            assert!(
                site.token_provenance.iter().all(|token| {
                    token.planes.contains(&OmenaQueryClassSitePlaneV0::TypeFact)
                        && token.planes.contains(&OmenaQueryClassSitePlaneV0::Joined)
                }),
                "joined provenance for {}",
                row.site_text
            );
        }
    }
    Ok(())
}

#[test]
fn conditional_class_contributions_are_may_only_and_keep_unconditional_siblings()
-> Result<(), Box<dyn std::error::Error>> {
    let sites = resolve_omena_query_class_site_values_for_source(
        "test/_fixtures/guarded-token-map/ClassSitePlane.tsx",
        FIXTURE_SOURCE,
        Some("typescriptreact"),
    );
    let sites = sites_by_source_text(FIXTURE_SOURCE, &sites)?;
    let actual = [
        "className={clsx(\"a\", flag && \"b\")}",
        "className={flag && \"x\"}",
        "className={clsx(\"d\", flag ? \"e\" : undefined)}",
    ]
    .map(|site_text| {
        let (must, may) = support_labels(sites[site_text]);
        (site_text, must, may)
    });
    assert_eq!(
        actual,
        [
            (
                "className={clsx(\"a\", flag && \"b\")}",
                vec!["a"],
                vec!["a", "b"],
            ),
            ("className={flag && \"x\"}", vec![], vec!["x"]),
            (
                "className={clsx(\"d\", flag ? \"e\" : undefined)}",
                vec!["d"],
                vec!["d", "e"],
            ),
        ]
    );
    Ok(())
}

fn sites_by_source_text<'a>(
    source: &'a str,
    sites: &'a [OmenaQueryClassSiteValueV0],
) -> Result<BTreeMap<&'a str, &'a OmenaQueryClassSiteValueV0>, String> {
    sites
        .iter()
        .map(|site| {
            source
                .get(site.site_byte_span.start..site.site_byte_span.end)
                .map(|text| (text, site))
                .ok_or_else(|| format!("site span is outside fixture: {:?}", site.site_byte_span))
        })
        .collect()
}

fn assert_support(
    site: &OmenaQueryClassSiteValueV0,
    expected_must: &[String],
    expected_may: &[String],
    label: &str,
) {
    let (actual_must, actual_may) = support_labels(site);
    assert_eq!(actual_must, expected_must, "must support for {label}");
    assert_eq!(actual_may, expected_may, "may support for {label}");
}

fn support_labels(site: &OmenaQueryClassSiteValueV0) -> (Vec<&str>, Vec<&str>) {
    site.support
        .as_ref()
        .map(|support| {
            (
                support
                    .must()
                    .iter()
                    .map(|token| token.as_str())
                    .collect::<Vec<_>>(),
                support
                    .may()
                    .iter()
                    .map(|token| token.as_str())
                    .collect::<Vec<_>>(),
            )
        })
        .unwrap_or_default()
}