Skip to content
Merged
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
4 changes: 1 addition & 3 deletions src/Analyser/ExprHandler/AssignOpHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\AssignOp;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PHPStan\Analyser\AssignTargetWalkMode;
use PHPStan\Analyser\ExpressionContext;
Expand Down Expand Up @@ -77,7 +75,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$valueContext = $context;
if ($expr instanceof Expr\AssignOp\Coalesce) {
$valueScope = $valueScope->filterByFalseyValue(
new BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
new Expr\Isset_([$expr->var]),
);

if ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
Expand Down
185 changes: 185 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15021.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php declare(strict_types = 1);

namespace Bug15021;

use function PHPStan\Testing\assertType;

/** @param array{foo?: string, bar?: string} $data */
function optionalOffset(array $data): void
{
$data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data);
}

/** @param array{foo?: string, bar?: string} $data */
function optionalOffsetResult(array $data): void
{
$data['foo'] ??= $data['bar'] ?? null;
assertType('array{foo: string|null, bar?: string}', $data);
}

/** @param array<string, string> $data */
function nonConstantArray(array $data, string $key): void
{
$data[$key] ??= assertType('array<string, string>', $data);
}

/** @param array<string, string> $data */
function nonConstantArrayConstantKey(array $data): void
{
$data['foo'] ??= assertType('array<string, string>', $data);
}

/** @param array<string, string|null> $data */
function nonConstantArrayNullableValue(array $data): void
{
$data['foo'] ??= assertType('string|null', $data['foo']);
}

/** @param array{a?: array{b?: string, c?: string}} $data */
function nestedOptionalOffset(array $data): void
{
$data['a']['b'] ??= assertType('array{a?: array{b?: string, c?: string}}', $data);
}

class Foo
{

public string $nonNullable = '';

public static string $staticNonNullable = '';

/** @var array{foo?: string, bar?: string} */
public array $data = [];

}

function nonNullableProperty(Foo $foo): void
{
$foo->nonNullable ??= assertType('string', $foo->nonNullable);
}

function nonNullableStaticProperty(): void
{
Foo::$staticNonNullable ??= assertType('string', Foo::$staticNonNullable);
}

function propertyOffset(Foo $foo): void
{
$foo->data['foo'] ??= assertType('array{foo?: string, bar?: string}', $foo->data);
}

/** @param \ArrayAccess<string, string> $a */
function nonNullableOffsetAccess(\ArrayAccess $a): void
{
$a['foo'] ??= assertType('string|null', $a['foo']);
}

/** @param \ArrayAccess<string, string> $a */
function nonNullableOffsetAccessCoalesce(\ArrayAccess $a): void
{
$x = $a['foo'] ?? assertType('string|null', $a['foo']);
}

/** @param \ArrayAccess<string, string> $a */
function nonNullableOffsetAccessDifferentOffset(\ArrayAccess $a): void
{
$a['foo'] ??= assertType('string|null', $a['bar']);
}

/** @param \ArrayAccess<string, string|null> $a */
function nullableOffsetAccess(\ArrayAccess $a): void
{
$a['foo'] ??= assertType('string|null', $a['foo']);
}

/** @param \ArrayAccess<string, string|null> $a */
function nullableOffsetAccessCoalesce(\ArrayAccess $a): void
{
$x = $a['foo'] ?? assertType('string|null', $a['foo']);
}

/** @param \ArrayObject<string, string> $data */
function arrayObjectOffset(\ArrayObject $data): void
{
$data['foo'] ??= assertType('string|null', $data['foo']);
}

/** @param \ArrayObject<string, string> $data */
function arrayObjectOffsetCoalesce(\ArrayObject $data): void
{
$x = $data['foo'] ?? assertType('string|null', $data['foo']);
}

/** @param array{foo?: string, bar?: string} $data */
function issetOnRightSide(array $data): void
{
$data['foo'] ??= isset($data['bar']) ? assertType('string', $data['bar']) : 'fallback';
}

/** @param array{foo?: string, bar?: string} $data */
function emptyOnRightSide(array $data): void
{
$data['foo'] ??= empty($data['bar']) ? 'fallback' : assertType('non-falsy-string', $data['bar']);
}

/** @param array{foo?: string, bar?: string} $data */
function unsetTargetBeforeAssignOp(array $data): void
{
unset($data['foo']);
$data['foo'] ??= assertType('array{bar?: string}', $data);
}

/** @param array{foo?: string, bar?: string} $data */
function emptyTargetBeforeAssignOp(array $data): void
{
if (empty($data['foo'])) {
$data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data);
}
}

/** @param array{foo?: string, bar?: string} $data */
function assignOpInsideEmpty(array $data): void
{
if (empty($data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data))) {
echo 'empty';
}
}

/** @param array{foo?: string, bar?: string} $data */
function assignOpInsideIsset(array $data): void
{
if (isset($data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data))) {
echo 'isset';
}
}

/** @param array{foo?: string, bar?: string} $data */
function assignOpInsideUnsetOffset(array $data): void
{
$other = ['x' => 1, 'fallback' => 2];
unset($other[$data['foo'] ??= assertType('array{foo?: string, bar?: string}', $data)]);
}

/** @param array{foo?: string, bar?: string} $data */
function assignOpResultInsideEmpty(array $data): void
{
if (empty($data['foo'] ??= $data['bar'] ?? 'fallback')) {
assertType("array{foo: ''|'0', bar?: string}", $data);
}
}

/** @param array{foo?: string, bar?: string} $data */
function assignOpResultInsideIsset(array $data): void
{
if (isset($data['foo'] ??= $data['bar'] ?? null)) {
assertType('array{foo: string, bar?: string}', $data);
}
}

/** @param array{foo?: string, bar?: string} $data */
function unsetAssignOpResultOffset(array $data): void
{
$other = ['x' => 1, 'fallback' => 2];
unset($other[$data['foo'] ??= $data['bar'] ?? 'fallback']);
assertType('array{x?: 1, fallback?: 2}', $other);
}
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Variables/EmptyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,11 @@ public function testIssetAfterRememberedConstructor(): void
]);
}

public function testNullCoalesceAssignRightSideScope(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/null-coalesce-assign-right-side-scope.php'], []);
}

}
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Variables/IssetRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -610,4 +610,11 @@ public function testBug14610(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14610.php'], []);
}

public function testNullCoalesceAssignRightSideScope(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/null-coalesce-assign-right-side-scope.php'], []);
}

}
39 changes: 39 additions & 0 deletions tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -558,4 +558,43 @@ public function testBug14393(): void
]);
}

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

public function testNullCoalesceAssignRightSideScope(): void
{
$this->analyse([__DIR__ . '/data/null-coalesce-assign-right-side-scope.php'], [
[
'Property NullCoalesceAssignRightSideScope\Foo::$nonNullable (string) on left side of ??= is not nullable.',
19,
],
[
'Property NullCoalesceAssignRightSideScope\Foo::$nonNullable (string) on left side of ?? is not nullable.',
19,
],
[
'Static property NullCoalesceAssignRightSideScope\Foo::$staticNonNullable (string) on left side of ??= is not nullable.',
24,
],
[
'Static property NullCoalesceAssignRightSideScope\Foo::$staticNonNullable (string) on left side of ?? is not nullable.',
24,
],
[
'Variable $undefined on left side of ??= is never defined.',
46,
],
[
'Variable $undefined on left side of ?? is never defined.',
46,
],
[
'Offset \'foo\' on array{bar?: string} on left side of ??= does not exist.',
89,
],
]);
}

}
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Variables/UnsetRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,9 @@ public function testUnsetHookedProperty(): void
]);
}

public function testNullCoalesceAssignRightSideScope(): void
{
$this->analyse([__DIR__ . '/data/null-coalesce-assign-right-side-scope.php'], []);
}

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

namespace Bug15021;

/** @param array{foo?: string, bar?: string} $data */
function foo(array $data): void {
$data['foo'] ??= $data['bar'] ?? null;
}
Loading
Loading