1use std::path::PathBuf;
7
8use anyhow::Result;
9use clap::Args;
10use serde::{Deserialize, Serialize};
11
12use tldr_core::{get_slice_rich, Language, SliceDirection};
13
14use crate::commands::daemon_router::{params_with_file_function_line, try_daemon_route};
15use crate::output::{OutputFormat, OutputWriter};
16
17#[derive(Debug, Args)]
19pub struct SliceArgs {
20 pub file: PathBuf,
22
23 pub function: String,
25
26 pub line: u32,
28
29 #[arg(long, short = 'd', default_value = "backward")]
31 pub direction: SliceDirectionArg,
32
33 #[arg(long)]
35 pub variable: Option<String>,
36
37 #[arg(long, short = 'l')]
39 pub lang: Option<Language>,
40}
41
42#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
44pub enum SliceDirectionArg {
45 #[default]
47 Backward,
48 Forward,
50}
51
52impl From<SliceDirectionArg> for SliceDirection {
53 fn from(arg: SliceDirectionArg) -> Self {
54 match arg {
55 SliceDirectionArg::Backward => SliceDirection::Backward,
56 SliceDirectionArg::Forward => SliceDirection::Forward,
57 }
58 }
59}
60
61#[derive(Debug, Serialize, Deserialize)]
63struct SliceLine {
64 line: u32,
65 code: String,
66 #[serde(skip_serializing_if = "Vec::is_empty")]
67 definitions: Vec<String>,
68 #[serde(skip_serializing_if = "Vec::is_empty")]
69 uses: Vec<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 dep_type: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 dep_label: Option<String>,
74}
75
76#[derive(Debug, Serialize, Deserialize)]
78struct SliceEdgeOutput {
79 from_line: u32,
80 to_line: u32,
81 dep_type: String,
82 label: String,
83}
84
85#[derive(Debug, Serialize, Deserialize)]
87struct SliceOutput {
88 file: PathBuf,
89 function: String,
90 criterion_line: u32,
91 direction: String,
92 variable: Option<String>,
93 lines: Vec<u32>,
95 #[serde(skip_serializing_if = "Vec::is_empty")]
97 slice_lines: Vec<SliceLine>,
98 #[serde(skip_serializing_if = "Vec::is_empty")]
100 edges: Vec<SliceEdgeOutput>,
101 line_count: usize,
102}
103
104#[derive(Debug, Serialize, Deserialize)]
106struct LegacySliceOutput {
107 file: PathBuf,
108 function: String,
109 criterion_line: u32,
110 direction: String,
111 variable: Option<String>,
112 lines: Vec<u32>,
113 line_count: usize,
114}
115
116impl SliceArgs {
117 pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
119 let writer = OutputWriter::new(format, quiet);
120
121 let language = self
123 .lang
124 .unwrap_or_else(|| Language::from_path(&self.file).unwrap_or(Language::Python));
125
126 let direction: SliceDirection = self.direction.into();
127 let direction_str = match direction {
128 SliceDirection::Backward => "backward",
129 SliceDirection::Forward => "forward",
130 };
131
132 let project = self.file.parent().unwrap_or(&self.file);
134 if let Some(output) = try_daemon_route::<LegacySliceOutput>(
135 project,
136 "slice",
137 params_with_file_function_line(&self.file, &self.function, self.line),
138 ) {
139 let source_lines = read_file_lines(&self.file);
141 if writer.is_text() {
142 let mut text = String::new();
143 text.push_str(&format!(
144 "Program Slice ({} from line {})\n",
145 output.direction, output.criterion_line
146 ));
147 text.push_str(&format!(
148 "Function: {}::{}\n",
149 output.file.display(),
150 output.function
151 ));
152 if let Some(var) = &output.variable {
153 text.push_str(&format!("Variable: {}\n", var));
154 }
155 text.push_str(&format!(
156 "\nSlice contains {} lines:\n\n",
157 output.lines.len()
158 ));
159 for &line_num in &output.lines {
160 let code = source_lines
161 .get((line_num as usize).wrapping_sub(1))
162 .map(|s| s.trim_end())
163 .unwrap_or("");
164 let marker = if line_num == output.criterion_line {
165 ">"
166 } else {
167 " "
168 };
169 let criterion_flag = if line_num == output.criterion_line {
170 " <-- criterion"
171 } else {
172 ""
173 };
174 text.push_str(&format!(
175 "{} {:>5} | {}{}\n",
176 marker, line_num, code, criterion_flag
177 ));
178 }
179 writer.write_text(&text)?;
180 return Ok(());
181 } else {
182 let slice_lines: Vec<SliceLine> = output
184 .lines
185 .iter()
186 .map(|&l| {
187 let code = source_lines
188 .get((l as usize).wrapping_sub(1))
189 .map(|s| s.trim_end().to_string())
190 .unwrap_or_default();
191 SliceLine {
192 line: l,
193 code,
194 definitions: Vec::new(),
195 uses: Vec::new(),
196 dep_type: None,
197 dep_label: None,
198 }
199 })
200 .collect();
201 let rich_output = SliceOutput {
202 file: output.file,
203 function: output.function,
204 criterion_line: output.criterion_line,
205 direction: output.direction,
206 variable: output.variable,
207 line_count: output.line_count,
208 lines: output.lines,
209 slice_lines,
210 edges: Vec::new(),
211 };
212 writer.write(&rich_output)?;
213 return Ok(());
214 }
215 }
216
217 writer.progress(&format!(
219 "Computing {} slice for line {} in {}::{}...",
220 direction_str,
221 self.line,
222 self.file.display(),
223 self.function
224 ));
225
226 let rich = get_slice_rich(
228 self.file.to_str().unwrap_or_default(),
229 &self.function,
230 self.line,
231 direction,
232 self.variable.as_deref(),
233 language,
234 )?;
235
236 let lines: Vec<u32> = rich.nodes.iter().map(|n| n.line).collect();
238
239 let slice_lines: Vec<SliceLine> = rich
241 .nodes
242 .iter()
243 .map(|n| SliceLine {
244 line: n.line,
245 code: n.code.clone(),
246 definitions: n.definitions.clone(),
247 uses: n.uses.clone(),
248 dep_type: n.dep_type.clone(),
249 dep_label: n.dep_label.clone(),
250 })
251 .collect();
252
253 let edges: Vec<SliceEdgeOutput> = rich
255 .edges
256 .iter()
257 .map(|e| SliceEdgeOutput {
258 from_line: e.from_line,
259 to_line: e.to_line,
260 dep_type: e.dep_type.clone(),
261 label: e.label.clone(),
262 })
263 .collect();
264
265 let data_count = edges.iter().filter(|e| e.dep_type == "data").count();
266 let ctrl_count = edges.iter().filter(|e| e.dep_type == "control").count();
267
268 let output = SliceOutput {
269 file: self.file.clone(),
270 function: self.function.clone(),
271 criterion_line: self.line,
272 direction: direction_str.to_string(),
273 variable: self.variable.clone(),
274 line_count: lines.len(),
275 lines,
276 slice_lines,
277 edges,
278 };
279
280 if writer.is_text() {
282 let text = format_rich_text(&output, data_count, ctrl_count);
283 writer.write_text(&text)?;
284 } else {
285 writer.write(&output)?;
286 }
287
288 Ok(())
289 }
290}
291
292fn format_rich_text(output: &SliceOutput, data_count: usize, ctrl_count: usize) -> String {
294 let mut text = String::new();
295
296 text.push_str(&format!(
297 "Program Slice ({} from line {})\n",
298 output.direction, output.criterion_line
299 ));
300 text.push_str(&format!(
301 "Function: {}::{}\n",
302 output.file.display(),
303 output.function
304 ));
305 if let Some(var) = &output.variable {
306 text.push_str(&format!("Variable: {}\n", var));
307 }
308
309 let non_blank_count = output
311 .slice_lines
312 .iter()
313 .filter(|sl| !sl.code.trim().is_empty())
314 .count();
315
316 if data_count > 0 || ctrl_count > 0 {
318 text.push_str(&format!(
319 "\nSlice contains {} lines ({} data deps, {} control deps):\n\n",
320 non_blank_count, data_count, ctrl_count
321 ));
322 } else {
323 text.push_str(&format!("\nSlice contains {} lines:\n\n", non_blank_count));
324 }
325
326 let mut prev_defs: Option<&Vec<String>> = None;
330 let mut prev_uses: Option<&Vec<String>> = None;
331
332 for sl in &output.slice_lines {
333 if sl.code.trim().is_empty() {
335 continue;
336 }
337
338 let marker = if sl.line == output.criterion_line {
339 ">"
340 } else {
341 " "
342 };
343
344 let same_as_prev = prev_defs == Some(&sl.definitions) && prev_uses == Some(&sl.uses);
346
347 let mut annotations = Vec::new();
348 if !same_as_prev {
349 if !sl.definitions.is_empty() {
350 annotations.push(format!("[defines: {}]", sl.definitions.join(", ")));
351 }
352 if !sl.uses.is_empty() {
353 annotations.push(format!("[uses: {}]", sl.uses.join(", ")));
354 }
355 }
356 if let Some(dt) = &sl.dep_type {
357 if dt == "control" && !same_as_prev {
358 annotations.push("ctrl".to_string());
359 }
360 }
361
362 prev_defs = Some(&sl.definitions);
363 prev_uses = Some(&sl.uses);
364
365 let criterion_flag = if sl.line == output.criterion_line {
366 " <-- criterion"
367 } else {
368 ""
369 };
370
371 let annotation_str = if annotations.is_empty() {
372 String::new()
373 } else {
374 format!(" {}", annotations.join(" "))
375 };
376
377 text.push_str(&format!(
378 "{} {:>5} | {}{}{}\n",
379 marker, sl.line, sl.code, annotation_str, criterion_flag
380 ));
381 }
382
383 if !output.edges.is_empty() {
385 text.push_str("\nDependencies:\n");
386 for edge in &output.edges {
387 if edge.dep_type == "data" && !edge.label.is_empty() {
388 text.push_str(&format!(
389 " {}@{} <- {}@{} (data: {})\n",
390 edge.label, edge.to_line, edge.label, edge.from_line, edge.label
391 ));
392 } else {
393 text.push_str(&format!(
394 " {} <- {} ({})\n",
395 edge.to_line, edge.from_line, edge.dep_type
396 ));
397 }
398 }
399 }
400
401 text
402}
403
404fn read_file_lines(path: &PathBuf) -> Vec<String> {
406 std::fs::read_to_string(path)
407 .map(|c| c.lines().map(|l| l.to_string()).collect())
408 .unwrap_or_default()
409}