Skip to content

Resolve Static throw points #4128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 5, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
47 changes: 47 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -4260,6 +4260,18 @@ private function getMethodThrowPoint(MethodReflection $methodReflection, Paramet

if ($throwType !== null) {
if (!$throwType->isVoid()->yes()) {
if ($throwType instanceof StaticType) {
$classReflections = $scope->getType($methodCall->var)->getObjectClassReflections();
if (count($classReflections) > 0) {
$types = [];
foreach ($classReflections as $classReflection) {
$types[] = $throwType->changeBaseClass($classReflection);
}

$throwType = TypeCombinator::union(...$types);
}
}

return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true);
}
} elseif ($this->implicitThrows) {
Expand Down Expand Up @@ -4297,6 +4309,10 @@ private function getConstructorThrowPoint(MethodReflection $constructorReflectio
if ($constructorReflection->getThrowType() !== null) {
$throwType = $constructorReflection->getThrowType();
if (!$throwType->isVoid()->yes()) {
if ($throwType instanceof StaticType && $this->reflectionProvider->hasClass($className->name)) {
$throwType = $throwType->changeBaseClass($this->reflectionProvider->getClass($className->name));
}

return ThrowPoint::createExplicit($scope, $throwType, $new, true);
}
} elseif ($this->implicitThrows) {
Expand Down Expand Up @@ -4329,6 +4345,25 @@ private function getStaticMethodThrowPoint(MethodReflection $methodReflection, P
if ($methodReflection->getThrowType() !== null) {
$throwType = $methodReflection->getThrowType();
if (!$throwType->isVoid()->yes()) {
if ($throwType instanceof StaticType) {
if ($methodCall->class instanceof Name) {
if ($this->reflectionProvider->hasClass($methodCall->class->name)) {
$throwType = $throwType->changeBaseClass($this->reflectionProvider->getClass($methodCall->class->name));
}
} else {
$classReflections = $scope->getType($methodCall->class)->getObjectClassReflections();

if (count($classReflections) > 0) {
$types = [];
foreach ($classReflections as $classReflection) {
$types[] = $throwType->changeBaseClass($classReflection);
}

$throwType = TypeCombinator::union(...$types);
}
}
}

return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true);
}
} elseif ($this->implicitThrows) {
Expand Down Expand Up @@ -4409,6 +4444,18 @@ private function getThrowPointsFromPropertyHook(

if ($throwType !== null) {
if (!$throwType->isVoid()->yes()) {
if ($throwType instanceof StaticType) {
$classReflections = $scope->getType($propertyFetch->var)->getObjectClassReflections();
if (count($classReflections) > 0) {
$types = [];
foreach ($classReflections as $classReflection) {
$types[] = $throwType->changeBaseClass($classReflection);
}

$throwType = TypeCombinator::union(...$types);
}
}

return [ThrowPoint::createExplicit($scope, $throwType, $propertyFetch, true)];
}
} elseif ($this->implicitThrows) {
Expand Down
41 changes: 41 additions & 0 deletions tests/PHPStan/Rules/Exceptions/Bug11900Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Exceptions;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
* @extends RuleTestCase<MissingCheckedExceptionInMethodThrowsRule>
*/
class Bug11900Test extends RuleTestCase
{

protected function getRule(): Rule
{
return new MissingCheckedExceptionInMethodThrowsRule(
new MissingCheckedExceptionInThrowsCheck(new DefaultExceptionTypeResolver(
self::createReflectionProvider(),
[],
[],
[],
[],
)),
);
}

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

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/bug-11900.neon',
];
}

}
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Exceptions/bug-11900.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
exceptions:
implicitThrows: false
check:
missingCheckedExceptionInThrows: true
tooWideThrowType: true
87 changes: 87 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/bug-11900.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php // lint >= 8.4

namespace Bug11900;

use Exception;
use Throwable;

abstract class ADataException extends Exception
{
public int $i {
/** @throws static */
get {
if (rand(0, 1)) {
throw new static();
}

return 42;
}
}

/** @throws static */
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
{
if (rand(0, 1)) {
throw new static();
}

parent::__construct($message, $code, $previous);
}

/**
* @return void
* @throws static
*/
public function throw1(): void
{
throw $this;
}

/**
* @return void
* @throws static
*/
public static function throw2(): void
{
throw new static();
}
}

final class TestDataException extends ADataException
{
}

class TestPhpStan
{
/**
* @throws TestDataException
*/
public function validate(TestDataException $e): void
{
$e->throw1();
}

/**
* @throws TestDataException
*/
public function validate2(): void
{
TestDataException::throw2();
}

/**
* @throws TestDataException
*/
public function validate3(TestDataException $e): void
{
$e->i;
}

/**
* @throws TestDataException
*/
public function validate4(): void
{
new TestDataException();
}
}
Loading