Skip to content

Remember call expression as truthy/falsey alongside @phpstan-assert-if-true argument narrowing#5880

Open
phpstan-bot wants to merge 4 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-sxihmja
Open

Remember call expression as truthy/falsey alongside @phpstan-assert-if-true argument narrowing#5880
phpstan-bot wants to merge 4 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-sxihmja

Conversation

@phpstan-bot

@phpstan-bot phpstan-bot commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

PHPStan reported a false positive Path in require() ... is not a file or it does not exist. when a require/include was guarded by is_readable(). RequireFileExistsRule suppresses the error when the path is guarded by file_exists() or is_file() by checking $scope->getType($call)->isTrue()->yes(). For is_readable() (and a few siblings) this check returned bool instead of true, so the guard was not recognized.

The root cause is more general than the rule: functions/methods carrying @phpstan-assert-if-true (such as is_readable/is_writable/is_executable in stubs/file.stub) narrowed only their arguments in the truthy branch and did not remember the call expression itself as truthy. Re-evaluating the call in the same branch therefore lost the result.

Changes

  • src/Analyser/ExprHandler/FuncCallHandler.php, MethodCallHandler.php, StaticCallHandler.php: when a call has assertions, union the assert-specified types with handleDefaultTruthyOrFalseyContext() so the call expression is remembered as truthy/falsey, while preserving the asserts' original root expression via setRootExpr().
  • src/Rules/Comparison/ImpossibleCheckTypeHelper.php: skip the self-referential narrowing entry (where the sure(-not) type expression is the checked node itself), since a check's own truthiness is not independent evidence of redundancy.
  • src/Rules/Keywords/RequireFileExistsRule.php: recognize is_readable, is_writable, is_writeable and is_executable as existence guards next to file_exists/is_file, extracted into a named FILE_EXISTENCE_FUNCTIONS constant.
  • Tests: tests/PHPStan/Analyser/nsrt/bug-14829.php asserts the call expression narrows to true for functions, methods and static methods with @phpstan-assert-if-true; tests/PHPStan/Rules/Keywords/data/include-in-file-exists.php covers the new guard functions.

Root cause

The pattern is "asserts narrow arguments but forget the call result". FuncCallHandler::specifyTypes() (and the method/static-call equivalents) returned early after specifyTypesFromAsserts(), never falling through to handleDefaultTruthyOrFalseyContext() which records the call expression's own truthiness. This was wrong identically in all three call handlers — all three are fixed.

The conditional-return-type path (specifyTypesFromConditionalReturnType) was probed and is already correct: re-evaluating recomputes the conditional return type from the now-narrowed argument, so the result resolves to true without needing the call expression to be remembered.

Adding the call-expression narrowing exposed an interaction with ImpossibleCheckTypeHelper, which inspects the raw SpecifiedTypes: the new self-referential entry was treated as Maybe and suppressed legitimate always-true/false detection, and its root expression tripped the helper's short-circuit. Both are handled (preserve the asserts' root expression; ignore the self entry).

Test

  • bug-14829.php (NodeScopeResolverTest): if (is_readable($path)) { assertType('true', is_readable($path)); assertType('non-empty-string', $path); }, plus method and static-method analogues — all fail (bool) without the fix.
  • RequireFileExistsRuleTest::testInFileExists: extended include-in-file-exists.php with is_readable/is_writable/is_writeable/is_executable guards; fails without the fix.
  • Full suite (make tests) and self-analysis (make phpstan) are green.

Fixes phpstan/phpstan#14829

Comment thread build/PHPStan/Build/OrChainIdenticalComparisonToInArrayRule.php
@staabm staabm force-pushed the create-pull-request/patch-sxihmja branch 2 times, most recently from ce1888f to 181e6e9 Compare June 15, 2026 15:26
@staabm staabm requested a review from VincentLanglet June 15, 2026 15:31
@staabm staabm force-pushed the create-pull-request/patch-sxihmja branch from 181e6e9 to 09df9e3 Compare June 15, 2026 16:07
phpstan-bot and others added 4 commits June 15, 2026 19:43
…if-true` argument narrowing

- When a function/method/static call carries `@phpstan-assert*` assertions, the
  call handlers previously returned only the assert-specified argument narrowing
  and skipped `handleDefaultTruthyOrFalseyContext`, so the call expression itself
  was not remembered as truthy/falsey in the branch. Re-evaluating it (e.g.
  `if (is_readable($p)) { require $p; }`) then yielded `bool` instead of `true`.
- `FuncCallHandler`, `MethodCallHandler` and `StaticCallHandler` now union the
  asserts result with the default truthy/falsey narrowing, while preserving the
  asserts' original root expression so impossible-check detection is unaffected.
- `ImpossibleCheckTypeHelper` now ignores the self-referential narrowing entry
  (the checked call expression itself), which carries no information about
  whether the check is redundant — the informative narrowing lives in the
  argument entries.
- `RequireFileExistsRule` additionally treats `is_readable`, `is_writable`,
  `is_writeable` and `is_executable` (next to `file_exists`/`is_file`) as guards
  that prove the path exists, via a named `FILE_EXISTENCE_FUNCTIONS` constant.
- Fixed a genuine `X && !X` bug in the build-only
  `OrChainIdenticalComparisonToInArrayRule::getSubjectAndValue()` that the
  improved narrowing newly detects.
@staabm staabm force-pushed the create-pull-request/patch-sxihmja branch from 8853ab8 to 969b695 Compare June 15, 2026 17:43
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.

"Path in require() __DIR__ . '/../vendor/autoload.php' is not a file or it does not exist." false positive

2 participants