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 interpolation;
53pub(crate) mod parser;
54pub mod phpdoc;
55pub(crate) mod precedence;
56pub mod source_map;
57pub(crate) mod stmt;
58pub mod version;
59
60use diagnostics::ParseError;
61use php_ast::{Comment, Program};
62use source_map::SourceMap;
63pub use version::PhpVersion;
64
65/// The result of parsing a PHP source string.
66pub struct ParseResult<'arena, 'src> {
67 /// The original source text. Useful for extracting text from spans
68 /// via `&result.source[span.start as usize..span.end as usize]`.
69 pub source: &'src str,
70 /// The parsed AST. Always produced, even when errors are present.
71 pub program: Program<'arena, 'src>,
72 /// All comments found in the source, in source order.
73 /// Comments are not attached to AST nodes; callers can map them by span.
74 pub comments: Vec<Comment<'src>>,
75 /// Parse errors and diagnostics. Empty on a successful parse.
76 pub errors: Vec<ParseError>,
77 /// `true` when the error list was capped at the internal limit and further
78 /// errors were silently dropped. Callers that need a complete error list
79 /// (e.g. linters) should treat this as an incomplete result.
80 pub errors_truncated: bool,
81 /// Pre-computed line index for resolving byte offsets in [`Span`](php_ast::Span)
82 /// to line/column positions. Use [`SourceMap::offset_to_line_col`] or
83 /// [`SourceMap::span_to_line_col`] to convert.
84 pub source_map: SourceMap,
85}
86
87/// Parse PHP `source` using the latest supported PHP version (currently 8.5).
88///
89/// The `arena` is used for all AST allocations, giving callers control over
90/// memory lifetime. The returned [`ParseResult`] borrows from both the arena
91/// and the source string.
92pub fn parse<'arena, 'src>(
93 arena: &'arena bumpalo::Bump,
94 source: &'src str,
95) -> ParseResult<'arena, 'src> {
96 let mut parser = parser::Parser::new(arena, source);
97 let program = parser.parse_program();
98 let errors_truncated = parser.errors_truncated();
99 ParseResult {
100 source,
101 program,
102 comments: parser.take_comments(),
103 errors: parser.into_errors(),
104 errors_truncated,
105 source_map: SourceMap::new(source),
106 }
107}
108
109/// Parse `source` targeting the given PHP `version`.
110///
111/// Syntax that requires a higher version than `version` is still parsed and
112/// included in the AST, but a [`diagnostics::ParseError::VersionTooLow`] error
113/// is also emitted so callers can report it to the user.
114pub fn parse_versioned<'arena, 'src>(
115 arena: &'arena bumpalo::Bump,
116 source: &'src str,
117 version: PhpVersion,
118) -> ParseResult<'arena, 'src> {
119 let mut parser = parser::Parser::with_version(arena, source, version);
120 let program = parser.parse_program();
121 let errors_truncated = parser.errors_truncated();
122 ParseResult {
123 source,
124 program,
125 comments: parser.take_comments(),
126 errors: parser.into_errors(),
127 errors_truncated,
128 source_map: SourceMap::new(source),
129 }
130}
131
132/// A reusable parse context that keeps a `bumpalo::Bump` arena alive between
133/// re-parses, resetting it (O(1)) instead of dropping and reallocating.
134///
135/// This is the preferred entry point for LSP servers or any tool that parses
136/// the same document repeatedly. Once the arena has grown to accommodate the
137/// largest document seen, subsequent parses reuse the backing memory without
138/// any new allocations.
139///
140/// The Rust lifetime system enforces safety: the returned [`ParseResult`]
141/// borrows from `self`, so the borrow checker prevents calling [`reparse`] or
142/// [`reparse_versioned`] again while the previous result is still alive.
143///
144/// [`reparse`]: ParserContext::reparse
145/// [`reparse_versioned`]: ParserContext::reparse_versioned
146///
147/// # Example
148///
149/// ```
150/// let mut ctx = php_rs_parser::ParserContext::new();
151///
152/// let result = ctx.reparse("<?php echo 1;");
153/// assert!(result.errors.is_empty());
154/// drop(result); // must be dropped before the next reparse
155///
156/// let result = ctx.reparse("<?php echo 2;");
157/// assert!(result.errors.is_empty());
158/// ```
159pub struct ParserContext {
160 arena: bumpalo::Bump,
161}
162
163impl ParserContext {
164 /// Create a new context with an empty arena.
165 pub fn new() -> Self {
166 Self {
167 arena: bumpalo::Bump::new(),
168 }
169 }
170
171 /// Reset the arena and parse `source` using PHP 8.5 (the latest version).
172 ///
173 /// The previous [`ParseResult`] **must be dropped** before calling this
174 /// method. The borrow checker enforces this: the returned result borrows
175 /// `self` for the duration of its lifetime, so a second call while the
176 /// first result is still live is a compile-time error.
177 pub fn reparse<'a, 'src>(&'a mut self, source: &'src str) -> ParseResult<'a, 'src> {
178 self.arena.reset();
179 parse(&self.arena, source)
180 }
181
182 /// Reset the arena and parse `source` targeting the given PHP `version`.
183 ///
184 /// See [`reparse`](ParserContext::reparse) for lifetime safety notes.
185 pub fn reparse_versioned<'a, 'src>(
186 &'a mut self,
187 source: &'src str,
188 version: PhpVersion,
189 ) -> ParseResult<'a, 'src> {
190 self.arena.reset();
191 parse_versioned(&self.arena, source, version)
192 }
193}
194
195impl Default for ParserContext {
196 fn default() -> Self {
197 Self::new()
198 }
199}