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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"hoa/compiler": "3.17.08.08",
"hoa/exception": "^1.0",
"hoa/file": "1.17.07.11",
"jetbrains/phpstorm-stubs": "dev-master#709e512210784a7c0a677b3a89d35def844a59b9",
"jetbrains/phpstorm-stubs": "dev-master#e4f5f6c3de39f3bab3e9f3fca4b8cdb8b061e681",
"nette/bootstrap": "^3.0",
"nette/di": "^3.1.10",
"nette/neon": "3.3.4",
Expand Down
14 changes: 7 additions & 7 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions resources/functionMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,7 @@
'grapheme_stristr' => ['hasSideEffects' => false],
'grapheme_strlen' => ['hasSideEffects' => false],
'grapheme_strpos' => ['hasSideEffects' => false],
'grapheme_strrev' => ['hasSideEffects' => false],
'grapheme_strripos' => ['hasSideEffects' => false],
'grapheme_strrpos' => ['hasSideEffects' => false],
'grapheme_strstr' => ['hasSideEffects' => false],
Expand Down Expand Up @@ -1324,6 +1325,8 @@
'locale_filter_matches' => ['hasSideEffects' => false],
'locale_get_all_variants' => ['hasSideEffects' => false],
'locale_get_default' => ['hasSideEffects' => false],
'locale_get_display_keyword' => ['hasSideEffects' => false],
'locale_get_display_keyword_value' => ['hasSideEffects' => false],
'locale_get_display_language' => ['hasSideEffects' => false],
'locale_get_display_name' => ['hasSideEffects' => false],
'locale_get_display_region' => ['hasSideEffects' => false],
Expand Down
8 changes: 7 additions & 1 deletion src/Analyser/StmtHandler/ExpressionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Analyser\StmtHandler;

use Error;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
Expand All @@ -22,6 +23,7 @@
use PHPStan\Node\PropertyAssignNode;
use PHPStan\Node\VariableAssignNode;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use function array_filter;
use function count;

Expand Down Expand Up @@ -73,7 +75,11 @@ public function processStmt(
}

$nodeScopeResolver->callNodeCallback($nodeCallback, $stmt, $entryScope, $storage);
$throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit());
// Errors signal programmer mistakes (ValueError, TypeError, DivisionByZeroError...),
// nobody calls an otherwise pure expression just to have them thrown, so they
// do not make the expression statement meaningful.
$errorType = new ObjectType(Error::class);
$throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit() && !$errorType->isSuperTypeOf($throwPoint->getType())->yes());
if (
count($result->getImpurePoints()) === 0
&& count($throwPoints) === 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use ReflectionClassConstant;
use function count;

#[AutowiredService]
final class ReflectionClassConstantConstructorThrowTypeExtension implements DynamicStaticMethodThrowTypeExtension
{

public function __construct(private ReflectionProvider $reflectionProvider)
{
}

public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === '__construct' && $methodReflection->getDeclaringClass()->getName() === ReflectionClassConstant::class;
}

public function getThrowTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
{
if (count($methodCall->getArgs()) < 2) {
return $methodReflection->getThrowType();
}

$valueType = $scope->getType($methodCall->getArgs()[0]->value);
$constantType = $scope->getType($methodCall->getArgs()[1]->value);
foreach ($valueType->getConstantStrings() as $constantString) {
if (!$this->reflectionProvider->hasClass($constantString->getValue())) {
return $methodReflection->getThrowType();
}

$classReflection = $this->reflectionProvider->getClass($constantString->getValue());
foreach ($constantType->getConstantStrings() as $constantConstantString) {
if (!$classReflection->hasConstant($constantConstantString->getValue())) {
return $methodReflection->getThrowType();
}
}

$valueType = TypeCombinator::remove($valueType, $constantString);
}

if (!$valueType instanceof NeverType) {
return $methodReflection->getThrowType();
}

// Look for non constantStrings value.
foreach ($constantType->getConstantStrings() as $constantConstantString) {
$constantType = TypeCombinator::remove($constantType, $constantConstantString);
}

if (!$constantType instanceof NeverType) {
return $methodReflection->getThrowType();
}

return null;
}

}
109 changes: 109 additions & 0 deletions src/Type/Php/TriggerErrorFunctionThrowTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionThrowTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Throwable;
use ValueError;
use function count;
use function in_array;
use function is_int;
use const E_USER_DEPRECATED;
use const E_USER_ERROR;
use const E_USER_NOTICE;
use const E_USER_WARNING;

/**
* trigger_error() itself throws only ValueError for an invalid error level, but the
* registered error handler may throw anything. E_USER_ERROR terminates the script
* unless the handler throws, so the call is modelled as throwing Throwable there,
* the same way never-returning calls are.
*/
#[AutowiredService]
final class TriggerErrorFunctionThrowTypeExtension implements DynamicFunctionThrowTypeExtension
{

private const NON_FATAL_ERROR_LEVELS = [E_USER_WARNING, E_USER_NOTICE, E_USER_DEPRECATED];

public function __construct(
private PhpVersion $phpVersion,
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
private bool $implicitThrows,
)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'trigger_error';
}

public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type
{
$args = $funcCall->getArgs();
if (count($args) === 0) {
return $functionReflection->getThrowType();
}

$errorHandlerThrowType = $this->implicitThrows ? new ObjectType(Throwable::class) : null;
if (count($args) === 1) {
return $errorHandlerThrowType;
}

$errorLevels = $scope->getType($args[1]->value)->getConstantScalarValues();
if (count($errorLevels) === 0) {
if ($errorHandlerThrowType !== null) {
return $errorHandlerThrowType;
}

return $this->getInvalidErrorLevelThrowType();
}

$throwTypes = [];
foreach ($errorLevels as $errorLevel) {
if ($errorLevel === E_USER_ERROR) {
$throwTypes[] = new ObjectType(Throwable::class);
continue;
}

if (is_int($errorLevel) && in_array($errorLevel, self::NON_FATAL_ERROR_LEVELS, true)) {
if ($errorHandlerThrowType !== null) {
$throwTypes[] = $errorHandlerThrowType;
}
continue;
}

$invalidErrorLevelThrowType = $this->getInvalidErrorLevelThrowType();
if ($invalidErrorLevelThrowType === null) {
continue;
}

$throwTypes[] = $invalidErrorLevelThrowType;
}

if (count($throwTypes) === 0) {
return null;
}

return TypeCombinator::union(...$throwTypes);
}

private function getInvalidErrorLevelThrowType(): ?Type
{
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
return null;
}

return new ObjectType(ValueError::class);
}

}
19 changes: 19 additions & 0 deletions tests/PHPStan/Rules/DeadCode/NoopRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,25 @@ public function testRule(): void
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testErrorThrows(): void
{
$this->analyse([__DIR__ . '/data/noop-error-throws.php'], [
[
'Expression "$a / $b" on a separate line does not do anything.',
6,
],
[
'Expression "$a % $b" on a separate line does not do anything.',
7,
],
[
'Expression "match ($a) {…" on a separate line does not do anything.',
8,
],
]);
}

public function testNullsafe(): void
{
$this->analyse([__DIR__ . '/data/nullsafe-property-fetch-noop.php'], [
Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/noop-error-throws.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php // lint >= 8.0

namespace DeadCodeNoopErrorThrows;

function (int $a, int $b, string $s) {
$a / $b;
$a % $b;
match ($a) {
1 => 'a',
};
new \DateTimeImmutable($s);
};
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,29 @@ public function testBug7799(): void
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testTriggerErrorThrowType(): void
{
$this->analyse([__DIR__ . '/data/trigger-error-throw-type.php'], [
[
'Dead catch - Exception is never thrown in the try block.',
14,
],
[
'Dead catch - Exception is never thrown in the try block.',
19,
],
[
'Dead catch - ValueError is never thrown in the try block.',
34,
],
[
'Dead catch - Exception is never thrown in the try block.',
39,
],
]);
}

public static function getAdditionalConfigFiles(): array
{
return array_merge(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ public function testRule(): void
'Dead catch - ArithmeticError is never thrown in the try block.',
762,
],
[
'Dead catch - Exception is never thrown in the try block.',
802,
],
]);
}

Expand Down Expand Up @@ -233,6 +237,10 @@ public function testRuleWithoutReportingUncheckedException(): void
'Dead catch - Exception is never thrown in the try block.',
555,
],
[
'Dead catch - Exception is never thrown in the try block.',
802,
],
]);
}

Expand Down Expand Up @@ -842,4 +850,15 @@ public function testBug9826(): void
$this->analyse([__DIR__ . '/data/bug-9826.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testTriggerErrorThrowType(): void
{
$this->analyse([__DIR__ . '/data/trigger-error-throw-type.php'], [
[
'Dead catch - Exception is never thrown in the try block.',
39,
],
]);
}

}
Loading
Loading