Skip to content

Commit 245baf6

Browse files
committed
Add stringable access check to ClassConstantRule
1 parent f74f499 commit 245baf6

File tree

7 files changed

+108
-2
lines changed

7 files changed

+108
-2
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@ conditionalTags:
1212
phpstan.restrictedClassNameUsageExtension: %featureToggles.internalTag%
1313
PHPStan\Rules\InternalTag\RestrictedInternalFunctionUsageExtension:
1414
phpstan.restrictedFunctionUsageExtension: %featureToggles.internalTag%
15+
PHPStan\Rules\Classes\ClassConstantRule:
16+
phpstan.checkNonStringableDynamicAccess: %featureToggles.checkNonStringableDynamicAccess%
1517
PHPStan\Rules\Classes\NewStaticInAbstractClassStaticMethodRule:
1618
phpstan.rules.rule: %featureToggles.newStaticInAbstractClassStaticMethod%
1719

1820
services:
21+
-
22+
class: PHPStan\Rules\Classes\ClassConstantRule
23+
1924
-
2025
class: PHPStan\Rules\Classes\NewStaticInAbstractClassStaticMethodRule
2126

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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function __construct(
4242
private RuleLevelHelper $ruleLevelHelper,
4343
private ClassNameCheck $classCheck,
4444
private PhpVersion $phpVersion,
45+
private bool $checkNonStringableDynamicAccess = true,
4546
)
4647
{
4748
}
@@ -63,6 +64,24 @@ public function processNode(Node $node, Scope $scope): array
6364
$name = $constantString->getValue();
6465
$constantNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name)));
6566
}
67+
68+
if ($this->checkNonStringableDynamicAccess) {
69+
$accepts = $this->ruleLevelHelper->accepts(new StringType(), $nameType, true);
70+
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
71+
$scope,
72+
$node->name,
73+
'',
74+
static fn (Type $type) => $type->toString()->isString()->yes()
75+
);
76+
77+
if (! $typeResult->getType()->isString()->yes() ||
78+
$typeResult->getType()->toString()->isNumericString()->yes()
79+
) {
80+
$errors[] = RuleErrorBuilder::message(sprintf('Cannot fetch class constant with a non-stringable type %s.', $nameType->describe(VerbosityLevel::typeOnly())))
81+
->identifier('classConstant.fetchInvalidExpression')
82+
->build();
83+
}
84+
}
6685
}
6786

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

tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class ClassConstantRuleTest extends RuleTestCase
2121

2222
private int $phpVersion;
2323

24+
private bool $checkNonStringableDynamicAccess;
25+
2426
protected function getRule(): Rule
2527
{
2628
$reflectionProvider = self::createReflectionProvider();
@@ -34,12 +36,14 @@ protected function getRule(): Rule
3436
self::getContainer(),
3537
),
3638
new PhpVersion($this->phpVersion),
39+
$this->checkNonStringableDynamicAccess,
3740
);
3841
}
3942

4043
public function testClassConstant(): void
4144
{
4245
$this->phpVersion = PHP_VERSION_ID;
46+
$this->checkNonStringableDynamicAccess = true;
4347
$this->analyse(
4448
[
4549
__DIR__ . '/data/class-constant.php',
@@ -103,6 +107,7 @@ public function testClassConstant(): void
103107
public function testClassConstantVisibility(): void
104108
{
105109
$this->phpVersion = PHP_VERSION_ID;
110+
$this->checkNonStringableDynamicAccess = true;
106111
$this->analyse([__DIR__ . '/data/class-constant-visibility.php'], [
107112
[
108113
'Access to private constant PRIVATE_BAR of class ClassConstantVisibility\Bar.',
@@ -172,6 +177,7 @@ public function testClassConstantVisibility(): void
172177
public function testClassExists(): void
173178
{
174179
$this->phpVersion = PHP_VERSION_ID;
180+
$this->checkNonStringableDynamicAccess = true;
175181
$this->analyse([__DIR__ . '/data/class-exists.php'], [
176182
[
177183
'Class UnknownClass\Bar not found.',
@@ -246,12 +252,14 @@ public static function dataClassConstantOnExpression(): array
246252
public function testClassConstantOnExpression(int $phpVersion, array $errors): void
247253
{
248254
$this->phpVersion = $phpVersion;
255+
$this->checkNonStringableDynamicAccess = true;
249256
$this->analyse([__DIR__ . '/data/class-constant-on-expr.php'], $errors);
250257
}
251258

252259
public function testAttributes(): void
253260
{
254261
$this->phpVersion = PHP_VERSION_ID;
262+
$this->checkNonStringableDynamicAccess = true;
255263
$this->analyse([__DIR__ . '/data/class-constant-attribute.php'], [
256264
[
257265
'Access to undefined constant ClassConstantAttribute\Foo::BAR.',
@@ -288,18 +296,21 @@ public function testAttributes(): void
288296
public function testRuleWithNullsafeVariant(): void
289297
{
290298
$this->phpVersion = PHP_VERSION_ID;
299+
$this->checkNonStringableDynamicAccess = true;
291300
$this->analyse([__DIR__ . '/data/class-constant-nullsafe.php'], []);
292301
}
293302

294303
public function testBug7675(): void
295304
{
296305
$this->phpVersion = PHP_VERSION_ID;
306+
$this->checkNonStringableDynamicAccess = true;
297307
$this->analyse([__DIR__ . '/data/bug-7675.php'], []);
298308
}
299309

300310
public function testBug8034(): void
301311
{
302312
$this->phpVersion = PHP_VERSION_ID;
313+
$this->checkNonStringableDynamicAccess = true;
303314
$this->analyse([__DIR__ . '/data/bug-8034.php'], [
304315
[
305316
'Access to undefined constant static(Bug8034\HelloWorld)::FIELDS.',
@@ -311,6 +322,7 @@ public function testBug8034(): void
311322
public function testClassConstFetchDefined(): void
312323
{
313324
$this->phpVersion = PHP_VERSION_ID;
325+
$this->checkNonStringableDynamicAccess = true;
314326
$this->analyse([__DIR__ . '/data/class-const-fetch-defined.php'], [
315327
[
316328
'Access to undefined constant ClassConstFetchDefined\Foo::TEST.',
@@ -412,6 +424,7 @@ public function testPhpstanInternalClass(): void
412424
$tip = 'This is most likely unintentional. Did you mean to type \AClass?';
413425

414426
$this->phpVersion = PHP_VERSION_ID;
427+
$this->checkNonStringableDynamicAccess = true;
415428
$this->analyse([__DIR__ . '/data/phpstan-internal-class.php'], [
416429
[
417430
'Referencing prefixed PHPStan class: _PHPStan_156ee64ba\AClass.',
@@ -425,6 +438,7 @@ public function testPhpstanInternalClass(): void
425438
public function testClassConstantAccessedOnTrait(): void
426439
{
427440
$this->phpVersion = PHP_VERSION_ID;
441+
$this->checkNonStringableDynamicAccess = true;
428442
$this->analyse([__DIR__ . '/data/class-constant-accessed-on-trait.php'], [
429443
[
430444
'Cannot access constant TEST on trait ClassConstantAccessedOnTrait\Foo.',
@@ -437,8 +451,17 @@ public function testClassConstantAccessedOnTrait(): void
437451
public function testDynamicAccess(): void
438452
{
439453
$this->phpVersion = PHP_VERSION_ID;
454+
$this->checkNonStringableDynamicAccess = true;
440455

441456
$this->analyse([__DIR__ . '/data/dynamic-constant-access.php'], [
457+
[
458+
'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.',
459+
17,
460+
],
461+
[
462+
'Cannot fetch class constant with a non-stringable type object.',
463+
19,
464+
],
442465
[
443466
'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.',
444467
20,
@@ -474,4 +497,41 @@ public function testDynamicAccess(): void
474497
]);
475498
}
476499

500+
public function testStringableDynamicAccess(): void
501+
{
502+
if (PHP_VERSION_ID < 80300) {
503+
$this->markTestSkipped('Test requires PHP 8.3.');
504+
}
505+
506+
$this->phpVersion = PHP_VERSION_ID;
507+
$this->checkNonStringableDynamicAccess = true;
508+
509+
$this->analyse([__DIR__ . '/data/dynamic-constant-stringable-access.php'], [
510+
[
511+
'Cannot fetch class constant with a non-stringable type mixed.',
512+
13,
513+
],
514+
[
515+
'Cannot fetch class constant with a non-stringable type string|null.',
516+
14,
517+
],
518+
[
519+
'Cannot fetch class constant with a non-stringable type Stringable|null.',
520+
15,
521+
],
522+
[
523+
'Cannot fetch class constant with a non-stringable type int.',
524+
16,
525+
],
526+
[
527+
'Cannot fetch class constant with a non-stringable type int|null.',
528+
17,
529+
],
530+
[
531+
'Cannot fetch class constant with a non-stringable type DateTime|string.',
532+
18,
533+
],
534+
]);
535+
}
536+
477537
}

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
}
20+
21+
}

0 commit comments

Comments
 (0)