1use std::path::PathBuf;
7
8use anyhow::Result;
9use clap::Args;
10use serde::{Deserialize, Serialize};
11
12use tldr_core::ast::function_finder::find_function_bounds_from_path_or_source;
13use tldr_core::{get_slice_rich, Language, SliceDirection};
14
15use crate::commands::daemon_router::{params_with_file_function_line, try_daemon_route};
16use crate::output::{OutputFormat, OutputWriter};
17
18#[derive(Debug, Args)]
20pub struct SliceArgs {
21 pub file: PathBuf,
23
24 pub function: String,
26
27 pub line: u32,
29
30 #[arg(long, short = 'd', default_value = "backward")]
32 pub direction: SliceDirectionArg,
33
34 #[arg(long)]
36 pub variable: Option<String>,
37
38 #[arg(long, short = 'l')]
40 pub lang: Option<Language>,
41}
42
43#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
45pub enum SliceDirectionArg {
46 #[default]
48 Backward,
49 Forward,
51}
52
53impl From<SliceDirectionArg> for SliceDirection {
54 fn from(arg: SliceDirectionArg) -> Self {
55 match arg {
56 SliceDirectionArg::Backward => SliceDirection::Backward,
57 SliceDirectionArg::Forward => SliceDirection::Forward,
58 }
59 }
60}
61
62#[derive(Debug, Serialize, Deserialize)]
64struct SliceLine {
65 line: u32,
66 code: String,
67 #[serde(skip_serializing_if = "Vec::is_empty")]
68 definitions: Vec<String>,
69 #[serde(skip_serializing_if = "Vec::is_empty")]
70 uses: Vec<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 dep_type: Option<String>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 dep_label: Option<String>,
75}
76
77#[derive(Debug, Serialize, Deserialize)]
79struct SliceEdgeOutput {
80 from_line: u32,
81 to_line: u32,
82 dep_type: String,
83 label: String,
84}
85
86#[derive(Debug, Serialize, Deserialize)]
88struct SliceOutput {
89 file: PathBuf,
90 function: String,
91 criterion_line: u32,
92 direction: String,
93 variable: Option<String>,
94 lines: Vec<u32>,
96 #[serde(skip_serializing_if = "Vec::is_empty")]
98 slice_lines: Vec<SliceLine>,
99 #[serde(skip_serializing_if = "Vec::is_empty")]
101 edges: Vec<SliceEdgeOutput>,
102 line_count: usize,
103 #[serde(skip_serializing_if = "Option::is_none")]
108 explanation: Option<String>,
109}
110
111#[derive(Debug, Serialize, Deserialize)]
113struct LegacySliceOutput {
114 file: PathBuf,
115 function: String,
116 criterion_line: u32,
117 direction: String,
118 variable: Option<String>,
119 lines: Vec<u32>,
120 line_count: usize,
121}
122
123impl SliceArgs {
124 pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
126 let writer = OutputWriter::new(format, quiet);
127
128 let language = self
130 .lang
131 .unwrap_or_else(|| Language::from_path(&self.file).unwrap_or(Language::Python));
132
133 let direction: SliceDirection = self.direction.into();
134 let direction_str = match direction {
135 SliceDirection::Backward => "backward",
136 SliceDirection::Forward => "forward",
137 };
138
139 let project = self.file.parent().unwrap_or(&self.file);
141 if let Some(output) = try_daemon_route::<LegacySliceOutput>(
142 project,
143 "slice",
144 params_with_file_function_line(&self.file, &self.function, self.line),
145 ) {
146 let source_lines = read_file_lines(&self.file);
148 if writer.is_text() {
149 let mut text = String::new();
150 text.push_str(&format!(
151 "Program Slice ({} from line {})\n",
152 output.direction, output.criterion_line
153 ));
154 text.push_str(&format!(
155 "Function: {}::{}\n",
156 output.file.display(),
157 output.function
158 ));
159 if let Some(var) = &output.variable {
160 text.push_str(&format!("Variable: {}\n", var));
161 }
162 if output.lines.is_empty() {
164 if let Some(diag) = slice_oor_explanation(
165 self.file.to_str().unwrap_or_default(),
166 &self.function,
167 self.line,
168 language,
169 ) {
170 text.push_str(&format!("\n{}\n", diag));
171 writer.write_text(&text)?;
172 return Ok(());
173 }
174 }
175 text.push_str(&format!(
176 "\nSlice contains {} lines:\n\n",
177 output.lines.len()
178 ));
179 for &line_num in &output.lines {
180 let code = source_lines
181 .get((line_num as usize).wrapping_sub(1))
182 .map(|s| s.trim_end())
183 .unwrap_or("");
184 let marker = if line_num == output.criterion_line {
185 ">"
186 } else {
187 " "
188 };
189 let criterion_flag = if line_num == output.criterion_line {
190 " <-- criterion"
191 } else {
192 ""
193 };
194 text.push_str(&format!(
195 "{} {:>5} | {}{}\n",
196 marker, line_num, code, criterion_flag
197 ));
198 }
199 writer.write_text(&text)?;
200 return Ok(());
201 } else {
202 let slice_lines: Vec<SliceLine> = output
204 .lines
205 .iter()
206 .map(|&l| {
207 let code = source_lines
208 .get((l as usize).wrapping_sub(1))
209 .map(|s| s.trim_end().to_string())
210 .unwrap_or_default();
211 SliceLine {
212 line: l,
213 code,
214 definitions: Vec::new(),
215 uses: Vec::new(),
216 dep_type: None,
217 dep_label: None,
218 }
219 })
220 .collect();
221 let explanation = if output.lines.is_empty() {
224 slice_oor_explanation(
225 self.file.to_str().unwrap_or_default(),
226 &self.function,
227 self.line,
228 language,
229 )
230 } else {
231 None
232 };
233 let rich_output = SliceOutput {
234 file: output.file,
235 function: output.function,
236 criterion_line: output.criterion_line,
237 direction: output.direction,
238 variable: output.variable,
239 line_count: output.line_count,
240 lines: output.lines,
241 slice_lines,
242 edges: Vec::new(),
243 explanation,
244 };
245 writer.write(&rich_output)?;
246 return Ok(());
247 }
248 }
249
250 writer.progress(&format!(
252 "Computing {} slice for line {} in {}::{}...",
253 direction_str,
254 self.line,
255 self.file.display(),
256 self.function
257 ));
258
259 let rich = get_slice_rich(
261 self.file.to_str().unwrap_or_default(),
262 &self.function,
263 self.line,
264 direction,
265 self.variable.as_deref(),
266 language,
267 )?;
268
269 let lines: Vec<u32> = rich.nodes.iter().map(|n| n.line).collect();
271
272 let slice_lines: Vec<SliceLine> = rich
274 .nodes
275 .iter()
276 .map(|n| SliceLine {
277 line: n.line,
278 code: n.code.clone(),
279 definitions: n.definitions.clone(),
280 uses: n.uses.clone(),
281 dep_type: n.dep_type.clone(),
282 dep_label: n.dep_label.clone(),
283 })
284 .collect();
285
286 let edges: Vec<SliceEdgeOutput> = rich
288 .edges
289 .iter()
290 .map(|e| SliceEdgeOutput {
291 from_line: e.from_line,
292 to_line: e.to_line,
293 dep_type: e.dep_type.clone(),
294 label: e.label.clone(),
295 })
296 .collect();
297
298 let data_count = edges.iter().filter(|e| e.dep_type == "data").count();
299 let ctrl_count = edges.iter().filter(|e| e.dep_type == "control").count();
300
301 let explanation = if lines.is_empty() {
306 slice_oor_explanation(
307 self.file.to_str().unwrap_or_default(),
308 &self.function,
309 self.line,
310 language,
311 )
312 } else {
313 None
314 };
315
316 let output = SliceOutput {
317 file: self.file.clone(),
318 function: self.function.clone(),
319 criterion_line: self.line,
320 direction: direction_str.to_string(),
321 variable: self.variable.clone(),
322 line_count: lines.len(),
323 lines,
324 slice_lines,
325 edges,
326 explanation,
327 };
328
329 if writer.is_text() {
331 let text = format_rich_text(&output, data_count, ctrl_count);
332 writer.write_text(&text)?;
333 } else {
334 writer.write(&output)?;
335 }
336
337 Ok(())
338 }
339}
340
341fn format_rich_text(output: &SliceOutput, data_count: usize, ctrl_count: usize) -> String {
343 let mut text = String::new();
344
345 text.push_str(&format!(
346 "Program Slice ({} from line {})\n",
347 output.direction, output.criterion_line
348 ));
349 text.push_str(&format!(
350 "Function: {}::{}\n",
351 output.file.display(),
352 output.function
353 ));
354 if let Some(var) = &output.variable {
355 text.push_str(&format!("Variable: {}\n", var));
356 }
357
358 if let Some(diag) = &output.explanation {
360 text.push_str(&format!("\n{}\n", diag));
361 return text;
362 }
363
364 let non_blank_count = output
366 .slice_lines
367 .iter()
368 .filter(|sl| !sl.code.trim().is_empty())
369 .count();
370
371 if data_count > 0 || ctrl_count > 0 {
373 text.push_str(&format!(
374 "\nSlice contains {} lines ({} data deps, {} control deps):\n\n",
375 non_blank_count, data_count, ctrl_count
376 ));
377 } else {
378 text.push_str(&format!("\nSlice contains {} lines:\n\n", non_blank_count));
379 }
380
381 let mut prev_defs: Option<&Vec<String>> = None;
385 let mut prev_uses: Option<&Vec<String>> = None;
386
387 for sl in &output.slice_lines {
388 if sl.code.trim().is_empty() {
390 continue;
391 }
392
393 let marker = if sl.line == output.criterion_line {
394 ">"
395 } else {
396 " "
397 };
398
399 let same_as_prev = prev_defs == Some(&sl.definitions) && prev_uses == Some(&sl.uses);
401
402 let mut annotations = Vec::new();
403 if !same_as_prev {
404 if !sl.definitions.is_empty() {
405 annotations.push(format!("[defines: {}]", sl.definitions.join(", ")));
406 }
407 if !sl.uses.is_empty() {
408 annotations.push(format!("[uses: {}]", sl.uses.join(", ")));
409 }
410 }
411 if let Some(dt) = &sl.dep_type {
412 if dt == "control" && !same_as_prev {
413 annotations.push("ctrl".to_string());
414 }
415 }
416
417 prev_defs = Some(&sl.definitions);
418 prev_uses = Some(&sl.uses);
419
420 let criterion_flag = if sl.line == output.criterion_line {
421 " <-- criterion"
422 } else {
423 ""
424 };
425
426 let annotation_str = if annotations.is_empty() {
427 String::new()
428 } else {
429 format!(" {}", annotations.join(" "))
430 };
431
432 text.push_str(&format!(
433 "{} {:>5} | {}{}{}\n",
434 marker, sl.line, sl.code, annotation_str, criterion_flag
435 ));
436 }
437
438 if !output.edges.is_empty() {
440 text.push_str("\nDependencies:\n");
441 for edge in &output.edges {
442 if edge.dep_type == "data" && !edge.label.is_empty() {
443 text.push_str(&format!(
444 " {}@{} <- {}@{} (data: {})\n",
445 edge.label, edge.to_line, edge.label, edge.from_line, edge.label
446 ));
447 } else {
448 text.push_str(&format!(
449 " {} <- {} ({})\n",
450 edge.to_line, edge.from_line, edge.dep_type
451 ));
452 }
453 }
454 }
455
456 text
457}
458
459fn slice_oor_explanation(
467 source_or_path: &str,
468 function_name: &str,
469 line: u32,
470 language: Language,
471) -> Option<String> {
472 let (start, end) =
473 find_function_bounds_from_path_or_source(source_or_path, function_name, language)?;
474 if line < start || line > end {
475 Some(format!(
476 "Analysis could not be completed: line {} is outside function '{}' (lines {}-{})",
477 line, function_name, start, end
478 ))
479 } else {
480 None
481 }
482}
483
484fn read_file_lines(path: &PathBuf) -> Vec<String> {
488 std::fs::read_to_string(path)
489 .map(|c| c.lines().map(|l| l.to_string()).collect())
490 .unwrap_or_default()
491}