Skip to content

Fix phpstan/phpstan#5473: TypeSpecifier should be called in processExprNode#5342

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-yq6mtdi
Closed

Fix phpstan/phpstan#5473: TypeSpecifier should be called in processExprNode#5342
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-yq6mtdi

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a function/method call with @phpstan-assert was used as a sub-expression (e.g., Assert::notNull($value)->throws(...)), the type narrowing from the assertion was lost. Similarly, assert($x !== null) && assert(strlen($x) > 1) as an expression statement did not properly combine both assertions.

Changes

  • Modified src/Analyser/TypeSpecifier.php:
    • In the MethodCall handler, for null context (statement-level), recursively process $expr->var to apply assertions from intermediate calls. This handles chained patterns like Assert::notNull($value)->throws(...).
    • When the method itself also has assertions, union both the method's and var's assertions while preserving the original rootExpr (to avoid breaking impossible check detection).
    • For BooleanAnd with null context, use unionWith instead of intersectWith to properly combine assertions from both sides of &&.

Root cause

The TypeSpecifier only examined the outermost call in a chain when processing statement-level expressions with null context. For Assert::notNull($value)->throws(...), it checked throws() for assertions (finding none) but never examined the intermediate Assert::notNull($value) call which carried the @phpstan-assert !null $value annotation.

For the && case, intersectWith was used for null context which takes the union of types (broader) for common entries and drops entries not in both — losing narrowing information. Using unionWith (which intersects types for common entries and keeps all unique entries) correctly combines both assertions.

Test

Added tests/PHPStan/Analyser/nsrt/bug-5473.php covering:

  • Standalone assert call (already worked)
  • Chained method call after assert (was broken, now fixed)
  • Assignment of assert result (already worked)
  • assert() && assert() expression statement (was broken, now fixed)
  • Property access after chained assert (was broken, now fixed)

Fixes phpstan/phpstan#5473

…ression calls

- In TypeSpecifier, when processing a MethodCall with null context, also
  recursively process the var (object) expression to pick up assertions
  from intermediate calls like Assert::notNull($value)->throws(...)
- For BooleanAnd with null context, use unionWith instead of intersectWith
  to properly combine assertions from both sides of &&
- New regression test in tests/PHPStan/Analyser/nsrt/bug-5473.php
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-yq6mtdi branch March 30, 2026 13:59
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.

2 participants