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
6 changes: 6 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2621,6 +2621,12 @@ public static function intersectButNotNever(Type $nativeType, Type $inferredType
}

$result = TypeCombinator::intersect($nativeType, $inferredType);
if ($result instanceof NeverType) {
// the inferred type says no value is ever produced - the native
// type's nullability must not resurrect one
return $result;
}

if (TypeCombinator::containsNull($nativeType)) {
return TypeCombinator::addNull($result);
}
Expand Down
99 changes: 99 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15285.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php // lint >= 8.2

declare(strict_types = 1);

namespace Bug15285;

use Closure;
use Generator;
use LogicException;
use RuntimeException;
use function PHPStan\Testing\assertType;

final class CartController
{

public function redirect(string $destination): never
{
throw new RuntimeException($destination);
}

/**
* @param Closure(string, (Closure(): never)|null=): never $errorCallback
*/
public function addToCart(bool $isAvailable, Closure $errorCallback): void
{
if ($isAvailable) {
return;
}

$errorCallback(
'This offer is no longer available.',
fn (): null => $this->redirect('/product'),
);
}

public function dump(): void
{
assertType('Closure(): never', fn () => $this->redirect('/product'));
assertType('Closure(): never', fn (): null => $this->redirect('/product'));
assertType('Closure(): never', fn (): int => throw new LogicException());
assertType('Closure(): never', fn (): ?int => throw new LogicException());

assertType('Closure(): never', function () {
$this->redirect('/product');
});
assertType('Closure(): never', function (): null {
$this->redirect('/product');
});
assertType('Closure(): never', function (): ?int {
throw new LogicException();
});

assertType('static-Closure(): never', static fn (): ?int => throw new LogicException());
assertType('static-Closure(): never', static function (): null {
throw new LogicException();
});

assertType('Closure(): never', function (): ?Generator {
throw new LogicException();
});
assertType('Closure(): Generator<int, 1, mixed, void>', function (): ?Generator {
yield 1;
});
}

}

/**
* @param list<never> $emptyList
*/
function neverCallableParameters(array $emptyList): void
{
array_map(function (?int $i): void {
assertType('never', $i);
}, $emptyList);

array_map(fn (?int $i) => assertType('never', $i), $emptyList);

array_map(function (null $i): void {
assertType('never', $i);
}, $emptyList);
}

/**
* @param callable(never...): void $cb
*/
function neverVariadicCallable(callable $cb): void
{
}

function neverVariadicCallableParameters(): void
{
neverVariadicCallable(function (?int $a, ?string $b): void {
assertType('never', $a);
assertType('never', $b);
});

neverVariadicCallable(fn (?int $a) => assertType('never', $a));
}
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,12 @@ public function testBug15251(): void
$this->analyse([__DIR__ . '/../Methods/data/bug-15251.php'], []);
}

#[RequiresPhp('>= 8.2.0')]
public function testBug15285(): void
{
$this->analyse([__DIR__ . '/data/bug-15285.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug11935WithCheckExplicitMixed(): void
{
Expand Down
33 changes: 33 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-15285.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php // lint >= 8.2

declare(strict_types = 1);

namespace Bug15285CallCallables;

use Closure;
use RuntimeException;

final class CartController
{

public function redirect(string $destination): never
{
throw new RuntimeException($destination);
}

/**
* @param Closure(string, (Closure(): never)|null=): never $errorCallback
*/
public function addToCart(bool $isAvailable, Closure $errorCallback): void
{
if ($isAvailable) {
return;
}

$errorCallback(
'This offer is no longer available.',
fn (): null => $this->redirect('/product'),
);
}

}
Loading