Skip to main content

aft/commands/
trace_data.rs

1use std::path::Path;
2
3use crate::context::AppContext;
4use crate::error::AftError;
5use crate::protocol::{RawRequest, Response};
6
7/// Handle a `trace_data` request.
8///
9/// Traces how an expression flows through variable assignments within a
10/// function body and across function boundaries via argument-to-parameter
11/// matching. Destructuring, spread, and unresolved calls produce approximate
12/// hops and stop tracking.
13///
14/// Expects:
15/// - `file` (string, required) — path to the source file containing the symbol
16/// - `symbol` (string, required) — name of the function containing the expression
17/// - `expression` (string, required) — the expression/variable name to track
18/// - `depth` (number, optional, default 5) — maximum cross-file hop depth
19///
20/// Returns `TraceDataResult` with fields: `expression`, `origin_file`,
21/// `origin_symbol`, `hops` (array of DataFlowHop), `depth_limited`.
22///
23/// Returns error if:
24/// - required params missing
25/// - call graph not initialized (configure not called)
26/// - symbol not found in the file
27pub fn handle_trace_data(req: &RawRequest, ctx: &AppContext) -> Response {
28    let file = match req.params.get("file").and_then(|v| v.as_str()) {
29        Some(f) => f,
30        None => {
31            return Response::error(
32                &req.id,
33                "invalid_request",
34                "trace_data: missing required param 'file'",
35            );
36        }
37    };
38
39    let symbol = match req.params.get("symbol").and_then(|v| v.as_str()) {
40        Some(s) => s,
41        None => {
42            return Response::error(
43                &req.id,
44                "invalid_request",
45                "trace_data: missing required param 'symbol'",
46            );
47        }
48    };
49
50    let expression = match req.params.get("expression").and_then(|v| v.as_str()) {
51        Some(e) => e,
52        None => {
53            return Response::error(
54                &req.id,
55                "invalid_request",
56                "trace_data: missing required param 'expression'",
57            );
58        }
59    };
60
61    let depth = req
62        .params
63        .get("depth")
64        .and_then(|v| v.as_u64())
65        .unwrap_or(5)
66        .min(100) as usize;
67
68    let mut cg_ref = ctx.callgraph().borrow_mut();
69    let graph = match cg_ref.as_mut() {
70        Some(g) => g,
71        None => {
72            return Response::error(
73                &req.id,
74                "not_configured",
75                "trace_data: project not configured — send 'configure' first",
76            );
77        }
78    };
79
80    let file_path = match ctx.validate_path(&req.id, Path::new(file)) {
81        Ok(path) => path,
82        Err(resp) => return resp,
83    };
84
85    let project_root = ctx.config().project_root.clone();
86    if let Some(project_root) = project_root {
87        let canonical_root = std::fs::canonicalize(&project_root).unwrap_or(project_root.clone());
88        let input_for_resolution = if file_path.is_relative() {
89            project_root.join(&file_path)
90        } else {
91            file_path.clone()
92        };
93        let canonical_input =
94            std::fs::canonicalize(&input_for_resolution).unwrap_or(input_for_resolution);
95        if !canonical_input.starts_with(&canonical_root) {
96            return Response::error(
97                &req.id,
98                "path_outside_project_root",
99                format!(
100                    "Callgraph operations require paths inside project_root. Got: {} (project_root: {})",
101                    file_path.display(),
102                    project_root.display(),
103                ),
104            );
105        }
106    }
107
108    // Build file data first to check if the symbol exists
109    match graph.build_file(&file_path) {
110        Ok(data) => {
111            let has_symbol = data.calls_by_symbol.contains_key(symbol)
112                || data.exported_symbols.contains(&symbol.to_string())
113                || data.symbol_metadata.contains_key(symbol);
114            if !has_symbol {
115                return Response::error(
116                    &req.id,
117                    "symbol_not_found",
118                    format!("trace_data: symbol '{}' not found in {}", symbol, file),
119                );
120            }
121        }
122        Err(e) => {
123            return Response::error(&req.id, e.code(), e.to_string());
124        }
125    }
126
127    let max_files = ctx.config().max_callgraph_files;
128
129    match graph.trace_data(&file_path, symbol, expression, depth, max_files) {
130        Ok(result) => {
131            let result_json = serde_json::to_value(&result).unwrap_or_default();
132            Response::success(&req.id, result_json)
133        }
134        Err(err @ AftError::ProjectTooLarge { .. }) => {
135            Response::error(&req.id, "project_too_large", format!("{}", err))
136        }
137        Err(e) => Response::error(&req.id, e.code(), e.to_string()),
138    }
139}