diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 528084e353..b04d6df04e 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -20,3 +20,4 @@ parameters: reportMethodPurityOverride: true checkDynamicConstantNameValues: true unusedLabel: true + newOnNonObject: true diff --git a/conf/config.neon b/conf/config.neon index df8f46567a..d7ee2e0df9 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -47,6 +47,7 @@ parameters: reportMethodPurityOverride: false checkDynamicConstantNameValues: false unusedLabel: false + newOnNonObject: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index dc07b020e0..cb580e908a 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -49,6 +49,7 @@ parametersSchema: reportMethodPurityOverride: bool() checkDynamicConstantNameValues: bool() unusedLabel: bool() + newOnNonObject: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 296bb78954..3719adb728 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -24,8 +24,15 @@ use PHPStan\Rules\RestrictedUsage\RewrittenDeclaringClassMethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Rules\RuleLevelHelper; use PHPStan\ShouldNotHappenException; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\ErrorType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use PHPStan\Type\UnionType; +use PHPStan\Type\VerbosityLevel; use function array_filter; use function array_map; use function array_merge; @@ -48,7 +55,10 @@ public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check, private ClassNameCheck $classCheck, + private RuleLevelHelper $ruleLevelHelper, private ConsistentConstructorHelper $consistentConstructorHelper, + #[AutowiredParameter(ref: '%featureToggles.newOnNonObject%')] + private bool $newOnNonObject, #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] private bool $discoveringSymbolsTip, ) @@ -62,6 +72,13 @@ public function getNodeType(): string public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { + if ($this->newOnNonObject && $node->class instanceof Node\Expr) { + $errors = $this->checkClassNameExprType($node->class, $scope); + if ($errors !== []) { + return $errors; + } + } + $errors = []; foreach ($this->getClassNames($node, $scope) as [$class, $isName]) { $errors = array_merge($errors, $this->checkClassName($class, $isName, $node, $scope)); @@ -69,6 +86,37 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE return $errors; } + /** + * @return list + */ + private function checkClassNameExprType(Node\Expr $class, Scope $scope): array + { + $acceptedType = new UnionType([new StringType(), new ObjectWithoutClassType()]); + $typeResult = $this->ruleLevelHelper->findTypeToCheck( + $scope, + $class, + '', + static fn (Type $type): bool => $acceptedType->isSuperTypeOf($type)->yes(), + ); + + $foundType = $typeResult->getType(); + if ($foundType instanceof ErrorType) { + // Unknown classes and mixed are reported elsewhere (e.g. "Instantiated class X not found."). + return []; + } + + if ($acceptedType->isSuperTypeOf($foundType)->yes()) { + return []; + } + + return [ + RuleErrorBuilder::message(sprintf( + 'Cannot instantiate class using %s.', + $foundType->describe(VerbosityLevel::typeOnly()), + ))->identifier('new.nonObject')->build(), + ]; + } + /** * @param Node\Expr\New_ $node * @return list diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php index 7e572caa9d..2d0d57de8d 100644 --- a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -53,7 +53,18 @@ protected function getRule(): Rule $reflectionProvider, $container, ), + new RuleLevelHelper( + $reflectionProvider, + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: false, + checkImplicitMixed: false, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ), new ConsistentConstructorHelper(), + newOnNonObject: true, discoveringSymbolsTip: true, ); } diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index a3538302b5..1a787a6267 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -55,7 +55,18 @@ protected function getRule(): Rule $reflectionProvider, $container, ), + new RuleLevelHelper( + $reflectionProvider, + checkNullables: true, + checkThisOnly: false, + checkUnionTypes: true, + checkExplicitMixed: $this->checkExplicitMixed, + checkImplicitMixed: false, + checkBenevolentUnionTypes: false, + discoveringSymbolsTip: true, + ), new ConsistentConstructorHelper(), + newOnNonObject: true, discoveringSymbolsTip: true, ); } @@ -682,4 +693,34 @@ public function testBug14499(): void $this->analyse([__DIR__ . '/data/bug-14499.php'], []); } + public function testInstantiationWithNonObjectType(): void + { + $this->analyse([__DIR__ . '/data/instantiation-non-object.php'], [ + [ + 'Cannot instantiate class using int.', + 32, + ], + [ + 'Cannot instantiate class using int.', + 36, + ], + [ + 'Cannot instantiate class using float.', + 37, + ], + [ + 'Cannot instantiate class using bool.', + 38, + ], + [ + 'Cannot instantiate class using int|string.', + 39, + ], + [ + 'Cannot instantiate class using array.', + 45, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/instantiation-non-object.php b/tests/PHPStan/Rules/Classes/data/instantiation-non-object.php new file mode 100644 index 0000000000..db63e2ded3 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/instantiation-non-object.php @@ -0,0 +1,46 @@ + $classStringOfFoo + */ +function doFoo( + string $string, + object $object, + int $int, + float $float, + bool $bool, + $intOrString, + string $classString, + string $classStringOfFoo, + Foo $foo +): void +{ + $class = get_class_name(); + new $class; + + new $string; + new $object; + new $int; + new $float; + new $bool; + new $intOrString; + new $classString; + new $classStringOfFoo; + new $foo; + + $array = ['a']; + new $array; +}