Skip to content

PHPStan 2.2.15 exhausts memory analyzing Symfony DoctrineExtractor.php (2.2.14 completes) #15310

Closed

Description

@X-Coder264

Bug report

PHPStan 2.2.15 exhausts memory while analyzing a single Symfony source file on PHP 8.4. PHPStan 2.2.14 analyzes the same file, at the same Symfony commit and with the same other installed dependencies, in about 1.25 seconds.

Observed locally (Ubuntu 24.04.5 x86_64, PHP 8.4.25, XDEBUG_MODE=off):

PHPStan Same one-file command, --debug --memory-limit=1G Peak memory consumption
2.2.14 Completes in 1.25 s and reports 7 ordinary diagnostics; exit 1 177.6 MiB
2.2.15 Exhausts the 1 GiB PHP limit after 26.14 s; exit 255 1,175.2 MiB

The 2.2.15 fatal error is:

PHP Fatal error: Allowed memory size of 1073741824 bytes exhausted (tried to allocate 262144 bytes) in phar://.../vendor/phpstan/phpstan/phpstan.phar/src/Type/NarrowedSubjectType.php on line 114

Line 114 is in NarrowedSubjectType::equals(). This class was introduced between 2.2.14 and 2.2.15 in phpstan-src@91dcf7e. The allocation site may be a symptom rather than the complete cause.

CI impact and version boundary:

  • Symfony's passing PHPStan job and first failing PHPStan job both checked out the exact same 8.2 base commit, 86b6948dc1e3e23036e0ef8ff1ef09f0f33cec14. The passing job installed phpstan/phpstan 2.2.14; the failing job installed 2.2.15. Both installed phpstan/phpstan-deprecation-rules 2.0.5.
  • The passing base-branch analysis finished in about 90 s. The failing analysis did not complete its report; its job ended with exit 143 after roughly four minutes. Another failing example explicitly logs that the runner received a shutdown signal. GitHub's log does not say why the host stopped, but the bounded local test establishes the memory regression independently.
  • Symfony's workflow resolves phpstan/phpstan:@stable on every run and sets PHP memory_limit=-1 (workflow at this commit). Pinning PHPStan back to 2.2.14 restores local analysis without changing any other dependency.

Code snippet that reproduces the problem

The single source file is Symfony's DoctrineExtractor.php at the fixed commit. It refers to Doctrine ORM types, so the following repository setup provides a reproducible CLI case without relying on a PHPStan Playground timeout.

On a Linux PHP 8.4 installation with Composer:

git clone https://github.com/symfony/symfony.git
cd symfony
git checkout 86b6948dc1e3e23036e0ef8ff1ef09f0f33cec14

export COMPOSER_HOME="$(mktemp -d)"
cp .github/composer-config.json "$COMPOSER_HOME/config.json"
export COMPOSER_ROOT_VERSION=8.2.x-dev COMPOSER_NO_INTERACTION=1 COMPOSER_NO_AUDIT=1
composer remove --dev --no-update --no-interaction symfony/phpunit-bridge
composer require --no-progress --no-plugins --ignore-platform-req=ext-mongodb   phpstan/phpstan:2.2.15 phpstan/phpstan-deprecation-rules:2.0.5   phpunit/phpunit:^11.5 php-http/discovery psr/event-dispatcher mongodb/mongodb

XDEBUG_MODE=off php -d memory_limit=-1 vendor/bin/phpstan analyse   src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php   --debug --no-progress --memory-limit=1G   --autoload-file=.github/sa-tools/rules/bootstrap.php

The --ignore-platform-req=ext-mongodb option is for machines without that PHP extension; my local reproduction used it. Symfony CI has the extension available.

For the control, change only the PHPStan package, then repeat the same analyse command:

composer require --no-progress --no-plugins --ignore-platform-req=ext-mongodb phpstan/phpstan:2.2.14

The full-tree analysis showed the same gap locally: 2.2.14 finished in 31.87 s and produced a JSON report, while 2.2.15 did not finish within a bounded 180 s run. No Symfony source file was changed between these local runs.

Expected output

PHPStan 2.2.15 should finish analyzing the file within a comparable time and memory budget to 2.2.14 and report any code diagnostics normally. It should not exhaust 1 GiB on this file or leave the full-tree CI analysis running until the runner stops.

Did PHPStan help you today? Did it make you happy in any way?

Symfony uses PHPStan's baseline comparison in CI to catch new static-analysis findings across pull requests.

Activity

  1. mergeable commented on Sep 25, 2026

    @mergeable

    This bug report is missing a link to reproduction at phpstan.org/try.

    It will most likely be closed after manual review.

  2. X-Coder264 commented on Sep 25, 2026

    @X-Coder264
    Author

    About the automated Playground-link warning: I could not reproduce this dependency-backed memory failure in the Playground. The report links the exact Symfony file at an immutable commit and includes copyable setup and one-file PHPStan commands. That one file alone exhausts 1 GiB with 2.2.15 in about 26 seconds; it completes in about 1.25 seconds with 2.2.14 when only the PHPStan package is changed. No full Symfony CI run is needed to reproduce it.

  3. X-Coder264 commented on Sep 25, 2026

    @X-Coder264
    Author

    I narrowed this down further. The relevant Symfony declaration is BuiltinType's generic @extends Type<(T is ... ? ... : ...)>. It has a chain of 15 conditional branches. DoctrineExtractor::getType() reaches this type through the Type factories; even reducing that method to return Type::object($class); still reproduces the failure. Replacing the conditional @extends declaration with @extends Type<mixed> in a temporary checkout makes the reduced analysis complete.

    Here is a standalone reproducer that requires no Symfony code, dependencies, or PHPStan configuration (save as repro.php):

    <?php
    
    enum ExampleIdentifier { case A; case B; case C; case D; case E; case F; case G; case H; case I; case J; case K; case L; case M; case N; case O; }
    
    /** @template-covariant TValue */
    abstract class ExampleType
    {
    }
    
    /**
     * @template T of ExampleIdentifier
     * @extends ExampleType<(
     *   T is ExampleIdentifier::A ? array :
     *   T is ExampleIdentifier::B ? bool :
     *   T is ExampleIdentifier::C ? callable :
     *   T is ExampleIdentifier::D ? false :
     *   T is ExampleIdentifier::E ? float :
     *   T is ExampleIdentifier::F ? int :
     *   T is ExampleIdentifier::G ? iterable :
     *   T is ExampleIdentifier::H ? mixed :
     *   T is ExampleIdentifier::I ? null :
     *   T is ExampleIdentifier::J ? object :
     *   T is ExampleIdentifier::K ? resource :
     *   T is ExampleIdentifier::L ? string :
     *   T is ExampleIdentifier::M ? true :
     *   T is ExampleIdentifier::N ? never :
     *   T is ExampleIdentifier::O ? void :
     *   mixed
     * )>
     */
    final class ExampleBuiltinType extends ExampleType
    {
        /** @param T $identifier */
        public function __construct(public ExampleIdentifier $identifier) {}
    }
    
    function reproduce(): ExampleType
    {
        return new ExampleBuiltinType(ExampleIdentifier::A);
    }

    Run php phpstan.phar analyse repro.php --debug --no-progress --memory-limit=256M on PHP 8.4.25:

    • PHPStan 2.2.14: completes with [OK] No errors.
    • PHPStan 2.2.15: crashes after about 6 seconds with Allowed memory size of 268435456 bytes exhausted (observed in NarrowedSubjectType.php:114; the exact allocation site varies).

    With Xdebug enabled, the stack repeatedly goes through ConditionalType::getNormalizedElse() → narrowSubjectIn() → NarrowedSubjectType::narrowReferences() → TypeTraverser::map() → ConditionalType::traverse(), until Xdebug stops at a depth of 512 frames. This suggests the new branch normalization in 91dcf7e repeatedly traverses the remaining conditional chain, causing a large expansion in work and memory.

  4. phpstan-bot commented on Sep 25, 2026

    @phpstan-bot
    Contributor

    Closed via merging phpstan/phpstan-src#6586

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions