agent-file-tools 0.36.0

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
use std::path::{Path, PathBuf};

use crate::commands::callgraph_store_adapter::{
    building_response, ensure_symbol_resolves, store_error_response, trace_to_symbol_candidates,
    trace_to_symbol_result, unavailable_response,
};
use crate::context::{AppContext, CallgraphStoreAccess};
use crate::protocol::{RawRequest, Response};

/// Handle a `trace_to_symbol` request.
pub fn handle_trace_to_symbol(req: &RawRequest, ctx: &AppContext) -> Response {
    let file = match req.params.get("file").and_then(|v| v.as_str()) {
        Some(f) => f,
        None => {
            return Response::error(
                &req.id,
                "invalid_request",
                "trace_to_symbol: missing required param 'file'",
            );
        }
    };

    let symbol = match req.params.get("symbol").and_then(|v| v.as_str()) {
        Some(s) => s,
        None => {
            return Response::error(
                &req.id,
                "invalid_request",
                "trace_to_symbol: missing required param 'symbol'",
            );
        }
    };

    let to_symbol = match req.params.get("toSymbol").and_then(|v| v.as_str()) {
        Some(s) => s,
        None => {
            return Response::error(
                &req.id,
                "invalid_request",
                "trace_to_symbol: missing required param 'toSymbol'",
            );
        }
    };

    let depth = req
        .params
        .get("depth")
        .and_then(|v| v.as_u64())
        .unwrap_or(10)
        .min(16) as usize;

    let project_root = ctx.config().project_root.clone();

    let file_path = match validate_callgraph_path(req, ctx, file) {
        Ok(path) => path,
        Err(resp) => return resp,
    };

    let to_file_arg = req.params.get("toFile").and_then(|v| v.as_str());
    if let Some(to_file) = to_file_arg {
        let requested_path = resolve_request_path(project_root.as_deref(), to_file);
        if !requested_path.exists() {
            return Response::error(
                &req.id,
                "to_file_not_found",
                format!("trace_to_symbol: toFile not found: {}", to_file),
            );
        }
    }

    let to_file_path = match to_file_arg {
        Some(to_file) => match validate_callgraph_path(req, ctx, to_file) {
            Ok(path) => Some(path),
            Err(resp) => return resp,
        },
        None => None,
    };

    let store = match ctx.callgraph_store_for_ops() {
        CallgraphStoreAccess::Ready(store) => store,
        CallgraphStoreAccess::Building => return building_response(&req.id, "trace_to_symbol"),
        CallgraphStoreAccess::Unavailable => {
            return unavailable_response(&req.id, "trace_to_symbol", ctx.is_worktree_bridge())
        }
        CallgraphStoreAccess::Error(error) => {
            return store_error_response(&req.id, "trace_to_symbol", error)
        }
    };

    if let Err(error) = ensure_symbol_resolves(&store, &file_path, symbol) {
        return store_error_response(&req.id, "trace_to_symbol", error);
    }

    let target_candidates = match trace_to_symbol_candidates(&store, to_symbol) {
        Ok(candidates) => candidates,
        Err(error) => return store_error_response(&req.id, "trace_to_symbol", error),
    };

    if let Some(to_file_path) = to_file_path.as_deref() {
        if !target_candidates.iter().any(|candidate| {
            trace_candidate_matches_file(project_root.as_deref(), &candidate.file, to_file_path)
        }) {
            let candidates_json = serde_json::to_value(&target_candidates).unwrap_or_default();
            return Response::error_with_data(
                &req.id,
                "target_symbol_not_in_file",
                format!(
                    "trace_to_symbol: target symbol '{}' is not defined in toFile: {}",
                    to_symbol,
                    to_file_arg.unwrap_or("<unknown>")
                ),
                serde_json::json!({ "candidates": candidates_json }),
            );
        }
    } else {
        match target_candidates.len() {
            0 => {
                return Response::error(
                    &req.id,
                    "target_symbol_not_found",
                    format!("trace_to_symbol: target symbol '{}' not found", to_symbol),
                );
            }
            1 => {}
            _ => {
                let candidates_json = serde_json::to_value(&target_candidates).unwrap_or_default();
                return Response::error_with_data(
                    &req.id,
                    "ambiguous_target",
                    format!(
                        "trace_to_symbol: target symbol '{}' exists in multiple files; pass 'toFile' to disambiguate",
                        to_symbol
                    ),
                    serde_json::json!({ "candidates": candidates_json }),
                );
            }
        }
    }

    match trace_to_symbol_result(
        &store,
        &file_path,
        symbol,
        to_symbol,
        to_file_path.as_deref(),
        depth,
    ) {
        Ok(result) => {
            let result_json = serde_json::to_value(&result).unwrap_or_default();
            Response::success(&req.id, result_json)
        }
        Err(error) => store_error_response(&req.id, "trace_to_symbol", error),
    }
}

fn resolve_request_path(project_root: Option<&Path>, file: &str) -> PathBuf {
    let path = Path::new(file);
    if path.is_relative() {
        project_root
            .map(|root| root.join(path))
            .unwrap_or_else(|| path.to_path_buf())
    } else {
        path.to_path_buf()
    }
}

fn trace_candidate_matches_file(
    project_root: Option<&Path>,
    candidate_file: &str,
    target_file: &Path,
) -> bool {
    let candidate_path = resolve_request_path(project_root, candidate_file);
    canonicalize_for_compare(&candidate_path) == canonicalize_for_compare(target_file)
}

fn canonicalize_for_compare(path: &Path) -> PathBuf {
    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}

fn validate_callgraph_path(
    req: &RawRequest,
    ctx: &AppContext,
    file: &str,
) -> Result<PathBuf, Response> {
    let file_path = ctx.validate_path(&req.id, Path::new(file))?;

    let project_root = ctx.config().project_root.clone();
    if let Some(project_root) = project_root {
        let canonical_root = std::fs::canonicalize(&project_root).unwrap_or(project_root.clone());
        let input_for_resolution = if file_path.is_relative() {
            project_root.join(&file_path)
        } else {
            file_path.clone()
        };
        let canonical_input =
            std::fs::canonicalize(&input_for_resolution).unwrap_or(input_for_resolution);
        if !canonical_input.starts_with(&canonical_root) {
            return Err(Response::error(
                &req.id,
                "path_outside_project_root",
                format!(
                    "Callgraph operations require paths inside project_root. Got: {} (project_root: {})",
                    file_path.display(),
                    project_root.display(),
                ),
            ));
        }
    }

    Ok(file_path)
}