Skip to content
Open
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
6 changes: 6 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -3619,6 +3619,12 @@ static function (): void {
$exprType = $scope->getType($expr->expr);
$toStringMethod = $scope->getMethodReflection($exprType, '__toString');
if ($toStringMethod !== null) {
if ($toStringMethod->getThrowType() !== null) {
Copy link
Contributor

@staabm staabm Nov 14, 2025

Choose a reason for hiding this comment

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

the rfc is PHP 7.4+, meaning this logic should be gated via PHPVersion class.

note though, that NodeScopeResolver is undergoing a major refactoring atm, so this might conflict easily

Copy link
Member

Choose a reason for hiding this comment

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

It won't conflict, I'm not doing any major changes in NodeScopeResolver that I'm not pushing.

But it's possible that the same change also needs to happen in PHPStan\Analyser\Generator namespace. In this case it doesn't because there's no StringCastHandler ported yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought the minimum version PHPStan supports was 7.4 Is it 7.1?

Copy link
Contributor

@staabm staabm Nov 14, 2025

Choose a reason for hiding this comment

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

there are 2 things.

the php version PHPStan can analyze against, configured in the .neon config:

schema(int(), min(70100), max(80599)),

and the PHP runtime version you use to run PHPStan itself:
https://github.com/phpstan/phpstan/blob/5afb019da9f70ae5a2b4247a1830b5103423a325/composer.json#L7

$throwPoints[] = InternalThrowPoint::createExplicit($scope, $toStringMethod->getThrowType(), $expr, false);
} else {
$throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
}

if (!$toStringMethod->hasSideEffects()->no()) {
$impurePoints[] = new ImpurePoint(
$scope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -643,4 +643,9 @@ public function testPropertyHooks(): void
]);
}

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

}
20 changes: 20 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/bug-13806.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Bug13806;
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this lack of test,

It should add a test for

function doFoo(Stringable $myVariable): void

to check the implicit throw tag

and a test for

class MyStringVoid {
	/** @throws void */
	public function __toString() {
		throw new \InvalidArgumentException();
	}
}

to check in this case the catch is reported as dead catch


function doFoo(MyString $myVariable): void
{
try {
(string) $myVariable;
} catch (\InvalidArgumentException) {
// Reported as dead catch, even though the `__toString()` method
// in `$myVariable` might throw an exception.
}
}

class MyString {
/** @throws \InvalidArgumentException */
public function __toString() {
throw new \InvalidArgumentException();
}
}
Loading