mago_cli/commands/
ast.rs

1use clap::Parser;
2use serde_json::json;
3use termtree::Tree;
4
5use mago_ast::node::NodeKind;
6use mago_ast::Node;
7use mago_interner::ThreadedInterner;
8use mago_reporting::reporter::Reporter;
9use mago_reporting::reporter::ReportingFormat;
10use mago_reporting::reporter::ReportingTarget;
11use mago_reporting::Issue;
12use mago_service::ast::AstService;
13use mago_source::SourceManager;
14
15use crate::enum_variants;
16use crate::utils::bail;
17
18#[derive(Parser, Debug)]
19#[command(
20    name = "ast",
21    about = "Prints the abstract syntax tree of a PHP file.",
22    long_about = "Given a PHP file, this command will parse the file and print the abstract syntax tree (AST) to the console."
23)]
24pub struct AstCommand {
25    #[arg(long, short = 'f', help = "The PHP file to parse.", required = true)]
26    pub file: String,
27
28    #[arg(long, help = "Outputs the result in JSON format.")]
29    pub json: bool,
30
31    #[arg(long, default_value_t, help = "The issue reporting target to use.", ignore_case = true, value_parser = enum_variants!(ReportingTarget))]
32    pub reporting_target: ReportingTarget,
33
34    #[arg(long, default_value_t, help = "The issue reporting format to use.", ignore_case = true, value_parser = enum_variants!(ReportingFormat))]
35    pub reporting_format: ReportingFormat,
36}
37
38pub async fn execute(command: AstCommand) -> i32 {
39    let file_path = std::path::Path::new(&command.file).to_path_buf();
40
41    // Check if the file exists and is readable
42    if !file_path.exists() {
43        mago_feedback::error!("file '{}' does not exist.", command.file);
44
45        return 1;
46    }
47
48    if !file_path.is_file() {
49        mago_feedback::error!("'{}' is not a valid file.", command.file);
50
51        return 1;
52    }
53
54    let interner = ThreadedInterner::new();
55    let source_manager = SourceManager::new(interner.clone());
56
57    let source_id = source_manager.insert_path(command.file, file_path, true);
58
59    let service = AstService::new(interner.clone(), source_manager.clone());
60
61    let (ast, error) = service.parse(source_id).await.unwrap_or_else(bail);
62
63    let has_error = error.is_some();
64    if command.json {
65        // Prepare JSON output
66        let result = json!({
67            "interner": interner.all().into_iter().collect::<Vec<_>>(),
68            "program": ast,
69            "error": error.map(|e| Into::<Issue>::into(&e)),
70        });
71
72        println!("{}", serde_json::to_string_pretty(&result).unwrap());
73    } else {
74        // Print the AST as a tree
75        let tree = node_to_tree(Node::Program(&ast));
76
77        println!("{tree}");
78
79        if let Some(error) = &error {
80            let issue = Into::<Issue>::into(error);
81
82            Reporter::new(interner, source_manager, command.reporting_target)
83                .report([issue], command.reporting_format)
84                .unwrap_or_else(bail);
85        }
86    }
87
88    if has_error {
89        1
90    } else {
91        0
92    }
93}
94
95fn node_to_tree(node: Node<'_>) -> Tree<NodeKind> {
96    let mut tree = Tree::new(node.kind());
97    for child in node.children() {
98        tree.push(node_to_tree(child));
99    }
100
101    tree
102}