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
8 changes: 8 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -4557,6 +4557,14 @@ private function preserveVacuousConditionalExpressions(
continue;
}

// Array shapes are unsealed in isSuperTypeOf(): array{a?: int} is a supertype
// of array{a?: int, b: int}, yet applying the holder would drop the 'b' key
// the other branch tracks. The consequent only holds on both paths when the
// other branch's state adds nothing to it.
if (!TypeCombinator::union($typeHolder->getType(), $otherTargetHolder->getType())->equals($typeHolder->getType())) {
continue;
}

$currentConditionalExpressions[$exprString][$key] = $holder;
}
}
Expand Down
29 changes: 29 additions & 0 deletions tests/PHPStan/Analyser/Bug15534Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

class Bug15534Test extends TypeInferenceTestCase
{

public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/bug-15534.php');
}

/**
* @param mixed ...$args
*/
#[DataProvider('dataFileAsserts')]
public function testFileAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

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

namespace Bug15534Types;

use function PHPStan\Testing\assertType;

function isEnabled(): bool { return (bool) rand(0, 1); }
function otherEnabled(): bool { return (bool) rand(0, 1); }
function getSavedPost(string $key): ?string { return rand(0, 1) === 1 ? $key : null; }

function doFoo(): void
{
$post = [];
if (getSavedPost('field0')) {
$post['field0'] = getSavedPost('field0');
}
if (getSavedPost('field1')) {
$post['field1'] = getSavedPost('field1');
}
if (otherEnabled()) {
if (getSavedPost('other0')) {
$post['other0'] = 'o';
}
}

if (isEnabled()) {
if (getSavedPost('c')) {
$post['company_address'] = 'c';
}
}

if (!otherEnabled()) {
assertType("array{}|array{field0?: non-falsy-string, field1?: non-falsy-string, other0?: 'o', company_address?: 'c'}", $post);
}
if (isEnabled()) {
assertType("array{}|array{field0?: non-falsy-string, field1?: non-falsy-string, other0?: 'o', company_address?: 'c'}", $post);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Variables;

use Override;
use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Properties\PropertyDescriptor;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* Array shapes are unsealed without bleeding edge.
*
* @extends RuleTestCase<NullCoalesceRule>
*/
class NullCoalesceRuleNoBleedingEdgeTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new NullCoalesceRule(
new IssetCheck(
new PropertyDescriptor(),
true,
$this->shouldTreatPhpDocTypesAsCertain(),
),
self::getContainer()->getByType(ConstantConditionInTraitHelper::class),
true,
);
}

#[Override]
public static function getAdditionalConfigFiles(): array
{
return [];
}

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

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

namespace Bug15534;

function isEnabled(): bool
{
return (bool) ($GLOBALS['enabled'] ?? false);
}

function isVisible(): bool
{
return (bool) ($GLOBALS['visible'] ?? false);
}

function otherEnabled(): bool
{
return (bool) ($GLOBALS['other'] ?? false);
}

function getSavedPost(string $key): ?string
{
if (!isset($_SESSION[$key]) || !is_string($_SESSION[$key])) {
return null;
}

return (string) $_SESSION[$key];
}

function translate(string $key): string
{
return $key;
}

$post = [];

if (getSavedPost('field0')) {
$post['field0'] = getSavedPost('field0');
}
if (getSavedPost('field1')) {
$post['field1'] = getSavedPost('field1');
}

if (otherEnabled()) {
if (getSavedPost('other0')) {
$post['other0'] = getSavedPost('other0');
}
}

if (isEnabled()) {
if (getSavedPost('company_address')) {
$post['company_address'] = getSavedPost('company_address');
}
}

if (isVisible() && !isEnabled()) {
echo 'other';
}

if (isVisible() && !otherEnabled()) {
echo translate(isEnabled() ? 'enabled' : 'disabled');
}

if (isEnabled()) {
echo $post['company_address'] ?? '';
}

8 changes: 8 additions & 0 deletions turbo-ext/src/MutatingScope.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8565,6 +8565,14 @@ class MutatingScope
bool yes;
if (UNEXPECTED(!isSuperTypeOfYes(Z_OBJ_P(typeHolderType.raw()), otherTargetType.raw(), yes))) return zv::Val();
if (!yes) continue;
/* array shapes are unsealed in isSuperTypeOf() - the consequent
* only holds on both paths when the other branch's state adds
* nothing to it */
zv::Val united = unionTypes(typeHolderType.raw(), otherTargetType.raw());
if (UNEXPECTED(united.isUndef())) return zv::Val();
bool unchanged;
if (UNEXPECTED(!typeEquals(united.raw(), typeHolderType.raw(), unchanged))) return zv::Val();
if (!unchanged) continue;
if (UNEXPECTED(!appendConditionalHolder(result, exprKey, exprIndex, key, keyIndex, holder))) return zv::Val();
}
}
Expand Down
Loading