Skip to content

Commit 0a5eebd

Browse files
committed
Add stringable access check to ClassConstantRule
1 parent 5d8c209 commit 0a5eebd

File tree

8 files changed

+129
-3
lines changed

8 files changed

+129
-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.level0.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ conditionalTags:
1616
phpstan.rules.rule: %featureToggles.newStaticInAbstractClassStaticMethod%
1717

1818
services:
19+
-
20+
class: PHPStan\Rules\Classes\ClassConstantRule
21+
1922
-
2023
class: PHPStan\Rules\Classes\NewStaticInAbstractClassStaticMethodRule
2124

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: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
use PHPStan\Rules\Rule;
2020
use PHPStan\Rules\RuleErrorBuilder;
2121
use PHPStan\Rules\RuleLevelHelper;
22+
use PHPStan\Type\Constant\ConstantStringType;
2223
use PHPStan\Type\ErrorType;
2324
use PHPStan\Type\StringType;
2425
use PHPStan\Type\ThisType;
2526
use PHPStan\Type\Type;
2627
use PHPStan\Type\TypeCombinator;
2728
use PHPStan\Type\VerbosityLevel;
29+
use function array_map;
2830
use function array_merge;
2931
use function in_array;
3032
use function sprintf;
@@ -42,6 +44,7 @@ public function __construct(
4244
private RuleLevelHelper $ruleLevelHelper,
4345
private ClassNameCheck $classCheck,
4446
private PhpVersion $phpVersion,
47+
private bool $checkNonStringableDynamicAccess = true,
4548
)
4649
{
4750
}
@@ -63,6 +66,23 @@ public function processNode(Node $node, Scope $scope): array
6366
$name = $constantString->getValue();
6467
$constantNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
6568
}
69+
70+
if ($this->checkNonStringableDynamicAccess) {
71+
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
72+
$scope,
73+
$node->name,
74+
'',
75+
static fn (Type $type) => $type->isString()->yes(),
76+
);
77+
78+
$type = $typeResult->getType();
79+
80+
if (!$type->isString()->yes()) {
81+
$errors[] = RuleErrorBuilder::message(sprintf('Cannot fetch class constant with a non-stringable type %s.', $nameType->describe(VerbosityLevel::precise())))
82+
->identifier('classConstant.fetchInvalidExpression')
83+
->build();
84+
}
85+
}
6686
}
6787

6888
foreach ($constantNameScopes as $constantName => $constantScope) {
@@ -76,6 +96,32 @@ public function processNode(Node $node, Scope $scope): array
7696
return $errors;
7797
}
7898

99+
/**
100+
* @return list<string>
101+
*/
102+
private function processClassNode(Scope $scope, ClassConstFetch $node): array
103+
{
104+
$class = $node->class;
105+
$messages = [];
106+
$classNames = [];
107+
108+
if ($class instanceof Node\Name) {
109+
$className = (string) $class;
110+
$classNames = [$className];
111+
} else {
112+
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
113+
$scope,
114+
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $class),
115+
'',
116+
static fn (Type $type): bool => $type->toString()->isString()->yes(),
117+
);
118+
$classType = $classTypeResult->getType();
119+
$classNames = array_map(static fn (ConstantStringType $type) => $type->getValue(), $classType->getConstantStrings());
120+
}
121+
122+
return $classNames;
123+
}
124+
79125
/**
80126
* @return list<IdentifierRuleError>
81127
*/

tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php

Lines changed: 54 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+
'Cannot fetch class constant with a non-stringable type object.',
453+
19,
454+
],
442455
[
443456
'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.',
444457
20,
@@ -474,4 +487,44 @@ public function testDynamicAccess(): void
474487
]);
475488
}
476489

490+
public function testStringableDynamicAccess(): void
491+
{
492+
if (PHP_VERSION_ID < 80300) {
493+
$this->markTestSkipped('Test requires PHP 8.3.');
494+
}
495+
496+
$this->phpVersion = PHP_VERSION_ID;
497+
498+
$this->analyse([__DIR__ . '/data/dynamic-constant-stringable-access.php'], [
499+
[
500+
'Cannot fetch class constant with a non-stringable type mixed.',
501+
13,
502+
],
503+
[
504+
'Cannot fetch class constant with a non-stringable type string|null.',
505+
14,
506+
],
507+
[
508+
'Cannot fetch class constant with a non-stringable type Stringable|null.',
509+
15,
510+
],
511+
[
512+
'Cannot fetch class constant with a non-stringable type int.',
513+
16,
514+
],
515+
[
516+
'Cannot fetch class constant with a non-stringable type int|null.',
517+
17,
518+
],
519+
[
520+
'Cannot fetch class constant with a non-stringable type DateTime|string.',
521+
18,
522+
],
523+
[
524+
'Cannot fetch class constant with a non-stringable type 1111.',
525+
19,
526+
],
527+
]);
528+
}
529+
477530
}

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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php // lint >= 8.3
2+
3+
namespace ClassConstantDynamicStringableAccess;
4+
5+
use Stringable;
6+
use DateTime;
7+
8+
final class Foo
9+
{
10+
11+
public function test(mixed $mixed, ?string $nullableStr, ?Stringable $nullableStringable, int $int, ?int $nullableInt, DateTime|string $datetimeOrStr): void
12+
{
13+
echo self::{$mixed};
14+
echo self::{$nullableStr};
15+
echo self::{$nullableStringable};
16+
echo self::{$int};
17+
echo self::{$nullableInt};
18+
echo self::{$datetimeOrStr};
19+
echo self::{1111};
20+
}
21+
22+
}

0 commit comments

Comments
 (0)