Thank you for your interest in contributing to rust-php-parser. This guide covers everything you need to go from a fresh clone to an open PR.
- Rust — latest stable toolchain (
rustup update stable) - PHP — PHP 8.2 or newer for
php -lfixture validation (the test suite skips syntax checks if PHP is not installed but CI runs them) cargoand standard Rust tooling (clippy,rustfmt)
git clone https://github.com/jorgsowa/rust-php-parser
cd rust-php-parser
cargo test # all tests should pass| Crate | Package name | Purpose |
|---|---|---|
crates/php-ast |
php-ast |
AST node types, Visitor trait, ScopeVisitor, PHPDoc tag types |
crates/php-lexer |
php-lexer |
Lazy tokenizer with peeking slots (arena-allocated) |
crates/php-parser |
php-rs-parser |
Recursive-descent parser, PHPDoc parser, semantic analysis helpers |
crates/php-printer |
php-printer |
Pretty printer (AST → PHP source) |
crates/php-test-macros |
php-test-macros |
Internal proc macros for test generation (not published) |
All workspace dependencies are declared in the root Cargo.toml. Each crate's Cargo.toml uses { workspace = true } for shared deps.
# Run all tests
cargo test
# Run tests for a single crate
cargo test -p php-rs-parser
cargo test -p php-printer
# Run specific test suites
cargo test --test integration # all .phpt parser fixture tests (including corpus)
cargo test --test php_syntax # validate every fixture via `php -l`
cargo test --test malformed_php # error recovery and diagnostics
cargo test --test visitor # visitor and scope-aware traversal
cargo test -p php-printer --test printer # printer fixtures
# Regenerate expected AST/errors in all .phpt fixtures
UPDATE_FIXTURES=1 cargo test
# Benchmarks
cargo bench
# Linting and formatting
cargo clippy --all-targets -- -D warnings
cargo fmt --checkNote: The crate is named php-rs-parser in Cargo (not php-parser). Use -p php-rs-parser when targeting the parser crate specifically.
Key design decisions:
- Arena allocation — AST nodes are bump-allocated via
bumpalo. The arena lifetime'arenathreads through the entire AST. This gives excellent allocation performance but makes in-place mutation of pointer-behind fields unsound (see Visitor section below). - Lazy lexer — Tokens are produced on demand, not pre-lexed into an array. The lexer has a small set of peeking slots.
- Pratt parser for expressions — Operator precedence is handled via a Pratt (top-down operator precedence) approach with binding-power tables. See
crates/php-parser/src/expr.rs. - Error recovery — The parser uses panic-mode recovery to produce a complete AST even for invalid PHP. Recovery points are statement boundaries.
- Version gating —
PhpVersioncontrols which syntax is accepted.parse_versioned()targets older PHP versions. Version-specific parse paths are tagged in the source.
All PHP parsing and printing tests use .phpt fixture files. Never write inline PHP in Rust test code.
===config=== (optional)
min_php=8.1 skip php -l on older PHP; sets the parse target version
max_php=8.3 skip php -l on newer PHP
===source=== (required)
<?php ...
===errors=== (optional; presence means parser errors are expected)
error message text one ParseError display message per line
===ast=== (optional; expected JSON AST — auto-generated)
{ ... }
===php_error=== (optional; auto-generated when php -l rejects the source)
PHP message from stderr
===source===
<?php ...
===print===
expected pretty-printed output
crates/php-parser/tests/fixtures/
categories/ feature-organized tests (enums, closures, match, traits, …)
errors/ tests where the parser is expected to emit errors
versioned/ version-specific syntax (use min_php to set target)
corpus/ adapted from nikic/PHP-Parser test suite
no_hang/ regression tests for parser hang issues
crates/php-printer/tests/fixtures/
- Create a
.phptfile in the appropriate directory (see the decision table below). - Add
===source===with the PHP code you want to test. - Run
UPDATE_FIXTURES=1 cargo test— this generates===ast===,===errors===, and===php_error===automatically. - Review the generated output. If the AST looks correct, commit the fixture.
- For version-specific syntax, add a
===config===section withmin_php=X.Y.
Error vs categories decision:
| Parser emits errors? | PHP rejects source? | Directory | Sections |
|---|---|---|---|
| Yes | Yes | errors/ |
===errors=== + ===php_error=== |
| Yes | No | errors/ |
===errors=== only |
| No | Yes | categories/ |
===php_error=== only |
| No | No | categories/ |
neither |
A typical feature addition touches these files:
crates/php-ast/src/ast.rs— add a new node variant or field to the AST typescrates/php-lexer/src/lexer.rs— add new token type(s) if neededcrates/php-parser/src/stmt.rsorexpr.rs— add the parse pathcrates/php-printer/src/printer.rs— handle the new variant in the pretty printercrates/php-ast/src/visitor.rs— add avisit_method andwalk_free function for the new node- Fixture files — add
.phpttests in the appropriatecategories/orversioned/directory
If the feature is version-gated:
- Add a version check in the parse path using
self.version(aPhpVersionvalue) - Add a
min_php=X.Yconfig in the test fixture - Emit a
ParseError::VersionTooLowdiagnostic when the feature is used below its minimum version
For complex new syntax, read an existing feature (e.g., match expressions in expr.rs, enums in stmt.rs) to understand the pattern before writing new code.
See crates/php-ast/src/visitor.rs for the full Visitor API reference.
The Visitor trait uses ControlFlow<()> returns so implementations can short-circuit traversal:
- Return
Continue(())to continue - Return
Break(())to stop traversal early (skip a subtree or exit entirely)
VisitorMut / Fold is not implemented. Arena allocation makes in-place mutation of pointer-behind fields unsound. A Fold that rebuilds nodes into a new arena is the correct design but has not been built yet. If you need to transform an AST, parse into a fresh arena.
See crates/php-parser/src/diagnostics.rs for the full list of ParseError variants and when to emit each one.
Quick rules:
- Emit
ParseError::UnexpectedTokenfor tokens that cannot appear in the current parse context. - Emit
ParseError::VersionTooLowwhen a feature is used below its minimum PHP version. - Use the
error_node!recovery mechanism for statement-level errors — it inserts anStmtKind::Errornode so the tree stays complete.
- No
todo!(),unimplemented!(), orpanic!()in parser/lexer hot paths. Prefer emitting aParseErrorand recovering. - No linting suppressions (
#[allow(...)],_prefix renames, etc.) — fix the root cause or delete dead code. - No inline PHP in Rust tests — all PHP source lives in
.phptfixture files. - Arena lifetimes propagate — when adding a new AST node that holds a reference, make sure its lifetime is
'arena. - Run
cargo fmtandcargo clippy -- -D warningsbefore opening a PR. - Commit messages use conventional commits style (e.g.,
feat:,fix:,docs:,test:,refactor:).
Performance-sensitive changes should be benchmarked before and after:
cargo benchKey lesson: profiling showed the lazy lexer with peeking slots outperforms a pre-lexed array approach. A branch-elimination change without profiling evidence caused a 13–125% regression. Measure first.
- AST node types:
docs.rs/php-ast/ast - Full API reference:
docs.rs/php-rs-parser,docs.rs/php-ast,docs.rs/php-lexer,docs.rs/php-printer - Error types:
crates/php-parser/src/diagnostics.rs - Visitor API:
crates/php-ast/src/visitor.rs - GitHub Issues — open an issue if you're unsure where to start or want to discuss a design before writing code