weavatrix-rust 2.11.0

Protocol-independent Rust repository intelligence: typed evidence graphs for impact, architecture, APIs, Git, search, and memory
Documentation
use super::{
    Call, Language, Path, RepositoryState, Token, TokenKind, argument_segments, call_chain,
    call_name_index, literal_value, matching_close, property, source_line, tokenize,
};
use std::collections::HashMap;
use std::fs;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum HttpMethodEvidence {
    Proven(&'static str),
    Unresolved,
}

impl HttpMethodEvidence {
    pub(super) fn as_json(self) -> &'static str {
        match self {
            Self::Proven(method) => method,
            Self::Unresolved => "UNRESOLVED",
        }
    }

    pub(super) fn mismatches(self, backend_method: &str) -> bool {
        match self {
            Self::Unresolved => false,
            Self::Proven(method) => {
                backend_method != "ANY" && backend_method != "ALL" && method != backend_method
            }
        }
    }
}

pub(super) struct ProvenCallsite {
    pub(super) line: u32,
    pub(super) method: HttpMethodEvidence,
    pub(super) exact_literal: bool,
    pub(super) evidence: String,
}

pub(super) struct SourceCache {
    files: HashMap<String, Option<(Language, String)>>,
}

impl SourceCache {
    pub(super) fn new() -> Self {
        Self {
            files: HashMap::new(),
        }
    }

    pub(super) fn resolve_callsite(
        &mut self,
        client: &RepositoryState,
        path: &str,
        line: u32,
        route: &str,
    ) -> Option<ProvenCallsite> {
        let (language, source) = self.load(client, path)?;
        find_proven_callsite(source, language, route, line)
    }

    fn load(&mut self, client: &RepositoryState, path: &str) -> Option<(Language, &str)> {
        if !self.files.contains_key(path) {
            let loaded = super::scan::language_for_path(path).and_then(|language| {
                let relative = Path::new(path);
                if relative.is_absolute()
                    || relative.components().any(|component| {
                        matches!(
                            component,
                            std::path::Component::ParentDir
                                | std::path::Component::RootDir
                                | std::path::Component::Prefix(_)
                        )
                    })
                {
                    return None;
                }
                fs::read_to_string(client.root().join(relative))
                    .ok()
                    .map(|source| (language, source))
            });
            self.files.insert(path.to_owned(), loaded);
        }
        self.files
            .get(path)
            .and_then(|entry| entry.as_ref())
            .map(|(language, source)| (*language, source.as_str()))
    }
}

pub(super) fn find_proven_callsite(
    source: &str,
    language: Language,
    route: &str,
    line: u32,
) -> Option<ProvenCallsite> {
    let tokens = tokenize(source, language)
        .into_iter()
        .filter(|token| !token.is_trivia())
        .collect::<Vec<_>>();
    let mut index = 0_usize;
    while index < tokens.len() {
        if tokens[index].text(source) != "(" {
            index += 1;
            continue;
        }
        let Some(name_index) = call_name_index(&tokens, source, index) else {
            index += 1;
            continue;
        };
        let Some(close) = matching_close(&tokens, source, index) else {
            break;
        };
        let name = tokens[name_index].text(source).to_owned();
        let chain = call_chain(&tokens, source, name_index);
        let call = Call {
            name,
            chain,
            receiver: None,
            args: &tokens[index + 1..close],
            line: tokens[name_index].line,
            column: tokens[name_index].column,
            evidence: source_line(source, tokens[name_index].line),
            source,
        };
        if call_has_route_literal_on_line(&call, route, line) && looks_like_http_call(&call) {
            let exact_literal = call.args.iter().any(|token| {
                token.kind == TokenKind::String
                    && token.line == line
                    && literal_value(token.text(source)).is_some_and(|value| value.contains(route))
            });
            return Some(ProvenCallsite {
                line: call.line,
                method: resolve_method(&call),
                exact_literal,
                evidence: call.evidence,
            });
        }
        index += 1;
    }
    None
}

fn call_has_route_literal_on_line(call: &Call<'_, '_>, route: &str, line: u32) -> bool {
    call.args.iter().any(|token| {
        token.kind == TokenKind::String
            && token.line == line
            && literal_value(token.text(call.source))
                .is_some_and(|value| super::http_route::route_matches(route, &value))
    })
}

fn looks_like_http_call(call: &Call<'_, '_>) -> bool {
    let name = call.name.to_ascii_lowercase();
    let chain = call.chain.to_ascii_lowercase();
    if matches!(
        name.as_str(),
        "fetch" | "request" | "ajax" | "axios" | "got" | "ofetch" | "ky"
    ) {
        return true;
    }
    if chain.contains("axios") || chain.contains("httpclient") || chain.contains("xmlhttprequest") {
        return true;
    }
    ["delete", "patch", "post", "put", "head", "options", "get"]
        .into_iter()
        .any(|verb| chain == verb || chain.ends_with(&format!(".{verb}")))
}

pub(super) fn resolve_method(call: &Call<'_, '_>) -> HttpMethodEvidence {
    let chain = call.chain.to_ascii_lowercase();
    for (suffix, method) in [
        (".delete", "DELETE"),
        (".patch", "PATCH"),
        (".post", "POST"),
        (".put", "PUT"),
        (".head", "HEAD"),
        (".options", "OPTIONS"),
        (".get", "GET"),
    ] {
        if chain == suffix.trim_start_matches('.') || chain.ends_with(suffix) {
            return HttpMethodEvidence::Proven(method);
        }
    }
    if let Some(value) = property(call, &["method", "httpmethod"]) {
        return normalize_method(&value)
            .map_or(HttpMethodEvidence::Unresolved, HttpMethodEvidence::Proven);
    }
    if has_method_key(call) {
        return HttpMethodEvidence::Unresolved;
    }
    let segments = argument_segments(call.args, call.source);
    if segments
        .get(1)
        .is_some_and(|segment| is_sole_identifier(segment))
    {
        return HttpMethodEvidence::Unresolved;
    }
    if call.name.eq_ignore_ascii_case("fetch")
        || call.name.eq_ignore_ascii_case("axios")
        || call.name.eq_ignore_ascii_case("ofetch")
        || call.name.eq_ignore_ascii_case("ky")
        || call.name.eq_ignore_ascii_case("got")
    {
        return HttpMethodEvidence::Proven("GET");
    }
    HttpMethodEvidence::Unresolved
}

fn normalize_method(value: &str) -> Option<&'static str> {
    match value.trim().to_ascii_uppercase().as_str() {
        "DELETE" => Some("DELETE"),
        "PATCH" => Some("PATCH"),
        "POST" => Some("POST"),
        "PUT" => Some("PUT"),
        "HEAD" => Some("HEAD"),
        "OPTIONS" => Some("OPTIONS"),
        "GET" => Some("GET"),
        _ => None,
    }
}

fn has_method_key(call: &Call<'_, '_>) -> bool {
    call.args.iter().enumerate().any(|(index, token)| {
        if token.kind != TokenKind::Identifier {
            return false;
        }
        let key = token.text(call.source).to_ascii_lowercase();
        if key != "method" && key != "httpmethod" {
            return false;
        }
        call.args
            .get(index + 1)
            .is_some_and(|separator| matches!(separator.text(call.source), ":" | "="))
    })
}

fn is_sole_identifier(segment: &[Token]) -> bool {
    matches!(segment, [token] if token.kind == TokenKind::Identifier)
}