Skip to main content

nu_command/debug/
ast.rs

1use nu_engine::command_prelude::*;
2use nu_parser::{flatten_block, parse};
3use nu_protocol::{engine::StateWorkingSet, record};
4use serde_json::{Value as JsonValue, json};
5
6// Constants for JSON field names to avoid magic strings
7const FIELD_START: &str = "start";
8const FIELD_END: &str = "end";
9const FIELD_SPAN_SOURCE: &str = "span_source";
10
11#[derive(Clone)]
12pub struct Ast;
13
14impl Command for Ast {
15    fn name(&self) -> &str {
16        "ast"
17    }
18
19    fn description(&self) -> &str {
20        "Print the abstract syntax tree (ast) for a pipeline."
21    }
22
23    fn signature(&self) -> Signature {
24        Signature::build("ast")
25            .input_output_types(vec![
26                (Type::Nothing, Type::table()),
27                (Type::Nothing, Type::record()),
28                (Type::Nothing, Type::String),
29            ])
30            .required(
31                "pipeline",
32                SyntaxShape::String,
33                "The pipeline to print the ast for.",
34            )
35            .switch("json", "Serialize to json.", Some('j'))
36            .switch("minify", "Minify the nuon or json output.", Some('m'))
37            .switch(
38                "flatten",
39                "An easier to read version of the ast.",
40                Some('f'),
41            )
42            .allow_variants_without_examples(true)
43            .category(Category::Debug)
44    }
45
46    fn examples(&self) -> Vec<Example<'_>> {
47        vec![
48            Example {
49                description: "Print the ast of a string.",
50                example: "ast 'hello'",
51                result: None,
52            },
53            Example {
54                description: "Print the ast of a pipeline.",
55                example: "ast 'ls | where name =~ README'",
56                result: None,
57            },
58            Example {
59                description: "Print the ast of a pipeline with an error.",
60                example: "ast 'for x in 1..10 { echo $x '",
61                result: None,
62            },
63            Example {
64                description: "Print the ast of a pipeline with an error, as json, in a nushell table.",
65                example: "ast 'for x in 1..10 { echo $x ' --json | get block | from json",
66                result: None,
67            },
68            Example {
69                description: "Print the ast of a pipeline with an error, as json, minified.",
70                example: "ast 'for x in 1..10 { echo $x ' --json --minify",
71                result: None,
72            },
73            Example {
74                description: "Print the ast of a string flattened.",
75                example: r#"ast "'hello'" --flatten"#,
76                result: Some(Value::test_list(vec![Value::test_record(record! {
77                    "content" => Value::test_string("'hello'"),
78                    "shape" => Value::test_string("shape_string"),
79                    "span" => Value::test_record(record! {
80                        "start" => Value::test_int(0),
81                        "end" => Value::test_int(7),}),
82                })])),
83            },
84            Example {
85                description: "Print the ast of a string flattened, as json, minified.",
86                example: r#"ast "'hello'" --flatten --json --minify"#,
87                result: Some(Value::test_string(
88                    r#"[{"content":"'hello'","shape":"shape_string","span":{"start":0,"end":7}}]"#,
89                )),
90            },
91            Example {
92                description: "Print the ast of a pipeline flattened.",
93                example: r#"ast 'ls | sort-by type name -i' --flatten"#,
94                result: Some(Value::test_list(vec![
95                    Value::test_record(record! {
96                        "content" => Value::test_string("ls"),
97                        "shape" => Value::test_string("shape_external"),
98                        "span" => Value::test_record(record! {
99                            "start" => Value::test_int(0),
100                            "end" => Value::test_int(2),}),
101                    }),
102                    Value::test_record(record! {
103                        "content" => Value::test_string("|"),
104                        "shape" => Value::test_string("shape_pipe"),
105                        "span" => Value::test_record(record! {
106                            "start" => Value::test_int(3),
107                            "end" => Value::test_int(4),}),
108                    }),
109                    Value::test_record(record! {
110                        "content" => Value::test_string("sort-by"),
111                        "shape" => Value::test_string("shape_internalcall"),
112                        "span" => Value::test_record(record! {
113                            "start" => Value::test_int(5),
114                            "end" => Value::test_int(12),}),
115                    }),
116                    Value::test_record(record! {
117                        "content" => Value::test_string("type"),
118                        "shape" => Value::test_string("shape_string"),
119                        "span" => Value::test_record(record! {
120                            "start" => Value::test_int(13),
121                            "end" => Value::test_int(17),}),
122                    }),
123                    Value::test_record(record! {
124                        "content" => Value::test_string("name"),
125                        "shape" => Value::test_string("shape_string"),
126                        "span" => Value::test_record(record! {
127                            "start" => Value::test_int(18),
128                            "end" => Value::test_int(22),}),
129                    }),
130                    Value::test_record(record! {
131                        "content" => Value::test_string("-i"),
132                        "shape" => Value::test_string("shape_flag"),
133                        "span" => Value::test_record(record! {
134                            "start" => Value::test_int(23),
135                            "end" => Value::test_int(25),}),
136                    }),
137                ])),
138            },
139        ]
140    }
141
142    fn run(
143        &self,
144        engine_state: &EngineState,
145        stack: &mut Stack,
146        call: &Call,
147        _input: PipelineData,
148    ) -> Result<PipelineData, ShellError> {
149        // Extract command arguments
150        let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
151        let to_json = call.has_flag(engine_state, stack, "json")?;
152        let minify = call.has_flag(engine_state, stack, "minify")?;
153        let flatten = call.has_flag(engine_state, stack, "flatten")?;
154
155        // Parse the pipeline into an AST
156        let mut working_set = StateWorkingSet::new(engine_state);
157        let offset = working_set.next_span_start();
158        let parsed_block = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
159
160        // Handle flattened output (shows tokens with their shapes and spans)
161        if flatten {
162            let flat = flatten_block(&working_set, &parsed_block);
163            if to_json {
164                let mut json_val: JsonValue = json!([]);
165                for (span, shape) in flat {
166                    let content =
167                        String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
168
169                    let json = json!(
170                        {
171                            "content": content,
172                            "shape": shape.to_string(),
173                            "span": {
174                                "start": span.start.checked_sub(offset),
175                                "end": span.end.checked_sub(offset),
176                            },
177                        }
178                    );
179                    json_merge(&mut json_val, &json);
180                }
181                let json_string = if minify {
182                    if let Ok(json_str) = serde_json::to_string(&json_val) {
183                        json_str
184                    } else {
185                        "{}".to_string()
186                    }
187                } else if let Ok(json_str) = serde_json::to_string_pretty(&json_val) {
188                    json_str
189                } else {
190                    "{}".to_string()
191                };
192
193                Ok(Value::string(json_string, pipeline.span).into_pipeline_data())
194            } else {
195                // let mut rec: Record = Record::new();
196                let mut rec = vec![];
197                for (span, shape) in flat {
198                    let content =
199                        String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
200                    let each_rec = record! {
201                        "content" => Value::test_string(content),
202                        "shape" => Value::test_string(shape.to_string()),
203                        "span" => Value::test_record(record!{
204                            "start" => Value::test_int(match span.start.checked_sub(offset) {
205                                Some(start) => start as i64,
206                                None => 0
207                            }),
208                            "end" => Value::test_int(match span.end.checked_sub(offset) {
209                                Some(end) => end as i64,
210                                None => 0
211                            }),
212                        }),
213                    };
214                    rec.push(Value::test_record(each_rec));
215                }
216                Ok(Value::list(rec, pipeline.span).into_pipeline_data())
217            }
218        } else {
219            let error_output = working_set.parse_errors.first();
220            let block_span = match &parsed_block.span {
221                Some(span) => span,
222                None => &pipeline.span,
223            };
224            if to_json {
225                // Get the block as json
226                let serde_block_str =
227                    serde_json::to_string(&*parsed_block).map_err(|e| ShellError::CantConvert {
228                        to_type: "string".to_string(),
229                        from_type: "block".to_string(),
230                        span: *block_span,
231                        help: Some(format!(
232                            "Error: {e}\nCan't convert {parsed_block:?} to string"
233                        )),
234                    })?;
235                let json_val: serde_json::Value =
236                    serde_json::from_str(&serde_block_str).map_err(|e| {
237                        ShellError::CantConvert {
238                            to_type: "string".to_string(),
239                            from_type: "block".to_string(),
240                            span: *block_span,
241                            help: Some(format!(
242                                "Error: {e}\nCan't convert block JSON to serde_json: {e}"
243                            )),
244                        }
245                    })?;
246                let mut json_val = json_val;
247
248                // Embed source code for all spans in the JSON AST
249                embed_span_sources(&mut json_val, &working_set);
250
251                let block_json = if minify {
252                    json_val.to_string()
253                } else {
254                    serde_json::to_string_pretty(&json_val).unwrap_or_else(|_| json_val.to_string())
255                };
256                // Get the error as json
257                let serde_error_str = if minify {
258                    serde_json::to_string(&error_output)
259                } else {
260                    serde_json::to_string_pretty(&error_output)
261                };
262
263                let error_json = match serde_error_str {
264                    Ok(json) => json,
265                    Err(e) => Err(ShellError::CantConvert {
266                        to_type: "string".to_string(),
267                        from_type: "error".to_string(),
268                        span: *block_span,
269                        help: Some(format!(
270                            "Error: {e}\nCan't convert {error_output:?} to string"
271                        )),
272                    })?,
273                };
274
275                // Create a new output record, merging the block and error
276                let output_record = Value::record(
277                    record! {
278                        "block" => Value::string(block_json, *block_span),
279                        "error" => Value::string(error_json, Span::test_data()),
280                    },
281                    pipeline.span,
282                );
283                Ok(output_record.into_pipeline_data())
284            } else {
285                let block_value = Value::string(
286                    if minify {
287                        format!("{parsed_block:?}")
288                    } else {
289                        format!("{parsed_block:#?}")
290                    },
291                    pipeline.span,
292                );
293                let error_value = Value::string(
294                    if minify {
295                        format!("{error_output:?}")
296                    } else {
297                        format!("{error_output:#?}")
298                    },
299                    pipeline.span,
300                );
301                let output_record = Value::record(
302                    record! {
303                        "block" => block_value,
304                        "error" => error_value,
305                    },
306                    pipeline.span,
307                );
308                Ok(output_record.into_pipeline_data())
309            }
310        }
311    }
312}
313
314fn json_merge(a: &mut JsonValue, b: &JsonValue) {
315    match (a, b) {
316        (JsonValue::Object(a), JsonValue::Object(b)) => {
317            for (k, v) in b {
318                json_merge(a.entry(k).or_insert(JsonValue::Null), v);
319            }
320        }
321        (JsonValue::Array(a), JsonValue::Array(b)) => {
322            a.extend(b.clone());
323        }
324        (JsonValue::Array(a), JsonValue::Object(b)) => {
325            a.extend([JsonValue::Object(b.clone())]);
326        }
327        (a, b) => {
328            *a = b.clone();
329        }
330    }
331}
332
333/// Embeds source code for all spans found in the JSON AST representation.
334///
335/// This function recursively traverses the JSON value and adds a "span_source" field
336/// to any object that contains both "start" and "end" fields representing a span.
337/// The span source is extracted directly from the working set's source code.
338///
339/// # Arguments
340/// * `value` - The JSON value to process (modified in place)
341/// * `working_set` - The working set containing the source code for span extraction
342fn embed_span_sources(value: &mut serde_json::Value, working_set: &StateWorkingSet) {
343    match value {
344        serde_json::Value::Object(obj) => {
345            // Check if this object represents a span (has start and end fields)
346            if let Some(span) = extract_span_from_json(obj) {
347                // Extract the source code for this span
348                let contents = working_set.get_span_contents(span);
349                let source = String::from_utf8_lossy(contents).to_string();
350
351                // Add the source to the JSON object
352                obj.insert(
353                    FIELD_SPAN_SOURCE.to_string(),
354                    serde_json::Value::String(source),
355                );
356            } else {
357                // Recursively process all child values
358                for (_, v) in obj.iter_mut() {
359                    embed_span_sources(v, working_set);
360                }
361            }
362        }
363        serde_json::Value::Array(arr) => {
364            // Process each element in the array
365            for v in arr {
366                embed_span_sources(v, working_set);
367            }
368        }
369        _ => {
370            // Other JSON types (null, bool, number, string) don't contain spans
371        }
372    }
373}
374
375/// Extracts a Span from a JSON object if it contains valid start and end fields.
376///
377/// Returns Some(Span) if the object has valid start/end numbers, None otherwise.
378/// The span is only valid if start >= 0, end >= 0, and start < end.
379fn extract_span_from_json(obj: &serde_json::Map<String, serde_json::Value>) -> Option<Span> {
380    let start_value = obj.get(FIELD_START)?;
381    let end_value = obj.get(FIELD_END)?;
382
383    // Extract numbers from JSON values
384    let start_num = match start_value {
385        serde_json::Value::Number(n) => n.as_i64()?,
386        _ => return None,
387    };
388    let end_num = match end_value {
389        serde_json::Value::Number(n) => n.as_i64()?,
390        _ => return None,
391    };
392
393    // Validate span bounds
394    if start_num < 0 || end_num < 0 || start_num >= end_num {
395        return None;
396    }
397
398    Some(Span::new(start_num as usize, end_num as usize))
399}
400
401#[cfg(test)]
402mod test {
403    #[test]
404    fn test_examples() {
405        use super::Ast;
406        use crate::test_examples;
407        test_examples(Ast {})
408    }
409}