Skip to content

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

Merged
staabm merged 4 commits into
phpstan:2.2.xfrom
staabm:backup
Jun 16, 2026
Merged

Remember call expression as truthy/falsey alongside @phpstan-assert-if-true argument narrowing#5885
staabm merged 4 commits into
phpstan:2.2.xfrom
staabm:backup

Conversation

@staabm

@staabm staabm commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

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

extracted from #5880

phpstan-bot and others added 2 commits June 16, 2026 01:56
…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 marked this pull request as draft June 16, 2026 08:03
@staabm

staabm commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

MethodCall/StaticCall handling for the same feature (default true-false-context after type-asserts) will be done in a separate PR, because it currently results in CI only errors (and its not necessary for the feature request this PR is trying to solve)

@staabm staabm marked this pull request as ready for review June 16, 2026 12:10
@staabm staabm requested a review from VincentLanglet June 16, 2026 12:10
@phpstan-bot

Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

@staabm staabm merged commit 618e671 into phpstan:2.2.x Jun 16, 2026
670 of 673 checks passed
@staabm staabm deleted the backup branch June 16, 2026 12:20
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

3 participants