diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 67f8d4ea1aa..f1adf9a1962 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -379,11 +379,15 @@ private function processSchema(array $statements, bool $required = true): Schema $parameterSchema = null; foreach ($statements as $statement) { $processedArguments = array_map(fn ($argument) => $this->processArgument($argument), $statement->arguments); + $entity = $statement->getEntity(); + if (!is_string($entity)) { + throw new ShouldNotHappenException(); + } if ($parameterSchema === null) { /** @var Type|AnyOf|Structure $parameterSchema */ - $parameterSchema = Expect::{$statement->getEntity()}(...$processedArguments); + $parameterSchema = Expect::{$entity}(...$processedArguments); } else { - $parameterSchema->{$statement->getEntity()}(...$processedArguments); + $parameterSchema->{$entity}(...$processedArguments); } } diff --git a/src/Rules/Classes/ClassConstantRule.php b/src/Rules/Classes/ClassConstantRule.php index a8220d5e11c..2dc357887ab 100644 --- a/src/Rules/Classes/ClassConstantRule.php +++ b/src/Rules/Classes/ClassConstantRule.php @@ -5,11 +5,9 @@ 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; -use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Internal\SprintfHelper; use PHPStan\Php\PhpVersion; @@ -18,6 +16,7 @@ use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; @@ -44,8 +43,7 @@ public function __construct( private RuleLevelHelper $ruleLevelHelper, private ClassNameCheck $classCheck, private PhpVersion $phpVersion, - #[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')] - private bool $checkNonStringableDynamicAccess, + private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck, ) { } @@ -68,25 +66,13 @@ public function processNode(Node $node, Scope $scope): array $constantNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); } - if ($this->checkNonStringableDynamicAccess) { - $nameTypeResult = $this->ruleLevelHelper->findTypeToCheck( - $scope, - $node->name, - '', - static fn (Type $type) => $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()); - - $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(); - } - } + $errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringName( + $scope, + $node->name, + 'Class constant name for %s must be a string, but %s was given.', + $node->class, + 'classConstant.nameNotString', + )); } foreach ($constantNameScopes as $constantName => $constantScope) { diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 51881bfbea2..61c7cd620c8 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -14,6 +14,7 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\Rule; use function array_merge; @@ -27,6 +28,7 @@ final class CallMethodsRule implements Rule public function __construct( private MethodCallCheck $methodCallCheck, private FunctionCallParametersCheck $parametersCheck, + private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck, ) { } @@ -48,6 +50,14 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $name = $constantString->getValue(); $methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); } + + $errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringName( + $scope, + $node->name, + 'Method name for %s must be a string, but %s was given.', + $node->var, + 'method.nameNotString', + )); } foreach ($methodNameScopes as $methodName => $methodScope) { diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index a275d3fb731..53d797361e5 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -14,6 +14,7 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\Rule; use function array_merge; use function sprintf; @@ -28,6 +29,7 @@ final class CallStaticMethodsRule implements Rule public function __construct( private StaticMethodCallCheck $methodCallCheck, private FunctionCallParametersCheck $parametersCheck, + private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck, ) { } @@ -49,6 +51,14 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $name = $constantString->getValue(); $methodNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); } + + $errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringName( + $scope, + $node->name, + 'Method name for %s must be a string, but %s was given.', + $node->class, + 'staticMethod.nameNotString', + )); } foreach ($methodNameScopes as $methodName => $methodScope) { diff --git a/src/Rules/NonStringableDynamicAccessCheck.php b/src/Rules/NonStringableDynamicAccessCheck.php new file mode 100644 index 00000000000..a7105bf25bf --- /dev/null +++ b/src/Rules/NonStringableDynamicAccessCheck.php @@ -0,0 +1,111 @@ +{$name}`, `$obj->{$name}()`, `Foo::{$name}()`, + * `Foo::${$name}`, `Foo::{$name}`) can actually be used as a name at runtime. + * + * Gated behind the `checkNonStringableDynamicAccess` feature toggle. + */ +#[AutowiredService] +final class NonStringableDynamicAccessCheck +{ + + public function __construct( + private RuleLevelHelper $ruleLevelHelper, + #[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')] + private bool $checkNonStringableDynamicAccess, + ) + { + } + + /** + * For names that PHP casts to string at runtime (variable variables, + * property and static property names) objects implementing __toString are + * accepted. + * + * @param Name|Expr|null $classNode the node whose name/type fills the leading `%s` placeholder, or null when the message has none + * @return list + */ + public function checkStringCastableName(Scope $scope, Expr $name, string $messageFormat, $classNode, string $identifier): array + { + if (!$this->checkNonStringableDynamicAccess) { + return []; + } + + $nameType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $name, + '', + static fn (Type $type) => !$type->toString() instanceof ErrorType && $type->toString()->isString()->yes(), + )->getType(); + + if ( + !$nameType instanceof ErrorType + && ($nameType->toString() instanceof ErrorType || !$nameType->toString()->isString()->yes()) + ) { + return [$this->buildError($scope, $name, $scope->getType($name), $messageFormat, $classNode, $identifier)]; + } + + return []; + } + + /** + * For names that must be actual strings (method, static method and class + * constant names) objects implementing __toString are not accepted. + * + * @param Name|Expr|null $classNode the node whose name/type fills the leading `%s` placeholder, or null when the message has none + * @return list + */ + public function checkStringName(Scope $scope, Expr $name, string $messageFormat, $classNode, string $identifier): array + { + if (!$this->checkNonStringableDynamicAccess) { + return []; + } + + $nameType = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $name, + '', + static fn (Type $type) => $type->isString()->yes(), + )->getType(); + + if (!$nameType instanceof ErrorType && !$nameType->isString()->yes()) { + return [$this->buildError($scope, $name, $nameType, $messageFormat, $classNode, $identifier)]; + } + + return []; + } + + /** + * @param Name|Expr|null $classNode + */ + private function buildError(Scope $scope, Expr $name, Type $nameType, string $messageFormat, $classNode, string $identifier): IdentifierRuleError + { + $messageArgs = []; + if ($classNode !== null) { + $messageArgs[] = $classNode instanceof Name + ? $scope->resolveName($classNode) + : $scope->getType($classNode)->describe(VerbosityLevel::typeOnly()); + } + $messageArgs[] = $nameType->describe(VerbosityLevel::precise()); + + return RuleErrorBuilder::message(sprintf($messageFormat, ...$messageArgs)) + ->line($name->getStartLine()) + ->identifier($identifier) + ->build(); + } + +} diff --git a/src/Rules/Properties/AccessPropertiesCheck.php b/src/Rules/Properties/AccessPropertiesCheck.php index ce6717addfc..c6bd1e3e5fa 100644 --- a/src/Rules/Properties/AccessPropertiesCheck.php +++ b/src/Rules/Properties/AccessPropertiesCheck.php @@ -18,6 +18,7 @@ use PHPStan\Reflection\MissingPropertyFromReflectionException; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\ConstantStringType; @@ -39,12 +40,11 @@ public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, private PhpVersion $phpVersion, + private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck, #[AutowiredParameter] private bool $reportMagicProperties, #[AutowiredParameter] private bool $checkDynamicProperties, - #[AutowiredParameter(ref: '%featureToggles.checkNonStringableDynamicAccess%')] - private bool $checkNonStringableDynamicAccess, ) { } @@ -60,28 +60,14 @@ public function check(PropertyFetch $node, Scope $scope, bool $write): array } else { $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); - if (!$write && $this->checkNonStringableDynamicAccess) { - $nameTypeResult = $this->ruleLevelHelper->findTypeToCheck( + if (!$write) { + $errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringCastableName( $scope, $node->name, - '', - static fn (Type $type) => !$type->toString() instanceof ErrorType && $type->toString()->isString()->yes(), - ); - $nameType = $nameTypeResult->getType(); - if ( - !$nameType instanceof ErrorType - && ( - $nameType->toString() instanceof ErrorType - || !$nameType->toString()->isString()->yes() - ) - ) { - $originalNameType = $scope->getType($node->name); - $className = $scope->getType($node->var)->describe(VerbosityLevel::typeOnly()); - $errors[] = RuleErrorBuilder::message(sprintf('Property name for %s must be a string, but %s was given.', $className, $originalNameType->describe(VerbosityLevel::precise()))) - ->line($node->name->getStartLine()) - ->identifier('property.nameNotString') - ->build(); - } + 'Property name for %s must be a string, but %s was given.', + $node->var, + 'property.nameNotString', + )); } } diff --git a/src/Rules/Properties/AccessStaticPropertiesCheck.php b/src/Rules/Properties/AccessStaticPropertiesCheck.php index c548a2f342c..92742621326 100644 --- a/src/Rules/Properties/AccessStaticPropertiesCheck.php +++ b/src/Rules/Properties/AccessStaticPropertiesCheck.php @@ -22,6 +22,7 @@ use PHPStan\Rules\ClassNameNodePair; use PHPStan\Rules\ClassNameUsageLocation; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Type\Constant\ConstantStringType; @@ -48,6 +49,7 @@ public function __construct( private RuleLevelHelper $ruleLevelHelper, private ClassNameCheck $classCheck, private PhpVersion $phpVersion, + private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck, #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] private bool $discoveringSymbolsTip, ) @@ -59,13 +61,23 @@ public function __construct( */ public function check(StaticPropertyFetch $node, Scope $scope, bool $write): array { + $errors = []; if ($node->name instanceof Node\VarLikeIdentifier) { $names = [$node->name->name]; } else { $names = array_map(static fn (ConstantStringType $type): string => $type->getValue(), $scope->getType($node->name)->getConstantStrings()); + + if (!$write) { + $errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringCastableName( + $scope, + $node->name, + 'Static property name for %s must be a string, but %s was given.', + $node->class, + 'staticProperty.nameNotString', + )); + } } - $errors = []; foreach ($names as $name) { $errors = array_merge($errors, $this->processSingleProperty($scope, $node, $name, $write)); } diff --git a/src/Rules/Variables/DefinedVariableRule.php b/src/Rules/Variables/DefinedVariableRule.php index 8fbb1e5d0b0..674e0801eab 100644 --- a/src/Rules/Variables/DefinedVariableRule.php +++ b/src/Rules/Variables/DefinedVariableRule.php @@ -10,6 +10,7 @@ use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Rules\IdentifierRuleError; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function array_merge; @@ -25,6 +26,7 @@ final class DefinedVariableRule implements Rule { public function __construct( + private NonStringableDynamicAccessCheck $nonStringableDynamicAccessCheck, #[AutowiredParameter] private bool $cliArgumentsVariablesRegistered, #[AutowiredParameter] @@ -50,6 +52,14 @@ public function processNode(Node $node, Scope $scope): array $name = $constantString->getValue(); $variableNameScopes[$name] = $scope->filterByTruthyValue(new Identical($node->name, new String_($name))); } + + $errors = array_merge($errors, $this->nonStringableDynamicAccessCheck->checkStringCastableName( + $scope, + $node->name, + 'Variable variable name must be a string, but %s was given.', + null, + 'variable.nameNotString', + )); } foreach ($variableNameScopes as $name => $variableScope) { diff --git a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php index 3cf21124155..9136f68c8c5 100644 --- a/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php +++ b/tests/PHPStan/Analyser/Bug9307CallMethodsRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Rules\FunctionCallParametersCheck; use PHPStan\Rules\Methods\CallMethodsRule; use PHPStan\Rules\Methods\MethodCallCheck; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\PropertyReflectionFinder; @@ -49,6 +50,7 @@ protected function getRule(): Rule checkExtraArguments: true, checkMissingTypehints: true, ), + new NonStringableDynamicAccessCheck($ruleLevelHelper, true), ); } diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index 7e2e6d8b86e..567b0c6b293 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -25,18 +26,19 @@ protected function getRule(): Rule { $reflectionProvider = self::createReflectionProvider(); $container = self::getContainer(); + $ruleLevelHelper = new RuleLevelHelper( + $reflectionProvider, + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: true, + checkImplicitMixed: true, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ); return new ClassConstantRule( $reflectionProvider, - new RuleLevelHelper( - $reflectionProvider, - checkNullables: true, - checkThisOnly: false, - checkUnionTypes: true, - checkExplicitMixed: true, - checkImplicitMixed: true, - checkBenevolentUnionTypes: false, - discoveringSymbolsTip: true, - ), + $ruleLevelHelper, new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, checkInternalClassCaseSensitivity: true), new ClassForbiddenNameCheck($container), @@ -44,7 +46,7 @@ protected function getRule(): Rule $container, ), new PhpVersion($this->phpVersion), - checkNonStringableDynamicAccess: true, + new NonStringableDynamicAccessCheck($ruleLevelHelper, true), ); } diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index a6e55ecbd30..6bbf57f5727 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Methods; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\PropertyReflectionFinder; @@ -60,6 +61,7 @@ protected function getRule(): Rule checkExtraArguments: true, checkMissingTypehints: true, ), + new NonStringableDynamicAccessCheck($ruleLevelHelper, true), ); } @@ -3614,6 +3616,10 @@ public function testDynamicCall(): void 'Call to an undefined method MethodsDynamicCall\Foo::bar().', 23, ], + [ + 'Method name for $this(MethodsDynamicCall\Foo) must be a string, but object was given.', + 25, + ], [ 'Call to an undefined method MethodsDynamicCall\Foo::doBar().', 26, @@ -3637,6 +3643,32 @@ public function testDynamicCall(): void ]); } + public function testDynamicMethodName(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/dynamic-method-name.php'], [ + [ + 'Method name for $this(DynamicMethodName\Foo) must be a string, but $this(DynamicMethodName\Foo) was given.', + 16, + ], + [ + 'Method name for $this(DynamicMethodName\Foo) must be a string, but object was given.', + 17, + ], + [ + 'Method name for $this(DynamicMethodName\Foo) must be a string, but Stringable was given.', + 18, + ], + [ + 'Method name for $this(DynamicMethodName\Foo) must be a string, but int was given.', + 19, + ], + ]); + } + public function testBug12884(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index be9dde7dd3e..cf2dc3a6807 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\PropertyReflectionFinder; @@ -71,6 +72,7 @@ protected function getRule(): Rule checkExtraArguments: true, checkMissingTypehints: true, ), + new NonStringableDynamicAccessCheck($ruleLevelHelper, true), ); } @@ -878,6 +880,10 @@ public function testDynamicCall(): void 'Call to an undefined static method MethodsDynamicCall\Foo::bar().', 33, ], + [ + 'Method name for MethodsDynamicCall\Foo must be a string, but object was given.', + 35, + ], [ 'Call to an undefined static method MethodsDynamicCall\Foo::doBar().', 36, @@ -901,6 +907,27 @@ public function testDynamicCall(): void ]); } + public function testDynamicStaticMethodName(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/dynamic-static-method-name.php'], [ + [ + 'Method name for DynamicStaticMethodName\Foo must be a string, but object was given.', + 16, + ], + [ + 'Method name for DynamicStaticMethodName\Foo must be a string, but Stringable was given.', + 17, + ], + [ + 'Method name for DynamicStaticMethodName\Foo must be a string, but int was given.', + 18, + ], + ]); + } + public function testBug13267(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php index 5cad631977b..12e280c2a7b 100644 --- a/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodCallWithPossiblyRenamedNamedArgumentRuleTest.php @@ -5,6 +5,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Reflection\Php\PhpClassReflectionExtension; use PHPStan\Rules\FunctionCallParametersCheck; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\NullsafeCheck; use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper; use PHPStan\Rules\Properties\PropertyReflectionFinder; @@ -33,6 +34,7 @@ protected function getRule(): Rule new CallMethodsRule( new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true), new FunctionCallParametersCheck($ruleLevelHelper, new NullsafeCheck(), new UnresolvableTypeHelper(), new PropertyReflectionFinder(), $reflectionProvider, true, true, true, true), + new NonStringableDynamicAccessCheck($ruleLevelHelper, true), ), new OverridingMethodRule( $phpVersion, diff --git a/tests/PHPStan/Rules/Methods/data/dynamic-method-name.php b/tests/PHPStan/Rules/Methods/data/dynamic-method-name.php new file mode 100644 index 00000000000..e8e2795e66b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/dynamic-method-name.php @@ -0,0 +1,24 @@ +{$this}(); // error - $this is not a string + $this->$object(); // error - object is not a string + $this->$stringable(); // error - method names cannot be Stringable + $this->$int(); // error - int is not a string + + $this->$name(); // valid + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/dynamic-static-method-name.php b/tests/PHPStan/Rules/Methods/data/dynamic-static-method-name.php new file mode 100644 index 00000000000..8e8019c456c --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/dynamic-static-method-name.php @@ -0,0 +1,23 @@ +checkThisOnly, + checkUnionTypes: $this->checkUnionTypes, + checkExplicitMixed: false, + checkImplicitMixed: false, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ); return new AccessPropertiesRule( new AccessPropertiesCheck( $reflectionProvider, - new RuleLevelHelper( - $reflectionProvider, - checkNullables: true, - checkThisOnly: $this->checkThisOnly, - checkUnionTypes: $this->checkUnionTypes, - checkExplicitMixed: false, - checkImplicitMixed: false, - checkBenevolentUnionTypes: false, - discoveringSymbolsTip: true, - ), + $ruleLevelHelper, new PhpVersion(PHP_VERSION_ID), + new NonStringableDynamicAccessCheck($ruleLevelHelper, true), reportMagicProperties: true, checkDynamicProperties: $this->checkDynamicProperties, - checkNonStringableDynamicAccess: true, ), ); } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php index f8ddba0d610..41e63ddac37 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesInAssignRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -21,19 +22,20 @@ class AccessStaticPropertiesInAssignRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper( + $reflectionProvider, + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: false, + checkImplicitMixed: false, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ); return new AccessStaticPropertiesInAssignRule( new AccessStaticPropertiesCheck( $reflectionProvider, - new RuleLevelHelper( - $reflectionProvider, - checkNullables: true, - checkThisOnly: false, - checkUnionTypes: true, - checkExplicitMixed: false, - checkImplicitMixed: false, - checkBenevolentUnionTypes: false, - discoveringSymbolsTip: true, - ), + $ruleLevelHelper, new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), @@ -41,6 +43,7 @@ protected function getRule(): Rule self::getContainer(), ), new PhpVersion(PHP_VERSION_ID), + new NonStringableDynamicAccessCheck($ruleLevelHelper, true), discoveringSymbolsTip: true, ), ); diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index a6e80a4dff5..4f6f0a6070e 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Rules\ClassCaseSensitivityCheck; use PHPStan\Rules\ClassForbiddenNameCheck; use PHPStan\Rules\ClassNameCheck; +use PHPStan\Rules\NonStringableDynamicAccessCheck; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleLevelHelper; use PHPStan\Testing\RuleTestCase; @@ -20,19 +21,20 @@ class AccessStaticPropertiesRuleTest extends RuleTestCase protected function getRule(): Rule { $reflectionProvider = self::createReflectionProvider(); + $ruleLevelHelper = new RuleLevelHelper( + $reflectionProvider, + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: false, + checkImplicitMixed: false, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ); return new AccessStaticPropertiesRule( new AccessStaticPropertiesCheck( $reflectionProvider, - new RuleLevelHelper( - $reflectionProvider, - checkNullables: true, - checkThisOnly: false, - checkUnionTypes: true, - checkExplicitMixed: false, - checkImplicitMixed: false, - checkBenevolentUnionTypes: false, - discoveringSymbolsTip: true, - ), + $ruleLevelHelper, new ClassNameCheck( new ClassCaseSensitivityCheck($reflectionProvider, true), new ClassForbiddenNameCheck(self::getContainer()), @@ -40,6 +42,7 @@ protected function getRule(): Rule self::getContainer(), ), new PhpVersion(PHP_VERSION_ID), + new NonStringableDynamicAccessCheck($ruleLevelHelper, true), discoveringSymbolsTip: true, ), ); @@ -297,6 +300,32 @@ public function testClassExists(): void $this->analyse([__DIR__ . '/data/static-properties-class-exists.php'], []); } + public function testDynamicStaticPropertyName(): void + { + $this->analyse([__DIR__ . '/data/dynamic-static-property-name.php'], [ + [ + 'Static property name for DynamicStaticPropertyName\Foo must be a string, but object was given.', + 19, + ], + [ + 'Static property name for DynamicStaticPropertyName\Foo must be a string, but array was given.', + 20, + ], + [ + 'Static property name for DynamicStaticPropertyName\Foo must be a string, but object was given.', + 26, + ], + [ + 'Static property name for DynamicStaticPropertyName\Foo must be a string, but object|string was given.', + 31, + ], + [ + 'Static property name for DynamicStaticPropertyName\Foo must be a string, but int|object was given.', + 32, + ], + ]); + } + public function testBug5143(): void { $this->analyse([__DIR__ . '/data/bug-5143.php'], []); diff --git a/tests/PHPStan/Rules/Properties/data/dynamic-static-property-name.php b/tests/PHPStan/Rules/Properties/data/dynamic-static-property-name.php new file mode 100644 index 00000000000..28c98b03c7a --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/dynamic-static-property-name.php @@ -0,0 +1,35 @@ +checkNonStringableDynamicAccess, + ), $this->cliArgumentsVariablesRegistered, $this->checkMaybeUndefinedVariables, ); @@ -1674,4 +1691,31 @@ public function testBug10090(): void ]); } + public function testVariableVariableName(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + $this->checkNonStringableDynamicAccess = true; + $this->analyse([__DIR__ . '/data/variable-variable-name.php'], [ + [ + 'Variable variable name must be a string, but $this(VariableVariableName\Greeter) was given.', + 12, + ], + [ + 'Variable variable name must be a string, but array was given.', + 25, + ], + [ + 'Variable variable name must be a string, but object was given.', + 26, + ], + [ + 'Variable variable name must be a string, but $this(VariableVariableName\Greeter) was given.', + 27, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/variable-variable-name.php b/tests/PHPStan/Rules/Variables/data/variable-variable-name.php new file mode 100644 index 00000000000..adc21ce5104 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/variable-variable-name.php @@ -0,0 +1,30 @@ +