Skip to content
6 changes: 0 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1617,12 +1617,6 @@ parameters:
count: 1
path: src/Type/Php/MbSubstituteCharacterDynamicReturnTypeExtension.php

-
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#'
identifier: phpstanApi.instanceofType
count: 1
path: src/Type/Php/MethodExistsTypeSpecifyingExtension.php

-
message: '#^Doing instanceof PHPStan\\Type\\ConstantScalarType is error\-prone and deprecated\. Use Type\:\:isConstantScalarValue\(\) or Type\:\:getConstantScalarTypes\(\) or Type\:\:getConstantScalarValues\(\) instead\.$#'
identifier: phpstanApi.instanceofType
Expand Down
58 changes: 34 additions & 24 deletions src/Type/Php/MethodExistsTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@
use PHPStan\Type\Accessory\HasMethodType;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\UnionType;
use PHPStan\Type\TypeCombinator;
use function count;

#[AutowiredService]
Expand Down Expand Up @@ -50,45 +48,57 @@ public function specifyTypes(
TypeSpecifierContext $context,
): SpecifiedTypes
{
$methodNameType = $scope->getType($node->getArgs()[1]->value);
if (!$methodNameType instanceof ConstantStringType) {
return $this->typeSpecifier->create(
new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()),
new ConstantBooleanType(true),
$context,
$scope,
);
$specifiedTypes = $this->typeSpecifier->create(
new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()),
new ConstantBooleanType(true),
$context,
$scope,
);

$methodNameTypes = $scope->getType($node->getArgs()[1]->value)->getConstantStrings();
if ($methodNameTypes === []) {
return $specifiedTypes;
}

$objectType = $scope->getType($node->getArgs()[0]->value);
if ($objectType->isString()->yes()) {
if ($objectType->isClassString()->yes()) {
return $this->typeSpecifier->create(
$node->getArgs()[0]->value,
new IntersectionType([
$types = [];
foreach ($methodNameTypes as $methodNameType) {
$types[] = TypeCombinator::intersect(
$objectType,
new HasMethodType($methodNameType->getValue()),
]),
);
}

return $specifiedTypes->unionWith($this->typeSpecifier->create(
$node->getArgs()[0]->value,
TypeCombinator::union(...$types),
$context,
$scope,
);
));
}

return new SpecifiedTypes([], []);
return $specifiedTypes;
}

return $this->typeSpecifier->create(
$node->getArgs()[0]->value,
new UnionType([
new IntersectionType([
$types = [];
foreach ($methodNameTypes as $methodNameType) {
$types[] = TypeCombinator::union(
TypeCombinator::intersect(
new ObjectWithoutClassType(),
new HasMethodType($methodNameType->getValue()),
]),
),
new ClassStringType(),
]),
);
}

return $specifiedTypes->unionWith($this->typeSpecifier->create(
$node->getArgs()[0]->value,
TypeCombinator::union(...$types),
$context,
$scope,
);
));
}

}
32 changes: 32 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13272.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Bug13272;

use function PHPStan\Testing\assertType;

function foo(object $bar): void
{
foreach (['qux', 'quux'] as $method) {
assertType("'quux'|'qux'", $method);

if (!method_exists($bar, $method)) {
throw new \Exception;
}

assertType("'quux'|'qux'", $method);
assertType("(object&hasMethod(quux))|(object&hasMethod(qux))", $bar); // could be object&hasMethod(quux)&hasMethod(qux)
}
}

/**
* @param 'quux'|'qux' $constUnion
*/
function fooBar(object $bar, string $constUnion): void
{
if (!method_exists($bar, $constUnion)) {
throw new \Exception;
}

// at this point we don't know whether $constUnion was 'quux' or 'qux'
assertType("(object&hasMethod(quux))|(object&hasMethod(qux))", $bar);
}
Loading