Skip to content

Commit 133fb21

Browse files
authored
Merge pull request #652 from Ocramius/fix/#646-#632-#645-only-variables-returned-by-ref-in-generated-code
#646 #632 #645 only variables should be returned by-ref in generated dynamic property access simulation code
2 parents 098e306 + 0590fc9 commit 133fb21

File tree

9 files changed

+181
-40
lines changed

9 files changed

+181
-40
lines changed

infection.json.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
"branch": "master"
1111
}
1212
},
13-
"minMsi": 84.3,
13+
"minMsi": 84.5,
1414
"minCoveredMsi": 86
1515
}

src/ProxyManager/ProxyGenerator/AccessInterceptorValueHolder/MethodGenerator/MagicGet.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function __construct(
3939
$callParent = PublicScopeSimulator::getPublicAccessSimulationCode(
4040
PublicScopeSimulator::OPERATION_GET,
4141
'name',
42-
'value',
42+
null,
4343
$valueHolder,
4444
'returnValue',
4545
$originalClass

src/ProxyManager/ProxyGenerator/AccessInterceptorValueHolder/MethodGenerator/MagicIsset.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function __construct(
3939
$callParent = PublicScopeSimulator::getPublicAccessSimulationCode(
4040
PublicScopeSimulator::OPERATION_ISSET,
4141
'name',
42-
'value',
42+
null,
4343
$valueHolder,
4444
'returnValue',
4545
$originalClass

src/ProxyManager/ProxyGenerator/AccessInterceptorValueHolder/MethodGenerator/MagicUnset.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function __construct(
3939
$callParent = PublicScopeSimulator::getPublicAccessSimulationCode(
4040
PublicScopeSimulator::OPERATION_UNSET,
4141
'name',
42-
'value',
42+
null,
4343
$valueHolder,
4444
'returnValue',
4545
$originalClass

src/ProxyManager/ProxyGenerator/Util/PublicScopeSimulator.php

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use Laminas\Code\Generator\PropertyGenerator;
99
use ReflectionClass;
1010

11+
use function array_filter;
12+
use function array_map;
13+
use function implode;
1114
use function sprintf;
1215
use function var_export;
1316

@@ -30,14 +33,17 @@ class PublicScopeSimulator
3033
*
3134
* @param string $operationType operation to execute: one of 'get', 'set', 'isset' or 'unset'
3235
* @param string $nameParameter name of the `name` parameter of the magic method
33-
* @param string|null $valueParameter name of the `value` parameter of the magic method
36+
* @param string|null $valueParameter name of the `value` parameter of the magic method, only to be
37+
* used with $operationType 'set'
3438
* @param PropertyGenerator $valueHolder name of the property containing the target object from which
3539
* to read the property. `$this` if none provided
3640
* @param string|null $returnPropertyName name of the property to which we want to assign the result of
3741
* the operation. Return directly if none provided
3842
* @param string|null $interfaceName name of the proxified interface if any
3943
*
4044
* @throws InvalidArgumentException
45+
*
46+
* @psalm-param $operationType self::OPERATION_*
4147
*/
4248
public static function getPublicAccessSimulationCode(
4349
string $operationType,
@@ -48,7 +54,6 @@ public static function getPublicAccessSimulationCode(
4854
?ReflectionClass $originalClass = null
4955
): string {
5056
$byRef = self::getByRefReturnValue($operationType);
51-
$value = $operationType === self::OPERATION_SET ? ', $value' : '';
5257
$target = '$this';
5358

5459
if ($valueHolder) {
@@ -59,27 +64,37 @@ public static function getPublicAccessSimulationCode(
5964
? 'new \\ReflectionClass(get_parent_class($this))'
6065
: 'new \\ReflectionClass(' . var_export($originalClass->getName(), true) . ')';
6166

67+
$accessorEvaluation = $returnPropertyName
68+
? '$' . $returnPropertyName . ' = ' . $byRef . '$accessor();'
69+
: '$returnValue = ' . $byRef . '$accessor();' . "\n\n" . 'return $returnValue;';
70+
71+
if ($operationType === self::OPERATION_UNSET) {
72+
$accessorEvaluation = '$accessor();';
73+
}
74+
6275
return '$realInstanceReflection = ' . $originalClassReflection . ';' . "\n\n"
6376
. 'if (! $realInstanceReflection->hasProperty($' . $nameParameter . ')) {' . "\n"
6477
. ' $targetObject = ' . $target . ';' . "\n\n"
6578
. self::getUndefinedPropertyNotice($operationType, $nameParameter)
6679
. ' ' . self::getOperation($operationType, $nameParameter, $valueParameter) . "\n"
67-
. " return;\n"
6880
. '}' . "\n\n"
6981
. '$targetObject = ' . self::getTargetObject($valueHolder) . ";\n"
70-
. '$accessor = function ' . $byRef . '() use ($targetObject, $name' . $value . ') {' . "\n"
82+
. '$accessor = function ' . $byRef . '() use ('
83+
. implode(', ', array_map(
84+
static fn (string $parameterName): string => '$' . $parameterName,
85+
array_filter(['targetObject', $nameParameter, $valueParameter])
86+
))
87+
. ') {' . "\n"
7188
. ' ' . self::getOperation($operationType, $nameParameter, $valueParameter) . "\n"
7289
. "};\n"
73-
. self::getScopeReBind()
74-
. (
75-
$returnPropertyName
76-
? '$' . $returnPropertyName . ' = ' . $byRef . '$accessor();'
77-
: '$returnValue = ' . $byRef . '$accessor();' . "\n\n" . 'return $returnValue;'
78-
);
90+
. self::generateScopeReBind()
91+
. $accessorEvaluation;
7992
}
8093

8194
/**
8295
* This will generate code that triggers a notice if access is attempted on a non-existing property
96+
*
97+
* @psalm-param $operationType self::OPERATION_*
8398
*/
8499
private static function getUndefinedPropertyNotice(string $operationType, string $nameParameter): string
85100
{
@@ -106,6 +121,8 @@ private static function getUndefinedPropertyNotice(string $operationType, string
106121
* Note: if the object is a wrapper, the wrapped instance is accessed directly. If the object
107122
* is a ghost or the proxy has no wrapper, then an instance of the parent class is created via
108123
* on-the-fly unserialization
124+
*
125+
* @psalm-param $operationType self::OPERATION_*
109126
*/
110127
private static function getByRefReturnValue(string $operationType): string
111128
{
@@ -126,25 +143,43 @@ private static function getTargetObject(?PropertyGenerator $valueHolder = null):
126143

127144
/**
128145
* @throws InvalidArgumentException
146+
*
147+
* @psalm-param $operationType self::OPERATION_*
129148
*/
130149
private static function getOperation(string $operationType, string $nameParameter, ?string $valueParameter): string
131150
{
151+
if ($valueParameter !== null && $operationType !== self::OPERATION_SET) {
152+
throw new InvalidArgumentException(
153+
'Parameter $valueParameter should be provided (only) when $operationType === "' . self::OPERATION_SET . '"'
154+
. self::class
155+
. '::OPERATION_SET'
156+
);
157+
}
158+
132159
switch ($operationType) {
133160
case self::OPERATION_GET:
134161
return 'return $targetObject->$' . $nameParameter . ';';
135162

136163
case self::OPERATION_SET:
137164
if ($valueParameter === null) {
138-
throw new InvalidArgumentException('Parameter $valueParameter not provided');
165+
throw new InvalidArgumentException(
166+
'Parameter $valueParameter should be provided (only) when $operationType === "' . self::OPERATION_SET . '"'
167+
. self::class
168+
. '::OPERATION_SET'
169+
);
139170
}
140171

141-
return 'return $targetObject->$' . $nameParameter . ' = $' . $valueParameter . ';';
172+
return '$targetObject->$' . $nameParameter . ' = $' . $valueParameter . ';'
173+
. "\n\n"
174+
. ' return $targetObject->$' . $nameParameter . ';';
142175

143176
case self::OPERATION_ISSET:
144177
return 'return isset($targetObject->$' . $nameParameter . ');';
145178

146179
case self::OPERATION_UNSET:
147-
return 'unset($targetObject->$' . $nameParameter . ');';
180+
return 'unset($targetObject->$' . $nameParameter . ');'
181+
. "\n\n"
182+
. ' return;';
148183
}
149184

150185
throw new InvalidArgumentException(sprintf('Invalid operation "%s" provided', $operationType));
@@ -153,11 +188,13 @@ private static function getOperation(string $operationType, string $nameParamete
153188
/**
154189
* Generates code to bind operations to the parent scope
155190
*/
156-
private static function getScopeReBind(): string
191+
private static function generateScopeReBind(): string
157192
{
158-
return '$backtrace = debug_backtrace(true, 2);' . "\n"
159-
. '$scopeObject = isset($backtrace[1][\'object\'])'
160-
. ' ? $backtrace[1][\'object\'] : new \ProxyManager\Stub\EmptyClassStub();' . "\n"
161-
. '$accessor = $accessor->bindTo($scopeObject, get_class($scopeObject));' . "\n";
193+
return <<<'PHP'
194+
$backtrace = debug_backtrace(true, 2);
195+
$scopeObject = isset($backtrace[1]['object']) ? $backtrace[1]['object'] : new \ProxyManager\Stub\EmptyClassStub();
196+
$accessor = $accessor->bindTo($scopeObject, get_class($scopeObject));
197+
198+
PHP;
162199
}
163200
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace ProxyManagerTest\ProxyGenerator\Functional;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use ProxyManager\ProxyGenerator\Util\PublicScopeSimulator;
9+
use ProxyManagerTestAsset\ClassWithMixedProperties;
10+
11+
use function sprintf;
12+
13+
/**
14+
* @covers \ProxyManager\ProxyGenerator\Util\PublicScopeSimulator
15+
* @coversNothing
16+
*/
17+
final class PublicScopeSimulatorFunctionalTest extends TestCase
18+
{
19+
/**
20+
* @group #632
21+
* @group #645
22+
* @group #646
23+
*/
24+
public function testAccessingUndefinedPropertiesDoesNotLeadToInvalidByRefAccess(): void
25+
{
26+
/** @psalm-var ClassWithMixedProperties $sut */
27+
$sut = eval(sprintf(
28+
<<<'PHP'
29+
return new class() extends %s {
30+
public function doGet($prop) : string { %s }
31+
public function doSet($prop, $val) : string { %s }
32+
public function doIsset($prop) : bool { %s }
33+
public function doUnset($prop) : void { %s }
34+
};
35+
PHP
36+
,
37+
ClassWithMixedProperties::class,
38+
PublicScopeSimulator::getPublicAccessSimulationCode(PublicScopeSimulator::OPERATION_GET, 'prop'),
39+
PublicScopeSimulator::getPublicAccessSimulationCode(PublicScopeSimulator::OPERATION_SET, 'prop', 'val'),
40+
PublicScopeSimulator::getPublicAccessSimulationCode(PublicScopeSimulator::OPERATION_ISSET, 'prop'),
41+
PublicScopeSimulator::getPublicAccessSimulationCode(PublicScopeSimulator::OPERATION_UNSET, 'prop')
42+
));
43+
44+
self::assertSame('publicProperty0', $sut->doGet('publicProperty0'));
45+
self::assertSame('bar', $sut->doSet('publicProperty0', 'bar'));
46+
self::assertTrue($sut->doIsset('publicProperty0'));
47+
self::assertNull($sut->doUnset('publicProperty0'));
48+
}
49+
}

tests/ProxyManagerTest/ProxyGenerator/AccessInterceptorScopeLocalizer/MethodGenerator/MagicUnsetTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function testBodyStructure(): void
3939

4040
self::assertSame('__unset', $magicGet->getName());
4141
self::assertCount(1, $magicGet->getParameters());
42-
self::assertStringMatchesFormat('%a$returnValue = $accessor();%a', $magicGet->getBody());
42+
self::assertStringMatchesFormat('%a$accessor();%a', $magicGet->getBody());
4343
}
4444

4545
/**

tests/ProxyManagerTest/ProxyGenerator/LazyLoadingValueHolder/MethodGenerator/MagicUnsetTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public function testBodyStructure(): void
4141
"\$this->foo && \$this->foo->__invoke(\$this->bar, \$this, '__unset', array('name' => \$name)"
4242
. ", \$this->foo);\n\n"
4343
. "if (isset(self::\$bar[\$name])) {\n unset(\$this->bar->\$name);\n\n return;\n}"
44-
. '%areturn %s;',
44+
. '%a',
4545
$magicIsset->getBody()
4646
);
4747
}

0 commit comments

Comments
 (0)