Repository navigation
Date native functions and methods by the versioned callmaps - #12009
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit fcb47be. Configure here.
| // stub (the scanner only lets it through below that version) | ||
| $is_polyfill = $duplicate_storage->stubbed | ||
| && $duplicate_storage->since_php_version_id !== null | ||
| && $storage->user_defined; |
There was a problem hiding this comment.
Polyfill merge ignores analysis version
Medium Severity
In ClassLikeStorageProvider::addMore, $is_polyfill treats any user-defined class as replacing a versioned native stub whenever the stub has since_php_version_id, without comparing analysis_php_version_id to that version. The inline scanner only allows that replacement below the introduced version, so parallel scan merges can overwrite the native stub at the analysed version or newer and skip DuplicateClass.
Reviewed by Cursor Bugbot for commit fcb47be. Configure here.
fcb47be to
1e5a585
Compare
076167f to
4322d4e
Compare
A native symbol newer than the analysed PHP version is not reported where a
guard proves the running PHP has it:
- PHP_VERSION_ID comparisons now narrow the constant through a pseudo-variable
in the context, so if/else, early returns, ternaries and && all apply, e.g.
after `if (PHP_VERSION_ID < 80300) { return ...; }`.
- function_exists(), class_exists(), interface_exists(), enum_exists() and
method_exists() on such a symbol assert PHP_VERSION_ID >= the version that
introduced it.
The class, method, function and property checks compare against the lowest
PHP version the branch runs on (Codebase::getGuardedPhpVersionId()) instead of
the analysed one. Type signatures and docblocks can't be guarded and are
unchanged.
Co-Authored-By: Claude Opus 5.5
A native function or method without a stub `@since` is now dated by the first per-version callmap it appears in, so e.g. str_contains() (8.0), array_is_list() (8.1), array_find() (8.4) or ReflectionClass::isEnum() (8.1) are reported when used below that version. Psalm otherwise resolves them through the reflection of the PHP it runs on and silently accepts them. - Only symbols of extensions bundled with PHP are dated: the callmaps are generated with PECL extensions (swoole, redis, mongodb, ...) whose versions are unrelated to the PHP version. A bundled extension reports PHP's own version. - The first-appearance index is built once, lazily, on the first native symbol missing from the analysed version's callmap. - Polyfills count as available. `function_exists()` on such a function now evaluates against the analysed version, so the body of a polyfill's `if (!function_exists(...))` is scanned (and not a DuplicateFunction), and a function declared in any scanned PHP file is not reported. Co-Authored-By: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5
str_contains()/str_starts_with()/str_ends_with(), Reflection*::getAttributes() and SimpleXMLElement iteration are PHP 8.0 APIs, now reported when these tests ran at the default 7.4. Also match the new message tests with a word boundary. Co-Authored-By: Claude Opus 5.5
4322d4e to
f276a65
Compare
…on cached runs #12009 stopped an autoloaded polyfill of a native function the analysed PHP version predates (e.g. symfony/polyfill-php84's array_any) from replacing the native signature, but only on a fresh scan. On a warm cache the file storages are restored by the Scanner, and a rescan of an already-stored function goes through a separate branch; both still registered the polyfill globally, so e.g. the native purity of array_any was lost (ImpureFunctionCall in Psalm's own Atomic.php on every cached run). Co-Authored-By: Claude Opus 5.5


Stacked on #12010 (version guards); the first commit is #12010. Builds on #12006 (merged).
With #12010, guarded uses are not reported, e.g.
PHP_VERSION_ID >= 80000 ? fdiv(...) : ...,if (PHP_VERSION_ID < 80300) { return new ReflectionMethod($m); } return ReflectionMethod::createFromMethodName($m);,function_exists()/method_exists().Summary
The callmaps are versioned (
dictionaries/CallMap_70.php…CallMap_85.php). A native function or method without a stub@sinceis now dated by the first callmap it appears in, and reported when used below that version. On 6.x these are silently accepted atphpVersion="7.4", because Psalm resolves them through the reflection of the PHP it runs on:str_contains(),fdiv()array_is_list()array_find()array_first()ReflectionClass::isEnum()Filtering extension noise
The callmaps are generated with PECL extensions installed, and those extensions' versions are unrelated to the PHP version. For example, hundreds of Swoole, Redis, MongoDB, uv, ds and ssh2 keys "first appear" at arbitrary PHP versions. So only extensions bundled with PHP are dated, and a bundled extension reports
PHP_VERSIONas its own version. These symbols are reachable only through the runtime's reflection anyway, so the runtime answers that question.The version is the first appearance across all callmaps, not the next one after the analysed version. That way the few keys with gaps in the generated maps don't produce false positives.
Cost
A first-appearance index (about 25k keys) is built once, lazily. That only happens the first time a native symbol from a bundled extension is missing from the analysed version's callmap. Each callmap takes about 14 ms to load.
Polyfills
function_exists('x')in a conditional now evaluates against the analysed version whenxis dated by the callmaps. Before, a runtime-native function made Psalm skip the body ofif (!function_exists('str_contains')) { function str_contains(…) }, so symfony/polyfill and project polyfills were never scanned.DuplicateFunction("already defined as a core function").Checked on scratch projects at 7.4:
symfony/polyfill-php80: nothing is reported forstr_contains/str_starts_with, whilearray_is_list(8.1) isfdivpolyfill in one file, used from others: no reports, with 1 or 4 threadsKnown gaps
@sincestubs in Load genuinely-new native classes on every PHP version, tagged with @since #12008.array_findviasymfony/polyfill-php84. This errs towards not reporting.🤖 Generated with Claude Code