Skip to content

Resolve a global constant name written with asterisks in a PHPDoc type - #6995

Open
gnutix wants to merge 2 commits into
phpstan:2.3.xfrom
gnutix:global-constant-wildcard
Open

gnutix wants to merge 2 commits into
phpstan:2.3.xfrom
gnutix:global-constant-wildcard

Conversation

@gnutix

@gnutix gnutix commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

@param PHP_ROUND_* $mode now resolves to the union of the global constants the pattern matches (1|2|3|4), the way @param Foo::BAR_* $x does for class constants.

Merge this before phpstan/phpdoc-parser#342, which makes such a pattern parse (it is an invalid PHPDoc today). That PR's downstream job runs this repository's tests on 2.3.x with the new parser, and existing tests already contain @param FILTER_VALIDATE_* $intFilter (tests/PHPStan/Analyser/nsrt/filter-*.php), which throws ShouldNotHappenException without this change. Merging this first is harmless: the current parser never produces a class-less ConstFetchNode with a *.

So the test here (TypeNodeResolverTest) resolves the node that phpdoc-parser change produces, without needing the parser. The PHPDoc-level tests (an nsrt file and an unresolvable-type rule test) come in a follow-up with the phpdoc-parser bump; I ran them locally against the patched parser, along with the whole suite.

What it enables once the parser reads it:

namespace App;

/**
 * @param PHP_ROUND_* $mode     1|2|3|4, so round(1.5, 0, 7) is reported
 * @param E_*_ERROR $level      16|64|256|4096
 * @param FLAG_* $flag          App\FLAG_* if any match, else the global FLAG_*
 * @param \MY_* $g              define()d constants too
 * @param NOPE_* $n             "contains unresolvable type"
 */

How it works:

  • TypeNodeResolver resolves the pattern. Like PHP does for an unqualified constant name, an unqualified pattern looks in the current namespace first and falls back to the global one; a qualified one is resolved like a class name. Each match goes through ConstantResolver, so its type is what fetching that constant gives (dynamicConstantNames included).
  • ReflectionProvider::findConstantNames() is new. The interface is @api-do-not-implement, so adding a method keeps the BC promise. BetterReflectionProvider matches the pattern against the names from the stubs map, get_defined_constants() and reflectAllConstants(), keeping only the constants the configured PHP version has.

Costs and limits worth a look:

  • reflectAllConstants() parses every analysed or scanned file that declares a global constant, once per process and only when a global wildcard is used. On this repository's test container that is 51 files, +29 MB and 0.4 s. A cheaper path would list constant names straight from the symbol scan of the directory locators, but that needs a names-only API through the source locator wrappers, so I left it out.
  • The result cache does not record which constants a PHPDoc pattern matched, so adding a matching constant elsewhere does not re-analyse the file using the pattern. Foo::BAR_* has the same gap today.
  • A single global constant (@param PHP_ROUND_HALF_UP $x) is still read as a class name; only patterns are new.

CI: the Larastan, phpstan-dba and phpstan-laravel integration jobs fail with float typing differences (numeric-string → non-empty-string for a quoted float, float|null → float<0.0, 3037000499.97605>|null), unrelated to this change. They come from #6993 (now 4a6e0e9 on 2.3.x), which failed the same three jobs, and they fail the same way on #6994.

Closes phpstan/phpstan#7272

🤖 Generated with Claude Code

@PARAM PHP_ROUND_* $mode now resolves to the union of the global
constants the pattern matches, 1|2|3|4, the way @PARAM Foo::BAR_* does
for class constants. Like PHP does for an unqualified constant name, an
unqualified pattern looks in the current namespace first and falls back
to the global one; a qualified one is resolved like a class name. A
pattern matching nothing is an unresolvable type.

ReflectionProvider::findConstantNames() finds the matching constants
among the built-in ones from the stubs, the ones defined at runtime and
the ones declared in the analysed and scanned files, keeping only those
the configured PHP version has.

Needs phpstan/phpdoc-parser to read such a pattern.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PHPDoc-level tests need phpstan/phpdoc-parser to read PHP_ROUND_*,
and that change cannot be merged before this one: its downstream test of
this repository already meets @PARAM FILTER_VALIDATE_* in
tests/PHPStan/Analyser/nsrt/filter-*.php and throws without it. So this
tests TypeNodeResolver on the node that change produces, and the PHPDoc
tests come back with the phpdoc-parser bump.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support wildcard for global PHP constants (ex: @param PHP_ROUND_* $roundingMode)

2 participants