Skip to content

Commit 75debf6

Browse files
authored
Support dynamic Expr name expressions in rules
1 parent 09992c7 commit 75debf6

11 files changed

+460
-16
lines changed

src/Rules/Classes/ClassConstantRule.php

+25-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
namespace PHPStan\Rules\Classes;
44

55
use PhpParser\Node;
6+
use PhpParser\Node\Expr\BinaryOp\Identical;
67
use PhpParser\Node\Expr\ClassConstFetch;
8+
use PhpParser\Node\Scalar\String_;
79
use PHPStan\Analyser\NullsafeOperatorHelper;
810
use PHPStan\Analyser\Scope;
911
use PHPStan\Internal\SprintfHelper;
1012
use PHPStan\Php\PhpVersion;
1113
use PHPStan\Reflection\ReflectionProvider;
1214
use PHPStan\Rules\ClassNameCheck;
1315
use PHPStan\Rules\ClassNameNodePair;
16+
use PHPStan\Rules\IdentifierRuleError;
1417
use PHPStan\Rules\Rule;
1518
use PHPStan\Rules\RuleErrorBuilder;
1619
use PHPStan\Rules\RuleLevelHelper;
@@ -47,11 +50,30 @@ public function getNodeType(): string
4750

4851
public function processNode(Node $node, Scope $scope): array
4952
{
50-
if (!$node->name instanceof Node\Identifier) {
51-
return [];
53+
$errors = [];
54+
if ($node->name instanceof Node\Identifier) {
55+
$constantNameScopes = [$node->name->name => $scope];
56+
} else {
57+
$nameType = $scope->getType($node->name);
58+
$constantNameScopes = [];
59+
foreach ($nameType->getConstantStrings() as $constantString) {
60+
$name = $constantString->getValue();
61+
$constantNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
62+
}
63+
}
64+
65+
foreach ($constantNameScopes as $constantName => $constantScope) {
66+
$errors = array_merge($errors, $this->processSingleClassConstFetch($constantScope, $node, $constantName));
5267
}
53-
$constantName = $node->name->name;
5468

69+
return $errors;
70+
}
71+
72+
/**
73+
* @return list<IdentifierRuleError>
74+
*/
75+
private function processSingleClassConstFetch(Scope $scope, ClassConstFetch $node, string $constantName): array
76+
{
5577
$class = $node->class;
5678
$messages = [];
5779
if ($class instanceof Node\Name) {

src/Rules/Methods/CallMethodsRule.php

+24-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
namespace PHPStan\Rules\Methods;
44

55
use PhpParser\Node;
6+
use PhpParser\Node\Expr\BinaryOp\Identical;
67
use PhpParser\Node\Expr\MethodCall;
8+
use PhpParser\Node\Scalar\String_;
79
use PHPStan\Analyser\Scope;
810
use PHPStan\Internal\SprintfHelper;
911
use PHPStan\Reflection\ParametersAcceptorSelector;
1012
use PHPStan\Rules\FunctionCallParametersCheck;
13+
use PHPStan\Rules\IdentifierRuleError;
1114
use PHPStan\Rules\Rule;
1215
use function array_merge;
1316

@@ -31,12 +34,30 @@ public function getNodeType(): string
3134

3235
public function processNode(Node $node, Scope $scope): array
3336
{
34-
if (!$node->name instanceof Node\Identifier) {
35-
return [];
37+
$errors = [];
38+
if ($node->name instanceof Node\Identifier) {
39+
$methodNameScopes = [$node->name->name => $scope];
40+
} else {
41+
$nameType = $scope->getType($node->name);
42+
$methodNameScopes = [];
43+
foreach ($nameType->getConstantStrings() as $constantString) {
44+
$name = $constantString->getValue();
45+
$methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
46+
}
3647
}
3748

38-
$methodName = $node->name->name;
49+
foreach ($methodNameScopes as $methodName => $methodScope) {
50+
$errors = array_merge($errors, $this->processSingleMethodCall($methodScope, $node, $methodName));
51+
}
52+
53+
return $errors;
54+
}
3955

56+
/**
57+
* @return list<IdentifierRuleError>
58+
*/
59+
private function processSingleMethodCall(Scope $scope, MethodCall $node, string $methodName): array
60+
{
4061
[$errors, $methodReflection] = $this->methodCallCheck->check($scope, $methodName, $node->var);
4162
if ($methodReflection === null) {
4263
return $errors;

src/Rules/Methods/CallStaticMethodsRule.php

+25-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
namespace PHPStan\Rules\Methods;
44

55
use PhpParser\Node;
6+
use PhpParser\Node\Expr\BinaryOp\Identical;
67
use PhpParser\Node\Expr\StaticCall;
8+
use PhpParser\Node\Scalar\String_;
79
use PHPStan\Analyser\Scope;
810
use PHPStan\Internal\SprintfHelper;
911
use PHPStan\Reflection\ParametersAcceptorSelector;
1012
use PHPStan\Rules\FunctionCallParametersCheck;
13+
use PHPStan\Rules\IdentifierRuleError;
1114
use PHPStan\Rules\Rule;
1215
use function array_merge;
1316
use function sprintf;
@@ -32,11 +35,30 @@ public function getNodeType(): string
3235

3336
public function processNode(Node $node, Scope $scope): array
3437
{
35-
if (!$node->name instanceof Node\Identifier) {
36-
return [];
38+
$errors = [];
39+
if ($node->name instanceof Node\Identifier) {
40+
$methodNameScopes = [$node->name->name => $scope];
41+
} else {
42+
$nameType = $scope->getType($node->name);
43+
$methodNameScopes = [];
44+
foreach ($nameType->getConstantStrings() as $constantString) {
45+
$name = $constantString->getValue();
46+
$methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
47+
}
3748
}
38-
$methodName = $node->name->name;
3949

50+
foreach ($methodNameScopes as $methodName => $methodScope) {
51+
$errors = array_merge($errors, $this->processSingleMethodCall($methodScope, $node, $methodName));
52+
}
53+
54+
return $errors;
55+
}
56+
57+
/**
58+
* @return list<IdentifierRuleError>
59+
*/
60+
private function processSingleMethodCall(Scope $scope, StaticCall $node, string $methodName): array
61+
{
4062
[$errors, $method] = $this->methodCallCheck->check($scope, $methodName, $node->class);
4163
if ($method === null) {
4264
return $errors;

src/Rules/Variables/DefinedVariableRule.php

+31-7
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
namespace PHPStan\Rules\Variables;
44

55
use PhpParser\Node;
6+
use PhpParser\Node\Expr\BinaryOp\Identical;
67
use PhpParser\Node\Expr\Variable;
8+
use PhpParser\Node\Scalar\String_;
79
use PHPStan\Analyser\Scope;
10+
use PHPStan\Rules\IdentifierRuleError;
811
use PHPStan\Rules\Rule;
912
use PHPStan\Rules\RuleErrorBuilder;
13+
use function array_merge;
1014
use function in_array;
1115
use function is_string;
1216
use function sprintf;
@@ -31,11 +35,31 @@ public function getNodeType(): string
3135

3236
public function processNode(Node $node, Scope $scope): array
3337
{
34-
if (!is_string($node->name)) {
35-
return [];
38+
$errors = [];
39+
if (is_string($node->name)) {
40+
$variableNameScopes = [$node->name => $scope];
41+
} else {
42+
$nameType = $scope->getType($node->name);
43+
$variableNameScopes = [];
44+
foreach ($nameType->getConstantStrings() as $constantString) {
45+
$name = $constantString->getValue();
46+
$variableNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
47+
}
48+
}
49+
50+
foreach ($variableNameScopes as $name => $variableScope) {
51+
$errors = array_merge($errors, $this->processSingleVariable($variableScope, $node, $name));
3652
}
3753

38-
if ($this->cliArgumentsVariablesRegistered && in_array($node->name, [
54+
return $errors;
55+
}
56+
57+
/**
58+
* @return list<IdentifierRuleError>
59+
*/
60+
private function processSingleVariable(Scope $scope, Variable $node, string $variableName): array
61+
{
62+
if ($this->cliArgumentsVariablesRegistered && in_array($variableName, [
3963
'argc',
4064
'argv',
4165
], true)) {
@@ -49,18 +73,18 @@ public function processNode(Node $node, Scope $scope): array
4973
return [];
5074
}
5175

52-
if ($scope->hasVariableType($node->name)->no()) {
76+
if ($scope->hasVariableType($variableName)->no()) {
5377
return [
54-
RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $node->name))
78+
RuleErrorBuilder::message(sprintf('Undefined variable: $%s', $variableName))
5579
->identifier('variable.undefined')
5680
->build(),
5781
];
5882
} elseif (
5983
$this->checkMaybeUndefinedVariables
60-
&& !$scope->hasVariableType($node->name)->yes()
84+
&& !$scope->hasVariableType($variableName)->yes()
6185
) {
6286
return [
63-
RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $node->name))
87+
RuleErrorBuilder::message(sprintf('Variable $%s might not be defined.', $variableName))
6488
->identifier('variable.undefined')
6589
->build(),
6690
];

tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php

+44
Original file line numberDiff line numberDiff line change
@@ -435,4 +435,48 @@ public function testClassConstantAccessedOnTrait(): void
435435
]);
436436
}
437437

438+
public function testDynamicAccess(): void
439+
{
440+
if (PHP_VERSION_ID < 80300) {
441+
$this->markTestSkipped('Test requires PHP 8.3.');
442+
}
443+
444+
$this->phpVersion = PHP_VERSION_ID;
445+
446+
$this->analyse([__DIR__ . '/data/dynamic-constant-access.php'], [
447+
[
448+
'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.',
449+
20,
450+
],
451+
[
452+
'Access to undefined constant ClassConstantDynamicAccess\Foo::BUZ.',
453+
20,
454+
],
455+
[
456+
'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.',
457+
37,
458+
],
459+
[
460+
'Access to undefined constant ClassConstantDynamicAccess\Foo::BUZ.',
461+
39,
462+
],
463+
[
464+
'Access to undefined constant ClassConstantDynamicAccess\Foo::QUX.',
465+
41,
466+
],
467+
[
468+
'Access to undefined constant ClassConstantDynamicAccess\Foo::QUX.',
469+
44,
470+
],
471+
[
472+
'Access to undefined constant ClassConstantDynamicAccess\Foo::BUZ.',
473+
44,
474+
],
475+
[
476+
'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.',
477+
44,
478+
],
479+
]);
480+
}
481+
438482
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php // lint >= 8.3
2+
3+
namespace ClassConstantDynamicAccess;
4+
5+
final class Foo
6+
{
7+
8+
private const BAR = 'BAR';
9+
10+
/** @var 'FOO'|'BAR'|'BUZ' */
11+
public $name;
12+
13+
public function test(string $string, object $obj): void
14+
{
15+
$bar = 'FOO';
16+
17+
echo self::{$foo};
18+
echo self::{$string};
19+
echo self::{$obj};
20+
echo self::{$this->name};
21+
}
22+
23+
public function testScope(): void
24+
{
25+
$name1 = 'FOO';
26+
$rand = rand();
27+
if ($rand === 1) {
28+
$foo = 1;
29+
$name = $name1;
30+
} elseif ($rand === 2) {
31+
$name = 'BUZ';
32+
} else {
33+
$name = 'QUX';
34+
}
35+
36+
if ($name === 'FOO') {
37+
echo self::{$name};
38+
} elseif ($name === 'BUZ') {
39+
echo self::{$name};
40+
} else {
41+
echo self::{$name};
42+
}
43+
44+
echo self::{$name};
45+
}
46+
47+
48+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

+35
Original file line numberDiff line numberDiff line change
@@ -3552,4 +3552,39 @@ public function testBug6828(): void
35523552
$this->analyse([__DIR__ . '/data/bug-6828.php'], []);
35533553
}
35543554

3555+
public function testDynamicCall(): void
3556+
{
3557+
$this->checkThisOnly = false;
3558+
$this->checkNullables = true;
3559+
$this->checkUnionTypes = true;
3560+
$this->checkExplicitMixed = true;
3561+
3562+
$this->analyse([__DIR__ . '/data/dynamic-call.php'], [
3563+
[
3564+
'Call to an undefined method MethodsDynamicCall\Foo::bar().',
3565+
23,
3566+
],
3567+
[
3568+
'Call to an undefined method MethodsDynamicCall\Foo::doBar().',
3569+
26,
3570+
],
3571+
[
3572+
'Call to an undefined method MethodsDynamicCall\Foo::doBuz().',
3573+
26,
3574+
],
3575+
[
3576+
'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, int|string given.',
3577+
53,
3578+
],
3579+
[
3580+
'Parameter #1 $s of method MethodsDynamicCall\Foo::doQux() expects string, int given.',
3581+
54,
3582+
],
3583+
[
3584+
'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, string given.',
3585+
55,
3586+
],
3587+
]);
3588+
}
3589+
35553590
}

tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php

+34
Original file line numberDiff line numberDiff line change
@@ -862,4 +862,38 @@ public function testBug12015(): void
862862
$this->analyse([__DIR__ . '/data/bug-12015.php'], []);
863863
}
864864

865+
public function testDynamicCall(): void
866+
{
867+
$this->checkThisOnly = false;
868+
$this->checkExplicitMixed = true;
869+
$this->checkImplicitMixed = true;
870+
871+
$this->analyse([__DIR__ . '/data/dynamic-call.php'], [
872+
[
873+
'Call to an undefined static method MethodsDynamicCall\Foo::bar().',
874+
33,
875+
],
876+
[
877+
'Call to an undefined static method MethodsDynamicCall\Foo::doBar().',
878+
36,
879+
],
880+
[
881+
'Call to an undefined static method MethodsDynamicCall\Foo::doBuz().',
882+
36,
883+
],
884+
[
885+
'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, int|string given.',
886+
58,
887+
],
888+
[
889+
'Parameter #1 $s of static method MethodsDynamicCall\Foo::doQux() expects string, int given.',
890+
59,
891+
],
892+
[
893+
'Parameter #1 $n of method MethodsDynamicCall\Foo::doFoo() expects int, string given.',
894+
60,
895+
],
896+
]);
897+
}
898+
865899
}

0 commit comments

Comments
 (0)