Skip to content
Open
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
15 changes: 14 additions & 1 deletion src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ public function findSpecifiedType(

if ($objectType->getObjectClassNames() !== []) {
if ($objectType->hasMethod($methodType->getValue())->yes()) {
foreach ($objectType->getObjectClassReflections() as $classReflection) {
if (!$classReflection->hasNativeMethod($methodType->getValue())) {
return null;
}
}
return true;
}

Expand All @@ -231,7 +236,15 @@ public function findSpecifiedType(

if ($genericType instanceof TypeWithClassName) {
if ($genericType->hasMethod($methodType->getValue())->yes()) {
return true;
$classReflection = $genericType->getClassReflection();
if (
$classReflection !== null
&& $classReflection->hasNativeMethod($methodType->getValue())
) {
return true;
}

return null;
Comment thread
VincentLanglet marked this conversation as resolved.
}

$classReflection = $genericType->getClassReflection();
Expand Down
43 changes: 37 additions & 6 deletions src/Type/Php/MethodExistsTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Accessory\HasMethodType;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantBooleanType;
Expand All @@ -27,6 +28,10 @@ final class MethodExistsTypeSpecifyingExtension implements FunctionTypeSpecifyin

private TypeSpecifier $typeSpecifier;

public function __construct(private ReflectionProvider $reflectionProvider)
{
}

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
Expand All @@ -53,17 +58,27 @@ public function specifyTypes(
$args = $node->getArgs();
$methodNameType = $scope->getType($args[1]->value);
if (!$methodNameType instanceof ConstantStringType) {
return $this->typeSpecifier->create(
new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()),
new ConstantBooleanType(true),
$context,
$scope,
);
return $this->createFuncCallSpec($node, $context, $scope);
}

$objectType = $scope->getType($args[0]->value);
if ($objectType->isString()->yes()) {
if ($objectType->isClassString()->yes()) {
foreach ($objectType->getConstantStrings() as $constantString) {
if ($this->reflectionProvider->hasClass($constantString->getValue())) {
$classReflection = $this->reflectionProvider->getClass($constantString->getValue());
if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) {
return $this->createFuncCallSpec($node, $context, $scope);
}
}
}

foreach ($objectType->getClassStringObjectType()->getObjectClassReflections() as $classReflection) {
if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) {
return $this->createFuncCallSpec($node, $context, $scope);
}
}

return $this->typeSpecifier->create(
$args[0]->value,
new IntersectionType([
Expand All @@ -78,6 +93,12 @@ public function specifyTypes(
return new SpecifiedTypes([], []);
}

foreach ($objectType->getObjectClassReflections() as $classReflection) {
if ($classReflection->hasMethod($methodNameType->getValue()) && !$classReflection->hasNativeMethod($methodNameType->getValue())) {
return $this->createFuncCallSpec($node, $context, $scope);
}
}

return $this->typeSpecifier->create(
$args[0]->value,
new UnionType([
Expand All @@ -92,4 +113,14 @@ public function specifyTypes(
);
}

private function createFuncCallSpec(FuncCall $node, TypeSpecifierContext $context, Scope $scope): SpecifiedTypes
{
return $this->typeSpecifier->create(
new FuncCall(new FullyQualified('method_exists'), $node->getRawArgs()),
new ConstantBooleanType(true),
$context,
$scope,
);
}

}
19 changes: 12 additions & 7 deletions src/Type/Php/PropertyExistsTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,7 @@ public function specifyTypes(
$args = $node->getArgs();
$propertyNameType = $scope->getType($args[1]->value);
if (!$propertyNameType instanceof ConstantStringType) {
return $this->typeSpecifier->create(
new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()),
new ConstantBooleanType(true),
$context,
$scope,
);
return $this->createFuncCallSpec($node, $context, $scope);
}

if ($propertyNameType->getValue() === '') {
Expand All @@ -85,7 +80,7 @@ public function specifyTypes(
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope);
if ($propertyReflection !== null) {
if (!$propertyReflection->isNative()) {
return new SpecifiedTypes([], []);
return $this->createFuncCallSpec($node, $context, $scope);
}
}

Expand All @@ -100,4 +95,14 @@ public function specifyTypes(
);
}

private function createFuncCallSpec(FuncCall $node, TypeSpecifierContext $context, Scope $scope): SpecifiedTypes
{
return $this->typeSpecifier->create(
new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()),
new ConstantBooleanType(true),
$context,
$scope,
);
}

}
19 changes: 19 additions & 0 deletions tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,25 @@ public function testBug6822(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-6822.php'], []);
}

public function testBug6211(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-6211.php'], [
[
'If condition is always true.',
93,
],
[
'If condition is always true.',
100,
],
[
'If condition is always true.',
114,
],
]);
}

public function testBug5020(): void
{
$this->treatPhpDocTypesAsCertain = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1230,4 +1230,48 @@ public function testBug8217(): void
$this->analyse([__DIR__ . '/data/bug-8217.php'], []);
}

public function testBug6211(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-6211.php'], [
[
'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.',
34,
],
[
'Call to function method_exists() with \'Bug6211\\\\Hell\' and \'test\' will always evaluate to true.',
39,
],
[
'Call to function method_exists() with Bug6211\Bar and \'realMethod\' will always evaluate to true.',
62,
],
[
'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.',
87,
],
[
'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.',
106,
],
[
'Call to function method_exists() with Bug6211\Hell and \'test\' will always evaluate to true.',
107,
],
[
'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.',
120,
],
[
'Call to function property_exists() with Bug6211\Baz and \'realProp\' will always evaluate to true.',
121,
],
[
'Call to function method_exists() with class-string<Bug6211\Foo> and \'test\' will always evaluate to true.',
136,
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
],
]);
}

}
139 changes: 139 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-6211.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php declare(strict_types = 1);

namespace Bug6211;

/**
* @method bool isTrue()
*/
interface Foo
{
public function test(): bool;
}

class Hell implements Foo
{
public function test(): bool
{
return true;
}
}

$hell = new Hell();

// @method should not make method_exists always true
if (\method_exists($hell, 'isTrue')) {

}

// @method with class string should not make method_exists always true
if (\method_exists(Hell::class, 'isTrue')) {

}

// native method should still be always true
if (\method_exists($hell, 'test')) {

}

// native method via class string should still be always true
if (\method_exists(Hell::class, 'test')) {

}

/**
* @method bool magicMethod()
*/
class Bar
{
public function realMethod(): bool
{
return true;
}
}

$bar = new Bar();

// @method on the class itself (not interface) should not make method_exists always true
if (\method_exists($bar, 'magicMethod')) {

}

// native method should still be always true
if (\method_exists($bar, 'realMethod')) {

}

/**
* @property int $magicProp
*/
class Baz
{
public int $realProp = 1;

public function __get(string $name): mixed
{
return null;
}
}

$baz = new Baz();

// @property should not make property_exists always true
if (\property_exists($baz, 'magicProp')) {

}

// native property should still be always true
if (\property_exists($baz, 'realProp')) {

}

// Nested method_exists with @method should report the inner as always-true
if (\method_exists($hell, 'isTrue')) {
if (\method_exists($hell, 'isTrue')) { // if condition always true

}
}

// Nested method_exists with @method via class-string
if (\method_exists(Hell::class, 'isTrue')) {
if (\method_exists(Hell::class, 'isTrue')) { // if condition always true

}
}

// Nested method_exists with native method (already always-true, inner is also)
if (\method_exists($hell, 'test')) {
if (\method_exists($hell, 'test')) {

}
}

// Nested property_exists with @property should report the inner as always-true
if (\property_exists($baz, 'magicProp')) {
if (\property_exists($baz, 'magicProp')) { // if condition always true

}
}

// Nested property_exists with native property (already always-true, inner is also)
if (\property_exists($baz, 'realProp')) {
if (\property_exists($baz, 'realProp')) {

}
}

/**
* @param class-string<Foo> $classString
*/
function testGenericClassString(string $classString): void {
// @method via generic class-string should not make method_exists always true
if (\method_exists($classString, 'isTrue')) {

}

// native method via generic class-string should still be always true
if (\method_exists($classString, 'test')) {

}
}
Loading