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 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 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 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}