bonsai-ninja-lang-python 0.2.0

Python language adapter.
Documentation
use bonsai_db::AnalyzerDb;
use bonsai_lang_api::{FlowEvent, LanguageRegistry};
use bonsai_vfs::Vfs;
use std::sync::Arc;

fn call_argument_places(source: &str) -> Vec<(String, Option<String>)> {
    let vfs = Arc::new(Vfs::new());
    vfs.write("sample.py".to_string(), Arc::<str>::from(source));
    let registry = Arc::new(LanguageRegistry::new());
    registry.register(Arc::new(bonsai_lang_python::PythonAdapter::new()));
    let db = AnalyzerDb::new(vfs, registry);
    let mut out = Vec::new();

    fn walk(events: &[FlowEvent], out: &mut Vec<(String, Option<String>)>) {
        for event in events {
            match event {
                FlowEvent::Call { name, args, .. } if name == "sink" => {
                    out.extend(args.iter().map(|arg| (arg.value_text.clone(), arg.place.clone())));
                }
                FlowEvent::Branch {
                    then_events,
                    else_events,
                    ..
                } => {
                    walk(then_events, out);
                    walk(else_events, out);
                }
                FlowEvent::Loop { body, .. }
                | FlowEvent::Defer { body, .. }
                | FlowEvent::Using { body, .. } => walk(body, out),
                FlowEvent::Try {
                    body,
                    catch_events,
                    finally_events,
                    ..
                } => {
                    walk(body, out);
                    walk(catch_events, out);
                    walk(finally_events, out);
                }
                _ => {}
            }
        }
    }

    let global = db.global_index();
    for file in global.all_files() {
        for declaration in global.decls_in(file) {
            walk(&declaration.flow_events, &mut out);
        }
    }
    out
}

fn return_places(source: &str) -> Vec<(String, Option<String>, Option<String>)> {
    let vfs = Arc::new(Vfs::new());
    vfs.write("sample.py".to_string(), Arc::<str>::from(source));
    let registry = Arc::new(LanguageRegistry::new());
    registry.register(Arc::new(bonsai_lang_python::PythonAdapter::new()));
    let db = AnalyzerDb::new(vfs, registry);
    let global = db.global_index();
    let mut out = Vec::new();

    fn walk(
        declaration: &str,
        events: &[FlowEvent],
        out: &mut Vec<(String, Option<String>, Option<String>)>,
    ) {
        for event in events {
            match event {
                FlowEvent::Return {
                    value_name,
                    value_flow,
                    ..
                } => out.push((
                    declaration.to_string(),
                    value_name.clone(),
                    value_flow.place.clone(),
                )),
                FlowEvent::Branch {
                    then_events,
                    else_events,
                    ..
                } => {
                    walk(declaration, then_events, out);
                    walk(declaration, else_events, out);
                }
                FlowEvent::Loop { body, .. }
                | FlowEvent::Defer { body, .. }
                | FlowEvent::Using { body, .. } => walk(declaration, body, out),
                FlowEvent::Try {
                    body,
                    catch_events,
                    finally_events,
                    ..
                } => {
                    walk(declaration, body, out);
                    walk(declaration, catch_events, out);
                    walk(declaration, finally_events, out);
                }
                _ => {}
            }
        }
    }

    for file in global.all_files() {
        for declaration in global.decls_in(file) {
            walk(&declaration.name, &declaration.flow_events, &mut out);
        }
    }
    out.sort();
    out
}

#[test]
fn static_subscripts_are_exact_compiler_places() {
    let places =
        call_argument_places("def entry(obj):\n    sink(obj['other'])\n    sink(obj[\"nested\"]['leaf'])\n");
    assert_eq!(
        places,
        vec![
            ("obj['other']".to_string(), Some("obj.other".to_string())),
            (
                "obj[\"nested\"]['leaf']".to_string(),
                Some("obj.nested.leaf".to_string()),
            ),
        ]
    );
}

#[test]
fn dynamic_subscripts_do_not_claim_an_exact_field() {
    let places = call_argument_places("def entry(obj, key):\n    sink(obj[key])\n");
    assert_eq!(
        places,
        vec![("obj[key]".to_string(), Some("obj.*".to_string()))],
        "a dynamic key addresses an unknown descendant, never a made-up exact field"
    );
}

#[test]
fn static_subscript_returns_are_exact_compiler_places() {
    let places = return_places(
        "def leaf(obj):\n    return obj['value']\n\ndef nested(obj):\n    return obj['nested']['leaf']\n",
    );
    assert_eq!(
        places,
        vec![
            (
                "leaf".to_string(),
                Some("obj.value".to_string()),
                Some("obj.value".to_string()),
            ),
            (
                "nested".to_string(),
                Some("obj.nested.leaf".to_string()),
                Some("obj.nested.leaf".to_string()),
            ),
        ]
    );
}

#[test]
fn dynamic_subscript_returns_remain_aggregate_reads() {
    let places = return_places("def entry(obj, key):\n    return obj[key]\n");
    assert_eq!(
        places,
        vec![("entry".to_string(), None, Some("obj.*".to_string()),)],
        "a dynamic key must remain an explicit wildcard descendant place"
    );
}