source-map-tauri 0.3.0

Static Tauri app scanner that emits Meilisearch-ready NDJSON
Documentation
use std::path::Path;

use regex::Regex;
use serde_json::{json, Map, Value};

use crate::{
    config::{normalize_path, ResolvedConfig},
    ids::document_id,
    model::ArtifactDoc,
    security::apply_artifact_security,
};

#[derive(Debug)]
struct ExportedFunction<'a> {
    name: &'a str,
    line: u32,
    body: &'a str,
}

fn line_number(text: &str, offset: usize) -> u32 {
    text[..offset].bytes().filter(|byte| *byte == b'\n').count() as u32 + 1
}

fn base_artifact(
    config: &ResolvedConfig,
    path: &Path,
    kind: &str,
    name: &str,
    line: u32,
) -> ArtifactDoc {
    let source_path = normalize_path(&config.root, path);
    let mut doc = ArtifactDoc {
        id: document_id(
            &config.repo,
            kind,
            Some(&source_path),
            Some(line),
            Some(name),
        ),
        repo: config.repo.clone(),
        kind: kind.to_owned(),
        side: Some("frontend".to_owned()),
        language: crate::frontend::language_for_path(path),
        name: Some(name.to_owned()),
        display_name: Some(name.to_owned()),
        source_path: Some(source_path),
        line_start: Some(line),
        line_end: Some(line),
        column_start: None,
        column_end: None,
        package_name: None,
        comments: Vec::new(),
        tags: Vec::new(),
        related_symbols: Vec::new(),
        related_tests: Vec::new(),
        risk_level: "low".to_owned(),
        risk_reasons: Vec::new(),
        contains_phi: false,
        has_related_tests: false,
        updated_at: chrono::Utc::now().to_rfc3339(),
        data: Map::new(),
    };
    apply_artifact_security(&mut doc);
    doc
}

pub fn extract_http_artifacts(
    config: &ResolvedConfig,
    path: &Path,
    text: &str,
) -> Vec<ArtifactDoc> {
    let mut artifacts = Vec::new();

    for exported in exported_functions(text) {
        if let Some((transport_name, method, normalized_path)) =
            wrapper_transport_call(exported.body)
        {
            let mut doc = base_artifact(
                config,
                path,
                "frontend_api_wrapper",
                exported.name,
                exported.line,
            );
            doc.display_name = Some(format!("{} wrapper", exported.name));
            doc.tags = vec!["api wrapper".to_owned(), "http".to_owned()];
            doc.data.insert(
                "transport_name".to_owned(),
                Value::String(transport_name.to_owned()),
            );
            doc.data
                .insert("http_method".to_owned(), Value::String(method.to_owned()));
            doc.data.insert(
                "normalized_path".to_owned(),
                Value::String(normalized_path.clone()),
            );
            doc.data.insert(
                "endpoint_key".to_owned(),
                Value::String(format!("{method} {normalized_path}")),
            );
            apply_artifact_security(&mut doc);
            artifacts.push(doc);
        } else if let Some((method, normalized_path)) = direct_http_call(exported.body) {
            let mut doc = base_artifact(
                config,
                path,
                "frontend_api_wrapper",
                exported.name,
                exported.line,
            );
            doc.display_name = Some(format!("{} wrapper", exported.name));
            doc.tags = vec!["api wrapper".to_owned(), "http".to_owned()];
            doc.data.insert(
                "transport_name".to_owned(),
                Value::String("tauriFetch".to_owned()),
            );
            doc.data
                .insert("http_method".to_owned(), Value::String(method.to_owned()));
            doc.data.insert(
                "normalized_path".to_owned(),
                Value::String(normalized_path.clone()),
            );
            doc.data.insert(
                "endpoint_key".to_owned(),
                Value::String(format!("{method} {normalized_path}")),
            );
            apply_artifact_security(&mut doc);
            artifacts.push(doc);
        }

        if let Some((method, client_name, path_param, url_pattern)) =
            transport_definition(exported.body)
        {
            let mut doc = base_artifact(
                config,
                path,
                "frontend_transport",
                exported.name,
                exported.line,
            );
            doc.display_name = Some(format!("{} transport", exported.name));
            doc.tags = vec!["transport".to_owned(), "http".to_owned()];
            doc.data
                .insert("http_method".to_owned(), Value::String(method.to_owned()));
            doc.data.insert(
                "http_client".to_owned(),
                Value::String(client_name.to_owned()),
            );
            doc.data.insert(
                "path_param".to_owned(),
                Value::String(path_param.to_owned()),
            );
            doc.data.insert(
                "url_pattern".to_owned(),
                Value::String(url_pattern.to_owned()),
            );
            doc.data.insert(
                "transport_signature".to_owned(),
                json!({
                    "client": client_name,
                    "method": method,
                    "path_param": path_param,
                }),
            );
            apply_artifact_security(&mut doc);
            artifacts.push(doc);
        }
    }

    artifacts
}

fn exported_functions(text: &str) -> Vec<ExportedFunction<'_>> {
    let function_re = Regex::new(r"(?m)^\s*export\s+function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(")
        .expect("valid regex");
    let const_re =
        Regex::new(r"(?m)^\s*export\s+const\s+([A-Za-z_][A-Za-z0-9_]*)\s*=").expect("valid regex");

    let mut items = Vec::new();
    for regex in [&function_re, &const_re] {
        for capture in regex.captures_iter(text) {
            let whole = capture.get(0).expect("match");
            let Some(name) = capture.get(1).map(|value| value.as_str()) else {
                continue;
            };
            let Some(body_start) = text[whole.start()..]
                .find('{')
                .map(|offset| whole.start() + offset)
            else {
                continue;
            };
            let Some(body_end) = find_matching_brace(text, body_start) else {
                continue;
            };
            items.push(ExportedFunction {
                name,
                line: line_number(text, whole.start()),
                body: &text[body_start + 1..body_end],
            });
        }
    }

    items.sort_by_key(|item| item.line);
    items
}

fn find_matching_brace(text: &str, open_index: usize) -> Option<usize> {
    let bytes = text.as_bytes();
    let mut depth = 0_u32;
    let mut index = open_index;
    let mut in_single = false;
    let mut in_double = false;
    let mut in_template = false;
    let mut line_comment = false;
    let mut block_comment = false;
    let mut escaped = false;

    while index < bytes.len() {
        let byte = bytes[index];
        let next = bytes.get(index + 1).copied();

        if line_comment {
            if byte == b'\n' {
                line_comment = false;
            }
            index += 1;
            continue;
        }

        if block_comment {
            if byte == b'*' && next == Some(b'/') {
                block_comment = false;
                index += 2;
            } else {
                index += 1;
            }
            continue;
        }

        if escaped {
            escaped = false;
            index += 1;
            continue;
        }

        match byte {
            b'\\' if in_single || in_double || in_template => {
                escaped = true;
                index += 1;
            }
            b'\'' if !in_double && !in_template => {
                in_single = !in_single;
                index += 1;
            }
            b'"' if !in_single && !in_template => {
                in_double = !in_double;
                index += 1;
            }
            b'`' if !in_single && !in_double => {
                in_template = !in_template;
                index += 1;
            }
            b'/' if !in_single && !in_double && !in_template && next == Some(b'/') => {
                line_comment = true;
                index += 2;
            }
            b'/' if !in_single && !in_double && !in_template && next == Some(b'*') => {
                block_comment = true;
                index += 2;
            }
            b'{' if !in_single && !in_double => {
                depth += 1;
                index += 1;
            }
            b'}' if !in_single && !in_double => {
                depth = depth.saturating_sub(1);
                if depth == 0 {
                    return Some(index);
                }
                index += 1;
            }
            _ => {
                index += 1;
            }
        }
    }

    None
}

fn wrapper_transport_call(body: &str) -> Option<(&'static str, &'static str, String)> {
    let call_re = Regex::new(
        r#"\b(usePostApi|usePostMutation|usePostUploadMutation|postApi)\s*(?:<[^()]+>)?\s*\(\s*["']([^"']+)["']"#,
    )
    .expect("valid regex");
    let capture = call_re.captures(body)?;
    let transport_name = capture.get(1)?.as_str();
    let raw_path = capture.get(2)?.as_str();
    let method = match transport_name {
        "usePostApi" | "usePostMutation" | "usePostUploadMutation" | "postApi" => "POST",
        _ => return None,
    };
    Some((
        transport_name_static(transport_name)?,
        method,
        normalize_http_path(raw_path),
    ))
}

fn transport_definition(
    body: &str,
) -> Option<(&'static str, &'static str, &'static str, &'static str)> {
    let pattern_re = Regex::new(
        r#"(?s)\btauriFetch\s*\(\s*`[^`]*\$\{API_URL\}/\$\{([A-Za-z_][A-Za-z0-9_]*)\}[^`]*`\s*,\s*\{.*?method\s*:\s*["']([A-Z]+)["']"#,
    )
    .expect("valid regex");
    let capture = pattern_re.captures(body)?;
    let path_param = capture.get(1)?.as_str();
    let method = capture.get(2)?.as_str();
    if path_param != "path" || method != "POST" {
        return None;
    }
    Some(("POST", "tauriFetch", "path", "${API_URL}/${path}"))
}

fn direct_http_call(body: &str) -> Option<(&'static str, String)> {
    let direct_re = Regex::new(
        r#"(?s)\btauriFetch\s*\(\s*`[^`]*\$\{API_URL\}/([^`$]+)`\s*,\s*\{.*?method\s*:\s*["']([A-Z]+)["']"#,
    )
    .expect("valid regex");
    let capture = direct_re.captures(body)?;
    let raw_path = capture.get(1)?.as_str().trim();
    let method = capture.get(2)?.as_str();
    if method != "POST" {
        return None;
    }
    Some((method_static(method)?, normalize_http_path(raw_path)))
}

fn normalize_http_path(path: &str) -> String {
    let trimmed = path.trim();
    if trimmed.starts_with('/') {
        trimmed.to_owned()
    } else {
        format!("/{trimmed}")
    }
}

fn transport_name_static(name: &str) -> Option<&'static str> {
    match name {
        "usePostApi" => Some("usePostApi"),
        "usePostMutation" => Some("usePostMutation"),
        "usePostUploadMutation" => Some("usePostUploadMutation"),
        "postApi" => Some("postApi"),
        _ => None,
    }
}

fn method_static(method: &str) -> Option<&'static str> {
    match method {
        "POST" => Some("POST"),
        "GET" => Some("GET"),
        "PUT" => Some("PUT"),
        "PATCH" => Some("PATCH"),
        "DELETE" => Some("DELETE"),
        _ => None,
    }
}