Skip to content

Commit b6417d9

Browse files
gnutixclaude
andcommitted
Check static method calls through constant class-name strings and class-string unions
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 184d34e commit b6417d9

6 files changed

Lines changed: 483 additions & 3 deletions

File tree

‎src/Rules/Methods/StaticMethodCallCheck.php‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\DependencyInjection\AutowiredParameter;
1212
use PHPStan\DependencyInjection\AutowiredService;
1313
use PHPStan\Internal\SprintfHelper;
14+
use PHPStan\Node\Expr\TypeExpr;
1415
use PHPStan\Reflection\ExtendedMethodReflection;
1516
use PHPStan\Reflection\MethodReflection;
1617
use PHPStan\Reflection\Native\NativeMethodReflection;
@@ -32,6 +33,7 @@
3233
use PHPStan\Type\TypeCombinator;
3334
use PHPStan\Type\VerbosityLevel;
3435
use function array_merge;
36+
use function count;
3537
use function in_array;
3638
use function sprintf;
3739
use function strtolower;
@@ -183,9 +185,22 @@ public function check(
183185
}
184186
}
185187
} else {
188+
$exprToCheck = NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class);
189+
$exprType = $scope->getType($exprToCheck);
190+
if ($exprType->isClassString()->yes() && !$exprType->hasMethod($methodName)->yes()) {
191+
// A constant class-name string or a class-string type describes the
192+
// object it names, so route the check through that object type. This
193+
// makes constant-string / class-string unions behave exactly like
194+
// object unions with regard to rule levels (member filtering when
195+
// checkUnionTypes is off, whole-union check when it is on).
196+
$objectType = $exprType->getClassStringObjectType();
197+
if ($this->canCheckThroughObjectType($objectType)) {
198+
$exprToCheck = new TypeExpr($objectType);
199+
}
200+
}
186201
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
187202
$scope,
188-
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class),
203+
$exprToCheck,
189204
sprintf('Call to static method %s() on an unknown class %%s.', SprintfHelper::escapeFormatString($methodName)),
190205
static fn (Type $type): bool => $type->canCallMethods()->yes() && $type->hasMethod($methodName)->yes(),
191206
);
@@ -349,4 +364,25 @@ public function check(
349364
return [$errors, $method];
350365
}
351366

367+
/**
368+
* Class-strings without a class name (class-string, class-string<object>,
369+
* class-string<T of object>) and class-strings naming a trait keep the
370+
* string code path: RuleLevelHelper would filter the object type out or
371+
* report the trait as an unknown class.
372+
*/
373+
private function canCheckThroughObjectType(Type $objectType): bool
374+
{
375+
if (count($objectType->getObjectClassNames()) === 0) {
376+
return false;
377+
}
378+
379+
foreach ($objectType->getObjectClassReflections() as $classReflection) {
380+
if ($classReflection->isTrait()) {
381+
return false;
382+
}
383+
}
384+
385+
return true;
386+
}
387+
352388
}

‎tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php‎

Lines changed: 187 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,22 @@ class CallStaticMethodsRuleTest extends RuleTestCase
3030

3131
private bool $checkThisOnly;
3232

33+
private bool $checkUnionTypes = true;
34+
3335
private bool $checkExplicitMixed = false;
3436

3537
private bool $checkImplicitMixed = false;
3638

39+
private bool $reportMagicMethods = true;
40+
3741
protected function getRule(): Rule
3842
{
3943
$reflectionProvider = self::createReflectionProvider();
4044
$ruleLevelHelper = new RuleLevelHelper(
4145
$reflectionProvider,
4246
checkNullables: $this->checkNullables,
4347
checkThisOnly: $this->checkThisOnly,
44-
checkUnionTypes: true,
48+
checkUnionTypes: $this->checkUnionTypes,
4549
checkExplicitMixed: $this->checkExplicitMixed,
4650
checkImplicitMixed: $this->checkImplicitMixed,
4751
checkBenevolentUnionTypes: false,
@@ -64,7 +68,7 @@ protected function getRule(): Rule
6468
),
6569
checkFunctionNameCase: true,
6670
discoveringSymbolsTip: true,
67-
reportMagicMethods: true,
71+
reportMagicMethods: $this->reportMagicMethods,
6872
),
6973
new FunctionCallParametersCheck(
7074
$ruleLevelHelper,
@@ -1132,4 +1136,185 @@ public function testBug15251(): void
11321136
]);
11331137
}
11341138

1139+
#[RequiresPhp('>= 8.1.0')]
1140+
public function testBug11353(): void
1141+
{
1142+
$this->checkThisOnly = false;
1143+
$this->analyse([__DIR__ . '/data/bug-11353.php'], [
1144+
[
1145+
'Call to an undefined static method Bug11353\A|Bug11353\B|Bug11353\C::create().',
1146+
46,
1147+
],
1148+
]);
1149+
}
1150+
1151+
#[RequiresPhp('>= 8.0.0')]
1152+
public function testStaticCallsOnStringUnions(): void
1153+
{
1154+
$this->checkThisOnly = false;
1155+
$this->analyse([__DIR__ . '/data/static-call-string-unions.php'], [
1156+
[
1157+
'Call to an undefined static method StaticCallStringUnions\Baz::create().',
1158+
53,
1159+
],
1160+
[
1161+
'Call to an undefined static method StaticCallStringUnions\Baz|StaticCallStringUnions\Foo::create().',
1162+
59,
1163+
],
1164+
[
1165+
'Call to an undefined static method StaticCallStringUnions\Baz|StaticCallStringUnions\Foo::create().',
1166+
71,
1167+
],
1168+
[
1169+
'Call to an undefined static method StaticCallStringUnions\Baz|StaticCallStringUnions\Foo::create().',
1170+
79,
1171+
],
1172+
[
1173+
'Call to an undefined static method StaticCallStringUnions\Baz::create().',
1174+
87,
1175+
],
1176+
[
1177+
'Static call to instance method StaticCallStringUnions\Foo::doBar().',
1178+
101,
1179+
],
1180+
[
1181+
'Call to an undefined static method StaticCallStringUnions\WithMagic::whatever().',
1182+
107,
1183+
],
1184+
[
1185+
'Call to an undefined static method StaticCallStringUnions\Foo|StaticCallStringUnions\WithMagic::create().',
1186+
115,
1187+
],
1188+
[
1189+
'Call to an undefined static method object::create().',
1190+
133,
1191+
],
1192+
[
1193+
'Call to an undefined static method T of object::create().',
1194+
142,
1195+
],
1196+
[
1197+
'Call to an undefined static method StaticCallStringUnions\Baz|StaticCallStringUnions\Foo::create().',
1198+
150,
1199+
],
1200+
[
1201+
'Call to private static method privateMethod() of class StaticCallStringUnions\WithNonPublic.',
1202+
162,
1203+
],
1204+
[
1205+
'Call to protected static method protectedMethod() of class StaticCallStringUnions\WithNonPublic.',
1206+
163,
1207+
],
1208+
[
1209+
'Call to static method create() on an unknown class StaticCallStringUnions\UnknownClass.',
1210+
171,
1211+
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
1212+
],
1213+
]);
1214+
}
1215+
1216+
#[RequiresPhp('>= 8.0.0')]
1217+
public function testStaticCallsOnStringUnionsWithoutCheckUnionTypes(): void
1218+
{
1219+
$this->checkThisOnly = false;
1220+
$this->checkUnionTypes = false;
1221+
$this->analyse([__DIR__ . '/data/static-call-string-unions.php'], [
1222+
[
1223+
'Call to an undefined static method StaticCallStringUnions\Baz::create().',
1224+
53,
1225+
],
1226+
[
1227+
'Call to an undefined static method StaticCallStringUnions\Baz::create().',
1228+
87,
1229+
],
1230+
[
1231+
'Static call to instance method StaticCallStringUnions\Foo::doBar().',
1232+
101,
1233+
],
1234+
[
1235+
'Call to an undefined static method StaticCallStringUnions\WithMagic::whatever().',
1236+
107,
1237+
],
1238+
[
1239+
'Call to an undefined static method object::create().',
1240+
133,
1241+
],
1242+
[
1243+
'Call to an undefined static method T of object::create().',
1244+
142,
1245+
],
1246+
[
1247+
'Call to private static method privateMethod() of class StaticCallStringUnions\WithNonPublic.',
1248+
162,
1249+
],
1250+
[
1251+
'Call to protected static method protectedMethod() of class StaticCallStringUnions\WithNonPublic.',
1252+
163,
1253+
],
1254+
[
1255+
'Call to static method create() on an unknown class StaticCallStringUnions\UnknownClass.',
1256+
171,
1257+
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
1258+
],
1259+
]);
1260+
}
1261+
1262+
#[RequiresPhp('>= 8.0.0')]
1263+
public function testStaticCallsOnStringUnionsWithoutReportMagicMethods(): void
1264+
{
1265+
$this->checkThisOnly = false;
1266+
$this->reportMagicMethods = false;
1267+
$this->analyse([__DIR__ . '/data/static-call-string-unions.php'], [
1268+
[
1269+
'Call to an undefined static method StaticCallStringUnions\Baz::create().',
1270+
53,
1271+
],
1272+
[
1273+
'Call to an undefined static method StaticCallStringUnions\Baz|StaticCallStringUnions\Foo::create().',
1274+
59,
1275+
],
1276+
[
1277+
'Call to an undefined static method StaticCallStringUnions\Baz|StaticCallStringUnions\Foo::create().',
1278+
71,
1279+
],
1280+
[
1281+
'Call to an undefined static method StaticCallStringUnions\Baz|StaticCallStringUnions\Foo::create().',
1282+
79,
1283+
],
1284+
[
1285+
'Call to an undefined static method StaticCallStringUnions\Baz::create().',
1286+
87,
1287+
],
1288+
[
1289+
'Static call to instance method StaticCallStringUnions\Foo::doBar().',
1290+
101,
1291+
],
1292+
[
1293+
'Call to an undefined static method object::create().',
1294+
133,
1295+
],
1296+
[
1297+
'Call to an undefined static method T of object::create().',
1298+
142,
1299+
],
1300+
[
1301+
'Call to an undefined static method StaticCallStringUnions\Baz|StaticCallStringUnions\Foo::create().',
1302+
150,
1303+
],
1304+
[
1305+
'Call to private static method privateMethod() of class StaticCallStringUnions\WithNonPublic.',
1306+
162,
1307+
],
1308+
[
1309+
'Call to protected static method protectedMethod() of class StaticCallStringUnions\WithNonPublic.',
1310+
163,
1311+
],
1312+
[
1313+
'Call to static method create() on an unknown class StaticCallStringUnions\UnknownClass.',
1314+
171,
1315+
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
1316+
],
1317+
]);
1318+
}
1319+
11351320
}

‎tests/PHPStan/Rules/Methods/StaticMethodCallableRuleTest.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ public function testRule(): void
104104
'Cannot call static method doFoo() on int.',
105105
22,
106106
],
107+
[
108+
'Call to an undefined static method StaticMethodCallable\Bar|StaticMethodCallable\Foo::doFoo().',
109+
77,
110+
],
107111
]);
108112
}
109113

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace Bug11353;
4+
5+
enum Identifier: string
6+
{
7+
case A = 'identifier_a';
8+
case B = 'identifier_b';
9+
case C = 'identifier_c';
10+
}
11+
12+
class A
13+
{
14+
public static function create(): self
15+
{
16+
return new self();
17+
}
18+
}
19+
20+
class B
21+
{
22+
public static function create(): self
23+
{
24+
return new self();
25+
}
26+
}
27+
28+
class C
29+
{
30+
// missing static function create(): self
31+
}
32+
33+
class SomeCliCommand
34+
{
35+
public function execute(string $input): void
36+
{
37+
$identifier = Identifier::from($input);
38+
39+
$classFQCN = match ($identifier) {
40+
Identifier::A => A::class,
41+
Identifier::B => B::class,
42+
Identifier::C => C::class,
43+
};
44+
45+
// No warning that C::create() does not exist
46+
$class = $classFQCN::create();
47+
}
48+
}

0 commit comments

Comments
 (0)