From 8e8bcd5ea747e3e0a3697c005395a6dfde209df2 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sun, 30 Mar 2025 03:54:54 +0900 Subject: [PATCH 1/6] Add stringable access check to ClassConstantRule --- conf/bleedingEdge.neon | 1 + conf/config.level0.neon | 3 ++ conf/config.neon | 1 + conf/parametersSchema.neon | 1 + src/Rules/Classes/ClassConstantRule.php | 18 +++++++ .../Rules/Classes/ClassConstantRuleTest.php | 52 ++++++++++++++++++- .../Classes/data/dynamic-constant-access.php | 3 +- .../dynamic-constant-stringable-access.php | 24 +++++++++ 8 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 81ea6c216b..1c0fec310f 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -1,6 +1,7 @@ parameters: featureToggles: bleedingEdge: true + checkNonStringableDynamicAccess: true checkParameterCastableToNumberFunctions: true skipCheckGenericClasses!: [] stricterFunctionMap: true diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 805ea348a9..864df73d39 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -16,6 +16,9 @@ conditionalTags: phpstan.rules.rule: %featureToggles.newStaticInAbstractClassStaticMethod% services: + - + class: PHPStan\Rules\Classes\ClassConstantRule + - class: PHPStan\Rules\Classes\NewStaticInAbstractClassStaticMethodRule diff --git a/conf/config.neon b/conf/config.neon index 73a8062c27..efdc585aa7 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -24,6 +24,7 @@ parameters: tooWideThrowType: true featureToggles: bleedingEdge: false + checkNonStringableDynamicAccess: false checkParameterCastableToNumberFunctions: false skipCheckGenericClasses: [] stricterFunctionMap: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 6af9e2737e..9c962d67ed 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -28,6 +28,7 @@ parametersSchema: ]) featureToggles: structure([ bleedingEdge: bool(), + checkNonStringableDynamicAccess: bool(), checkParameterCastableToNumberFunctions: bool(), skipCheckGenericClasses: listOf(string()), stricterFunctionMap: bool() diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index 3e3f6d3ead..fbc8ecb145 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -42,6 +42,7 @@ public function __construct( private RuleLevelHelper $ruleLevelHelper, private ClassNameCheck $classCheck, private PhpVersion $phpVersion, + private bool $checkNonStringableDynamicAccess = true, ) { } @@ -63,6 +64,23 @@ public function processNode(Node $node, Scope $scope): array $name = $constantString->getValue(); $constantNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); } + + if ($this->checkNonStringableDynamicAccess) { + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $node->name, + '', + static fn (Type $type) => $type->isString()->yes(), + ); + + $type = $typeResult->getType(); + + if (!$type->isString()->yes()) { + $errors[] = RuleErrorBuilder::message(sprintf('Cannot fetch class constant with a non-stringable type %s.', $nameType->describe(VerbosityLevel::precise()))) + ->identifier('classConstant.fetchInvalidExpression') + ->build(); + } + } } foreach ($constantNameScopes as $constantName => $constantScope) { diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 3ecc4572f9..aa304be38d 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -26,7 +26,7 @@ protected function getRule(): Rule $reflectionProvider = self::createReflectionProvider(); return new ClassConstantRule( $reflectionProvider, - new RuleLevelHelper($reflectionProvider, true, false, true, false, false, false, true), + new RuleLevelHelper($reflectionProvider, true, false, true, true, true, false, true), new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), @@ -34,6 +34,7 @@ protected function getRule(): Rule self::getContainer(), ), new PhpVersion($this->phpVersion), + true, ); } @@ -59,6 +60,10 @@ public function testClassConstant(): void 'Access to undefined constant ClassConstantNamespace\Foo::DOLOR.', 10, ], + [ + 'Cannot access constant LOREM on mixed.', + 11, + ], [ 'Access to undefined constant ClassConstantNamespace\Foo::DOLOR.', 16, @@ -439,6 +444,14 @@ public function testDynamicAccess(): void $this->phpVersion = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/dynamic-constant-access.php'], [ + [ + 'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.', + 17, + ], + [ + 'Cannot fetch class constant with a non-stringable type object.', + 19, + ], [ 'Access to undefined constant ClassConstantDynamicAccess\Foo::FOO.', 20, @@ -474,4 +487,41 @@ public function testDynamicAccess(): void ]); } + #[RequiresPhp('>= 8.3')] + public function testStringableDynamicAccess(): void + { + $this->phpVersion = PHP_VERSION_ID; + + $this->analyse([__DIR__ . '/data/dynamic-constant-stringable-access.php'], [ + [ + 'Cannot fetch class constant with a non-stringable type mixed.', + 13, + ], + [ + 'Cannot fetch class constant with a non-stringable type string|null.', + 14, + ], + [ + 'Cannot fetch class constant with a non-stringable type Stringable|null.', + 15, + ], + [ + 'Cannot fetch class constant with a non-stringable type int.', + 16, + ], + [ + 'Cannot fetch class constant with a non-stringable type int|null.', + 17, + ], + [ + 'Cannot fetch class constant with a non-stringable type DateTime|string.', + 18, + ], + [ + 'Cannot fetch class constant with a non-stringable type 1111.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php b/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php index 10809e566a..09c0b176fe 100644 --- a/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php +++ b/tests/PHPStan/Rules/Classes/data/dynamic-constant-access.php @@ -14,7 +14,7 @@ public function test(string $string, object $obj): void { $bar = 'FOO'; - echo self::{$foo}; + echo self::{$bar}; echo self::{$string}; echo self::{$obj}; echo self::{$this->name}; @@ -44,5 +44,4 @@ public function testScope(): void echo self::{$name}; } - } diff --git a/tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php b/tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php new file mode 100644 index 0000000000..598874a5d7 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php @@ -0,0 +1,24 @@ += 8.3 + +namespace ClassConstantDynamicStringableAccess; + +use Stringable; +use DateTime; + +final class Foo +{ + + public function test(mixed $mixed, ?string $nullableStr, ?Stringable $nullableStringable, int $int, ?int $nullableInt, DateTime|string $datetimeOrStr, Stringable $stringable): void + { + echo self::{$mixed}; + echo self::{$nullableStr}; + echo self::{$nullableStringable}; + echo self::{$int}; + echo self::{$nullableInt}; + echo self::{$datetimeOrStr}; + echo self::{1111}; + echo self::{(string)$stringable}; + echo self::{$stringable}; // Uncast Stringable objects will cause a runtime error + } + +} From 4e59d4f946b45edb859f70c2d0409911f1870722 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Fri, 11 Jul 2025 22:20:54 +0900 Subject: [PATCH 2/6] Add class name to message --- src/Rules/Classes/ClassConstantRule.php | 7 +++- .../Rules/Classes/ClassConstantRuleTest.php | 42 ++++++++++++++----- .../dynamic-constant-stringable-access.php | 20 ++++++++- 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index fbc8ecb145..a10c7bb810 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\ClassConstFetch; +use PhpParser\Node\Name; use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; @@ -76,7 +77,11 @@ public function processNode(Node $node, Scope $scope): array $type = $typeResult->getType(); if (!$type->isString()->yes()) { - $errors[] = RuleErrorBuilder::message(sprintf('Cannot fetch class constant with a non-stringable type %s.', $nameType->describe(VerbosityLevel::precise()))) + $className = $node->class instanceof Name + ? $scope->resolveName($node->class) + : $scope->getType($node->class)->describe(VerbosityLevel::typeOnly()); + + $errors[] = RuleErrorBuilder::message(sprintf('Cannot fetch constant from %s with a non-stringable type %s.', $className, $nameType->describe(VerbosityLevel::precise()))) ->identifier('classConstant.fetchInvalidExpression') ->build(); } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index aa304be38d..a5b721f31e 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -449,7 +449,7 @@ public function testDynamicAccess(): void 17, ], [ - 'Cannot fetch class constant with a non-stringable type object.', + 'Cannot fetch constant from ClassConstantDynamicAccess\Foo with a non-stringable type object.', 19, ], [ @@ -494,33 +494,53 @@ public function testStringableDynamicAccess(): void $this->analyse([__DIR__ . '/data/dynamic-constant-stringable-access.php'], [ [ - 'Cannot fetch class constant with a non-stringable type mixed.', - 13, - ], - [ - 'Cannot fetch class constant with a non-stringable type string|null.', + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type mixed.', 14, ], [ - 'Cannot fetch class constant with a non-stringable type Stringable|null.', + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type string|null.', 15, ], [ - 'Cannot fetch class constant with a non-stringable type int.', + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type Stringable|null.', 16, ], [ - 'Cannot fetch class constant with a non-stringable type int|null.', + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type int.', 17, ], [ - 'Cannot fetch class constant with a non-stringable type DateTime|string.', + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type int|null.', 18, ], [ - 'Cannot fetch class constant with a non-stringable type 1111.', + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type DateTime|string.', 19, ], + [ + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type 1111.', + 20, + ], + [ + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type Stringable.', + 22, + ], + [ + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type mixed.', + 32, + ], + [ + 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Bar with a non-stringable type mixed.', + 33, + ], + [ + 'Cannot fetch constant from DateTime|DateTimeImmutable with a non-stringable type mixed.', + 38, + ], + [ + 'Cannot fetch constant from object with a non-stringable type mixed.', + 39, + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php b/tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php index 598874a5d7..e944e19947 100644 --- a/tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php +++ b/tests/PHPStan/Rules/Classes/data/dynamic-constant-stringable-access.php @@ -4,8 +4,9 @@ use Stringable; use DateTime; +use DateTimeImmutable; -final class Foo +abstract class Foo { public function test(mixed $mixed, ?string $nullableStr, ?Stringable $nullableStringable, int $int, ?int $nullableInt, DateTime|string $datetimeOrStr, Stringable $stringable): void @@ -22,3 +23,20 @@ public function test(mixed $mixed, ?string $nullableStr, ?Stringable $nullableSt } } + +final class Bar extends Foo +{ + + public function test(mixed $mixed, ?string $nullableStr, ?Stringable $nullableStringable, int $int, ?int $nullableInt, DateTime|string $datetimeOrStr, Stringable $stringable): void + { + echo parent::{$mixed}; + echo self::{$mixed}; + } + + public function testClassDynamic(DateTime|DateTimeImmutable $datetime, object $obj, mixed $mixed): void + { + echo $datetime::{$mixed}; + echo $obj::{$mixed}; + } + +} From f9d4a829ad269a06854c68d4a57122a4e90bb340 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sun, 13 Jul 2025 04:29:40 +0900 Subject: [PATCH 3/6] fixup! Add class name to message --- src/Rules/Classes/ClassConstantRule.php | 4 +-- .../Rules/Classes/ClassConstantRuleTest.php | 26 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index a10c7bb810..f25346479e 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -81,8 +81,8 @@ public function processNode(Node $node, Scope $scope): array ? $scope->resolveName($node->class) : $scope->getType($node->class)->describe(VerbosityLevel::typeOnly()); - $errors[] = RuleErrorBuilder::message(sprintf('Cannot fetch constant from %s with a non-stringable type %s.', $className, $nameType->describe(VerbosityLevel::precise()))) - ->identifier('classConstant.fetchInvalidExpression') + $errors[] = RuleErrorBuilder::message(sprintf('Class constant name for %s must be a string, but %s was given.', $className, $nameType->describe(VerbosityLevel::precise()))) + ->identifier('classConstant.nameNotString') ->build(); } } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index a5b721f31e..9d24119731 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -449,7 +449,7 @@ public function testDynamicAccess(): void 17, ], [ - 'Cannot fetch constant from ClassConstantDynamicAccess\Foo with a non-stringable type object.', + 'Class constant name for ClassConstantDynamicAccess\Foo must be a string, but object was given.', 19, ], [ @@ -494,51 +494,51 @@ public function testStringableDynamicAccess(): void $this->analyse([__DIR__ . '/data/dynamic-constant-stringable-access.php'], [ [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type mixed.', + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but mixed was given.', 14, ], [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type string|null.', + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but string|null was given.', 15, ], [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type Stringable|null.', + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but Stringable|null was given.', 16, ], [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type int.', + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but int was given.', 17, ], [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type int|null.', + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but int|null was given.', 18, ], [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type DateTime|string.', + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but DateTime|string was given.', 19, ], [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type 1111.', + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but 1111 was given.', 20, ], [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type Stringable.', + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but Stringable was given.', 22, ], [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Foo with a non-stringable type mixed.', + 'Class constant name for ClassConstantDynamicStringableAccess\Foo must be a string, but mixed was given.', 32, ], [ - 'Cannot fetch constant from ClassConstantDynamicStringableAccess\Bar with a non-stringable type mixed.', + 'Class constant name for ClassConstantDynamicStringableAccess\Bar must be a string, but mixed was given.', 33, ], [ - 'Cannot fetch constant from DateTime|DateTimeImmutable with a non-stringable type mixed.', + 'Class constant name for DateTime|DateTimeImmutable must be a string, but mixed was given.', 38, ], [ - 'Cannot fetch constant from object with a non-stringable type mixed.', + 'Class constant name for object must be a string, but mixed was given.', 39, ], ]); From 808ed2edd9ae88932683f488abe032c93fd3e1f5 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sun, 13 Jul 2025 04:15:36 +0900 Subject: [PATCH 4/6] fixup! Add stringable access check to ClassConstantRule --- src/Rules/Classes/ClassConstantRule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index f25346479e..f055daf851 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -67,14 +67,14 @@ public function processNode(Node $node, Scope $scope): array } if ($this->checkNonStringableDynamicAccess) { - $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $nameTypeResult = $this->ruleLevelHelper->findTypeToCheck( $scope, $node->name, '', static fn (Type $type) => $type->isString()->yes(), ); - $type = $typeResult->getType(); + $type = $nameTypeResult->getType(); if (!$type->isString()->yes()) { $className = $node->class instanceof Name From b0539eb35437bb69f72abdb11716768f2a564a73 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sun, 13 Jul 2025 04:11:38 +0900 Subject: [PATCH 5/6] fixup! Add stringable access check to ClassConstantRule --- conf/config.level0.neon | 3 --- src/Rules/Classes/ClassConstantRule.php | 4 +++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 864df73d39..805ea348a9 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -16,9 +16,6 @@ conditionalTags: phpstan.rules.rule: %featureToggles.newStaticInAbstractClassStaticMethod% services: - - - class: PHPStan\Rules\Classes\ClassConstantRule - - class: PHPStan\Rules\Classes\NewStaticInAbstractClassStaticMethodRule diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index f055daf851..d3d52d6dbf 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -9,6 +9,7 @@ use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\NullsafeOperatorHelper; use PHPStan\Analyser\Scope; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Php\PhpVersion; @@ -43,7 +44,8 @@ public function __construct( private RuleLevelHelper $ruleLevelHelper, private ClassNameCheck $classCheck, private PhpVersion $phpVersion, - private bool $checkNonStringableDynamicAccess = true, + #[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')] + private bool $checkNonStringableDynamicAccess, ) { } From 399e22d22777ab9062f693cd731380f39e6423eb Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sun, 13 Jul 2025 16:10:42 +0900 Subject: [PATCH 6/6] fixup! fixup! Add stringable access check to ClassConstantRule --- src/Rules/Classes/ClassConstantRule.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index d3d52d6dbf..a8220d5e11 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -76,9 +76,8 @@ public function processNode(Node $node, Scope $scope): array static fn (Type $type) => $type->isString()->yes(), ); - $type = $nameTypeResult->getType(); - - if (!$type->isString()->yes()) { + $nameType = $nameTypeResult->getType(); + if (!$nameType instanceof ErrorType && !$nameType->isString()->yes()) { $className = $node->class instanceof Name ? $scope->resolveName($node->class) : $scope->getType($node->class)->describe(VerbosityLevel::typeOnly());