Skip to content

PHPantom — Bug Fixes

Every bug below must be fixed at its root cause. "Detect the symptom and suppress the diagnostic" is not an acceptable fix. If the type resolution pipeline produces wrong data, fix the pipeline so it produces correct data. Downstream consumers (diagnostics, hover, completion, definition) should never need to second-guess upstream output.

Each entry below carries an Impact · Complexity rating using the same scale defined in docs/todo.md; that table is also where each bug's row lives in the current sprint/backlog.

Crashes

No outstanding items.

Type comparison

No outstanding items.

Standard-library return types

No outstanding items.

Narrowing

No outstanding items.

Arithmetic

No outstanding items.

Symbol resolution

B218. new ReflectionProperty(Foo::class, 'bar') forgets what it reflects

Impact: Low · Complexity: Medium

A reflection value built by ReflectionClass::getProperty('bar') carries the class and the property name, so reading it types as the property declares. Constructing the same value directly does not:

$viaClass = (new \ReflectionClass(Configuration::class))->getProperty('shell');
$viaClass->getValue($config);   // ?Shell

$direct = new \ReflectionProperty(Configuration::class, 'shell');
$direct->getValue($config);     // mixed

The two spellings are interchangeable in real code, so the second should resolve like the first. The binding cannot come from the constructor's docblock: class-string<T>|T $class would bind the class through the existing machinery, but the $property name is a string literal, and a literal only binds to a @template whose bound is a type operator (key-of<…> and friends). Either the two new-expression resolution paths need the same rule the two call paths got, or literal binding has to be widened to a @template TName of string, which is what PHPStan does for literal string types and would want measuring against the whole corpus first.

B226. A function-static variable's type is not tracked across its own reads

Impact: Low-Medium · Complexity: Medium-High

type_engine/variable/forward_walk/ has no handling at all for a static $var; declaration (there is no StaticVariable case anywhere under it); the walker treats the name as an ordinary, unassigned local until it sees an assignment to it in the same top-to-bottom pass. That loses the one thing a static local actually means: its value can carry over from an earlier call that assigned it in a branch the current call never reaches.

function info(?Configuration $config = null) {
    static $lastConfig;
    if ($config !== null) {
        $lastConfig = $config;
        return null;
    }
    $config = $lastConfig ?: new Configuration();
    // $shell::VERSION below needs $config resolved to Configuration for
    // Sudo::fetchProperty($config, 'shell') to type as ?Shell (the
    // pass-through accessor inference already handles that part).
    $shell = Sudo::fetchProperty($config, 'shell');
    if ($shell) {
        $shellInfo = ['PsySH version' => $shell::VERSION];
    }
}

On the call that falls through to the second half, $lastConfig is read without ever having been assigned within this walk, so $config resolves too conservatively for the accessor pass-through (see the ReflectionProperty/Sudo::fetchProperty inference above) to carry Configuration::$shell's declared type through to $shell, and $shell::VERSION cannot be resolved. Found via php-typing-conformance's LSP navigation probe against psysh (Psy\Shell::VERSION, src/functions.php:383): find-references reports 20 of 21 known references, missing exactly this one; Intelephense and Phpactor miss the same reference, but DEVSENSE resolves it, which is worth chasing. The same gap also degrades hover and inferred types wherever code narrows on a static local this way, not just find-references. A correct fix needs to seed a static $var's type from the union of every assignment reachable anywhere in the enclosing function body (not only the ones preceding the read in this pass), since the assignment that matters can sit in a branch this call never takes.

Array types

No outstanding items.

Docblock handling

No outstanding items.

Miscellaneous

No outstanding items.