Skip to content

Commit 3ac9db8

Browse files
phpstan-botVincentLangletclaude
authored
Report new on an expression whose type is not string|object via new.nonObject (#5866)
Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c53a7b8 commit 3ac9db8

7 files changed

Lines changed: 166 additions & 20 deletions

File tree

‎conf/bleedingEdge.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ parameters:
2121
reportMethodPurityOverride: true
2222
checkDynamicConstantNameValues: true
2323
unusedLabel: true
24+
newOnNonObject: true

‎conf/config.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ parameters:
4848
reportMethodPurityOverride: false
4949
checkDynamicConstantNameValues: false
5050
unusedLabel: false
51+
newOnNonObject: false
5152
fileExtensions:
5253
- php
5354
checkAdvancedIsset: false

‎conf/parametersSchema.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ parametersSchema:
5050
reportMethodPurityOverride: bool()
5151
checkDynamicConstantNameValues: bool()
5252
unusedLabel: bool()
53+
newOnNonObject: bool()
5354
])
5455
fileExtensions: listOf(string())
5556
checkAdvancedIsset: bool()

‎src/Rules/Classes/InstantiationRule.php‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,15 @@
2424
use PHPStan\Rules\RestrictedUsage\RewrittenDeclaringClassMethodReflection;
2525
use PHPStan\Rules\Rule;
2626
use PHPStan\Rules\RuleErrorBuilder;
27+
use PHPStan\Rules\RuleLevelHelper;
2728
use PHPStan\ShouldNotHappenException;
2829
use PHPStan\Type\Constant\ConstantStringType;
30+
use PHPStan\Type\ErrorType;
31+
use PHPStan\Type\ObjectWithoutClassType;
32+
use PHPStan\Type\StringType;
33+
use PHPStan\Type\Type;
34+
use PHPStan\Type\UnionType;
35+
use PHPStan\Type\VerbosityLevel;
2936
use function array_filter;
3037
use function array_map;
3138
use function array_merge;
@@ -48,7 +55,10 @@ public function __construct(
4855
private ReflectionProvider $reflectionProvider,
4956
private FunctionCallParametersCheck $check,
5057
private ClassNameCheck $classCheck,
58+
private RuleLevelHelper $ruleLevelHelper,
5159
private ConsistentConstructorHelper $consistentConstructorHelper,
60+
#[AutowiredParameter(ref: '%featureToggles.newOnNonObject%')]
61+
private bool $newOnNonObject,
5262
#[AutowiredParameter(ref: '%tips.discoveringSymbols%')]
5363
private bool $discoveringSymbolsTip,
5464
)
@@ -62,13 +72,51 @@ public function getNodeType(): string
6272

6373
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
6474
{
75+
if ($this->newOnNonObject && $node->class instanceof Node\Expr) {
76+
$errors = $this->checkClassNameExprType($node->class, $scope);
77+
if ($errors !== []) {
78+
return $errors;
79+
}
80+
}
81+
6582
$errors = [];
6683
foreach ($this->getClassNames($node, $scope) as [$class, $isName]) {
6784
$errors = array_merge($errors, $this->checkClassName($class, $isName, $node, $scope));
6885
}
6986
return $errors;
7087
}
7188

89+
/**
90+
* @return list<IdentifierRuleError>
91+
*/
92+
private function checkClassNameExprType(Node\Expr $class, Scope $scope): array
93+
{
94+
$acceptedType = new UnionType([new StringType(), new ObjectWithoutClassType()]);
95+
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
96+
$scope,
97+
$class,
98+
'',
99+
static fn (Type $type): bool => $acceptedType->isSuperTypeOf($type)->yes(),
100+
);
101+
102+
$foundType = $typeResult->getType();
103+
if ($foundType instanceof ErrorType) {
104+
// Unknown classes and mixed are reported elsewhere (e.g. "Instantiated class X not found.").
105+
return [];
106+
}
107+
108+
if ($acceptedType->isSuperTypeOf($foundType)->yes()) {
109+
return [];
110+
}
111+
112+
return [
113+
RuleErrorBuilder::message(sprintf(
114+
'Cannot instantiate class using %s.',
115+
$foundType->describe(VerbosityLevel::typeOnly()),
116+
))->identifier('new.nonObject')->build(),
117+
];
118+
}
119+
72120
/**
73121
* @param Node\Expr\New_ $node
74122
* @return list<IdentifierRuleError>

‎tests/PHPStan/Rules/Classes/ForbiddenNameCheckExtensionRuleTest.php‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,21 @@ protected function getRule(): Rule
2424
{
2525
$reflectionProvider = self::createReflectionProvider();
2626
$container = self::getContainer();
27+
$ruleLevelHelper = new RuleLevelHelper(
28+
$reflectionProvider,
29+
checkNullables: true,
30+
checkThisOnly: false,
31+
checkUnionTypes: true,
32+
checkExplicitMixed: false,
33+
checkImplicitMixed: false,
34+
checkBenevolentUnionTypes: false,
35+
discoveringSymbolsTip: true,
36+
);
2737
return new InstantiationRule(
2838
$container,
2939
$reflectionProvider,
3040
new FunctionCallParametersCheck(
31-
new RuleLevelHelper(
32-
$reflectionProvider,
33-
checkNullables: true,
34-
checkThisOnly: false,
35-
checkUnionTypes: true,
36-
checkExplicitMixed: false,
37-
checkImplicitMixed: false,
38-
checkBenevolentUnionTypes: false,
39-
discoveringSymbolsTip: true,
40-
),
41+
$ruleLevelHelper,
4142
new NullsafeCheck(),
4243
new UnresolvableTypeHelper(),
4344
new PropertyReflectionFinder(),
@@ -53,7 +54,9 @@ protected function getRule(): Rule
5354
$reflectionProvider,
5455
$container,
5556
),
57+
$ruleLevelHelper,
5658
new ConsistentConstructorHelper(),
59+
newOnNonObject: true,
5760
discoveringSymbolsTip: true,
5861
);
5962
}

‎tests/PHPStan/Rules/Classes/InstantiationRuleTest.php‎

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,21 @@ protected function getRule(): Rule
2626
{
2727
$reflectionProvider = self::createReflectionProvider();
2828
$container = self::getContainer();
29+
$ruleLevelHelper = new RuleLevelHelper(
30+
$reflectionProvider,
31+
checkNullables: true,
32+
checkThisOnly: false,
33+
checkUnionTypes: true,
34+
checkExplicitMixed: $this->checkExplicitMixed,
35+
checkImplicitMixed: false,
36+
checkBenevolentUnionTypes: false,
37+
discoveringSymbolsTip: true,
38+
);
2939
return new InstantiationRule(
3040
$container,
3141
$reflectionProvider,
3242
new FunctionCallParametersCheck(
33-
new RuleLevelHelper(
34-
$reflectionProvider,
35-
checkNullables: true,
36-
checkThisOnly: false,
37-
checkUnionTypes: true,
38-
checkExplicitMixed: $this->checkExplicitMixed,
39-
checkImplicitMixed: false,
40-
checkBenevolentUnionTypes: false,
41-
discoveringSymbolsTip: true,
42-
),
43+
$ruleLevelHelper,
4344
new NullsafeCheck(),
4445
new UnresolvableTypeHelper(),
4546
new PropertyReflectionFinder(),
@@ -55,7 +56,9 @@ protected function getRule(): Rule
5556
$reflectionProvider,
5657
$container,
5758
),
59+
$ruleLevelHelper,
5860
new ConsistentConstructorHelper(),
61+
newOnNonObject: true,
5962
discoveringSymbolsTip: true,
6063
);
6164
}
@@ -682,4 +685,42 @@ public function testBug14499(): void
682685
$this->analyse([__DIR__ . '/data/bug-14499.php'], []);
683686
}
684687

688+
public function testInstantiationWithNonObjectType(): void
689+
{
690+
$this->analyse([__DIR__ . '/data/instantiation-non-object.php'], [
691+
[
692+
'Cannot instantiate class using int.',
693+
35,
694+
],
695+
[
696+
'Cannot instantiate class using int.',
697+
39,
698+
],
699+
[
700+
'Cannot instantiate class using float.',
701+
40,
702+
],
703+
[
704+
'Cannot instantiate class using bool.',
705+
41,
706+
],
707+
[
708+
'Cannot instantiate class using int|string.',
709+
42,
710+
],
711+
[
712+
'Cannot instantiate class using float|int.',
713+
43,
714+
],
715+
[
716+
'Cannot instantiate class using string|null.',
717+
44,
718+
],
719+
[
720+
'Cannot instantiate class using array<int, string>.',
721+
50,
722+
],
723+
]);
724+
}
725+
685726
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace InstantiationNonObject;
4+
5+
function get_class_name(): int
6+
{
7+
return 123;
8+
}
9+
10+
class Foo
11+
{
12+
}
13+
14+
/**
15+
* @param int|string $intOrString
16+
* @param int|float $intOrFloat
17+
* @param class-string $classString
18+
* @param class-string<Foo> $classStringOfFoo
19+
*/
20+
function doFoo(
21+
string $string,
22+
object $object,
23+
int $int,
24+
float $float,
25+
bool $bool,
26+
$intOrString,
27+
$intOrFloat,
28+
?string $nullableString,
29+
string $classString,
30+
string $classStringOfFoo,
31+
Foo $foo
32+
): void
33+
{
34+
$class = get_class_name();
35+
new $class;
36+
37+
new $string;
38+
new $object;
39+
new $int;
40+
new $float;
41+
new $bool;
42+
new $intOrString;
43+
new $intOrFloat;
44+
new $nullableString;
45+
new $classString;
46+
new $classStringOfFoo;
47+
new $foo;
48+
49+
$array = ['a'];
50+
new $array;
51+
}

0 commit comments

Comments
 (0)