Skip to content

Commit 4eed4c7

Browse files
committed
Merge branch '1.9.x' into 1.10.x
2 parents 4e311c4 + 856a57e commit 4eed4c7

9 files changed

+221
-1
lines changed

src/Rules/Comparison/ImpossibleCheckTypeHelper.php

+29
Original file line numberDiff line numberDiff line change
@@ -17,11 +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;
28+
use PHPStan\Type\TypeWithClassName;
29+
use PHPStan\Type\UnionType;
2530
use PHPStan\Type\VerbosityLevel;
2631
use function array_map;
2732
use function array_pop;
@@ -181,6 +186,30 @@ public function findSpecifiedType(
181186
return false;
182187
}
183188
}
189+
190+
$genericType = TypeTraverser::map($objectType, static function (Type $type, callable $traverse): Type {
191+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
192+
return $traverse($type);
193+
}
194+
if ($type instanceof GenericClassStringType) {
195+
return $type->getGenericType();
196+
}
197+
return new MixedType();
198+
});
199+
200+
if ($genericType instanceof TypeWithClassName) {
201+
if ($genericType->hasMethod($methodType->getValue())->yes()) {
202+
return true;
203+
}
204+
205+
$classReflection = $genericType->getClassReflection();
206+
if (
207+
$classReflection !== null
208+
&& $classReflection->isFinal()
209+
&& $genericType->hasMethod($methodType->getValue())->no()) {
210+
return false;
211+
}
212+
}
184213
}
185214
}
186215
}

src/Rules/Methods/MethodCallCheck.php

+1-1
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

+5
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ public function isClassStringType(): TrinaryLogic
248248
return TrinaryLogic::createMaybe();
249249
}
250250

251+
public function hasMethod(string $methodName): TrinaryLogic
252+
{
253+
return TrinaryLogic::createMaybe();
254+
}
255+
251256
public function isVoid(): TrinaryLogic
252257
{
253258
return TrinaryLogic::createNo();

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -1196,6 +1196,7 @@ public function dataFileAsserts(): iterable
11961196
yield from $this->gatherAssertTypes(__DIR__ . '/data/get-native-type.php');
11971197
yield from $this->gatherAssertTypes(__DIR__ . '/data/callsite-cast-narrowing.php');
11981198
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8775.php');
1199+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8752.php');
11991200
}
12001201

12011202
/**
+21
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

+43
Original file line numberDiff line numberDiff line change
@@ -706,4 +706,47 @@ public function testBug8474(): void
706706
$this->analyse([__DIR__ . '/data/bug-8474.php'], []);
707707
}
708708

709+
public function testBug8752(): void
710+
{
711+
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
712+
$this->treatPhpDocTypesAsCertain = true;
713+
$this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], []);
714+
}
715+
716+
public function testImpossibleMethodExistOnGenericClassString(): void
717+
{
718+
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
719+
$this->treatPhpDocTypesAsCertain = true;
720+
721+
$tipText = '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%</>.';
722+
$this->analyse([__DIR__ . '/data/impossible-method-exists-on-generic-class-string.php'], [
723+
[
724+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\S>&literal-string and 'staticAbc' will always evaluate to true.",
725+
18,
726+
$tipText,
727+
],
728+
[
729+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\S>&literal-string and 'nonStaticAbc' will always evaluate to true.",
730+
23,
731+
$tipText,
732+
],
733+
[
734+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>&literal-string and 'nonExistent' will always evaluate to false.",
735+
34,
736+
$tipText,
737+
],
738+
[
739+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>&literal-string and 'staticAbc' will always evaluate to true.",
740+
39,
741+
$tipText,
742+
],
743+
[
744+
"Call to function method_exists() with class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>&literal-string and 'nonStaticAbc' will always evaluate to true.",
745+
44,
746+
$tipText,
747+
],
748+
749+
]);
750+
}
751+
709752
}
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

+50
Original file line numberDiff line numberDiff line change
@@ -2736,6 +2736,7 @@ public function testNonEmptyArray(): void
27362736
$this->checkThisOnly = false;
27372737
$this->checkNullables = true;
27382738
$this->checkUnionTypes = true;
2739+
27392740
$this->analyse([__DIR__ . '/data/non-empty-array.php'], [
27402741
[
27412742
'Parameter #1 $nonEmpty of method AcceptNonEmptyArray\Foo::requireNonEmpty() expects non-empty-array<int>, array<int> given.',
@@ -2750,6 +2751,20 @@ public function testNonEmptyArray(): void
27502751
]);
27512752
}
27522753

2754+
public function testBug8752(): void
2755+
{
2756+
$this->checkThisOnly = false;
2757+
$this->checkNullables = true;
2758+
$this->checkUnionTypes = true;
2759+
$this->checkExplicitMixed = true;
2760+
$this->analyse([__DIR__ . '/../../Analyser/data/bug-8752.php'], [
2761+
[
2762+
'Cannot call method abc() on class-string.',
2763+
18,
2764+
],
2765+
]);
2766+
}
2767+
27532768
public function dataCallablesWithoutCheckNullables(): iterable
27542769
{
27552770
yield [false, false, []];
@@ -2797,4 +2812,39 @@ public function testCallablesWithoutCheckNullables(bool $checkNullables, bool $c
27972812
$this->analyse([__DIR__ . '/data/callables-without-check-nullables.php'], $expectedErrors);
27982813
}
27992814

2815+
public function testCannotCallOnGenericClassString(): void
2816+
{
2817+
$this->checkThisOnly = false;
2818+
$this->checkNullables = true;
2819+
$this->checkUnionTypes = true;
2820+
$this->checkExplicitMixed = true;
2821+
2822+
$this->analyse([__DIR__ . '/../Comparison/data/impossible-method-exists-on-generic-class-string.php'], [
2823+
[
2824+
'Cannot call method nonExistent() on class-string<ImpossibleMethodExistsOnGenericClassString\S>.',
2825+
14,
2826+
],
2827+
[
2828+
'Cannot call method staticAbc() on class-string<ImpossibleMethodExistsOnGenericClassString\S>.',
2829+
20,
2830+
],
2831+
[
2832+
'Cannot call method nonStaticAbc() on class-string<ImpossibleMethodExistsOnGenericClassString\S>.',
2833+
25,
2834+
],
2835+
[
2836+
'Cannot call method nonExistent() on class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>.',
2837+
35,
2838+
],
2839+
[
2840+
'Cannot call method staticAbc() on class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>.',
2841+
41,
2842+
],
2843+
[
2844+
'Cannot call method nonStaticAbc() on class-string<ImpossibleMethodExistsOnGenericClassString\FinalS>.',
2845+
46,
2846+
],
2847+
]);
2848+
}
2849+
28002850
}

tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php

+10
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)