Skip to content

Global constant "not found" when an earlier function body in its file uses ::class #15482

Closed
phpstan/phpstan-src
#6756

Description

@Victor-emil

Bug report

A top-level const in a file of an analysed directory is reported as Constant … not found when an earlier function body in the same file uses Foo::class. Functions declared after that point are still found. We hit it in a Pest tests/Pest.php, which declares helper functions and constants side by side.

The cause seems to be in PhpFileCleaner::clean(), as SymbolFinderInFiles::findSymbols() calls it:

  • findSymbols() passes the number of keyword matches as $maxMatches.
  • When that number isn't 1, the cleaner sets $inType = true on any class, interface, trait or enum. It skips the (?<![\$:>]) lookbehind that $type['pattern'] applies in the single-match path, so the class in stdClass::class counts.
  • $typeLevel isn't tracked outside a type, so the function's closing } takes it to -1, and it doesn't get back to 0.
  • From then on, every top-level const NAME is rewritten to class_const NAME. The symbol regex never matches that, so the constant is missing from OptimizedDirectorySourceLocator's map.

Reproduced with 2.2.15. PhpFileCleaner on the 2.3.x branch has the same code.

Code snippet that reproduces the problem

The playground analyses one file, so it can't show this: the constant must come from another file in an analysed directory.

src/a_usage.php:

<?php

echo BAR;

src/z_functions.php:

<?php

function foo(): string
{
    return stdClass::class;
}

const BAR = 'bar';

phpstan.neon:

parameters:
    level: max
    paths:
        - src

vendor/bin/phpstan analyse reports:

 ------ ------------------------------------------------------------
  Line   a_usage.php
 ------ ------------------------------------------------------------
  3      Constant BAR not found.
         🪪  constant.notFound
  3      Parameter #1 (mixed) of echo cannot be converted to string.
         🪪  echo.nonString
 ------ ------------------------------------------------------------

Variations:

  • With return 'stdClass'; in foo(), there are no errors.
  • With a function named className(), there are none either. The cleaner then enters the "type" at the name and leaves it at the function's closing brace.

Expected output

No errors: BAR is a global constant declared in an analysed file.

A possible fix is to apply the lookbehind before setting $inType in the multi-match path as well. Only the lookbehind, not the whole pattern, so that new class { still counts. Keeping $typeLevel from going below 0 might also work.

Activity

  1. mergeable commented on Oct 15, 2026

    @mergeable

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

    It will most likely be closed after manual review.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions