Skip to content

Commit 856a57e

Browse files
authored
Fixed method_exists() on class-string&literal-string
1 parent 5bd68ef commit 856a57e

File tree

9 files changed

+212
-2
lines changed

9 files changed

+212
-2
lines changed

src/Rules/Comparison/ImpossibleCheckTypeHelper.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@
1717
use PHPStan\Type\Constant\ConstantArrayType;
1818
use PHPStan\Type\Constant\ConstantBooleanType;
1919
use PHPStan\Type\Constant\ConstantStringType;
20+
use PHPStan\Type\Generic\GenericClassStringType;
21+
use PHPStan\Type\IntersectionType;
2022
use PHPStan\Type\MixedType;
2123
use PHPStan\Type\NeverType;
2224
use PHPStan\Type\ObjectType;
2325
use PHPStan\Type\Type;
26+
use PHPStan\Type\TypeTraverser;
2427
use PHPStan\Type\TypeUtils;
2528
use PHPStan\Type\TypeWithClassName;
29+
use PHPStan\Type\UnionType;
2630
use PHPStan\Type\VerbosityLevel;
2731
use function array_map;
2832
use function array_pop;
@@ -176,6 +180,30 @@ public function findSpecifiedType(
176180
return false;
177181
}
178182
}
183+
184+
$genericType = TypeTraverser::map($objectType, static function (Type $type, callable $traverse): Type {
185+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
186+
return $traverse($type);
187+
}
188+
if ($type instanceof GenericClassStringType) {
189+
return $type->getGenericType();
190+
}
191+
return new MixedType();
192+
});
193+
194+
if ($genericType instanceof TypeWithClassName) {
195+
if ($genericType->hasMethod($methodType->getValue())->yes()) {
196+
return true;
197+
}
198+
199+
$classReflection = $genericType->getClassReflection();
200+
if (
201+
$classReflection !== null
202+
&& $classReflection->isFinal()
203+
&& $genericType->hasMethod($methodType->getValue())->no()) {
204+
return false;
205+
}
206+
}
179207
}
180208
}
181209
}

src/Rules/Methods/MethodCallCheck.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function check(
5050
if ($type instanceof ErrorType) {
5151
return [$typeResult->getUnknownClassErrors(), null];
5252
}
53-
if (!$type->canCallMethods()->yes()) {
53+
if (!$type->canCallMethods()->yes() || $type->isClassStringType()->yes()) {
5454
return [
5555
[
5656
RuleErrorBuilder::message(sprintf(

src/Type/Accessory/AccessoryLiteralStringType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@ public function isClassStringType(): TrinaryLogic
232232
return TrinaryLogic::createMaybe();
233233
}
234234

235+
public function hasMethod(string $methodName): TrinaryLogic
236+
{
237+
return TrinaryLogic::createMaybe();
238+
}
239+
235240
public function isVoid(): TrinaryLogic
236241
{
237242
return TrinaryLogic::createNo();

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1176,9 +1176,9 @@ public function dataFileAsserts(): iterable
11761176
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8621.php');
11771177
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8084.php');
11781178
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3019.php');
1179-
11801179
yield from $this->gatherAssertTypes(__DIR__ . '/data/callsite-cast-narrowing.php');
11811180
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8775.php');
1181+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8752.php');
11821182
}
11831183

11841184
/**
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Bug8752;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/**
10+
* @param class-string&literal-string $s
11+
*/
12+
public function sayHello(string $s): void
13+
{
14+
if (method_exists($s, 'abc')) {
15+
assertType('class-string&hasMethod(abc)&literal-string', $s);
16+
17+
$s::abc();
18+
$s->abc();
19+
}
20+
}
21+
}

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,4 +671,40 @@ public function testBug8474(): void
671671
$this->analyse([__DIR__ . '/data/bug-8474.php'], []);
672672
}
673673

674+
public function testBug8752(): void
675+
{
676+
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
677+
$this->treatPhpDocTypesAsCertain = true;
678+
$this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], []);
679+
}
680+
681+
public function testImpossibleMethodExistOnGenericClassString(): void
682+
{
683+
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
684+
$this->treatPhpDocTypesAsCertain = true;
685+
$this->analyse([__DIR__ . '/data/impossible-method-exists-on-generic-class-string.php'], [
686+
[
687+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\S>&literal-string and 'staticAbc' will always evaluate to true.",
688+
18,
689+
],
690+
[
691+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\S>&literal-string and 'nonStaticAbc' will always evaluate to true.",
692+
23,
693+
],
694+
[
695+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>&literal-string and 'nonExistent' will always evaluate to false.",
696+
34,
697+
],
698+
[
699+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>&literal-string and 'staticAbc' will always evaluate to true.",
700+
39,
701+
],
702+
[
703+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>&literal-string and 'nonStaticAbc' will always evaluate to true.",
704+
44,
705+
],
706+
707+
]);
708+
}
709+
674710
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace ImpossibleMethodExistsOnGenericClassString;
4+
5+
class HelloWorld
6+
{
7+
/**
8+
* @param class-string<S>&literal-string $s
9+
*/
10+
public function sayGenericHello(string $s): void
11+
{
12+
// no erros on non-final class
13+
if (method_exists($s, 'nonExistent')) {
14+
$s->nonExistent();
15+
$s::nonExistent();
16+
}
17+
18+
if (method_exists($s, 'staticAbc')) {
19+
$s::staticAbc();
20+
$s->staticAbc();
21+
}
22+
23+
if (method_exists($s, 'nonStaticAbc')) {
24+
$s::nonStaticAbc();
25+
$s->nonStaticAbc();
26+
}
27+
}
28+
29+
/**
30+
* @param class-string<FinalS>&literal-string $s
31+
*/
32+
public function sayFinalGenericHello(string $s): void
33+
{
34+
if (method_exists($s, 'nonExistent')) {
35+
$s->nonExistent();
36+
$s::nonExistent();
37+
}
38+
39+
if (method_exists($s, 'staticAbc')) {
40+
$s::staticAbc();
41+
$s->staticAbc();
42+
}
43+
44+
if (method_exists($s, 'nonStaticAbc')) {
45+
$s::nonStaticAbc();
46+
$s->nonStaticAbc();
47+
}
48+
}
49+
}
50+
51+
class S {
52+
public static function staticAbc():void {}
53+
54+
public function nonStaticAbc():void {}
55+
}
56+
57+
final class FinalS {
58+
public static function staticAbc():void {}
59+
60+
public function nonStaticAbc():void {}
61+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2719,4 +2719,53 @@ public function testNewInstanceArgsIssue8679(): void
27192719
$this->analyse([__DIR__ . '/data/reflection-class-issue-8679.php'], []);
27202720
}
27212721

2722+
public function testBug8752(): void
2723+
{
2724+
$this->checkThisOnly = false;
2725+
$this->checkNullables = true;
2726+
$this->checkUnionTypes = true;
2727+
$this->checkExplicitMixed = true;
2728+
$this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], [
2729+
[
2730+
'Cannot call method abc() on class-string.',
2731+
18,
2732+
],
2733+
]);
2734+
}
2735+
2736+
public function testCannotCallOnGenericClassString(): void
2737+
{
2738+
$this->checkThisOnly = false;
2739+
$this->checkNullables = true;
2740+
$this->checkUnionTypes = true;
2741+
$this->checkExplicitMixed = true;
2742+
2743+
$this->analyse([__DIR__ . '/../Comparison/data/impossible-method-exists-on-generic-class-string.php'], [
2744+
[
2745+
'Cannot call method nonExistent() on class-string<ImpossibleMethodExistsOnGenericClassString\S>.',
2746+
14,
2747+
],
2748+
[
2749+
'Cannot call method staticAbc() on class-string<ImpossibleMethodExistsOnGenericClassString\S>.',
2750+
20,
2751+
],
2752+
[
2753+
'Cannot call method nonStaticAbc() on class-string<ImpossibleMethodExistsOnGenericClassString\S>.',
2754+
25,
2755+
],
2756+
[
2757+
'Cannot call method nonExistent() on class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>.',
2758+
35,
2759+
],
2760+
[
2761+
'Cannot call method staticAbc() on class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>.',
2762+
41,
2763+
],
2764+
[
2765+
'Cannot call method nonStaticAbc() on class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>.',
2766+
46,
2767+
],
2768+
]);
2769+
}
2770+
27222771
}

tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,14 @@ public function testRule(): void
9494
]);
9595
}
9696

97+
public function testBug8752(): void
98+
{
99+
$this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], []);
100+
}
101+
102+
public function testCallsOnGenericClassString(): void
103+
{
104+
$this->analyse([__DIR__ . '/../Comparison/data/impossible-method-exists-on-generic-class-string.php'], []);
105+
}
106+
97107
}

0 commit comments

Comments
 (0)