From 9def6e97eaebdac205308deb7530fd83fbeb1ac2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 14 Jun 2026 10:14:13 +0200 Subject: [PATCH 1/3] Rule --- src/Rules/Classes/InstantiationRule.php | 46 +++++++++++++++++++ .../ForbiddenNameCheckExtensionRuleTest.php | 10 ++++ .../Rules/Classes/InstantiationRuleTest.php | 40 ++++++++++++++++ .../Classes/data/instantiation-non-object.php | 45 ++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 tests/PHPStan/Rules/Classes/data/instantiation-non-object.php diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 296bb78954a..fe75392f448 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,6 +55,7 @@ public function __construct( private ReflectionProvider $reflectionProvider, private FunctionCallParametersCheck $check, private ClassNameCheck $classCheck, + private RuleLevelHelper $ruleLevelHelper, private ConsistentConstructorHelper $consistentConstructorHelper, #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] private bool $discoveringSymbolsTip, @@ -62,6 +70,13 @@ public function getNodeType(): string public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { + if ($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 +84,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 7e572caa9d9..1487c828226 100644 --- a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -53,6 +53,16 @@ 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(), discoveringSymbolsTip: true, ); diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index a3538302b56..1f88065aa42 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -55,6 +55,16 @@ 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(), discoveringSymbolsTip: true, ); @@ -682,4 +692,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.', + 31, + ], + [ + 'Cannot instantiate class using int.', + 35, + ], + [ + 'Cannot instantiate class using float.', + 36, + ], + [ + 'Cannot instantiate class using bool.', + 37, + ], + [ + 'Cannot instantiate class using int|string.', + 38, + ], + [ + 'Cannot instantiate class using array.', + 44, + ], + ]); + } + } 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 00000000000..5f6df9597c0 --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/instantiation-non-object.php @@ -0,0 +1,45 @@ + $classStringOfFoo + */ +function doFoo( + string $string, + object $object, + int $int, + float $float, + bool $bool, + int|string $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; +} From 6a01b2b8cded585030afc9b3d34a4af0c5fe4cf7 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 14 Jun 2026 17:39:30 +0000 Subject: [PATCH 2/3] Use PHPDoc for union type in test data for PHP 7.4 compatibility Co-Authored-By: Claude Opus 4.8 --- .../PHPStan/Rules/Classes/InstantiationRuleTest.php | 12 ++++++------ .../Rules/Classes/data/instantiation-non-object.php | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 1f88065aa42..7b14f789f03 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -697,27 +697,27 @@ public function testInstantiationWithNonObjectType(): void $this->analyse([__DIR__ . '/data/instantiation-non-object.php'], [ [ 'Cannot instantiate class using int.', - 31, + 32, ], [ 'Cannot instantiate class using int.', - 35, + 36, ], [ 'Cannot instantiate class using float.', - 36, + 37, ], [ 'Cannot instantiate class using bool.', - 37, + 38, ], [ 'Cannot instantiate class using int|string.', - 38, + 39, ], [ 'Cannot instantiate class using array.', - 44, + 45, ], ]); } diff --git a/tests/PHPStan/Rules/Classes/data/instantiation-non-object.php b/tests/PHPStan/Rules/Classes/data/instantiation-non-object.php index 5f6df9597c0..db63e2ded3f 100644 --- a/tests/PHPStan/Rules/Classes/data/instantiation-non-object.php +++ b/tests/PHPStan/Rules/Classes/data/instantiation-non-object.php @@ -12,6 +12,7 @@ class Foo } /** + * @param int|string $intOrString * @param class-string $classString * @param class-string $classStringOfFoo */ @@ -21,7 +22,7 @@ function doFoo( int $int, float $float, bool $bool, - int|string $intOrString, + $intOrString, string $classString, string $classStringOfFoo, Foo $foo From d17ff9dc3af297779a3fb0428a268bf621b93d41 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 14 Jun 2026 18:08:11 +0000 Subject: [PATCH 3/3] Put new.nonObject check behind newOnNonObject bleeding edge toggle Co-Authored-By: Claude Opus 4.8 --- conf/bleedingEdge.neon | 1 + conf/config.neon | 1 + conf/parametersSchema.neon | 1 + src/Rules/Classes/InstantiationRule.php | 4 +++- .../Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php | 1 + tests/PHPStan/Rules/Classes/InstantiationRuleTest.php | 1 + 6 files changed, 8 insertions(+), 1 deletion(-) diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 528084e3532..b04d6df04e1 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 df8f46567a1..d7ee2e0df95 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 dc07b020e09..cb580e908a6 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 fe75392f448..3719adb7282 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -57,6 +57,8 @@ public function __construct( private ClassNameCheck $classCheck, private RuleLevelHelper $ruleLevelHelper, private ConsistentConstructorHelper $consistentConstructorHelper, + #[AutowiredParameter(ref: '%featureToggles.newOnNonObject%')] + private bool $newOnNonObject, #[AutowiredParameter(ref: '%tips.discoveringSymbols%')] private bool $discoveringSymbolsTip, ) @@ -70,7 +72,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array { - if ($node->class instanceof Node\Expr) { + if ($this->newOnNonObject && $node->class instanceof Node\Expr) { $errors = $this->checkClassNameExprType($node->class, $scope); if ($errors !== []) { return $errors; diff --git a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php index 1487c828226..2d0d57de8d6 100644 --- a/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php @@ -64,6 +64,7 @@ protected function getRule(): Rule 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 7b14f789f03..1a787a6267d 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -66,6 +66,7 @@ protected function getRule(): Rule discoveringSymbolsTip: true, ), new ConsistentConstructorHelper(), + newOnNonObject: true, discoveringSymbolsTip: true, ); }