Skip to main content

mago_syntax/parser/
mod.rs

1use mago_allocator::prelude::*;
2
3use mago_database::file::File;
4use mago_database::file::FileId;
5use mago_database::file::HasFileId;
6use mago_syntax_core::input::Input;
7
8use crate::cst::Program;
9use crate::cst::sequence::Sequence;
10use crate::error::ParseError;
11use crate::lexer::Lexer;
12use crate::parser::stream::TokenStream;
13use crate::settings::ParserSettings;
14
15mod internal;
16
17pub mod stream;
18
19/// Maximum recursion depth for expression parsing.
20/// This prevents stack overflow on deeply nested expressions and statements.
21const MAX_RECURSION_DEPTH: u16 = 512;
22
23#[derive(Debug, Default)]
24pub struct State {
25    pub within_string_interpolation: bool,
26    pub recursion_depth: u16,
27}
28
29/// The main parser for PHP source code.
30///
31/// The parser holds an arena reference, the token stream, and parsing state.
32#[derive(Debug)]
33#[allow(clippy::field_scoped_visibility_modifiers)]
34pub struct Parser<'input, 'arena, A>
35where
36    'input: 'arena,
37    A: Arena,
38{
39    pub(crate) arena: &'arena A,
40    pub(crate) state: State,
41    pub(crate) stream: TokenStream<'input, 'arena, A>,
42    pub(crate) errors: Vec<'arena, ParseError, A>,
43}
44
45impl<'input, 'arena, A> Parser<'input, 'arena, A>
46where
47    A: Arena,
48{
49    /// Creates a new parser for the given content.
50    ///
51    /// # Parameters
52    ///
53    /// - `arena`: The memory arena for allocations.
54    /// - `file_id`: The ID of the file being parsed.
55    /// - `content`: The content to parse.
56    /// - `settings`: The parser settings.
57    ///
58    /// # Returns
59    ///
60    /// A new `Parser` instance.
61    #[inline]
62    pub fn new(arena: &'arena A, file_id: FileId, content: &'input [u8], settings: ParserSettings) -> Self {
63        let input = Input::new(file_id, content);
64        let lexer = Lexer::new(input, settings.lexer);
65        let stream = TokenStream::new(arena, lexer);
66
67        Self { arena, state: State::default(), stream, errors: Vec::new_in(arena) }
68    }
69
70    /// Creates a new parser for the given file.
71    ///
72    /// # Parameters
73    ///
74    /// - `arena`: The memory arena for allocations.
75    /// - `file`: The file to parse.
76    /// - `settings`: The parser settings.
77    ///
78    /// # Returns
79    ///
80    /// A new `Parser` instance.
81    pub fn for_file(arena: &'arena A, file: &'input File, settings: ParserSettings) -> Self {
82        Self::new(arena, file.file_id(), file.contents.as_ref(), settings)
83    }
84
85    /// Parses and returns the program AST.
86    fn parse(mut self, source_text: &'arena [u8], file_id: FileId) -> &'arena Program<'arena> {
87        let mut statements = Vec::new_in(self.arena);
88
89        loop {
90            let reached_eof = match self.stream.has_reached_eof() {
91                Ok(eof) => eof,
92                Err(err) => {
93                    self.errors.push(ParseError::from(err));
94                    break;
95                }
96            };
97
98            if reached_eof {
99                break;
100            }
101
102            // Record position before parsing to detect infinite loops
103            let position_before = self.stream.current_position();
104
105            match self.parse_statement() {
106                Ok(statement) => statements.push(statement),
107                Err(err) => self.errors.push(err),
108            }
109
110            // Safety check: if we didn't advance at all, skip a token to prevent infinite loop.
111            // This can happen with orphan keywords like `finally`, `catch`, `else`, etc.
112            // that are preserved by the expression parser but not handled by the statement parser.
113            let position_after = self.stream.current_position();
114            if position_after == position_before
115                && let Ok(Some(token)) = self.stream.lookahead(0)
116            {
117                self.errors.push(self.stream.unexpected(Some(token), &[]));
118                let _ = self.stream.consume();
119            }
120        }
121
122        self.arena.alloc(Program {
123            file_id,
124            source_text,
125            statements: Sequence::new(statements),
126            trivia: self.stream.get_trivia(),
127            errors: self.errors.leak(),
128        })
129    }
130}
131
132/// Parses the given file and returns the program AST.
133///
134/// # Parameters
135///
136/// - `arena`: The memory arena for allocations.
137/// - `file`: The file to parse.
138///
139/// # Returns
140///
141/// The parsed `Program` AST.
142#[inline]
143pub fn parse_file<'arena, A>(arena: &'arena A, file: &File) -> &'arena Program<'arena>
144where
145    A: Arena,
146{
147    parse_file_content(arena, file.file_id(), file.contents.as_ref())
148}
149
150/// Parses the given file with custom settings and returns the program AST.
151///
152/// # Parameters
153///
154/// - `arena`: The memory arena for allocations.
155/// - `file`: The file to parse.
156/// - `settings`: The parser settings.
157///
158/// # Returns
159///
160/// The parsed `Program` AST.
161#[inline]
162pub fn parse_file_with_settings<'arena, A>(
163    arena: &'arena A,
164    file: &File,
165    settings: ParserSettings,
166) -> &'arena Program<'arena>
167where
168    A: Arena,
169{
170    parse_file_content_with_settings(arena, file.file_id(), file.contents.as_ref(), settings)
171}
172
173/// Parses the given file content and returns the program AST.
174///
175/// # Parameters
176///
177/// - `arena`: The memory arena for allocations.
178/// - `file_id`: The ID of the file being parsed.
179/// - `content`: The content to parse.
180///
181/// # Returns
182///
183/// The parsed `Program` AST.
184pub fn parse_file_content<'arena, A>(arena: &'arena A, file_id: FileId, content: &[u8]) -> &'arena Program<'arena>
185where
186    A: Arena,
187{
188    let source_text = arena.alloc_slice_copy(content);
189    Parser::new(arena, file_id, source_text, ParserSettings::default()).parse(source_text, file_id)
190}
191
192/// Parses the given file content with custom settings and returns the program AST.
193///
194/// # Parameters
195///
196/// - `arena`: The memory arena for allocations.
197/// - `file_id`: The ID of the file being parsed.
198/// - `content`: The content to parse.
199/// - `settings`: The parser settings.
200///
201/// # Returns
202///
203/// The parsed `Program` AST.
204pub fn parse_file_content_with_settings<'arena, A>(
205    arena: &'arena A,
206    file_id: FileId,
207    content: &[u8],
208    settings: ParserSettings,
209) -> &'arena Program<'arena>
210where
211    A: Arena,
212{
213    let source_text = arena.alloc_slice_copy(content);
214    Parser::new(arena, file_id, source_text, settings).parse(source_text, file_id)
215}