bonsai-ninja-lang-ruby 0.2.13

Ruby language adapter.
use bonsai_conformance::run_language_suite;
use std::sync::Arc;

#[test]
fn conformance_traced() {
    let adapter: Arc<dyn bonsai_lang_api::LanguageAdapter> = Arc::new(bonsai_lang_ruby::RubyAdapter::new());
    run_language_suite!(
        adapter,
        trace_from = "main",
        [("a.rb", "def main\n  puts 1\nend\n")]
    );
}

#[test]
fn case_match_binds_values_but_not_map_keys_or_pins() {
    use bonsai_lang_api::FlowEvent;

    let adapter: Arc<dyn bonsai_lang_api::LanguageAdapter> = Arc::new(bonsai_lang_ruby::RubyAdapter::new());
    let ws = bonsai_testkit::workspace_with(
        vec![adapter],
        &[(
            "a.rb",
            r#"def main(subject, expected)
  case subject
  in {value:, nested: {item:}, **rest}
    sink(value, item, rest)
  in ^expected
    sink(expected)
  end
end
"#,
        )],
    );
    let global = ws.db().global_index();
    let main = global
        .all_files()
        .flat_map(|file| global.decls_in(file))
        .find(|decl| decl.name == "main")
        .expect("main declaration");
    let mut facts = Vec::new();
    collect_assignments(&main.flow_events, &mut facts);
    for target in ["value", "item", "rest"] {
        assert!(
            facts
                .iter()
                .any(|(actual, source)| actual == target && source.as_deref() == Some("subject")),
            "missing {target} <- subject: {facts:#?}"
        );
    }
    for non_binding in ["nested", "expected"] {
        assert!(
            facts.iter().all(|(target, _)| target != non_binding),
            "map key or pinned value became a binding: {non_binding}: {facts:#?}"
        );
    }

    fn collect_assignments(events: &[FlowEvent], out: &mut Vec<(String, Option<String>)>) {
        for event in events {
            match event {
                FlowEvent::Assign {
                    target, source_name, ..
                } => out.push((target.clone(), source_name.clone())),
                FlowEvent::Branch {
                    then_events,
                    else_events,
                    ..
                } => {
                    collect_assignments(then_events, out);
                    collect_assignments(else_events, out);
                }
                _ => {}
            }
        }
    }
}

#[test]
fn keyword_arguments_keep_their_ast_name_and_value() {
    use bonsai_lang_api::FlowEvent;

    let workspace = bonsai_testkit::workspace_with(
        vec![Arc::new(bonsai_lang_ruby::RubyAdapter::new())],
        &[(
            "jwt.rb",
            "def decode(input)\n  JWT.decode(input, input, verify: true)\nend\n",
        )],
    );
    let global = workspace.db().global_index();
    let decode = global
        .all_files()
        .flat_map(|file| global.decls_in(file))
        .find(|decl| decl.name == "decode")
        .expect("decode declaration");

    assert!(
        decode.flow_events.iter().any(|event| matches!(
            event,
            FlowEvent::Call { name, args, .. }
                if name == "JWT.decode"
                    && args.iter().any(|arg| {
                        arg.name.as_deref() == Some("verify") && arg.value_text.trim() == "true"
                    })
        )),
        "events={:#?}",
        decode.flow_events
    );
}

#[test]
fn bare_method_names_are_not_formal_parameters() {
    let workspace = bonsai_testkit::workspace_with(
        vec![Arc::new(bonsai_lang_ruby::RubyAdapter::new())],
        &[(
            "callbacks.rb",
            r#"class Store
  def show
    nil
  end

  def self.with_root
    yield "/srv/data"
  end
end
"#,
        )],
    );
    let global = workspace.db().global_index();
    let show = global
        .all_files()
        .flat_map(|file| global.decls_in(file))
        .find(|decl| decl.name == "show")
        .expect("show declaration");
    assert!(show.params.is_empty(), "method name became a formal: {show:#?}");

    let with_root = global
        .all_files()
        .flat_map(|file| global.decls_in(file))
        .find(|decl| decl.name == "with_root")
        .expect("with_root declaration");
    assert_eq!(
        with_root.params,
        vec!["<ruby-yield-block>"],
        "singleton method name shifted the compiler-owned block formal"
    );
}

#[test]
fn complete_local_string_maps_and_literal_call_arguments_are_exact_facts() {
    use bonsai_lang_api::{StaticScalarValue, StaticStringMapEntry};

    let workspace = bonsai_testkit::workspace_with(
        vec![Arc::new(bonsai_lang_ruby::RubyAdapter::new())],
        &[(
            "maps.rb",
            r#"def choose(selector, runtime)
  choices = {"first" => "alpha", second: "beta"}
  dynamic_value = {"first" => runtime}
  spread_map = {"first" => "alpha", **runtime}
  choices.lookup(selector, "fallback")
  choices.lookup(selector, runtime)
end
"#,
        )],
    );
    let file = workspace.vfs().all_files()[0];
    let index = workspace.db().decl_index(file).expect("Ruby declaration index");

    assert_eq!(
        index.static_string_maps.len(),
        1,
        "only the complete string-to-string literal map is compiler-proven: {:#?}",
        index.static_string_maps
    );
    assert_eq!(index.static_string_maps[0].target, "choices");
    assert_eq!(
        index.static_string_maps[0].entries,
        vec![
            StaticStringMapEntry {
                key: "first".to_string(),
                value: "alpha".to_string(),
            },
            StaticStringMapEntry {
                key: "second".to_string(),
                value: "beta".to_string(),
            },
        ]
    );

    let mut lookup_arguments = index
        .call_argument_values
        .iter()
        .filter(|fact| {
            index.defs.iter().any(|decl| {
                decl.flow_events.iter().any(|event| {
                    matches!(
                        event,
                        bonsai_lang_api::FlowEvent::Call { span, name, .. }
                            if *span == fact.call_span && name == "choices.lookup"
                    )
                })
            }) && fact.argument_index == 1
        })
        .collect::<Vec<_>>();
    lookup_arguments.sort_by_key(|fact| fact.call_span.start);
    assert_eq!(lookup_arguments.len(), 2);
    assert_eq!(
        lookup_arguments[0].static_value,
        Some(StaticScalarValue::String("fallback".to_string()))
    );
    assert_eq!(lookup_arguments[1].static_value, None);
}

#[test]
fn frozen_constant_symbol_maps_are_exact_immutable_facts() {
    use bonsai_lang_api::{StaticScalarValue, StaticStringMapEntry};

    let workspace = bonsai_testkit::workspace_with(
        vec![Arc::new(bonsai_lang_ruby::RubyAdapter::new())],
        &[(
            "maps.rb",
            r#"class OrderRepo
  SORTABLE = {"total" => :total, "created_at" => :created_at}.freeze
  MUTABLE = {"total" => :total}
  DYNAMIC = {"total" => runtime}.freeze

  def self.list(sort)
    SORTABLE.fetch(sort, :id)
  end
end
"#,
        )],
    );
    let file = workspace.vfs().all_files()[0];
    let index = workspace.db().decl_index(file).expect("Ruby declaration index");

    assert_eq!(
        index.static_string_maps,
        vec![bonsai_lang_api::StaticStringMapFact {
            assignment_span: index.static_string_maps[0].assignment_span,
            target: "SORTABLE".to_string(),
            target_is_immutable: true,
            entries: vec![
                StaticStringMapEntry {
                    key: "total".to_string(),
                    value: "total".to_string(),
                },
                StaticStringMapEntry {
                    key: "created_at".to_string(),
                    value: "created_at".to_string(),
                },
            ],
        }],
        "only a frozen complete constant map may become immutable selection evidence"
    );

    let fallback = index
        .call_argument_values
        .iter()
        .find(|fact| fact.argument_index == 1)
        .expect("fetch fallback argument");
    assert_eq!(
        fallback.static_value,
        Some(StaticScalarValue::String("id".to_string())),
        "a simple Ruby symbol is an exact static scalar, not runtime text"
    );
}