Skip to main content

php_rs_parser/
lib.rs

1//! Fast, fault-tolerant PHP parser that produces a fully typed AST.
2//!
3//! This crate parses PHP source code (PHP 7.4–8.5) into a [`php_ast::Program`]
4//! tree, recovering from syntax errors so that downstream tools always receive
5//! a complete AST.
6//!
7//! # Quick start
8//!
9//! ```
10//! let arena = bumpalo::Bump::new();
11//! let result = php_rs_parser::parse(&arena, "<?php echo 'hello';");
12//! assert!(result.errors.is_empty());
13//! ```
14//!
15//! # Version-aware parsing
16//!
17//! Use [`parse_versioned`] to target a specific PHP version. Syntax that
18//! requires a higher version is still parsed into the AST, but a
19//! [`diagnostics::ParseError::VersionTooLow`] diagnostic is emitted.
20//!
21//! ```
22//! let arena = bumpalo::Bump::new();
23//! let result = php_rs_parser::parse_versioned(
24//!     &arena,
25//!     "<?php enum Status { case Active; }",
26//!     php_rs_parser::PhpVersion::Php80,
27//! );
28//! assert!(!result.errors.is_empty()); // enums require PHP 8.1
29//! ```
30//!
31//! # Reusing arenas across re-parses (LSP usage)
32//!
33//! Use [`ParserContext`] to avoid allocator churn when the same document is
34//! re-parsed on every edit. The context owns a `bumpalo::Bump` arena and resets
35//! it in O(1) before each parse, reusing the backing memory once it has grown
36//! to a stable size.
37//!
38//! ```
39//! let mut ctx = php_rs_parser::ParserContext::new();
40//!
41//! let result = ctx.reparse("<?php echo 1;");
42//! assert!(result.errors.is_empty());
43//! drop(result); // must be dropped before the next reparse
44//!
45//! let result = ctx.reparse("<?php echo 2;");
46//! assert!(result.errors.is_empty());
47//! ```
48
49pub mod diagnostics;
50pub(crate) mod expr;
51pub mod instrument;
52pub(crate) mod parser;
53pub use phpdoc_parser as phpdoc;
54pub(crate) mod precedence;
55pub mod source_map;
56pub(crate) mod stmt;
57pub mod version;
58
59use diagnostics::ParseError;
60use php_ast::{Comment, Program};
61use source_map::SourceMap;
62pub use version::PhpVersion;
63
64/// The result of parsing a PHP source string.
65pub struct ParseResult<'arena, 'src> {
66    /// The original source text. Useful for extracting text from spans
67    /// via `&result.source[span.start as usize..span.end as usize]`.
68    pub source: &'src str,
69    /// The parsed AST. Always produced, even when errors are present.
70    pub program: Program<'arena, 'src>,
71    /// All comments found in the source, in source order, **except** `/** */`
72    /// doc-block comments that are immediately attached to a declaration.
73    ///
74    /// When the parser encounters a `/** */` comment directly before a
75    /// function, class, method, property, constant, or enum case, it removes
76    /// that comment from this list and stores it in the declaration node's
77    /// `doc_comment` field instead. The two collections are therefore
78    /// **disjoint**: iterating both without deduplication will double-count
79    /// nothing, but iterating only one will miss the other's entries.
80    ///
81    /// To process every comment in the file, visit both:
82    ///
83    /// ```ignore
84    /// for comment in &result.comments { /* line/hash/block + unattached docs */ }
85    /// // doc comments on declarations are on each node's doc_comment field
86    /// ```
87    ///
88    /// Or use [`php_ast::visitor::walk_comments`] with a [`Visitor`] that also
89    /// overrides the declaration visit methods.
90    pub comments: Vec<Comment<'src>>,
91    /// Parse errors and diagnostics. Empty on a successful parse.
92    pub errors: Vec<ParseError>,
93    /// `true` when the error list was capped at the internal limit and further
94    /// errors were silently dropped. Callers that need a complete error list
95    /// (e.g. linters) should treat this as an incomplete result.
96    pub errors_truncated: bool,
97    /// Pre-computed line index for resolving byte offsets in [`Span`](php_ast::Span)
98    /// to line/column positions. Use [`SourceMap::offset_to_line_col`] or
99    /// [`SourceMap::span_to_line_col`] to convert.
100    pub source_map: SourceMap,
101}
102
103/// Parse PHP `source` using the latest supported PHP version (currently 8.5).
104///
105/// The `arena` is used for all AST allocations, giving callers control over
106/// memory lifetime. The returned [`ParseResult`] borrows from both the arena
107/// and the source string.
108pub fn parse<'arena, 'src>(
109    arena: &'arena bumpalo::Bump,
110    source: &'src str,
111) -> ParseResult<'arena, 'src> {
112    let mut parser = parser::Parser::new(arena, source);
113    let program = parser.parse_program();
114    let errors_truncated = parser.errors_truncated();
115    ParseResult {
116        source,
117        program,
118        comments: parser.take_comments(),
119        errors: parser.into_errors(),
120        errors_truncated,
121        source_map: SourceMap::new(source),
122    }
123}
124
125/// Parse `source` targeting the given PHP `version`.
126///
127/// Syntax that requires a higher version than `version` is still parsed and
128/// included in the AST, but a [`diagnostics::ParseError::VersionTooLow`] error
129/// is also emitted so callers can report it to the user.
130pub fn parse_versioned<'arena, 'src>(
131    arena: &'arena bumpalo::Bump,
132    source: &'src str,
133    version: PhpVersion,
134) -> ParseResult<'arena, 'src> {
135    let mut parser = parser::Parser::with_version(arena, source, version);
136    let program = parser.parse_program();
137    let errors_truncated = parser.errors_truncated();
138    ParseResult {
139        source,
140        program,
141        comments: parser.take_comments(),
142        errors: parser.into_errors(),
143        errors_truncated,
144        source_map: SourceMap::new(source),
145    }
146}
147
148/// A reusable parse context that keeps a `bumpalo::Bump` arena alive between
149/// re-parses, resetting it (O(1)) instead of dropping and reallocating.
150///
151/// This is the preferred entry point for LSP servers or any tool that parses
152/// the same document repeatedly. Once the arena has grown to accommodate the
153/// largest document seen, subsequent parses reuse the backing memory without
154/// any new allocations.
155///
156/// The Rust lifetime system enforces safety: the returned [`ParseResult`]
157/// borrows from `self`, so the borrow checker prevents calling [`reparse`] or
158/// [`reparse_versioned`] again while the previous result is still alive.
159///
160/// [`reparse`]: ParserContext::reparse
161/// [`reparse_versioned`]: ParserContext::reparse_versioned
162///
163/// # Example
164///
165/// ```
166/// let mut ctx = php_rs_parser::ParserContext::new();
167///
168/// let result = ctx.reparse("<?php echo 1;");
169/// assert!(result.errors.is_empty());
170/// drop(result); // must be dropped before the next reparse
171///
172/// let result = ctx.reparse("<?php echo 2;");
173/// assert!(result.errors.is_empty());
174/// ```
175pub struct ParserContext {
176    arena: bumpalo::Bump,
177}
178
179impl ParserContext {
180    /// Create a new context with an empty arena.
181    pub fn new() -> Self {
182        Self {
183            arena: bumpalo::Bump::new(),
184        }
185    }
186
187    /// Reset the arena and parse `source` using PHP 8.5 (the latest version).
188    ///
189    /// The previous [`ParseResult`] **must be dropped** before calling this
190    /// method. The borrow checker enforces this: the returned result borrows
191    /// `self` for the duration of its lifetime, so a second call while the
192    /// first result is still live is a compile-time error.
193    pub fn reparse<'a, 'src>(&'a mut self, source: &'src str) -> ParseResult<'a, 'src> {
194        self.arena.reset();
195        parse(&self.arena, source)
196    }
197
198    /// Reset the arena and parse `source` targeting the given PHP `version`.
199    ///
200    /// See [`reparse`](ParserContext::reparse) for lifetime safety notes.
201    pub fn reparse_versioned<'a, 'src>(
202        &'a mut self,
203        source: &'src str,
204        version: PhpVersion,
205    ) -> ParseResult<'a, 'src> {
206        self.arena.reset();
207        parse_versioned(&self.arena, source, version)
208    }
209}
210
211impl Default for ParserContext {
212    fn default() -> Self {
213        Self::new()
214    }
215}