Skip to content
1 change: 0 additions & 1 deletion src/Analyser/ExprHandler/FuncCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
if (
$functionReflection !== null
&& $this->rememberPossiblyImpureFunctionValues
&& $parametersAcceptor !== null
&& $functionReflection->hasSideEffects()->maybe()
&& !$functionReflection->isBuiltin()
) {
Expand Down
12 changes: 11 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3787,6 +3787,13 @@ private function createConditionalExpressions(
): array
{
$newVariableTypes = $ourExpressionTypes;

// When our-branch type is a subtype of their-branch type, the union
// absorbs it (merged === their). Such a variable is a poor *guard* —
// asserting its our-branch type later wouldn't reliably select this
// branch — but it remains a valid conditional *target*, so only exclude
// it from guard selection instead of dropping it entirely.
$guardsToExclude = [];
foreach ($theirExpressionTypes as $exprString => $holder) {
if (!array_key_exists($exprString, $mergedExpressionTypes)) {
continue;
Expand All @@ -3804,7 +3811,7 @@ private function createConditionalExpressions(
continue;
}

unset($newVariableTypes[$exprString]);
$guardsToExclude[$exprString] = true;
}

$typeGuards = [];
Expand All @@ -3818,6 +3825,9 @@ private function createConditionalExpressions(
if (!$holder->getCertainty()->yes()) {
continue;
}
if (array_key_exists($exprString, $guardsToExclude)) {
continue;
}

if (
array_key_exists($exprString, $theirExpressionTypes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier):

if ($identifier->isClass()) {
$fetchedClassNode = null;
$fetchedFile = null;
foreach ($files as $file) {
$fetchedClassNodes = $this->fileNodesFetcher->fetchNodes($file)->getClassNodes();

Expand All @@ -121,13 +122,10 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier):

/** @var FetchedNode<Node\Stmt\ClassLike> $fetchedClassNode */
$fetchedClassNode = current($fetchedClassNodes[$identifierName]);
$fetchedFile = $file;
}

if ($fetchedClassNode === null) {
return null;
}

[$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($file, $identifier);
[$reflectionCacheKey, $variableCacheKey] = $this->getCacheKeys($fetchedFile, $identifier);
$classReflection = $this->nodeToReflection($reflector, $fetchedClassNode);
$this->cache->save($reflectionCacheKey, $variableCacheKey, $classReflection->exportToCache());

Expand Down
40 changes: 40 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11281.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug11281;

use function PHPStan\Testing\assertType;

function hello2(string $values): void
{
$values = json_decode($values);
$hasError = false;
try {
$values = array_map(static function ($item) {
return Hello::fromObject($item);
}, $values);
assertType('array<Bug11281\Hello>', $values);
} catch (\Throwable) {
$hasError = true;
}
if (!$hasError) {
// The successful try-branch proves $values is array<Hello>; the
// pre-assignment mixed must not make the merged type collapse to mixed.
assertType('array<Bug11281\Hello>', $values);
}
}

final class Hello
{

public function __construct(public int $a)
{
}

public static function fromObject(\stdClass $object): self
{
return new self(...(array) $object);
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-5051.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function testWithBooleans($data): void

if ($update) {
assertType('10', $data);
assertType('bool', $foo);
assertType('true', $foo);
} else {
assertType('1|2|3', $data);
assertType('bool', $foo);
Expand Down
71 changes: 71 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-7948.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1);

namespace Bug7948;

use function PHPStan\Testing\assertType;

class HelloWorld
{
/**
* @param string|array<string, mixed> $name
* @param mixed $value
*/
public function testMixed($name, $value): void
{
if (is_array($name)) {
$value = null;
}

if (is_array($name)) {
assertType('null', $value);
}
}

/**
* @param string|array<string, mixed> $name
* @param int $value
*/
public function testInt($name, $value): void
{
if (is_array($name)) {
$value = null;
}

if (is_array($name)) {
assertType('null', $value);
}
}

/**
* Assigned value (5) is a subtype of the original type (int|string),
* so the merged type absorbs it just like null|mixed does.
*
* @param string|array<string, mixed> $name
* @param int|string $value
*/
public function testSubtype($name, $value): void
{
if (is_array($name)) {
$value = 5;
}

if (is_array($name)) {
assertType('5', $value);
}
}

/**
* @param string|array<string, mixed> $name
* @param mixed $value
*/
public function testMixedNegated($name, $value): void
{
if (!is_array($name)) {
$value = null;
}

if (!is_array($name)) {
assertType('null', $value);
}
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-8467b.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function foo (?string $cwd, bool $initialClone = false): void {

if ($initialClone && isset($origCwd)) {
assertType('string', $origCwd);
assertType('string|null', $cwd); // could be null
assertType('null', $cwd);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public function variants(string $s) {
assertType('string', $s);

if (strpos($s, ':') === 5) {
assertType('string', $s); // could be non-empty-string
assertType('non-falsy-string', $s);
}
assertType('string', $s);
if (strpos($s, ':') !== 5) {
Expand Down Expand Up @@ -164,7 +164,7 @@ public function variants(string $s) {
assertType('string', $s);

if (mb_strpos($s, ':') === 5) {
assertType('string', $s); // could be non-empty-string
assertType('non-falsy-string', $s);
}
assertType('string', $s);
if (mb_strpos($s, ':') !== 5) {
Expand Down
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1302,4 +1302,12 @@ public function testBug2861(): void
$this->analyse([__DIR__ . '/data/bug-2861.php'], []);
}

public function testBug4905(): void
{
$this->checkThisOnly = false;
$this->checkUnionTypes = true;
$this->checkDynamicProperties = false;
$this->analyse([__DIR__ . '/data/bug-4905.php'], []);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,6 @@ public function testTypesAssignedToPropertiesExpressionNames(): void
'Property PropertiesFromArrayIntoObject\Foo::$lall (int) does not accept string.',
69,
],
[
'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float.',
Comment thread
VincentLanglet marked this conversation as resolved.
83,
],
[
'Property PropertiesFromArrayIntoObject\Foo::$foo (string) does not accept float|int|string.',
97,
Expand Down
31 changes: 31 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-4905.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types = 1); // lint >= 8.0

namespace Bug4905;

class Event {
public ?EventPreset $eventPreset = null;
public function isSpecial(): bool {
return rand(1, 5) === 5;
}
}

class EventPreset {
public bool $test = false;
}

class HelloWorld
{
function test(): void {
$event = rand(1, 5) === 5 ? new Event() : null;
$eventPreset = $event?->eventPreset;
$isSpecial = $event?->isSpecial();

if ($isSpecial) {
assert($eventPreset instanceof EventPreset);
}

if ($isSpecial && $eventPreset->test) {

}
}
}
Loading