Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 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
10 changes: 10 additions & 0 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -1584,6 +1584,16 @@ private function getConditionalSpecifiedTypes(
$ifType = $conditionalType->getIf();
$elseType = $conditionalType->getElse();

if (
(
$argumentExpr instanceof Node\Scalar
|| ($argumentExpr instanceof ConstFetch && in_array(strtolower($argumentExpr->name->toString()), ['true', 'false', 'null'], true))
&& ($ifType instanceof NeverType || $elseType instanceof NeverType)
)
Comment thread
VincentLanglet marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You didn't put my suggestion on the right line.

You wrote

$argumentExpr instanceof Node\Scalar
				|| ($argumentExpr instanceof ConstFetch && in_array(strtolower($argumentExpr->name->toString()), ['true', 'false', 'null'], true))
				&& ($ifType instanceof NeverType || $elseType instanceof NeverType)

instead of

(
$argumentExpr instanceof Node\Scalar
				|| ($argumentExpr instanceof ConstFetch && in_array(strtolower($argumentExpr->name->toString()), ['true', 'false', 'null'], true))
)
				&& ($ifType instanceof NeverType || $elseType instanceof NeverType)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized it a second ago :-/

) {
return null;
}

if ($leftType->isSuperTypeOf($ifType)->yes() && $rightType->isSuperTypeOf($elseType)->yes()) {
$context = $conditionalType->isNegated() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createTrue();
} elseif ($leftType->isSuperTypeOf($elseType)->yes() && $rightType->isSuperTypeOf($ifType)->yes()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,6 @@ public function testImpossibleCheckTypeFunctionCall(): void
'Call to function is_callable() with \'nonexistentFunction\' will always evaluate to false.',
87,
],
[
'Call to function is_numeric() with \'123\' will always evaluate to true.',
102,
],
[
'Call to function is_numeric() with \'blabla\' will always evaluate to false.',
105,
],
[
'Call to function is_numeric() with 123|float will always evaluate to true.',
118,
Expand Down Expand Up @@ -231,19 +223,11 @@ public function testImpossibleCheckTypeFunctionCall(): void
718,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Call to function is_numeric() with \'123\' will always evaluate to true.',
718,
],
[
'Call to function assert() with false will always evaluate to false.',
719,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
[
'Call to function is_numeric() with \'blabla\' will always evaluate to false.',
719,
],
[
'Call to function assert() with true will always evaluate to true.',
726,
Expand Down Expand Up @@ -1179,4 +1163,19 @@ public function testBug14177(): void
$this->analyse([__DIR__ . '/data/bug-14177.php'], []);
}

public function testBug13566(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-13566.php'], [
[
'Call to function is_numeric() with 123 will always evaluate to true.',
199,
],
[
'Call to function assertIsInt() with int will always evaluate to true.',
204,
],
]);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,18 @@ public function testBug12087b(): void
]);
}

public function testBug13566(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-13566.php'], []);
}

public function testBug10337(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-10337.php'], []);
}

public static function getAdditionalConfigFiles(): array
{
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ public function testBug12087b(): void
]);
}

public function testBug13566(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-13566.php'], []);
}

public static function getAdditionalConfigFiles(): array
{
return [
Expand Down
62 changes: 62 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-10337.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php declare(strict_types=1);

namespace Bug10337;

use function PHPStan\Testing\assertType;

class App
{
/**
* @return ($calledFromShutdownHandler is true ? void : never)
*/
public function callExit(bool $calledFromShutdownHandler = false): void
{
// run before shutdown code here

if (!$calledFromShutdownHandler) {
exit;
}
}

public function testOnlyVoid(): void
{
(new App())->callExit(true);
}

/**
* @return never
*/
public function testVoidAndNever(): void
{
$app = new App();
assertType('null', $app->callExit(true));
assertType('never', $app->callExit(false));
}

/**
* @return never
*/
public function testVoidAndNever2(): void
{
$app = new class() extends App {
};
assertType('null', $app->callExit(true));
assertType('never', $app->callExit(false));
}

/**
* @return never
*/
public function testVoidAndNever3(): void
{
$app = new class() extends App {
#[\Override]
public function callExit(bool $calledFromShutdownHandler = false): void
{
parent::callExit($calledFromShutdownHandler);
}
};
assertType('null', $app->callExit(true));
assertType('never', $app->callExit(false));
}
}
212 changes: 212 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-13566.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug13566;

use LogicException;

class ReturnViaBool
{
/** @return ($exit is true ? never : void) */
public static function notFound(bool $exit = true): void
{
header('HTTP/1.1 404 Not Found', true, 404);

if ($exit) {
echo '404 Not Found';
exit;
}
}

public function test(): void
{
// send 404 header without exiting
self::notFound(false);
}
}

class ReturnViaInt
{
/** @return ($exit is 1 ? never : void) */
Comment thread
VincentLanglet marked this conversation as resolved.
public static function notFound(int $exit = 1): void
{
header('HTTP/1.1 404 Not Found', true, 404);

if ($exit == 1) {
echo '404 Not Found';
exit;
}
}

public function test(): void
{
// send 404 header
self::notFound(0);
}
}

class ReturnViaString
{
/** @return ($exit is '1' ? never : void) */
public static function notFound(string $exit = '1'): void
{
header('HTTP/1.1 404 Not Found', true, 404);

if ($exit == '1') {
echo '404 Not Found';
exit;
}
}

public function test(): void
{
// send 404 header
self::notFound('0');
}
}

class ReturnViaIntNonNative
{
/** @return ($exit is 1 ? never : void) */
public static function notFound(int $exit = 1)
{
header('HTTP/1.1 404 Not Found', true, 404);

if ($exit == 1) {
echo '404 Not Found';
exit;
}
}

public function test(): void
{
// send 404 header
self::notFound(0);
}
}

class ReturnsMaybeNever
{
/** @return ($exit is true ? never : 1) */
public static function notFound(bool $exit = true)
{
header('HTTP/1.1 404 Not Found', true, 404);

if ($exit) {
echo '404 Not Found';
exit;
}
return 1;
}

public function test(): void
{
// send 404 header
self::notFound(false);

}
}

class ReturnsMaybeVoid
{
/** @return ($exit is true ? void : 1) */
public static function notFound(bool $exit = true)
{
header('HTTP/1.1 404 Not Found', true, 404);

if ($exit) {
echo '404 Not Found';
return;
}
return 1;
}

public function test(): void
{
// send 404 header
self::notFound(false);

}
}



class ReturnsWithInstanceMethod
{
/** @return ($exit is true ? never-return : void) */
public function notFound(bool $exit = true): void
{
header('HTTP/1.1 404 Not Found', true, 404);

if ($exit) {
echo '404 Not Found';
exit;
}
}


public function test(): void
{
// send 404 header
$this->notFound(false);

}
}

/** @return ($exit is true ? never-return : void) */
function notFound(bool $exit = true): void
{
header('HTTP/1.1 404 Not Found', true, 404);

if ($exit) {
echo '404 Not Found';
exit;
}
}

function test(): void
{
// send 404 header
notFound(false);
}


class ReturnViaUnion
{
/**
* @return ($exit is int ? void : never)
*/
public static function notFound(int|string $exit = 'hello world'): void
{
header('HTTP/1.1 404 Not Found', true, 404);

if (!is_int($exit)) {
echo '404 Not Found';
exit;
}
}

public function test(): void
{
// send 404 header
self::notFound(0);
}
}


$x = 123;
if (is_numeric($x)) {
}

function doFoo($mixed) {
assertIsInt($mixed);
assertIsInt($mixed);
}

/** @phpstan-assert int $i */
function assertIsInt($i):void {
if (!is_int($i)) {
throw new \LogicException();
}
}
Loading