weavatrix-rust 2.16.0

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use super::code_span;
use super::identity::resolve_import;
use super::span::span_for;
use crate::language::ImportFact;
use weavatrix_graph::SourceSpan;

const VIEM_APIS: &[&str] = &[
    "readContract",
    "writeContract",
    "simulateContract",
    "getContract",
    "watchContractEvent",
    "getContractEvents",
    "encodeFunctionData",
    "decodeFunctionResult",
    "decodeEventLog",
    "parseEventLogs",
];

#[derive(Debug, Clone)]
pub(super) struct ConsumerOccurrence {
    pub api: String,
    pub member_name: Option<String>,
    pub member_kind: &'static str,
    pub abi_local: Option<String>,
    pub import_path: Option<String>,
    pub resolved: bool,
    pub span: SourceSpan,
    pub ordinal: usize,
}

#[must_use]
pub(super) fn extract(path: &str, raw: &str, imports: &[ImportFact]) -> Vec<ConsumerOccurrence> {
    let code = code_span::mask(raw);
    if !has_library_import(imports, raw, &code) {
        return Vec::new();
    }
    let mut occurrences = Vec::new();
    let mut search_from = 0_usize;
    while let Some(rel) = next_api(raw, search_from, &code) {
        let start = rel.0;
        let api = rel.1;
        let body = object_after(raw, start + api.len());
        let Some((object, end)) = body else {
            search_from = start + api.len();
            continue;
        };
        if unknown_spread_overwrites(object) {
            occurrences.push(ConsumerOccurrence {
                api: api.to_owned(),
                member_name: literal_prop(object, "functionName")
                    .or_else(|| literal_prop(object, "eventName")),
                member_kind: member_kind(api),
                abi_local: ident_prop(object, "abi"),
                import_path: None,
                resolved: false,
                span: span_for(path, raw, start, end),
                ordinal: occurrences.len() + 1,
            });
            search_from = end;
            continue;
        }
        let abi_local = ident_prop(object, "abi");
        let import_path = abi_local
            .as_deref()
            .and_then(|local| import_target(imports, local))
            .and_then(|spec| resolve_import(path, spec));
        let resolved = import_path.is_some();
        occurrences.push(ConsumerOccurrence {
            api: api.to_owned(),
            member_name: literal_prop(object, "functionName")
                .or_else(|| literal_prop(object, "eventName")),
            member_kind: member_kind(api),
            abi_local,
            import_path,
            resolved,
            span: span_for(path, raw, start, end),
            ordinal: occurrences.len() + 1,
        });
        search_from = end;
    }
    occurrences
}

fn has_library_import(imports: &[ImportFact], raw: &str, code: &[bool]) -> bool {
    imports.iter().any(is_web3_import) || named_import(raw, code)
}

fn is_web3_import(import: &ImportFact) -> bool {
    let target = import.target.as_str();
    target == "viem"
        || target == "wagmi"
        || target.starts_with("viem/")
        || target.starts_with("wagmi/")
        || target.starts_with("@wagmi/")
}

fn named_import(raw: &str, code: &[bool]) -> bool {
    raw.match_indices("from ")
        .filter(|(index, _)| code_span::at(code, *index))
        .any(|(index, _)| {
            let rest = raw[index + 5..].trim_start();
            rest.starts_with("\"viem\"")
                || rest.starts_with("'viem'")
                || rest.starts_with("\"wagmi\"")
                || rest.starts_with("'wagmi'")
                || rest.starts_with("\"viem/")
                || rest.starts_with("'viem/")
                || rest.starts_with("\"wagmi/")
                || rest.starts_with("'wagmi/")
                || rest.starts_with("\"@wagmi/")
                || rest.starts_with("'@wagmi/")
        })
}

fn next_api(raw: &str, from: usize, code: &[bool]) -> Option<(usize, &'static str)> {
    let rest = raw.get(from..)?;
    let mut best: Option<(usize, &'static str)> = None;
    for api in VIEM_APIS {
        let mut offset = 0;
        while let Some(at) = rest.get(offset..).and_then(|slice| slice.find(api)) {
            let local = offset + at;
            let absolute = from + local;
            if code_span::at(code, absolute)
                && code_span::ident_start(raw, absolute)
                && is_call(&rest[local + api.len()..])
            {
                if best.is_none_or(|(current, _)| local < current) {
                    best = Some((absolute, api));
                }
                break;
            }
            offset = local + api.len();
        }
    }
    best
}

fn is_call(after: &str) -> bool {
    after.trim_start().starts_with('(')
}

fn object_after(raw: &str, from: usize) -> Option<(&str, usize)> {
    let open = raw.get(from..)?.find('{')? + from;
    let mut depth = 0_i32;
    for (index, ch) in raw[open..].char_indices() {
        match ch {
            '{' => depth += 1,
            '}' => {
                depth -= 1;
                if depth == 0 {
                    let end = open + index + 1;
                    return Some((&raw[open..end], end));
                }
            }
            _ => {}
        }
    }
    None
}

fn unknown_spread_overwrites(object: &str) -> bool {
    object.contains("...")
}

fn literal_prop(object: &str, key: &str) -> Option<String> {
    let patterns = [
        format!("{key}:"),
        format!("{key} :"),
        format!("\"{key}\":"),
        format!("'{key}':"),
    ];
    for pattern in patterns {
        if let Some(at) = object.find(&pattern) {
            let rest = object[at + pattern.len()..].trim_start();
            if let Some(value) = quoted(rest) {
                return Some(value);
            }
        }
    }
    None
}

fn ident_prop(object: &str, key: &str) -> Option<String> {
    let rest = object.split(&format!("{key}:")).nth(1)?.trim_start();
    let ident = rest
        .chars()
        .take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
        .collect::<String>();
    if ident.is_empty() { None } else { Some(ident) }
}

fn quoted(rest: &str) -> Option<String> {
    let mut chars = rest.chars();
    let quote = chars.next()?;
    if quote != '\'' && quote != '"' {
        return None;
    }
    let mut out = String::new();
    for ch in chars {
        if ch == quote {
            return Some(out);
        }
        out.push(ch);
    }
    None
}

fn import_target<'a>(imports: &'a [ImportFact], local: &str) -> Option<&'a str> {
    imports
        .iter()
        .find(|import| import.bindings.iter().any(|binding| binding.local == local))
        .map(|import| import.target.as_str())
}

fn member_kind(api: &str) -> &'static str {
    if api.contains("Event") || api.contains("Log") {
        "event"
    } else {
        "function"
    }
}