A fast PHP static analyzer written in Rust — a port of Psalm.
Read the backstory: From Psalm to Pzoom.
Pzoom uses the PHP Parser from Mago: https://github.com/carthage-software/mago
Note
This is not something I ever intend to support — Caveat Emptor.
Requires a recent stable Rust toolchain.
git clone https://github.com/muglug/pzoom
cd pzoom
cargo build --releaseThe binary ends up at target/release/pzoom.
Run it from (or point it at) a project with a psalm.xml:
target/release/pzoom path/to/php/projectpzoom reads Psalm's XML configuration format (psalm.xml, psalm.xml.dist or
psalm.dist.xml in the project root), so an existing Psalm setup works as-is —
see the Psalm configuration docs.
pzoom --help lists the handful of CLI options (output format, thread count,
config path).
# Inference tests (tests/inference/**)
cargo run --release -p pzoom-test-runner
# Unit tests
cargo testPzoom aims to broadly match Psalm's analysis, with a few deliberate divergences:
- Case-sensitive name resolution. PHP and Psalm resolve class, function
and method names case-insensitively. pzoom resolves them case-sensitively: a
wrong-cased reference is reported as
UndefinedClass/UndefinedDocblockClass/UndefinedFunction/UndefinedMethod, with the correctly-cased name suggested in the message (e.g.Class foo does not exist (incorrect casing of Foo)). Runtime-truth checks still honor PHP semantics:method_exists()matches case-insensitively, and method declarations override parent methods case-insensitively. - ComplexMethod calculation Pzoom's ComplexMethod metric is derived from the graph of connections between assignments within a function. Pzoom’s graph is larger than Psalm’s (it tracks more connections) and so we approximate ComplexMethod issues in Psalm.
- Return-type mismatches reported per statement. pzoom does not emit the
function-level
MoreSpecificReturnType,InvalidNullableReturnTypeorInvalidFalsableReturnType. A declared-vs-inferred return mismatch is reported at the offendingreturninstead —InvalidReturnStatement,NullableReturnStatement,FalsableReturnStatementorLessSpecificReturnStatement. The function-levelInvalidReturnTypeis kept only for the structural cases that have no singlereturnto point at: an implicit fall-through ("not all code paths of … end in a return"), a body with noreturnstatements at all, aneverbody that nonetheless returns, or a generator whose aggregated yield/return type is wrong. Unlike Psalm — which guards the per-returnchecks inside trait bodies and relies on the function-level check there — pzoom runs them in trait methods too: the declared return is localized to each using class (self/staticbind to that class and the trait's@templateparams resolve to theirasbound, mirroring how Psalm checks a generic trait), so a trait method's bad return is caught at thereturnwithout spuriousself-vs-staticor template-binding-width mismatches.
Pzoom is slower than Mago on some codebases. The main contributors are the same reasons that make Psalm slightly slower than other PHP static analysis tools:
- More accurate analysis of conditional statements
- Property initialisation checks (which often requires the tool to re-parse files)
These differences mean that Mago is faster, but emits false-positive errors on roughly a third of Psalm & Pzoom’s happy-path tests.