Skip to content
Closed
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
39 changes: 39 additions & 0 deletions src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\Type\UnionTypeMethodReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\DynamicMethodThrowTypeExtension;
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use ReflectionFunction;
use ReflectionMethod;
use Throwable;
use function count;
use function in_array;

#[AutowiredService]
Expand Down Expand Up @@ -63,6 +66,42 @@ public function getThrowPoint(
Type $methodCallReturnType,
): ?InternalThrowPoint
{
if ($methodReflection instanceof UnionTypeMethodReflection) {
$throwPoints = [];
foreach ($methodReflection->getMethods() as $innerMethodReflection) {
$throwPoint = $this->getThrowPoint($innerMethodReflection, $parametersAcceptor, $normalizedMethodCall, $scope, $context, $methodCallReturnType);
if ($throwPoint === null) {
continue;
}

$throwPoints[] = $throwPoint;
}

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

$throwTypes = [];
$canContainAnyThrowable = false;
$isExplicit = false;
foreach ($throwPoints as $throwPoint) {
$throwTypes[] = $throwPoint->getType();
if ($throwPoint->canContainAnyThrowable()) {
$canContainAnyThrowable = true;
}
if (!$throwPoint->isExplicit()) {
continue;
}
$isExplicit = true;
}

if (!$isExplicit) {
return InternalThrowPoint::createImplicit($scope, $normalizedMethodCall);
}

return InternalThrowPoint::createExplicit($scope, TypeCombinator::union(...$throwTypes), $normalizedMethodCall, $canContainAnyThrowable);
}

if ($normalizedMethodCall instanceof MethodCall) {
foreach ($this->dynamicMethodThrowTypeExtensions->getAll() as $extension) {
if (!$extension->isMethodSupported($methodReflection)) {
Expand Down
10 changes: 10 additions & 0 deletions src/Reflection/Type/UnionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\TrinaryLogic;
use PHPStan\Turbo\ReferencedByTurboExtension;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_map;
Expand All @@ -19,6 +20,7 @@
use function implode;
use function is_bool;

#[ReferencedByTurboExtension(key: 'unionTypeMethodReflection')]
final class UnionTypeMethodReflection implements ExtendedMethodReflection
{

Expand All @@ -34,6 +36,14 @@ public function getDeclaringClass(): ClassReflection
return $this->methods[0]->getDeclaringClass();
}

/**
* @return ExtendedMethodReflection[]
*/
public function getMethods(): array
{
return $this->methods;
}

public function isStatic(): bool
{
foreach ($this->methods as $method) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,12 @@ public function testBug14479(): void
$this->analyse([__DIR__ . '/data/bug-14479.php'], []);
}

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

public function testBug5952(): void
{
$this->analyse([__DIR__ . '/data/bug-5952.php'], [
Expand Down
38 changes: 38 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/bug-14467.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php // lint >= 8.3

// FQN must sort after \DateTime so methods[0] resolves to DateTime.
namespace TestBug14467;

class Foo
{

/** @throws \DateMalformedStringException */
public function modify(string $s): self
{
return $this;
}

}

class Bar
{

/** @param \DateTime|Foo $obj */
public function caseDateTimeFirst($obj): void
{
try {
$obj->modify('+1 day');
} catch (\DateMalformedStringException $e) {
}
}

/** @param Foo|\DateTime $obj */
public function caseFooFirst($obj): void
{
try {
$obj->modify('+1 day');
} catch (\DateMalformedStringException $e) {
}
}

}
53 changes: 53 additions & 0 deletions turbo-ext/src/MethodThrowPointHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ namespace sigs = ptdecl::MethodThrowPointHelper::sig;
#include "TypeOps.h"
#include "AcceptorValues.h"
#include "ParserVisitors.h"
#include "AnalyserValues.h"

static zend_class_entry *pt_ce_method_throw_point_helper;

Expand Down Expand Up @@ -104,6 +105,12 @@ class MethodThrowPointHelper
* exception */
zv::Val getThrowPoint(zval *methodReflection, zval *parametersAcceptor, zval *normalizedMethodCall, zval *scope, zval *context, zval *methodCallReturnType) const
{
zend_class_entry *unionTypeMethodReflectionCe = pt_class(PT_CLASS_UNION_TYPE_METHOD_REFLECTION);
if (UNEXPECTED(unionTypeMethodReflectionCe == NULL)) return zv::Val();
if (instanceof_function(Z_OBJCE_P(methodReflection), unionTypeMethodReflectionCe)) {
return getUnionThrowPoint(methodReflection, parametersAcceptor, normalizedMethodCall, scope, context, methodCallReturnType);
}

zend_class_entry *methodCallCe = pt_class(PT_CLASS_METHOD_CALL);
if (UNEXPECTED(methodCallCe == NULL)) return zv::Val();
bool isMethodCall = instanceof_function(Z_OBJCE_P(normalizedMethodCall), methodCallCe);
Expand Down Expand Up @@ -258,6 +265,52 @@ class MethodThrowPointHelper
private:
zend_object *self;

/* Mirrors getThrowPoint()'s UnionTypeMethodReflection arm: the inner
* methods' throw points, combined */
zv::Val getUnionThrowPoint(zval *methodReflection, zval *parametersAcceptor, zval *normalizedMethodCall, zval *scope, zval *context, zval *methodCallReturnType) const
{
zv::Val methods = pt_type_call(Z_OBJ_P(methodReflection), PT_LC("getmethods"), 0, NULL);
if (UNEXPECTED(methods.isUndef())) return zv::Val();

zv::Arr throwTypes = zv::Arr::create(0);
bool canContainAnyThrowable = false;
bool isExplicit = false;
if (EXPECTED(Z_TYPE_P(methods.raw()) == IS_ARRAY)) {
for (zv::ArrayEntry entry : zv::ArrRef(methods.raw())) {
zval *innerMethodReflection = entry.value().deref().raw();
if (UNEXPECTED(Z_TYPE_P(innerMethodReflection) != IS_OBJECT)) {
zend_type_error("PHPStan\\Analyser\\ExprHandler\\Helper\\MethodThrowPointHelper::getThrowPoint(): Argument #1 ($methodReflection) must be of type PHPStan\\Reflection\\MethodReflection, %s given", zend_zval_value_name(innerMethodReflection));
return zv::Val();
}
zv::Val throwPoint = getThrowPoint(innerMethodReflection, parametersAcceptor, normalizedMethodCall, scope, context, methodCallReturnType);
if (UNEXPECTED(throwPoint.isUndef())) return zv::Val();
if (Z_TYPE_P(throwPoint.raw()) == IS_NULL) continue;

zv::Val typeHold;
zval *type = pt_internal_throw_point_type(throwPoint.raw(), typeHold);
if (UNEXPECTED(type == NULL)) return zv::Val();
throwTypes.push(zv::Val::copyOf(zv::Ref(type)));
bool innerCanContainAnyThrowable;
if (UNEXPECTED(!pt_internal_throw_point_can_contain_any_throwable(throwPoint.raw(), innerCanContainAnyThrowable))) return zv::Val();
if (innerCanContainAnyThrowable) canContainAnyThrowable = true;
bool innerIsExplicit;
if (UNEXPECTED(!pt_internal_throw_point_is_explicit(throwPoint.raw(), innerIsExplicit))) return zv::Val();
if (innerIsExplicit) isExplicit = true;
}
}

HashTable *table = throwTypes.table();
uint32_t count = zend_hash_num_elements(table);
if (count == 0) return zv::Val::null();
if (!isExplicit) return createImplicit(scope, normalizedMethodCall);

zv::Val throwType = EXPECTED(HT_IS_PACKED(table) && table->nNumUsed == count)
? pt_type_combinator_union(count, table->arPacked)
: pt_type_combinator_call_spread(PT_LC("union"), table);
if (UNEXPECTED(throwType.isUndef())) return zv::Val();
return createExplicit(scope, throwType.raw(), normalizedMethodCall, canContainAnyThrowable);
}

zval *slot(uint32_t index) const { return OBJ_PROP_NUM(self, index); }

/* the engine's Error for reading a never-written typed property */
Expand Down
1 change: 1 addition & 0 deletions turbo-ext/src/support.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = {
/* PT_CLASS_NATIVE_PHP_TYPE_NODE_HELPER */ {"nativePhpTypeNodeHelper", "PHPStan\\Type\\NativePhpTypeNodeHelper"},
/* PT_CLASS_FULLY_QUALIFIED_NAME_NODE */ {"fullyQualifiedNameNode", "PHPStan\\PhpDocParser\\Ast\\Type\\FullyQualifiedNameNode"},
/* PT_CLASS_TYPE_NODE_DESCRIBER */ {"typeNodeDescriber", "PHPStan\\Type\\TypeNodeDescriber"},
/* PT_CLASS_UNION_TYPE_METHOD_REFLECTION */ {"unionTypeMethodReflection", "PHPStan\\Reflection\\Type\\UnionTypeMethodReflection"},
};

zend_class_entry *pt_class(int idx)
Expand Down
2 changes: 2 additions & 0 deletions turbo-ext/src/support.h
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ enum {
/* the class names of Type::toPhpDocNode() and how describe() prints them */
PT_CLASS_FULLY_QUALIFIED_NAME_NODE,
PT_CLASS_TYPE_NODE_DESCRIBER,
/* MethodThrowPointHelper's union arm */
PT_CLASS_UNION_TYPE_METHOD_REFLECTION,
PT_CLASS_COUNT
};

Expand Down
Loading