Skip to content

Commit fe4bf2c

Browse files
authored
Add stringable access check to ClassConstantRule
1 parent ac65494 commit fe4bf2c

File tree

7 files changed

+141
-3
lines changed

7 files changed

+141
-3
lines changed

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
parameters:
22
featureToggles:
33
bleedingEdge: true
4+
checkNonStringableDynamicAccess: true
45
checkParameterCastableToNumberFunctions: true
56
skipCheckGenericClasses!: []
67
stricterFunctionMap: true

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ parameters:
2424
tooWideThrowType: true
2525
featureToggles:
2626
bleedingEdge: false
27+
checkNonStringableDynamicAccess: false
2728
checkParameterCastableToNumberFunctions: false
2829
skipCheckGenericClasses: []
2930
stricterFunctionMap: false

conf/parametersSchema.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ parametersSchema:
2828
])
2929
featureToggles: structure([
3030
bleedingEdge: bool(),
31+
checkNonStringableDynamicAccess: bool(),
3132
checkParameterCastableToNumberFunctions: bool(),
3233
skipCheckGenericClasses: listOf(string()),
3334
stricterFunctionMap: bool()

src/Rules/Classes/ClassConstantRule.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
use PhpParser\Node;
66
use PhpParser\Node\Expr\BinaryOp\Identical;
77
use PhpParser\Node\Expr\ClassConstFetch;
8+
use PhpParser\Node\Name;
89
use PhpParser\Node\Scalar\String_;
910
use PHPStan\Analyser\NullsafeOperatorHelper;
1011
use PHPStan\Analyser\Scope;
12+
use PHPStan\DependencyInjection\AutowiredParameter;
1113
use PHPStan\DependencyInjection\RegisteredRule;
1214
use PHPStan\Internal\SprintfHelper;
1315
use PHPStan\Php\PhpVersion;
@@ -42,6 +44,8 @@ public function __construct(
4244
private RuleLevelHelper $ruleLevelHelper,
4345
private ClassNameCheck $classCheck,
4446
private PhpVersion $phpVersion,
47+
#[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')]
48+
private bool $checkNonStringableDynamicAccess,
4549
)
4650
{
4751
}
@@ -63,6 +67,26 @@ public function processNode(Node $node, Scope $scope): array
6367
$name = $constantString->getValue();
6468
$constantNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
6569
}
70+
71+
if ($this->checkNonStringableDynamicAccess) {
72+
$nameTypeResult = $this->ruleLevelHelper->findTypeToCheck(
73+
$scope,
74+
$node->name,
75+
'',
76+
static fn (Type $type) => $type->isString()->yes(),
77+
);
78+
79+
$nameType = $nameTypeResult->getType();
80+
if (!$nameType instanceof ErrorType && !$nameType->isString()->yes()) {
81+
$className = $node->class instanceof Name
82+
? $scope->resolveName($node->class)
83+
: $scope->getType($node->class)->describe(VerbosityLevel::typeOnly());
84+
85+
$errors[] = RuleErrorBuilder::message(sprintf('Class constant name for %s must be a string, but %s was given.', $className, $nameType->describe(VerbosityLevel::precise())))
86+
->identifier('classConstant.nameNotString')
87+
->build();
88+
}
89+
}
6690
}
6791

6892
foreach ($constantNameScopes as $constantName => $constantScope) {

tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ protected function getRule(): Rule
2626
$reflectionProvider = self::createReflectionProvider();
2727
return new ClassConstantRule(
2828
$reflectionProvider,
29-
new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true),
29+
new RuleLevelHelper($reflectionProvider, true, false, true, true, true, false, true),
3030
new ClassNameCheck(
3131
new ClassCaseSensitivityCheck($reflectionProvider, true),
3232
new ClassForbiddenNameCheck(self::getContainer()),
3333
$reflectionProvider,
3434
self::getContainer(),
3535
),
3636
new PhpVersion($this->phpVersion),
37+
true,
3738
);
3839
}
3940

@@ -59,6 +60,10 @@ public function testClassConstant(): void
5960
'Access to undefined constant ClassConstantNamespace\Foo::DOLOR.',
6061
10,
6162
],
63+
[
64+
'Cannot access constant LOREM on mixed.',
65+
11,
66+
],
6267
[
6368
'Access to undefined constant ClassConstantNamespace\Foo::DOLOR.',
6469
16,
@@ -439,6 +444,14 @@ public function testDynamicAccess(): void
439444
$this->phpVersion = PHP_VERSION_ID;
440445

441446
$this->analyse([__DIR__ . '/data/dynamic-constant-access.php'], [
447+
[
448+
'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.',
449+
17,
450+
],
451+
[
452+
'Class constant name for ClassConstantDynamicAccess\Foo must be a string, but object was given.',
453+
19,
454+
],
442455
[
443456
'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.',
444457
20,
@@ -474,4 +487,61 @@ public function testDynamicAccess(): void
474487
]);
475488
}
476489

490+
#[RequiresPhp('>= 8.3')]
491+
public function testStringableDynamicAccess(): void
492+
{
493+
$this->phpVersion = PHP_VERSION_ID;
494+
495+
$this->analyse([__DIR__ . '/data/dynamic-constant-stringable-access.php'], [
496+
[
497+
'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but mixed was given.',
498+
14,
499+
],
500+
[
501+
'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but string|null was given.',
502+
15,
503+
],
504+
[
505+
'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but Stringable|null was given.',
506+
16,
507+
],
508+
[
509+
'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but int was given.',
510+
17,
511+
],
512+
[
513+
'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but int|null was given.',
514+
18,
515+
],
516+
[
517+
'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but DateTime|string was given.',
518+
19,
519+
],
520+
[
521+
'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but 1111 was given.',
522+
20,
523+
],
524+
[
525+
'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but Stringable was given.',
526+
22,
527+
],
528+
[
529+
'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but mixed was given.',
530+
32,
531+
],
532+
[
533+
'Class constant name for ClassConstantDynamicStringableAccess\Bar must be a string, but mixed was given.',
534+
33,
535+
],
536+
[
537+
'Class constant name for DateTime|DateTimeImmutable must be a string, but mixed was given.',
538+
38,
539+
],
540+
[
541+
'Class constant name for object must be a string, but mixed was given.',
542+
39,
543+
],
544+
]);
545+
}
546+
477547
}

tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public function test(string $string, object $obj): void
1414
{
1515
$bar = 'FOO';
1616

17-
echo self::{$foo};
17+
echo self::{$bar};
1818
echo self::{$string};
1919
echo self::{$obj};
2020
echo self::{$this->name};
@@ -44,5 +44,4 @@ public function testScope(): void
4444
echo self::{$name};
4545
}
4646

47-
4847
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php // lint >= 8.3
2+
3+
namespace ClassConstantDynamicStringableAccess;
4+
5+
use Stringable;
6+
use DateTime;
7+
use DateTimeImmutable;
8+
9+
abstract class Foo
10+
{
11+
12+
public function test(mixed $mixed, ?string $nullableStr, ?Stringable $nullableStringable, int $int, ?int $nullableInt, DateTime|string $datetimeOrStr, Stringable $stringable): void
13+
{
14+
echo self::{$mixed};
15+
echo self::{$nullableStr};
16+
echo self::{$nullableStringable};
17+
echo self::{$int};
18+
echo self::{$nullableInt};
19+
echo self::{$datetimeOrStr};
20+
echo self::{1111};
21+
echo self::{(string)$stringable};
22+
echo self::{$stringable}; // Uncast Stringable objects will cause a runtime error
23+
}
24+
25+
}
26+
27+
final class Bar extends Foo
28+
{
29+
30+
public function test(mixed $mixed, ?string $nullableStr, ?Stringable $nullableStringable, int $int, ?int $nullableInt, DateTime|string $datetimeOrStr, Stringable $stringable): void
31+
{
32+
echo parent::{$mixed};
33+
echo self::{$mixed};
34+
}
35+
36+
public function testClassDynamic(DateTime|DateTimeImmutable $datetime, object $obj, mixed $mixed): void
37+
{
38+
echo $datetime::{$mixed};
39+
echo $obj::{$mixed};
40+
}
41+
42+
}

0 commit comments

Comments
 (0)