Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 16 additions & 5 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -4091,11 +4091,22 @@ public function applySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
$trackedType = $currentTypes[0];
$trackedNativeType ??= $currentTypes[1];
}
} elseif (!$specifiedTypes->shouldOverwrite() && $scope->isComplexUnionType($trackedType)) {
// mirrors addTypeToExpression()/removeTypeFromExpression(): narrowing
// a combinatorially-grown offset union doubles it with every isset()-
// style check and gets skipped (overwrites assign, they never narrow)
continue;
} else {
if (!$specifiedTypes->shouldOverwrite() && $scope->isComplexUnionType($trackedType)) {
// mirrors addTypeToExpression()/removeTypeFromExpression(): narrowing
// a combinatorially-grown offset union doubles it with every isset()-
// style check and gets skipped (overwrites assign, they never narrow)
continue;
}
if ($trackedNativeType === null) {
// a phpdoc-only holder (e.g. one a conditional expression put in
// place) - the native base comes from the native state, not from
// the phpdoc-derived specified type
$currentTypes = $scope->getCurrentTypesOfSpecifiedExpr($expr);
if ($currentTypes !== null) {
$trackedNativeType = $currentTypes[1];
}
}
}

if (isset($typeSpecification['terms'])) {
Expand Down
106 changes: 106 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15404.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php declare(strict_types = 1);

namespace Bug15404;

use function PHPStan\Testing\assertNativeType;
use function PHPStan\Testing\assertType;

/** @param array{projects?: list<string>} $s */
function a(array $s): void
{
$has = isset($s['projects']);
if (!$has) {
return;
}
foreach ($s['projects'] as $p) {
assertType('string', $p);
assertNativeType('mixed', $p);
if (!is_string($p)) {
throw new \InvalidArgumentException();
}
}
}

/** @param array{projects?: list<string>} $s */
function inlineIsset(array $s): void
{
if (!isset($s['projects'])) {
return;
}
foreach ($s['projects'] as $p) {
assertType('string', $p);
assertNativeType('mixed', $p);
}
}

/** @param array{projects?: list<string>} $s */
function arrayKeyExists(array $s): void
{
$has = array_key_exists('projects', $s);
if (!$has) {
return;
}
foreach ($s['projects'] as $p) {
assertType('string', $p);
assertNativeType('mixed', $p);
}
}

/** @param array{projects?: list<string>} $s */
function notEmptyArray(array $s): void
{
$has = isset($s['projects']);
if (!$has) {
return;
}
if ($s['projects'] !== []) {
assertType('non-empty-list<string>', $s['projects']);
assertNativeType('mixed~array{}', $s['projects']);
}
}

/** @param array{projects?: list<string>} $s */
function isArrayCheck(array $s): void
{
$has = isset($s['projects']);
if (!$has) {
return;
}
if (is_array($s['projects'])) {
assertType('list<string>', $s['projects']);
assertNativeType('array<mixed, mixed>', $s['projects']);
}
}

/** @param array{projects?: list<string>} $s */
function notIsArrayCheck(array $s): void
{
$has = isset($s['projects']);
if (!$has) {
return;
}
if (!is_int($s['projects'])) {
assertType('list<string>', $s['projects']);
assertNativeType('mixed~int', $s['projects']);
}
}

class Foo
{

/** @var list<string>|null */
public $projects;

public function doFoo(): void
{
$has = $this->projects !== null;
if (!$has) {
return;
}
foreach ($this->projects as $p) {
assertType('string', $p);
assertNativeType('mixed', $p);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,9 @@ public function testBug15250(): void
$this->analyse([__DIR__ . '/data/bug-15250.php'], []);
}

public function testBug15404(): void
{
$this->analyse([__DIR__ . '/data/bug-15404.php'], []);
}

}
17 changes: 17 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-15404.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);

namespace Bug15404Rule;

/** @param array{projects?: list<string>} $s */
function a(array $s): void
{
$has = isset($s['projects']);
if (!$has) {
return;
}
foreach ($s['projects'] as $p) {
if (!is_string($p)) {
throw new \InvalidArgumentException();
}
}
}
23 changes: 23 additions & 0 deletions turbo-ext/src/MutatingScope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7826,6 +7826,29 @@ class MutatingScope
skipSpecification = true;
}
}
if (!skipSpecification && !hasTrackedNative) {
/* a phpdoc-only holder - the native base comes from the
* native state, not from the phpdoc-derived specified type */
zval exprZv;
ZVAL_OBJ(&exprZv, expr);
zv::Val currentTypes = otherPrivate(scopeObject, PT_LC("getcurrenttypesofspecifiedexpr"), 1, &exprZv, [&](MutatingScope &other) {
return other.getCurrentTypesOfSpecifiedExpr(expr);
});
if (UNEXPECTED(currentTypes.isUndef())) return zv::Val();
if (Z_TYPE_P(currentTypes.raw()) != IS_NULL) {
if (UNEXPECTED(Z_TYPE_P(currentTypes.raw()) != IS_ARRAY)) {
zend_throw_error(NULL, "phpstan_turbo: getCurrentTypesOfSpecifiedExpr() answered an unexpected shape");
return zv::Val();
}
zval *native = zend_hash_index_find(Z_ARRVAL_P(currentTypes.raw()), 1);
if (UNEXPECTED(native == NULL)) {
zend_throw_error(NULL, "phpstan_turbo: getCurrentTypesOfSpecifiedExpr() answered an unexpected shape");
return zv::Val();
}
trackedNativeType = zv::Val::copyOf(zv::Ref(native));
hasTrackedNative = true;
}
}
}
if (skipSpecification) continue;

Expand Down
Loading