weavatrix-rust 2.1.1

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use super::{
    BTreeSet, Bindings, Call, Certainty, Detection, ProviderHints, Token, TokenKind, Transport,
    detect_amqp, detect_aws, detect_jms, detect_kafka, detect_nats,
};

pub(super) fn propagate_assignment_binding(
    tokens: &[Token],
    source: &str,
    equals: usize,
    bindings: &mut Bindings,
) {
    let Some(variable) = (equals.saturating_sub(4)..equals).rev().find_map(|index| {
        (tokens[index].kind == TokenKind::Identifier).then(|| tokens[index].text(source).to_owned())
    }) else {
        return;
    };
    let line = tokens[equals].line;
    let transports = tokens
        .iter()
        .skip(equals + 1)
        .take_while(|token| token.line == line && token.text(source) != ";")
        .filter(|token| token.kind == TokenKind::Identifier)
        .filter_map(|token| bindings.transports.get(token.text(source)))
        .flatten()
        .copied()
        .collect::<BTreeSet<_>>();
    if !transports.is_empty() {
        bindings
            .transports
            .entry(variable)
            .or_default()
            .extend(transports);
    }
}

pub(super) fn detect(
    call: &Call<'_, '_>,
    hints: ProviderHints,
    bindings: &Bindings,
) -> Vec<Detection> {
    let semantic_name = bindings
        .aliases
        .get(&call.name)
        .map_or(call.name.as_str(), String::as_str);
    let name = semantic_name.to_ascii_lowercase();
    let chain = call.chain.to_ascii_lowercase();
    let candidates = call_transport_candidates(call, &name, &chain, hints, bindings);
    if candidates.is_empty() {
        return detect_with_hints(call, &name, &chain, hints, bindings)
            .into_iter()
            .collect();
    }
    let union = candidates.len() > 1;
    candidates
        .into_iter()
        .filter_map(|transport| {
            let selected = hints_for(transport);
            let mut detection = detect_with_hints(call, &name, &chain, selected, bindings)?;
            if union {
                detection.certainty = Certainty::Ambiguous;
                detection.uncertainty = Some(
                    "multiple concrete transports are reachable from receiver/import provenance"
                        .to_owned(),
                );
            }
            Some(detection)
        })
        .collect()
}

pub(super) fn detect_with_hints(
    call: &Call<'_, '_>,
    name: &str,
    chain: &str,
    hints: ProviderHints,
    bindings: &Bindings,
) -> Option<Detection> {
    detect_amqp(call, name, chain, hints, bindings)
        .map(|mut detection| {
            if hints.rabbitmq {
                detection.transport = Transport::RabbitMq;
            }
            detection
        })
        .or_else(|| detect_aws(call, name, chain, hints))
        .or_else(|| detect_kafka(call, name, chain, hints))
        .or_else(|| detect_jms(call, name, chain, hints, bindings))
        .or_else(|| detect_nats(call, name, chain, hints))
}

pub(super) fn call_transport_candidates(
    call: &Call<'_, '_>,
    name: &str,
    chain: &str,
    hints: ProviderHints,
    bindings: &Bindings,
) -> BTreeSet<Transport> {
    let mut candidates = BTreeSet::new();
    if let Some(receiver) = call.receiver.as_ref()
        && let Some(transports) = bindings.transports.get(receiver)
    {
        candidates.extend(transports);
    }
    for identifier in chain.split(['.', ':']) {
        if let Some(transports) = bindings.transports.get(identifier) {
            candidates.extend(transports);
        }
    }
    if let Some(transports) = bindings.transports.get(&call.name) {
        candidates.extend(transports);
    }
    for transport in [
        Transport::Kafka,
        Transport::Amqp,
        Transport::RabbitMq,
        Transport::Jms,
        Transport::Nats,
        Transport::Sqs,
        Transport::Sns,
    ] {
        if provider_text_matches(transport, chain) {
            candidates.insert(transport);
        }
    }
    if !candidates.is_empty() {
        return candidates;
    }
    match name {
        "publish" => {
            candidates.extend(
                [
                    (Transport::Amqp, hints.amqp),
                    (Transport::RabbitMq, hints.rabbitmq),
                    (Transport::Nats, hints.nats),
                    (Transport::Sns, hints.sns),
                ]
                .into_iter()
                .filter_map(|(transport, present)| present.then_some(transport)),
            );
        }
        "subscribe" => {
            candidates.extend(
                [
                    (Transport::Kafka, hints.kafka),
                    (Transport::Nats, hints.nats),
                    (Transport::Sns, hints.sns),
                ]
                .into_iter()
                .filter_map(|(transport, present)| present.then_some(transport)),
            );
        }
        "createqueue" => {
            candidates.extend(
                [
                    (Transport::Amqp, hints.amqp),
                    (Transport::RabbitMq, hints.rabbitmq),
                    (Transport::Jms, hints.jms),
                    (Transport::Sqs, hints.sqs),
                ]
                .into_iter()
                .filter_map(|(transport, present)| present.then_some(transport)),
            );
        }
        "createtopic" => {
            candidates.extend(
                [(Transport::Jms, hints.jms), (Transport::Sns, hints.sns)]
                    .into_iter()
                    .filter_map(|(transport, present)| present.then_some(transport)),
            );
        }
        "send" => {
            candidates.extend(
                [(Transport::Kafka, hints.kafka), (Transport::Jms, hints.jms)]
                    .into_iter()
                    .filter_map(|(transport, present)| present.then_some(transport)),
            );
        }
        _ => {}
    }
    candidates
}

pub(super) const fn hints_for(transport: Transport) -> ProviderHints {
    ProviderHints {
        kafka: matches!(transport, Transport::Kafka),
        amqp: matches!(transport, Transport::Amqp),
        rabbitmq: matches!(transport, Transport::RabbitMq),
        jms: matches!(transport, Transport::Jms),
        nats: matches!(transport, Transport::Nats),
        sqs: matches!(transport, Transport::Sqs),
        sns: matches!(transport, Transport::Sns),
    }
}

pub(super) fn provider_text_matches(transport: Transport, text: &str) -> bool {
    match transport {
        Transport::Kafka => ["kafka", "kafkajs", "rdkafka", "sarama", "confluent_kafka"]
            .iter()
            .any(|needle| text.contains(needle)),
        Transport::Amqp => {
            text.contains("amqp")
                && !["amqplib", "rabbitmq", "rabbit", "pika", "lapin", "kombu"]
                    .iter()
                    .any(|needle| text.contains(needle))
        }
        Transport::RabbitMq => ["amqplib", "rabbitmq", "rabbit", "pika", "lapin", "kombu"]
            .iter()
            .any(|needle| text.contains(needle)),
        Transport::Jms => ["jms", "activemq", "artemis"]
            .iter()
            .any(|needle| text.contains(needle)),
        Transport::Nats => text.contains("nats"),
        Transport::Sqs => text.contains("sqs"),
        Transport::Sns => text.contains("sns"),
    }
}

pub(super) fn is_import_keyword(identifier: &str) -> bool {
    [
        "as", "const", "from", "import", "let", "mut", "new", "package", "require", "use", "var",
    ]
    .iter()
    .any(|keyword| identifier.eq_ignore_ascii_case(keyword))
}