Skip to content

Commit 5a9471a

Browse files
committed
[PHP 8.5] clone with support
1 parent 75f243d commit 5a9471a

17 files changed

+372
-0
lines changed

src/Analyser/MutatingScope.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
use PHPStan\Type\Accessory\AccessoryArrayListType;
9292
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
9393
use PHPStan\Type\Accessory\HasOffsetValueType;
94+
use PHPStan\Type\Accessory\HasPropertyType;
9495
use PHPStan\Type\Accessory\NonEmptyArrayType;
9596
use PHPStan\Type\Accessory\OversizedArrayType;
9697
use PHPStan\Type\ArrayType;
@@ -2525,6 +2526,30 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
25252526
);
25262527
$normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node);
25272528
if ($normalizedNode !== null) {
2529+
if ($functionReflection->getName() === 'clone' && count($normalizedNode->getArgs()) > 0) {
2530+
$cloneType = $this->getType(new Expr\Clone_($normalizedNode->getArgs()[0]->value));
2531+
if (count($normalizedNode->getArgs()) === 2) {
2532+
$propertiesType = $this->getType($normalizedNode->getArgs()[1]->value);
2533+
if ($propertiesType->isConstantArray()->yes()) {
2534+
$constantArrays = $propertiesType->getConstantArrays();
2535+
if (count($constantArrays) === 1) {
2536+
$accessories = [];
2537+
foreach ($constantArrays[0]->getKeyTypes() as $keyType) {
2538+
$constantKeyTypes = $keyType->getConstantScalarValues();
2539+
if (count($constantKeyTypes) !== 1) {
2540+
return $cloneType;
2541+
}
2542+
$accessories[] = new HasPropertyType((string) $constantKeyTypes[0]);
2543+
}
2544+
if (count($accessories) > 0 && count($accessories) <= 16) {
2545+
return TypeCombinator::intersect($cloneType, ...$accessories);
2546+
}
2547+
}
2548+
}
2549+
}
2550+
2551+
return $cloneType;
2552+
}
25282553
$resolvedType = $this->getDynamicFunctionReturnType($parametersAcceptor, $normalizedNode, $functionReflection);
25292554
if ($resolvedType !== null) {
25302555
return $resolvedType;

src/Analyser/NodeScopeResolver.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2624,6 +2624,43 @@ static function (): void {
26242624
$returnType = $parametersAcceptor->getReturnType();
26252625
$isAlwaysTerminating = $isAlwaysTerminating || $returnType instanceof NeverType && $returnType->isExplicit();
26262626
}
2627+
2628+
if (
2629+
$expr->name instanceof Name
2630+
&& $functionReflection !== null
2631+
&& $functionReflection->getName() === 'clone'
2632+
&& count($expr->getArgs()) === 2
2633+
) {
2634+
$clonePropertiesArgType = $scope->getType($expr->getArgs()[1]->value);
2635+
$cloneExpr = new TypeExpr($scope->getType(new Expr\Clone_($expr->getArgs()[0]->value)));
2636+
$clonePropertiesArgTypeConstantArrays = $clonePropertiesArgType->getConstantArrays();
2637+
foreach ($clonePropertiesArgTypeConstantArrays as $clonePropertiesArgTypeConstantArray) {
2638+
foreach ($clonePropertiesArgTypeConstantArray->getKeyTypes() as $i => $clonePropertyKeyType) {
2639+
$clonePropertyKeyTypeScalars = $clonePropertyKeyType->getConstantScalarValues();
2640+
$propertyAttributes = $expr->getAttributes();
2641+
$propertyAttributes['inCloneWith'] = true;
2642+
if (count($clonePropertyKeyTypeScalars) === 1) {
2643+
$this->processVirtualAssign(
2644+
$scope,
2645+
$stmt,
2646+
new PropertyFetch($cloneExpr, (string) $clonePropertyKeyTypeScalars[0], $propertyAttributes),
2647+
new TypeExpr($clonePropertiesArgTypeConstantArray->getValueTypes()[$i]),
2648+
$nodeCallback,
2649+
);
2650+
continue;
2651+
}
2652+
2653+
$this->processVirtualAssign(
2654+
$scope,
2655+
$stmt,
2656+
new PropertyFetch($cloneExpr, new TypeExpr($clonePropertyKeyType), $propertyAttributes),
2657+
new TypeExpr($clonePropertiesArgTypeConstantArray->getValueTypes()[$i]),
2658+
$nodeCallback,
2659+
);
2660+
}
2661+
}
2662+
}
2663+
26272664
$result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context);
26282665
$scope = $result->getScope();
26292666
$hasYield = $result->hasYield();

src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public function processNode(Node $node, Scope $scope): array
4747
return [];
4848
}
4949

50+
$inCloneWith = (bool) $propertyFetch->getAttribute('inCloneWith', false);
51+
if ($inCloneWith) {
52+
return [];
53+
}
54+
5055
$inFunction = $scope->getFunction();
5156
if (
5257
$inFunction instanceof PhpMethodFromParserNodeReflection

src/Rules/Properties/ReadOnlyPropertyAssignRule.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public function processNode(Node $node, Scope $scope): array
4646
return [];
4747
}
4848

49+
$inCloneWith = (bool) $propertyFetch->getAttribute('inCloneWith', false);
50+
if ($inCloneWith) {
51+
return [];
52+
}
53+
4954
$errors = [];
5055
$reflections = $this->propertyReflectionFinder->findPropertyReflectionsFromNode($propertyFetch, $scope);
5156
foreach ($reflections as $propertyReflection) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php // lint >= 8.5
2+
3+
namespace CloneWithType;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
public function doFoo(?object $object, $mixed): void
11+
{
12+
assertType('object', clone($object, []));
13+
assertType('object', clone($mixed, []));
14+
assertType('static(CloneWithType\\Foo)', clone(new $this, []));
15+
assertType('static(CloneWithType\\Foo)', clone(new static, []));
16+
assertType(self::class, clone(new self, []));
17+
}
18+
19+
public function doBar(object $object): void
20+
{
21+
assertType('object&hasProperty(bar)&hasProperty(foo)', clone($object, [
22+
'foo' => 1,
23+
'bar' => 2,
24+
]));
25+
}
26+
27+
}

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,4 +2461,19 @@ public function testPipeOperator(): void
24612461
]);
24622462
}
24632463

2464+
#[RequiresPhp('>= 8.5')]
2465+
public function testClone(): void
2466+
{
2467+
$this->analyse([__DIR__ . '/data/clone-function.php'], [
2468+
[
2469+
'Parameter #2 $withProperties of function clone expects array, int given.',
2470+
12,
2471+
],
2472+
[
2473+
'Function clone invoked with 3 parameters, 1-2 required.',
2474+
13,
2475+
],
2476+
]);
2477+
}
2478+
24642479
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php // lint >= 8.5
2+
3+
namespace CloneFunction;
4+
5+
class Foo
6+
{
7+
8+
}
9+
10+
function (): void {
11+
clone ($foo, []);
12+
clone ($foo, 1);
13+
clone ($foo, [], 1);
14+
};

tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,31 @@ public function testBug13123(): void
191191
$this->analyse([__DIR__ . '/data/bug-13123.php'], []);
192192
}
193193

194+
#[RequiresPhp('>= 8.5')]
195+
public function testCloneWith(): void
196+
{
197+
$this->analyse([__DIR__ . '/data/clone-with.php'], [
198+
[
199+
'Access to private property AccessPropertiesInAssignCloneWith\Foo::$priv.',
200+
26,
201+
],
202+
[
203+
'Access to protected property AccessPropertiesInAssignCloneWith\Foo::$prot.',
204+
26,
205+
],
206+
[
207+
'Access to private property AccessPropertiesInAssignCloneWith\FooReadonly::$priv.',
208+
56,
209+
],
210+
[
211+
'Access to protected property AccessPropertiesInAssignCloneWith\FooReadonly::$prot.',
212+
56,
213+
],
214+
[
215+
'Assign to protected(set) property AccessPropertiesInAssignCloneWith\FooReadonly::$pub.',
216+
56,
217+
],
218+
]);
219+
}
220+
194221
}

tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,10 @@ public function testPropertyHooks(): void
185185
]);
186186
}
187187

188+
#[RequiresPhp('>= 8.5')]
189+
public function testCloneWith(): void
190+
{
191+
$this->analyse([__DIR__ . '/data/readonly-phpdoc-property-assign-clone-with.php'], []);
192+
}
193+
188194
}

tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,10 @@ public function testBug12537(): void
174174
$this->analyse([__DIR__ . '/data/bug-12537.php'], []);
175175
}
176176

177+
#[RequiresPhp('>= 8.5')]
178+
public function testCloneWith(): void
179+
{
180+
$this->analyse([__DIR__ . '/data/readonly-property-assign-clone-with.php'], []);
181+
}
182+
177183
}

0 commit comments

Comments
 (0)