Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -972,7 +972,27 @@ private function resolveType(string $exprString, Expr $node): Type
&& !$node instanceof Expr\ArrowFunction
&& $this->hasExpressionType($node)->yes()
) {
return $this->expressionTypes[$exprString]->getType();
$type = $this->expressionTypes[$exprString]->getType();

if (
$node instanceof FuncCall
&& $node->name instanceof Name
&& !$node->isFirstClassCallable()
&& count($node->getArgs()) >= 1
) {
$funcName = $node->name->toLowerString();
if (in_array($funcName, ['count', 'sizeof'], true)) {
$argType = $this->getType($node->getArgs()[0]->value);
$type = TypeCombinator::intersect($type, $argType->getArraySize());
} elseif (
in_array($funcName, ['strlen', 'mb_strlen'], true)
&& $this->getType($node->getArgs()[0]->value)->isNonEmptyString()->yes()
) {
$type = TypeCombinator::intersect($type, IntegerRangeType::fromInterval(1, null));
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to patch the types after the fact, instead of remembering the proper types in the first place?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed. Here's a summary of what changed:

The reviewer's concern: Why patch cached types on retrieval instead of storing the correct types from the start?

The fix: Moved the refinement logic from resolveType() (retrieval time) to specifyExpressionType() (storage time). When a variable's type is narrowed via specifyExpressionType, the new refineDependentFuncCallTypes method scans expressionTypes for count()/sizeof()/strlen()/mb_strlen() FuncCall entries whose first argument matches the narrowed variable, and intersects their cached types with the constraint derived from the new variable type. This ensures cached expression types are always correct when retrieved, rather than being patched after the fact.

The resolveType() method is back to simply returning the cached type directly.

  • make phpstan — no errors
  • make tests — all 12,105 tests pass
  • Verified test fails without any fix, passes with the storage-time fix


return $type;
}

/** @var ExprHandler<Expr> $exprHandler */
Expand Down
84 changes: 84 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13750.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Bug13750;

use function PHPStan\Testing\assertType;

class HelloWorld
{
/** @param array<mixed> $arr */
public function forgetCount(array $arr): void
{
if (count($arr) > 2) {
assertType('non-empty-array<mixed>', $arr);
assertType('int<3, max>', count($arr));
}
assertType('array<mixed>', $arr);
assertType('int<0, max>', count($arr));
if (count($arr, COUNT_RECURSIVE) > 2) {
assertType('non-empty-array<mixed>', $arr);
assertType('int<1, max>', count($arr));
}
}

/** @param array<mixed> $arr */
public function sizeofAfterCount(array $arr): void
{
if (sizeof($arr) > 2) {
assertType('int<3, max>', sizeof($arr));
}
assertType('int<0, max>', sizeof($arr));
if (sizeof($arr, COUNT_RECURSIVE) > 2) {
assertType('non-empty-array<mixed>', $arr);
assertType('int<1, max>', sizeof($arr));
}
}

/** @param array<mixed> $arr */
public function countAfterNonEmptyNarrowing(array $arr): void
{
if (count($arr) > 2) {
assertType('int<3, max>', count($arr));
}
assertType('int<0, max>', count($arr));
if ($arr !== []) {
assertType('non-empty-array<mixed>', $arr);
assertType('int<1, max>', count($arr));
}
}

/** @param array<mixed> $arr */
public function nestedCountPreservation(array $arr): void
{
if (count($arr) > 2) {
assertType('int<3, max>', count($arr));
if (count($arr, COUNT_RECURSIVE) > 10) {
assertType('int<3, max>', count($arr));
}
}
}

public function strlenAfterNonEmptyNarrowing(string $str): void
{
if (strlen($str) > 5) {
assertType('int<6, max>', strlen($str));
}
assertType('int<0, max>', strlen($str));
if ($str !== '') {
assertType('non-empty-string', $str);
assertType('int<1, max>', strlen($str));
}
}

public function mbStrlenAfterNonEmptyNarrowing(string $str): void
{
if (mb_strlen($str) > 5) {
assertType('int<6, max>', mb_strlen($str));
}
assertType('int<0, max>', mb_strlen($str));
if ($str !== '') {
assertType('non-empty-string', $str);
assertType('int<1, max>', mb_strlen($str));
}
}
}
Loading