Skip to content

Fix phpstan/phpstan#14384: Result of function_exists() can't be "cached"#5303

Merged
staabm merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-d81bevj
Mar 26, 2026
Merged

Fix phpstan/phpstan#14384: Result of function_exists() can't be "cached"#5303
staabm merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-d81bevj

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When storing the result of function_exists() in a variable and using that variable as a condition later, PHPStan reported function.notFound because the function existence narrowing was not propagated through the variable assignment.

Changes

  • Added FuncCall to the allowed expression types in processSureTypesForConditionalExpressionsAfterAssign() and processSureNotTypesForConditionalExpressionsAfterAssign() in src/Analyser/ExprHandler/AssignHandler.php
  • Added regression test in tests/PHPStan/Rules/Functions/data/bug-14384.php

Root cause

FunctionExistsFunctionTypeSpecifyingExtension creates a synthetic FuncCall expression as the target of type narrowing (e.g., function_exists('some_func') mapped to ConstantBooleanType(true)). When assigning $var = function_exists('some_func'), the AssignHandler processes the specified types to create ConditionalExpressionHolder objects that link the variable's truthiness to the narrowed expressions. However, the filter in processSureTypesForConditionalExpressionsAfterAssign only allowed Variable, PropertyFetch, and ArrayDimFetch expressions through, silently dropping FuncCall expressions. This meant the function existence narrowing was never stored as a conditional expression, so if ($var) didn't know the function existed.

The same fix also applies to class_exists() / interface_exists() / trait_exists() / enum_exists() which use the same pattern with synthetic FuncCall expressions.

Test

Added tests/PHPStan/Rules/Functions/data/bug-14384.php which stores function_exists() result in a variable and calls the function conditionally. The test expects no errors (false positive fix).

Fixes phpstan/phpstan#14384

…e not narrowing scope

- Added FuncCall to the allowed expression types in processSureTypesForConditionalExpressionsAfterAssign and processSureNotTypesForConditionalExpressionsAfterAssign
- Previously only Variable, PropertyFetch, and ArrayDimFetch expressions were propagated through variable assignments as ConditionalExpressionHolders
- New regression test in tests/PHPStan/Rules/Functions/data/bug-14384.php
@staabm staabm requested a review from VincentLanglet March 26, 2026 12:43
@VincentLanglet
Copy link
Copy Markdown
Contributor

I wonder if this wasn't done on purpose.
I also wonder if there is more Expr to not skip (method call ? Static call ?).

But i have nothing against this bugfix...

@staabm staabm merged commit d3a9a3d into phpstan:2.1.x Mar 26, 2026
654 of 656 checks passed
@staabm staabm deleted the create-pull-request/patch-d81bevj branch March 26, 2026 12:59
@thg2k
Copy link
Copy Markdown
Contributor

thg2k commented Mar 26, 2026

I believe i recently ran into a similar issue with file_exists

@staabm
Copy link
Copy Markdown
Contributor

staabm commented Mar 26, 2026

@thg2k if you find a problem please reproduce on the playground and file an issue

@thg2k
Copy link
Copy Markdown
Contributor

thg2k commented Mar 26, 2026

I know the drill :-) too much work, also cause i'm still on 1.12 and didn't have time to check first if it was fixed in 2.x.

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.

4 participants